summaryrefslogtreecommitdiffstats
path: root/browser/components/urlbar
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/urlbar')
-rw-r--r--browser/components/urlbar/.eslintrc.js42
-rw-r--r--browser/components/urlbar/UrlbarController.jsm899
-rw-r--r--browser/components/urlbar/UrlbarEventBufferer.jsm358
-rw-r--r--browser/components/urlbar/UrlbarInput.jsm3443
-rw-r--r--browser/components/urlbar/UrlbarMuxerUnifiedComplete.jsm571
-rw-r--r--browser/components/urlbar/UrlbarPrefs.jsm467
-rw-r--r--browser/components/urlbar/UrlbarProviderAutofill.jsm801
-rw-r--r--browser/components/urlbar/UrlbarProviderExtension.jsm390
-rw-r--r--browser/components/urlbar/UrlbarProviderHeuristicFallback.jsm326
-rw-r--r--browser/components/urlbar/UrlbarProviderInterventions.jsm810
-rw-r--r--browser/components/urlbar/UrlbarProviderOmnibox.jsm178
-rw-r--r--browser/components/urlbar/UrlbarProviderOpenTabs.jsm215
-rw-r--r--browser/components/urlbar/UrlbarProviderPrivateSearch.jsm132
-rw-r--r--browser/components/urlbar/UrlbarProviderSearchSuggestions.jsm524
-rw-r--r--browser/components/urlbar/UrlbarProviderSearchTips.jsm505
-rw-r--r--browser/components/urlbar/UrlbarProviderTabToSearch.jsm435
-rw-r--r--browser/components/urlbar/UrlbarProviderTokenAliasEngines.jsm228
-rw-r--r--browser/components/urlbar/UrlbarProviderTopSites.jsm249
-rw-r--r--browser/components/urlbar/UrlbarProviderUnifiedComplete.jsm359
-rw-r--r--browser/components/urlbar/UrlbarProvidersManager.jsm714
-rw-r--r--browser/components/urlbar/UrlbarResult.jsm339
-rw-r--r--browser/components/urlbar/UrlbarSearchOneOffs.jsm374
-rw-r--r--browser/components/urlbar/UrlbarSearchUtils.jsm293
-rw-r--r--browser/components/urlbar/UrlbarTokenizer.jsm407
-rw-r--r--browser/components/urlbar/UrlbarUtils.jsm1758
-rw-r--r--browser/components/urlbar/UrlbarValueFormatter.jsm498
-rw-r--r--browser/components/urlbar/UrlbarView.jsm2232
-rw-r--r--browser/components/urlbar/content/interventions.ftl40
-rw-r--r--browser/components/urlbar/docs/contact.rst9
-rw-r--r--browser/components/urlbar/docs/debugging.rst4
-rw-r--r--browser/components/urlbar/docs/experiments.rst727
-rw-r--r--browser/components/urlbar/docs/index.rst26
-rw-r--r--browser/components/urlbar/docs/overview.rst413
-rw-r--r--browser/components/urlbar/docs/telemetry.rst445
-rw-r--r--browser/components/urlbar/docs/utilities.rst26
-rw-r--r--browser/components/urlbar/moz.build47
-rw-r--r--browser/components/urlbar/tests/UrlbarTestUtils.jsm915
-rw-r--r--browser/components/urlbar/tests/browser-tips/README.txt7
-rw-r--r--browser/components/urlbar/tests/browser-tips/browser.ini17
-rw-r--r--browser/components/urlbar/tests/browser-tips/browser_interventions.js209
-rw-r--r--browser/components/urlbar/tests/browser-tips/browser_picks.js213
-rw-r--r--browser/components/urlbar/tests/browser-tips/browser_searchTips.js305
-rw-r--r--browser/components/urlbar/tests/browser-tips/browser_searchTips_interaction.js620
-rw-r--r--browser/components/urlbar/tests/browser-tips/browser_selection.js284
-rw-r--r--browser/components/urlbar/tests/browser-tips/browser_updateAsk.js70
-rw-r--r--browser/components/urlbar/tests/browser-tips/browser_updateRefresh.js48
-rw-r--r--browser/components/urlbar/tests/browser-tips/browser_updateRestart.js45
-rw-r--r--browser/components/urlbar/tests/browser-tips/browser_updateWeb.js48
-rw-r--r--browser/components/urlbar/tests/browser-tips/head.js754
-rw-r--r--browser/components/urlbar/tests/browser/POSTSearchEngine.xml6
-rw-r--r--browser/components/urlbar/tests/browser/authenticate.sjs220
-rw-r--r--browser/components/urlbar/tests/browser/browser.ini297
-rw-r--r--browser/components/urlbar/tests/browser/browser_UrlbarInput_formatValue.js174
-rw-r--r--browser/components/urlbar/tests/browser/browser_UrlbarInput_hiddenFocus.js21
-rw-r--r--browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow.js156
-rw-r--r--browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow_resize.js58
-rw-r--r--browser/components/urlbar/tests/browser/browser_UrlbarInput_setURI.js125
-rw-r--r--browser/components/urlbar/tests/browser/browser_UrlbarInput_tooltip.js83
-rw-r--r--browser/components/urlbar/tests/browser/browser_UrlbarInput_trimURLs.js127
-rw-r--r--browser/components/urlbar/tests/browser/browser_aboutHomeLoading.js189
-rw-r--r--browser/components/urlbar/tests/browser/browser_action_searchengine.js127
-rw-r--r--browser/components/urlbar/tests/browser/browser_action_searchengine_alias.js77
-rw-r--r--browser/components/urlbar/tests/browser/browser_autoFill_backspaced.js262
-rw-r--r--browser/components/urlbar/tests/browser/browser_autoFill_canonize.js62
-rw-r--r--browser/components/urlbar/tests/browser/browser_autoFill_caretNotAtEnd.js34
-rw-r--r--browser/components/urlbar/tests/browser/browser_autoFill_firstResult.js199
-rw-r--r--browser/components/urlbar/tests/browser/browser_autoFill_paste.js38
-rw-r--r--browser/components/urlbar/tests/browser/browser_autoFill_placeholder.js257
-rw-r--r--browser/components/urlbar/tests/browser/browser_autoFill_preserve.js260
-rw-r--r--browser/components/urlbar/tests/browser/browser_autoFill_trimURLs.js183
-rw-r--r--browser/components/urlbar/tests/browser/browser_autoFill_typed.js179
-rw-r--r--browser/components/urlbar/tests/browser/browser_autoFill_undo.js50
-rw-r--r--browser/components/urlbar/tests/browser/browser_autoOpen.js93
-rw-r--r--browser/components/urlbar/tests/browser/browser_autocomplete_a11y_label.js155
-rw-r--r--browser/components/urlbar/tests/browser/browser_autocomplete_autoselect.js128
-rw-r--r--browser/components/urlbar/tests/browser/browser_autocomplete_cursor.js37
-rw-r--r--browser/components/urlbar/tests/browser/browser_autocomplete_edit_completed.js75
-rw-r--r--browser/components/urlbar/tests/browser/browser_autocomplete_enter_race.js178
-rw-r--r--browser/components/urlbar/tests/browser/browser_autocomplete_no_title.js34
-rw-r--r--browser/components/urlbar/tests/browser/browser_autocomplete_readline_navigation.js71
-rw-r--r--browser/components/urlbar/tests/browser/browser_autocomplete_tag_star_visibility.js167
-rw-r--r--browser/components/urlbar/tests/browser/browser_blanking.js54
-rw-r--r--browser/components/urlbar/tests/browser/browser_canonizeURL.js255
-rw-r--r--browser/components/urlbar/tests/browser/browser_caret_navigation.js115
-rw-r--r--browser/components/urlbar/tests/browser/browser_closePanelOnClick.js30
-rw-r--r--browser/components/urlbar/tests/browser/browser_content_opener.js23
-rw-r--r--browser/components/urlbar/tests/browser/browser_copy_during_load.js51
-rw-r--r--browser/components/urlbar/tests/browser/browser_copying.js386
-rw-r--r--browser/components/urlbar/tests/browser/browser_customizeMode.js73
-rw-r--r--browser/components/urlbar/tests/browser/browser_cutting.js17
-rw-r--r--browser/components/urlbar/tests/browser/browser_decode.js144
-rw-r--r--browser/components/urlbar/tests/browser/browser_delete.js51
-rw-r--r--browser/components/urlbar/tests/browser/browser_deleteAllText.js100
-rw-r--r--browser/components/urlbar/tests/browser/browser_display_selectedAction_Extensions.js57
-rw-r--r--browser/components/urlbar/tests/browser/browser_dns_first_for_single_words.js49
-rw-r--r--browser/components/urlbar/tests/browser/browser_downArrowKeySearch.js83
-rw-r--r--browser/components/urlbar/tests/browser/browser_dragdropURL.js108
-rw-r--r--browser/components/urlbar/tests/browser/browser_dynamicResults.js740
-rw-r--r--browser/components/urlbar/tests/browser/browser_edit_invalid_url.js91
-rw-r--r--browser/components/urlbar/tests/browser/browser_enter.js268
-rw-r--r--browser/components/urlbar/tests/browser/browser_enterAfterMouseOver.js97
-rw-r--r--browser/components/urlbar/tests/browser/browser_focusedCmdK.js15
-rw-r--r--browser/components/urlbar/tests/browser/browser_handleCommand_fallback.js151
-rw-r--r--browser/components/urlbar/tests/browser/browser_hashChangeProxyState.js148
-rw-r--r--browser/components/urlbar/tests/browser/browser_heuristicNotAddedFirst.js165
-rw-r--r--browser/components/urlbar/tests/browser/browser_ime_composition.js287
-rw-r--r--browser/components/urlbar/tests/browser/browser_inputHistory.js361
-rw-r--r--browser/components/urlbar/tests/browser/browser_inputHistory_emptystring.js100
-rw-r--r--browser/components/urlbar/tests/browser/browser_keepStateAcrossTabSwitches.js68
-rw-r--r--browser/components/urlbar/tests/browser/browser_keyword.js240
-rw-r--r--browser/components/urlbar/tests/browser/browser_keywordBookmarklets.js133
-rw-r--r--browser/components/urlbar/tests/browser/browser_keywordSearch.js61
-rw-r--r--browser/components/urlbar/tests/browser/browser_keywordSearch_postData.js78
-rw-r--r--browser/components/urlbar/tests/browser/browser_keyword_override.js61
-rw-r--r--browser/components/urlbar/tests/browser/browser_keyword_select_and_type.js97
-rw-r--r--browser/components/urlbar/tests/browser/browser_loadRace.js90
-rw-r--r--browser/components/urlbar/tests/browser/browser_locationBarCommand.js291
-rw-r--r--browser/components/urlbar/tests/browser/browser_locationBarExternalLoad.js94
-rw-r--r--browser/components/urlbar/tests/browser/browser_locationchange_urlbar_edit_dos.js67
-rw-r--r--browser/components/urlbar/tests/browser/browser_new_tab_urlbar_reset.js39
-rw-r--r--browser/components/urlbar/tests/browser/browser_oneOffs.js961
-rw-r--r--browser/components/urlbar/tests/browser/browser_oneOffs_contextMenu.js80
-rw-r--r--browser/components/urlbar/tests/browser/browser_oneOffs_heuristicRestyle.js506
-rw-r--r--browser/components/urlbar/tests/browser/browser_oneOffs_keyModifiers.js389
-rw-r--r--browser/components/urlbar/tests/browser/browser_oneOffs_searchSuggestions.js354
-rw-r--r--browser/components/urlbar/tests/browser/browser_oneOffs_settings.js88
-rw-r--r--browser/components/urlbar/tests/browser/browser_pasteAndGo.js92
-rw-r--r--browser/components/urlbar/tests/browser/browser_percent_encoded.js63
-rw-r--r--browser/components/urlbar/tests/browser/browser_placeholder.js295
-rw-r--r--browser/components/urlbar/tests/browser/browser_populateAfterPushState.js32
-rw-r--r--browser/components/urlbar/tests/browser/browser_primary_selection_safe_on_new_tab.js71
-rw-r--r--browser/components/urlbar/tests/browser/browser_privateBrowsingWindowChange.js51
-rw-r--r--browser/components/urlbar/tests/browser/browser_raceWithTabs.js86
-rw-r--r--browser/components/urlbar/tests/browser/browser_redirect_error.js134
-rw-r--r--browser/components/urlbar/tests/browser/browser_remoteness_switch.js51
-rw-r--r--browser/components/urlbar/tests/browser/browser_remotetab.js111
-rw-r--r--browser/components/urlbar/tests/browser/browser_removeUnsafeProtocolsFromURLBarPaste.js95
-rw-r--r--browser/components/urlbar/tests/browser/browser_remove_match.js227
-rw-r--r--browser/components/urlbar/tests/browser/browser_restoreEmptyInput.js60
-rw-r--r--browser/components/urlbar/tests/browser/browser_resultSpan.js264
-rw-r--r--browser/components/urlbar/tests/browser/browser_result_onSelection.js55
-rw-r--r--browser/components/urlbar/tests/browser/browser_retainedResultsOnFocus.js435
-rw-r--r--browser/components/urlbar/tests/browser/browser_revert.js33
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchFunction.js256
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchHistoryLimit.js92
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchMode_alias_replacement.js280
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchMode_autofill.js137
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchMode_clickLink.js94
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchMode_engineRemoval.js103
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchMode_excludeResults.js210
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchMode_heuristic.js226
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchMode_indicator.js388
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchMode_localOneOffs_actionText.js462
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchMode_no_results.js290
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchMode_oneOffButton.js118
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchMode_pickResult.js89
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchMode_preview.js494
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchMode_sessionStore.js313
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchMode_setURI.js119
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchMode_suggestions.js585
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchMode_switchTabs.js305
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchSettings.js32
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchSingleWordNotification.js356
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchSuggestions.js343
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchTelemetry.js225
-rw-r--r--browser/components/urlbar/tests/browser/browser_selectStaleResults.js301
-rw-r--r--browser/components/urlbar/tests/browser/browser_selectionKeyNavigation.js158
-rw-r--r--browser/components/urlbar/tests/browser/browser_separatePrivateDefault.js215
-rw-r--r--browser/components/urlbar/tests/browser/browser_separatePrivateDefault_differentEngine.js358
-rw-r--r--browser/components/urlbar/tests/browser/browser_speculative_connect.js202
-rw-r--r--browser/components/urlbar/tests/browser/browser_speculative_connect_not_with_client_cert.js219
-rw-r--r--browser/components/urlbar/tests/browser/browser_stop.js75
-rw-r--r--browser/components/urlbar/tests/browser/browser_stopSearchOnSelection.js115
-rw-r--r--browser/components/urlbar/tests/browser/browser_stop_pending.js220
-rw-r--r--browser/components/urlbar/tests/browser/browser_suggestedIndex.js120
-rw-r--r--browser/components/urlbar/tests/browser/browser_switchTab_closesUrlbarPopup.js42
-rw-r--r--browser/components/urlbar/tests/browser/browser_switchTab_currentTab.js41
-rw-r--r--browser/components/urlbar/tests/browser/browser_switchTab_decodeuri.js51
-rw-r--r--browser/components/urlbar/tests/browser/browser_switchTab_override.js100
-rw-r--r--browser/components/urlbar/tests/browser/browser_switchToTabHavingURI_aOpenParams.js216
-rw-r--r--browser/components/urlbar/tests/browser/browser_switchToTab_closes_newtab.js63
-rw-r--r--browser/components/urlbar/tests/browser/browser_switchToTab_fullUrl_repeatedKeydown.js60
-rw-r--r--browser/components/urlbar/tests/browser/browser_tabKeyBehavior.js322
-rw-r--r--browser/components/urlbar/tests/browser/browser_tabMatchesInAwesomebar.js224
-rw-r--r--browser/components/urlbar/tests/browser/browser_tabMatchesInAwesomebar_perwindowpb.js119
-rw-r--r--browser/components/urlbar/tests/browser/browser_tabToSearch.js679
-rw-r--r--browser/components/urlbar/tests/browser/browser_textruns.js58
-rw-r--r--browser/components/urlbar/tests/browser/browser_tokenAlias.js861
-rw-r--r--browser/components/urlbar/tests/browser/browser_top_sites.js477
-rw-r--r--browser/components/urlbar/tests/browser/browser_top_sites_private.js170
-rw-r--r--browser/components/urlbar/tests/browser/browser_typed_value.js67
-rw-r--r--browser/components/urlbar/tests/browser/browser_updateForDomainCompletion.js24
-rw-r--r--browser/components/urlbar/tests/browser/browser_updateRows.js240
-rw-r--r--browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry.js1498
-rw-r--r--browser/components/urlbar/tests/browser/browser_urlbar_selection.js299
-rw-r--r--browser/components/urlbar/tests/browser/browser_urlbar_telemetry.js1346
-rw-r--r--browser/components/urlbar/tests/browser/browser_urlbar_telemetry_dynamic.js160
-rw-r--r--browser/components/urlbar/tests/browser/browser_urlbar_telemetry_extension.js181
-rw-r--r--browser/components/urlbar/tests/browser/browser_urlbar_telemetry_places.js330
-rw-r--r--browser/components/urlbar/tests/browser/browser_urlbar_telemetry_remotetab.js211
-rw-r--r--browser/components/urlbar/tests/browser/browser_urlbar_telemetry_searchmode.js579
-rw-r--r--browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tabtosearch.js356
-rw-r--r--browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tip.js149
-rw-r--r--browser/components/urlbar/tests/browser/browser_urlbar_telemetry_topsite.js151
-rw-r--r--browser/components/urlbar/tests/browser/browser_userTypedValue.js46
-rw-r--r--browser/components/urlbar/tests/browser/browser_valueOnTabSwitch.js166
-rw-r--r--browser/components/urlbar/tests/browser/browser_view_emptyResultSet.js40
-rw-r--r--browser/components/urlbar/tests/browser/browser_view_resultDisplay.js354
-rw-r--r--browser/components/urlbar/tests/browser/browser_view_resultTypes_display.js319
-rw-r--r--browser/components/urlbar/tests/browser/browser_waitForLoadOrTimeout.js37
-rw-r--r--browser/components/urlbar/tests/browser/browser_whereToOpen.js194
-rw-r--r--browser/components/urlbar/tests/browser/dummy_page.html9
-rw-r--r--browser/components/urlbar/tests/browser/dynamicResult0.css50
-rw-r--r--browser/components/urlbar/tests/browser/dynamicResult1.css50
-rw-r--r--browser/components/urlbar/tests/browser/file_blank_but_not_blank.html2
-rw-r--r--browser/components/urlbar/tests/browser/file_urlbar_edit_dos.html18
-rw-r--r--browser/components/urlbar/tests/browser/file_userTypedValue.html1
-rw-r--r--browser/components/urlbar/tests/browser/head-common.js85
-rw-r--r--browser/components/urlbar/tests/browser/head.js114
-rw-r--r--browser/components/urlbar/tests/browser/moz.pngbin0 -> 580 bytes
-rw-r--r--browser/components/urlbar/tests/browser/print_postdata.sjs22
-rw-r--r--browser/components/urlbar/tests/browser/redirect_error.sjs16
-rw-r--r--browser/components/urlbar/tests/browser/searchSuggestionEngine.sjs53
-rw-r--r--browser/components/urlbar/tests/browser/searchSuggestionEngine.xml11
-rw-r--r--browser/components/urlbar/tests/browser/searchSuggestionEngine2.xml13
-rw-r--r--browser/components/urlbar/tests/browser/searchSuggestionEngineMany.xml11
-rw-r--r--browser/components/urlbar/tests/browser/searchSuggestionEngineSlow.xml11
-rw-r--r--browser/components/urlbar/tests/browser/slow-page.sjs23
-rw-r--r--browser/components/urlbar/tests/browser/urlbarTelemetrySearchSuggestions.sjs9
-rw-r--r--browser/components/urlbar/tests/browser/urlbarTelemetrySearchSuggestions.xml6
-rw-r--r--browser/components/urlbar/tests/browser/urlbarTelemetryUrlbarDynamic.css45
-rw-r--r--browser/components/urlbar/tests/ext/api.js256
-rw-r--r--browser/components/urlbar/tests/ext/browser/.eslintrc.js7
-rw-r--r--browser/components/urlbar/tests/ext/browser/browser.ini18
-rw-r--r--browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_attributionURL.js16
-rw-r--r--browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_clearInput.js31
-rw-r--r--browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_dynamicResult.js137
-rw-r--r--browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_engagementTelemetry.js18
-rw-r--r--browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_extensionTimeout.js16
-rw-r--r--browser/components/urlbar/tests/ext/browser/dynamicResult.css36
-rw-r--r--browser/components/urlbar/tests/ext/browser/head.js251
-rw-r--r--browser/components/urlbar/tests/ext/schema.json113
-rw-r--r--browser/components/urlbar/tests/unit/data/engine-suggestions.xml16
-rw-r--r--browser/components/urlbar/tests/unit/data/engine-tail-suggestions.xml14
-rw-r--r--browser/components/urlbar/tests/unit/head.js946
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarController_integration.js104
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarController_telemetry.js256
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarController_unit.js389
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarPrefs.js40
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarQueryContext.js73
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarQueryContext_restrictSource.js145
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarSearchUtils.jsm292
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarUtils_addToUrlbarHistory.js63
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarUtils_getShortcutOrURIAndPostData.js257
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarUtils_getTokenMatches.js294
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_about_urls.js100
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_bookmarked.js148
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_functional.js112
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_origins.js638
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_originsAndQueries.js2408
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_prefix_fallback.js74
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_search_engine_aliases.js90
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_search_engines.js234
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_urls.js218
-rw-r--r--browser/components/urlbar/tests/unit/test_avoid_middle_complete.js284
-rw-r--r--browser/components/urlbar/tests/unit/test_avoid_stripping_to_empty_tokens.js121
-rw-r--r--browser/components/urlbar/tests/unit/test_casing.js356
-rw-r--r--browser/components/urlbar/tests/unit/test_dedupe_prefix.js116
-rw-r--r--browser/components/urlbar/tests/unit/test_dupe_urls.js63
-rw-r--r--browser/components/urlbar/tests/unit/test_encoded_urls.js97
-rw-r--r--browser/components/urlbar/tests/unit/test_heuristic_cancel.js136
-rw-r--r--browser/components/urlbar/tests/unit/test_keywords.js207
-rw-r--r--browser/components/urlbar/tests/unit/test_muxer.js246
-rw-r--r--browser/components/urlbar/tests/unit/test_providerHeuristicFallback.js613
-rw-r--r--browser/components/urlbar/tests/unit/test_providerOmnibox.js818
-rw-r--r--browser/components/urlbar/tests/unit/test_providerOpenTabs.js45
-rw-r--r--browser/components/urlbar/tests/unit/test_providerTabToSearch.js477
-rw-r--r--browser/components/urlbar/tests/unit/test_providerTabToSearch_partialHost.js150
-rw-r--r--browser/components/urlbar/tests/unit/test_providerUnifiedComplete.js242
-rw-r--r--browser/components/urlbar/tests/unit/test_providerUnifiedComplete_duplicate_entries.js42
-rw-r--r--browser/components/urlbar/tests/unit/test_providersManager.js74
-rw-r--r--browser/components/urlbar/tests/unit/test_providersManager_filtering.js405
-rw-r--r--browser/components/urlbar/tests/unit/test_providersManager_maxResults.js37
-rw-r--r--browser/components/urlbar/tests/unit/test_queryScorer.js405
-rw-r--r--browser/components/urlbar/tests/unit/test_query_url.js123
-rw-r--r--browser/components/urlbar/tests/unit/test_search_engine_host.js97
-rw-r--r--browser/components/urlbar/tests/unit/test_search_suggestions.js1695
-rw-r--r--browser/components/urlbar/tests/unit/test_search_suggestions_aliases.js359
-rw-r--r--browser/components/urlbar/tests/unit/test_search_suggestions_tail.js358
-rw-r--r--browser/components/urlbar/tests/unit/test_tokenizer.js450
-rw-r--r--browser/components/urlbar/tests/unit/test_trimming.js222
-rw-r--r--browser/components/urlbar/tests/unit/xpcshell.ini54
292 files changed, 71193 insertions, 0 deletions
diff --git a/browser/components/urlbar/.eslintrc.js b/browser/components/urlbar/.eslintrc.js
new file mode 100644
index 0000000000..8218b7f089
--- /dev/null
+++ b/browser/components/urlbar/.eslintrc.js
@@ -0,0 +1,42 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+module.exports = {
+ rules: {
+ "mozilla/var-only-at-top-level": "error",
+ "require-jsdoc": [
+ "error",
+ {
+ require: {
+ FunctionDeclaration: false,
+ MethodDefinition: false,
+ ClassDeclaration: true,
+ ArrowFunctionExpression: false,
+ FunctionExpression: false,
+ },
+ },
+ ],
+ "valid-jsdoc": [
+ "error",
+ {
+ prefer: {
+ return: "returns",
+ },
+ preferType: {
+ Boolean: "boolean",
+ Number: "number",
+ String: "string",
+ Object: "object",
+ bool: "boolean",
+ },
+ requireParamDescription: false,
+ requireReturn: false,
+ requireReturnDescription: false,
+ },
+ ],
+ "no-unused-expressions": "error",
+ },
+};
diff --git a/browser/components/urlbar/UrlbarController.jsm b/browser/components/urlbar/UrlbarController.jsm
new file mode 100644
index 0000000000..15aad7c48b
--- /dev/null
+++ b/browser/components/urlbar/UrlbarController.jsm
@@ -0,0 +1,899 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["UrlbarController"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AppConstants: "resource://gre/modules/AppConstants.jsm",
+ BrowserSearchTelemetry: "resource:///modules/BrowserSearchTelemetry.jsm",
+ FormHistory: "resource://gre/modules/FormHistory.jsm",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
+ UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm",
+ UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.jsm",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+});
+
+const TELEMETRY_1ST_RESULT = "PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS";
+const TELEMETRY_6_FIRST_RESULTS = "PLACES_AUTOCOMPLETE_6_FIRST_RESULTS_TIME_MS";
+const NOTIFICATIONS = {
+ QUERY_STARTED: "onQueryStarted",
+ QUERY_RESULTS: "onQueryResults",
+ QUERY_RESULT_REMOVED: "onQueryResultRemoved",
+ QUERY_CANCELLED: "onQueryCancelled",
+ QUERY_FINISHED: "onQueryFinished",
+ VIEW_OPEN: "onViewOpen",
+ VIEW_CLOSE: "onViewClose",
+};
+
+/**
+ * The address bar controller handles queries from the address bar, obtains
+ * results and returns them to the UI for display.
+ *
+ * Listeners may be added to listen for the results. They may support the
+ * following methods which may be called when a query is run:
+ *
+ * - onQueryStarted(queryContext)
+ * - onQueryResults(queryContext)
+ * - onQueryCancelled(queryContext)
+ * - onQueryFinished(queryContext)
+ * - onQueryResultRemoved(index)
+ * - onViewOpen()
+ * - onViewClose()
+ */
+class UrlbarController {
+ /**
+ * Initialises the class. The manager may be overridden here, this is for
+ * test purposes.
+ *
+ * @param {object} options
+ * The initial options for UrlbarController.
+ * @param {UrlbarInput} options.input
+ * The input this controller is operating with.
+ * @param {object} [options.manager]
+ * Optional fake providers manager to override the built-in providers manager.
+ * Intended for use in unit tests only.
+ */
+ constructor(options = {}) {
+ if (!options.input) {
+ throw new Error("Missing options: input");
+ }
+ if (!options.input.window) {
+ throw new Error("input is missing 'window' property.");
+ }
+ if (
+ !options.input.window.location ||
+ options.input.window.location.href != AppConstants.BROWSER_CHROME_URL
+ ) {
+ throw new Error("input.window should be an actual browser window.");
+ }
+ if (!("isPrivate" in options.input)) {
+ throw new Error("input.isPrivate must be set.");
+ }
+
+ this.input = options.input;
+ this.browserWindow = options.input.window;
+
+ this.manager = options.manager || UrlbarProvidersManager;
+
+ this._listeners = new Set();
+ this._userSelectionBehavior = "none";
+
+ this.engagementEvent = new TelemetryEvent(
+ this,
+ options.eventTelemetryCategory
+ );
+ }
+
+ get NOTIFICATIONS() {
+ return NOTIFICATIONS;
+ }
+
+ /**
+ * Hooks up the controller with a view.
+ *
+ * @param {UrlbarView} view
+ * The UrlbarView instance associated with this controller.
+ */
+ setView(view) {
+ this.view = view;
+ }
+
+ /**
+ * Takes a query context and starts the query based on the user input.
+ *
+ * @param {UrlbarQueryContext} queryContext The query details.
+ */
+ async startQuery(queryContext) {
+ // Cancel any running query.
+ this.cancelQuery();
+
+ // Wrap the external queryContext, to track a unique object, in case
+ // the external consumer reuses the same context multiple times.
+ // This also allows to add properties without polluting the context.
+ // Note this can't be null-ed or deleted once a query is done, because it's
+ // used by handleDeleteEntry and handleKeyNavigation, that can run after
+ // a query is cancelled or finished.
+ let contextWrapper = (this._lastQueryContextWrapper = { queryContext });
+
+ queryContext.lastResultCount = 0;
+ TelemetryStopwatch.start(TELEMETRY_1ST_RESULT, queryContext);
+ TelemetryStopwatch.start(TELEMETRY_6_FIRST_RESULTS, queryContext);
+
+ // For proper functionality we must ensure this notification is fired
+ // synchronously, as soon as startQuery is invoked, but after any
+ // notifications related to the previous query.
+ this.notify(NOTIFICATIONS.QUERY_STARTED, queryContext);
+ await this.manager.startQuery(queryContext, this);
+ // If the query has been cancelled, onQueryFinished was notified already.
+ // Note this._lastQueryContextWrapper may have changed in the meanwhile.
+ if (
+ contextWrapper === this._lastQueryContextWrapper &&
+ !contextWrapper.done
+ ) {
+ contextWrapper.done = true;
+ // TODO (Bug 1549936) this is necessary to avoid leaks in PB tests.
+ this.manager.cancelQuery(queryContext);
+ this.notify(NOTIFICATIONS.QUERY_FINISHED, queryContext);
+ }
+ return queryContext;
+ }
+
+ /**
+ * Cancels an in-progress query. Note, queries may continue running if they
+ * can't be cancelled.
+ */
+ cancelQuery() {
+ // If the query finished already, don't handle cancel.
+ if (!this._lastQueryContextWrapper || this._lastQueryContextWrapper.done) {
+ return;
+ }
+
+ this._lastQueryContextWrapper.done = true;
+
+ let { queryContext } = this._lastQueryContextWrapper;
+ TelemetryStopwatch.cancel(TELEMETRY_1ST_RESULT, queryContext);
+ TelemetryStopwatch.cancel(TELEMETRY_6_FIRST_RESULTS, queryContext);
+ this.manager.cancelQuery(queryContext);
+ this.notify(NOTIFICATIONS.QUERY_CANCELLED, queryContext);
+ this.notify(NOTIFICATIONS.QUERY_FINISHED, queryContext);
+ }
+
+ /**
+ * Receives results from a query.
+ *
+ * @param {UrlbarQueryContext} queryContext The query details.
+ */
+ receiveResults(queryContext) {
+ if (queryContext.lastResultCount < 1 && queryContext.results.length >= 1) {
+ TelemetryStopwatch.finish(TELEMETRY_1ST_RESULT, queryContext);
+ }
+ if (queryContext.lastResultCount < 6 && queryContext.results.length >= 6) {
+ TelemetryStopwatch.finish(TELEMETRY_6_FIRST_RESULTS, queryContext);
+ }
+
+ if (queryContext.firstResultChanged) {
+ // Notify the input so it can make adjustments based on the first result.
+ if (this.input.onFirstResult(queryContext.results[0])) {
+ // The input canceled the query and started a new one.
+ return;
+ }
+
+ // The first time we receive results try to connect to the heuristic
+ // result.
+ this.speculativeConnect(
+ queryContext.results[0],
+ queryContext,
+ "resultsadded"
+ );
+ }
+
+ this.notify(NOTIFICATIONS.QUERY_RESULTS, queryContext);
+ // Update lastResultCount after notifying, so the view can use it.
+ queryContext.lastResultCount = queryContext.results.length;
+ }
+
+ /**
+ * Adds a listener for query actions and results.
+ *
+ * @param {object} listener The listener to add.
+ * @throws {TypeError} Throws if the listener is not an object.
+ */
+ addQueryListener(listener) {
+ if (!listener || typeof listener != "object") {
+ throw new TypeError("Expected listener to be an object");
+ }
+ this._listeners.add(listener);
+ }
+
+ /**
+ * Removes a query listener.
+ *
+ * @param {object} listener The listener to add.
+ */
+ removeQueryListener(listener) {
+ this._listeners.delete(listener);
+ }
+
+ /**
+ * Checks whether a keyboard event that would normally open the view should
+ * instead be handled natively by the input field.
+ * On certain platforms, the up and down keys can be used to move the caret,
+ * in which case we only want to open the view if the caret is at the
+ * start or end of the input.
+ *
+ * @param {KeyboardEvent} event
+ * The DOM KeyboardEvent.
+ * @returns {boolean}
+ * Returns true if the event should move the caret instead of opening the
+ * view.
+ */
+ keyEventMovesCaret(event) {
+ if (this.view.isOpen) {
+ return false;
+ }
+ if (AppConstants.platform != "macosx" && AppConstants.platform != "linux") {
+ return false;
+ }
+ let isArrowUp = event.keyCode == KeyEvent.DOM_VK_UP;
+ let isArrowDown = event.keyCode == KeyEvent.DOM_VK_DOWN;
+ if (!isArrowUp && !isArrowDown) {
+ return false;
+ }
+ let start = this.input.selectionStart;
+ let end = this.input.selectionEnd;
+ if (
+ end != start ||
+ (isArrowUp && start > 0) ||
+ (isArrowDown && end < this.input.value.length)
+ ) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Receives keyboard events from the input and handles those that should
+ * navigate within the view or pick the currently selected item.
+ *
+ * @param {KeyboardEvent} event
+ * The DOM KeyboardEvent.
+ * @param {boolean} executeAction
+ * Whether the event should actually execute the associated action, or just
+ * be managed (at a preventDefault() level). This is used when the event
+ * will be deferred by the event bufferer, but preventDefault() and friends
+ * should still happen synchronously.
+ */
+ handleKeyNavigation(event, executeAction = true) {
+ const isMac = AppConstants.platform == "macosx";
+ // Handle readline/emacs-style navigation bindings on Mac.
+ if (
+ isMac &&
+ this.view.isOpen &&
+ event.ctrlKey &&
+ (event.key == "n" || event.key == "p")
+ ) {
+ if (executeAction) {
+ this.view.selectBy(1, { reverse: event.key == "p" });
+ }
+ event.preventDefault();
+ return;
+ }
+
+ if (this.view.isOpen && executeAction && this._lastQueryContextWrapper) {
+ let { queryContext } = this._lastQueryContextWrapper;
+ let handled = this.view.oneOffSearchButtons.handleKeyDown(
+ event,
+ this.view.visibleElementCount,
+ this.view.allowEmptySelection,
+ queryContext.searchString
+ );
+ if (handled) {
+ return;
+ }
+ }
+
+ switch (event.keyCode) {
+ case KeyEvent.DOM_VK_ESCAPE:
+ if (executeAction) {
+ if (this.view.isOpen) {
+ this.view.close();
+ } else {
+ this.input.handleRevert();
+ }
+ }
+ event.preventDefault();
+ break;
+ case KeyEvent.DOM_VK_RETURN:
+ if (executeAction) {
+ this.input.handleCommand(event);
+ }
+ event.preventDefault();
+ break;
+ case KeyEvent.DOM_VK_TAB:
+ // It's always possible to tab through results when the urlbar was
+ // focused with the mouse, or has a search string.
+ // We allow tabbing without a search string when in search mode preview,
+ // since that means the user has interacted with the Urlbar since
+ // opening it.
+ // When there's no search string, we want to focus the next toolbar item
+ // instead, for accessibility reasons.
+ let allowTabbingThroughResults =
+ this.input.focusedViaMousedown ||
+ this.input.searchMode?.isPreview ||
+ (this.input.value &&
+ this.input.getAttribute("pageproxystate") != "valid");
+ if (
+ // Even if the view is closed, we may be waiting results, and in
+ // such a case we don't want to tab out of the urlbar.
+ (this.view.isOpen || !executeAction) &&
+ !event.ctrlKey &&
+ !event.altKey &&
+ allowTabbingThroughResults
+ ) {
+ if (executeAction) {
+ this.userSelectionBehavior = "tab";
+ this.view.selectBy(1, {
+ reverse: event.shiftKey,
+ userPressedTab: true,
+ });
+ }
+ event.preventDefault();
+ }
+ break;
+ case KeyEvent.DOM_VK_DOWN:
+ case KeyEvent.DOM_VK_UP:
+ case KeyEvent.DOM_VK_PAGE_DOWN:
+ case KeyEvent.DOM_VK_PAGE_UP:
+ if (event.ctrlKey || event.altKey) {
+ break;
+ }
+ if (this.view.isOpen) {
+ if (executeAction) {
+ this.userSelectionBehavior = "arrow";
+ this.view.selectBy(
+ event.keyCode == KeyEvent.DOM_VK_PAGE_DOWN ||
+ event.keyCode == KeyEvent.DOM_VK_PAGE_UP
+ ? UrlbarUtils.PAGE_UP_DOWN_DELTA
+ : 1,
+ {
+ reverse:
+ event.keyCode == KeyEvent.DOM_VK_UP ||
+ event.keyCode == KeyEvent.DOM_VK_PAGE_UP,
+ }
+ );
+ }
+ } else {
+ if (this.keyEventMovesCaret(event)) {
+ break;
+ }
+ if (executeAction) {
+ this.userSelectionBehavior = "arrow";
+ this.input.startQuery({
+ searchString: this.input.value,
+ event,
+ });
+ }
+ }
+ event.preventDefault();
+ break;
+ case KeyEvent.DOM_VK_RIGHT:
+ case KeyEvent.DOM_VK_END:
+ this.input.maybeConfirmSearchModeFromResult({
+ entry: "typed",
+ });
+ // Fall through.
+ case KeyEvent.DOM_VK_LEFT:
+ case KeyEvent.DOM_VK_HOME:
+ this.view.removeAccessibleFocus();
+ break;
+ case KeyEvent.DOM_VK_BACK_SPACE:
+ if (
+ this.input.searchMode &&
+ this.input.selectionStart == 0 &&
+ this.input.selectionEnd == 0 &&
+ !event.shiftKey
+ ) {
+ this.input.searchMode = null;
+ this.input.view.oneOffSearchButtons.selectedButton = null;
+ this.input.startQuery({
+ allowAutofill: false,
+ event,
+ });
+ }
+ // Fall through.
+ case KeyEvent.DOM_VK_DELETE:
+ if (!this.view.isOpen) {
+ break;
+ }
+ if (event.shiftKey) {
+ if (!executeAction || this._handleDeleteEntry()) {
+ event.preventDefault();
+ }
+ } else if (executeAction) {
+ this.userSelectionBehavior = "none";
+ }
+ break;
+ }
+ }
+
+ /**
+ * Tries to initialize a speculative connection on a result.
+ * Speculative connections are only supported for a subset of all the results.
+ * @param {UrlbarResult} result Tthe result to speculative connect to.
+ * @param {UrlbarQueryContext} context The queryContext
+ * @param {string} reason Reason for the speculative connect request.
+ * @note speculative connect to:
+ * - Search engine heuristic results
+ * - autofill results
+ * - http/https results
+ */
+ speculativeConnect(result, context, reason) {
+ // Never speculative connect in private contexts.
+ if (!this.input || context.isPrivate || !context.results.length) {
+ return;
+ }
+ let { url } = UrlbarUtils.getUrlFromResult(result);
+ if (!url) {
+ return;
+ }
+
+ switch (reason) {
+ case "resultsadded": {
+ // We should connect to an heuristic result, if it exists.
+ if (
+ (result == context.results[0] && result.heuristic) ||
+ result.autofill
+ ) {
+ if (result.type == UrlbarUtils.RESULT_TYPE.SEARCH) {
+ // Speculative connect only if search suggestions are enabled.
+ if (
+ UrlbarPrefs.get("suggest.searches") &&
+ UrlbarPrefs.get("browser.search.suggest.enabled")
+ ) {
+ let engine = Services.search.getEngineByName(
+ result.payload.engine
+ );
+ UrlbarUtils.setupSpeculativeConnection(
+ engine,
+ this.browserWindow
+ );
+ }
+ } else if (result.autofill) {
+ UrlbarUtils.setupSpeculativeConnection(url, this.browserWindow);
+ }
+ }
+ return;
+ }
+ case "mousedown": {
+ // On mousedown, connect only to http/https urls.
+ if (url.startsWith("http")) {
+ UrlbarUtils.setupSpeculativeConnection(url, this.browserWindow);
+ }
+ return;
+ }
+ default: {
+ throw new Error("Invalid speculative connection reason");
+ }
+ }
+ }
+
+ /**
+ * Stores the selection behavior that the user has used to select a result.
+ *
+ * @param {"arrow"|"tab"|"none"} behavior
+ * The behavior the user used.
+ */
+ set userSelectionBehavior(behavior) {
+ // Don't change the behavior to arrow if tab has already been recorded,
+ // as we want to know that the tab was used first.
+ if (behavior == "arrow" && this._userSelectionBehavior == "tab") {
+ return;
+ }
+ this._userSelectionBehavior = behavior;
+ }
+
+ /**
+ * Records details of the selected result in telemetry. We only record the
+ * selection behavior, type and index.
+ *
+ * @param {Event} event
+ * The event which triggered the result to be selected.
+ * @param {UrlbarResult} result
+ * The selected result.
+ */
+ recordSelectedResult(event, result) {
+ let resultIndex = result ? result.rowIndex : -1;
+ let selectedResult = -1;
+ if (resultIndex >= 0) {
+ // Except for the history popup, the urlbar always has a selection. The
+ // first result at index 0 is the "heuristic" result that indicates what
+ // will happen when you press the Enter key. Treat it as no selection.
+ selectedResult = resultIndex > 0 || !result.heuristic ? resultIndex : -1;
+ }
+ BrowserSearchTelemetry.recordSearchSuggestionSelectionMethod(
+ event,
+ "urlbar",
+ selectedResult,
+ this._userSelectionBehavior
+ );
+
+ if (!result) {
+ return;
+ }
+
+ // Do not modify existing telemetry types. To add a new type:
+ //
+ // * Set telemetryType appropriately. Since telemetryType is used as the
+ // probe name, it must be alphanumeric with optional underscores.
+ // * Add a new keyed scalar probe into the urlbar.picked category for the
+ // newly added telemetryType.
+ // * Add a test named browser_UsageTelemetry_urlbar_newType.js to
+ // browser/modules/test/browser.
+ //
+ // The "topsite" type overrides the other ones, because it starts from a
+ // unique user interaction, that we want to count apart. We do this here
+ // rather than in telemetryTypeFromResult because other consumers, like
+ // events telemetry, are reporting this information separately.
+ let telemetryType =
+ result.providerName == "UrlbarProviderTopSites"
+ ? "topsite"
+ : UrlbarUtils.telemetryTypeFromResult(result);
+ Services.telemetry.keyedScalarAdd(
+ `urlbar.picked.${telemetryType}`,
+ resultIndex,
+ 1
+ );
+ if (this.input.searchMode && !this.input.searchMode.isPreview) {
+ Services.telemetry.keyedScalarAdd(
+ `urlbar.picked.searchmode.${this.input.searchMode.entry}`,
+ resultIndex,
+ 1
+ );
+ }
+
+ // These histograms should be removed after a deprecation time where we'll
+ // confirm goodness of the new scalar above.
+ if (!(telemetryType in UrlbarUtils.SELECTED_RESULT_TYPES)) {
+ Cu.reportError(`Unsupported telemetry type ${telemetryType}`);
+ return;
+ }
+ Services.telemetry
+ .getHistogramById("FX_URLBAR_SELECTED_RESULT_INDEX")
+ .add(resultIndex);
+ Services.telemetry
+ .getHistogramById("FX_URLBAR_SELECTED_RESULT_TYPE_2")
+ .add(UrlbarUtils.SELECTED_RESULT_TYPES[telemetryType]);
+ Services.telemetry
+ .getKeyedHistogramById("FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2")
+ .add(telemetryType, resultIndex);
+ }
+
+ /**
+ * Internal function handling deletion of entries. We only support removing
+ * of history entries - other result sources will be ignored.
+ *
+ * @returns {boolean} Returns true if the deletion was acted upon.
+ */
+ _handleDeleteEntry() {
+ if (!this._lastQueryContextWrapper) {
+ Cu.reportError("Cannot delete - the latest query is not present");
+ return false;
+ }
+
+ const selectedResult = this.input.view.selectedResult;
+ if (
+ !selectedResult ||
+ selectedResult.source != UrlbarUtils.RESULT_SOURCE.HISTORY ||
+ selectedResult.heuristic
+ ) {
+ return false;
+ }
+
+ let { queryContext } = this._lastQueryContextWrapper;
+ let index = queryContext.results.indexOf(selectedResult);
+ if (index < 0) {
+ Cu.reportError("Failed to find the selected result in the results");
+ return false;
+ }
+
+ queryContext.results.splice(index, 1);
+ this.notify(NOTIFICATIONS.QUERY_RESULT_REMOVED, index);
+
+ // Form history or url restyled as search.
+ if (selectedResult.type == UrlbarUtils.RESULT_TYPE.SEARCH) {
+ if (!queryContext.formHistoryName) {
+ return false;
+ }
+ // Generate the search url to remove it from browsing history.
+ let { url } = UrlbarUtils.getUrlFromResult(selectedResult);
+ PlacesUtils.history.remove(url).catch(Cu.reportError);
+ // Now remove form history.
+ FormHistory.update(
+ {
+ op: "remove",
+ fieldname: queryContext.formHistoryName,
+ value: selectedResult.payload.suggestion,
+ },
+ {
+ handleError(error) {
+ Cu.reportError(`Removing form history failed: ${error}`);
+ },
+ }
+ );
+ return true;
+ }
+
+ // Remove browsing history entries from Places.
+ PlacesUtils.history
+ .remove(selectedResult.payload.url)
+ .catch(Cu.reportError);
+ return true;
+ }
+
+ /**
+ * Notifies listeners of results.
+ *
+ * @param {string} name Name of the notification.
+ * @param {object} params Parameters to pass with the notification.
+ */
+ notify(name, ...params) {
+ for (let listener of this._listeners) {
+ // Can't use "in" because some tests proxify these.
+ if (typeof listener[name] != "undefined") {
+ try {
+ listener[name](...params);
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Tracks and records telemetry events for the given category, if provided,
+ * otherwise it's a no-op.
+ * It is currently designed around the "urlbar" category, even if it can
+ * potentially be extended to other categories.
+ * To record an event, invoke start() with a starting event, then either
+ * invoke record() with a final event, or discard() to drop the recording.
+ * @see Events.yaml
+ */
+class TelemetryEvent {
+ constructor(controller, category) {
+ this._controller = controller;
+ this._category = category;
+ this._isPrivate = controller.input.isPrivate;
+ }
+
+ /**
+ * Start measuring the elapsed time from a user-generated event.
+ * After this has been invoked, any subsequent calls to start() are ignored,
+ * until either record() or discard() are invoked. Thus, it is safe to keep
+ * invoking this on every input event as the user is typing, for example.
+ * @param {event} event A DOM event.
+ * @param {string} [searchString] Pass a search string related to the event if
+ * you have one. The event by itself sometimes isn't enough to
+ * determine the telemetry details we should record.
+ * @note This should never throw, or it may break the urlbar.
+ * @see the in-tree urlbar telemetry documentation.
+ */
+ start(event, searchString = null) {
+ // In case of a "returned" interaction ongoing, the user may either
+ // continue the search, or restart with a new search string. In that case
+ // we want to change the interaction type to "restarted".
+ // Detecting all the possible ways of clearing the input would be tricky,
+ // thus this makes a guess by just checking the first char matches; even if
+ // the user backspaces a part of the string, we still count that as a
+ // "returned" interaction.
+ if (
+ this._startEventInfo &&
+ this._startEventInfo.interactionType == "returned" &&
+ (!searchString || this._startEventInfo.searchString[0] != searchString[0])
+ ) {
+ this._startEventInfo.interactionType = "restarted";
+ }
+
+ // start is invoked on a user-generated event, but we only count the first
+ // one. Once an engagement or abandoment happens, we clear _startEventInfo.
+ if (!this._category || this._startEventInfo) {
+ return;
+ }
+ if (!event) {
+ Cu.reportError("Must always provide an event");
+ return;
+ }
+ const validEvents = [
+ "click",
+ "command",
+ "drop",
+ "input",
+ "keydown",
+ "mousedown",
+ "tabswitch",
+ "focus",
+ ];
+ if (!validEvents.includes(event.type)) {
+ Cu.reportError("Can't start recording from event type: " + event.type);
+ return;
+ }
+
+ let interactionType = "topsites";
+ if (event.interactionType) {
+ interactionType = event.interactionType;
+ } else if (event.type == "input") {
+ interactionType = UrlbarUtils.isPasteEvent(event) ? "pasted" : "typed";
+ } else if (event.type == "drop") {
+ interactionType = "dropped";
+ } else if (searchString) {
+ interactionType = "typed";
+ }
+
+ this._startEventInfo = {
+ timeStamp: event.timeStamp || Cu.now(),
+ interactionType,
+ searchString,
+ };
+
+ this._controller.manager.notifyEngagementChange(this._isPrivate, "start");
+ }
+
+ /**
+ * Record an engagement telemetry event.
+ * When the user picks a result from a search through the mouse or keyboard,
+ * an engagement event is recorded. If instead the user abandons a search, by
+ * blurring the input field, an abandonment event is recorded.
+ * @param {event} [event] A DOM event.
+ * @param {object} details An object describing action details.
+ * @param {string} details.searchString The user's search string. Note that
+ * this string is not sent with telemetry data. It is only used
+ * locally to discern other data, such as the number of characters and
+ * words in the string.
+ * @param {string} details.selIndex Index of the selected result, undefined
+ * for "blur".
+ * @param {string} details.selType type of the selected element, undefined
+ * for "blur". One of "unknown", "autofill", "visiturl", "bookmark",
+ * "history", "keyword", "searchengine", "searchsuggestion",
+ * "switchtab", "remotetab", "extension", "oneoff".
+ * @param {string} details.provider The name of the provider for the selected
+ * result.
+ * @note event can be null, that usually happens for paste&go or drop&go.
+ * If there's no _startEventInfo this is a no-op.
+ */
+ record(event, details) {
+ // This should never throw, or it may break the urlbar.
+ try {
+ this._internalRecord(event, details);
+ } catch (ex) {
+ Cu.reportError("Could not record event: " + ex);
+ } finally {
+ this._startEventInfo = null;
+ this._discarded = false;
+ }
+ }
+
+ _internalRecord(event, details) {
+ if (!this._category || !this._startEventInfo) {
+ if (this._discarded && this._category) {
+ this._controller.manager.notifyEngagementChange(
+ this._isPrivate,
+ "discard"
+ );
+ }
+ return;
+ }
+ if (
+ !event &&
+ this._startEventInfo.interactionType != "pasted" &&
+ this._startEventInfo.interactionType != "dropped"
+ ) {
+ // If no event is passed, we must be executing either paste&go or drop&go.
+ throw new Error("Event must be defined, unless input was pasted/dropped");
+ }
+ if (!details) {
+ throw new Error("Invalid event details: " + details);
+ }
+
+ let endTime = (event && event.timeStamp) || Cu.now();
+ let startTime = this._startEventInfo.timeStamp || endTime;
+ // Synthesized events in tests may have a bogus timeStamp, causing a
+ // subtraction between monotonic and non-monotonic timestamps; that's why
+ // abs is necessary here. It should only happen in tests, anyway.
+ let elapsed = Math.abs(Math.round(endTime - startTime));
+
+ let action;
+ if (!event) {
+ action =
+ this._startEventInfo.interactionType == "dropped"
+ ? "drop_go"
+ : "paste_go";
+ } else if (event.type == "blur") {
+ action = "blur";
+ } else {
+ action = event instanceof MouseEvent ? "click" : "enter";
+ }
+ let method = action == "blur" ? "abandonment" : "engagement";
+ let value = this._startEventInfo.interactionType;
+
+ // Rather than listening to the pref, just update status when we record an
+ // event, if the pref changed from the last time.
+ let recordingEnabled = UrlbarPrefs.get("eventTelemetry.enabled");
+ if (this._eventRecordingEnabled != recordingEnabled) {
+ this._eventRecordingEnabled = recordingEnabled;
+ Services.telemetry.setEventRecordingEnabled("urlbar", recordingEnabled);
+ }
+
+ // numWords is not a perfect measurement, since it will return an incorrect
+ // value for languages that do not use spaces or URLs containing spaces in
+ // its query parameters, for example.
+ let extra = {
+ elapsed: elapsed.toString(),
+ numChars: details.searchString.length.toString(),
+ numWords: details.searchString
+ .trim()
+ .split(UrlbarTokenizer.REGEXP_SPACES)
+ .filter(t => t)
+ .length.toString(),
+ };
+ if (method == "engagement") {
+ extra.selIndex = details.selIndex.toString();
+ extra.selType = details.selType;
+ extra.provider = details.provider || "";
+ }
+
+ // We invoke recordEvent regardless, if recording is disabled this won't
+ // report the events remotely, but will count it in the event_counts scalar.
+ Services.telemetry.recordEvent(
+ this._category,
+ method,
+ action,
+ value,
+ extra
+ );
+
+ this._controller.manager.notifyEngagementChange(this._isPrivate, method);
+ }
+
+ /**
+ * Resets the currently tracked user-generated event that was registered via
+ * start(), so it won't be recorded. If there's no tracked event, this is a
+ * no-op.
+ */
+ discard() {
+ if (this._startEventInfo) {
+ this._startEventInfo = null;
+ this._discarded = true;
+ }
+ }
+
+ /**
+ * Extracts a telemetry type from an element for event telemetry.
+ * @param {Element} element The element to analyze.
+ * @returns {string} a string type for the telemetry event.
+ */
+ typeFromElement(element) {
+ if (!element) {
+ return "none";
+ }
+ let row = element.closest(".urlbarView-row");
+ if (row.result && row.result.providerName != "UrlbarProviderTopSites") {
+ // Element handlers go here.
+ if (
+ row.result.type == UrlbarUtils.RESULT_TYPE.TIP &&
+ element.classList.contains("urlbarView-tip-help")
+ ) {
+ return "tiphelp";
+ }
+ }
+ // Now handle the result.
+ return UrlbarUtils.telemetryTypeFromResult(row.result);
+ }
+}
diff --git a/browser/components/urlbar/UrlbarEventBufferer.jsm b/browser/components/urlbar/UrlbarEventBufferer.jsm
new file mode 100644
index 0000000000..3c43403b19
--- /dev/null
+++ b/browser/components/urlbar/UrlbarEventBufferer.jsm
@@ -0,0 +1,358 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["UrlbarEventBufferer"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AppConstants: "resource://gre/modules/AppConstants.jsm",
+ clearTimeout: "resource://gre/modules/Timer.jsm",
+ Services: "resource://gre/modules/Services.jsm",
+ setTimeout: "resource://gre/modules/Timer.jsm",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(this, "logger", () =>
+ UrlbarUtils.getLogger({ prefix: "EventBufferer" })
+);
+
+// Maximum time events can be deferred for. In automation providers can be quite
+// slow, thus we need a longer timeout to avoid intermittent failures.
+const DEFERRING_TIMEOUT_MS = Cu.isInAutomation ? 1000 : 300;
+
+// Array of keyCodes to defer.
+const DEFERRED_KEY_CODES = new Set([
+ KeyboardEvent.DOM_VK_RETURN,
+ KeyboardEvent.DOM_VK_DOWN,
+ KeyboardEvent.DOM_VK_TAB,
+]);
+
+// Status of the current or last query.
+const QUERY_STATUS = {
+ UKNOWN: 0,
+ RUNNING: 1,
+ COMPLETE: 2,
+};
+
+/**
+ * The UrlbarEventBufferer can queue up events and replay them later, to make
+ * the urlbar results more predictable.
+ *
+ * Search results arrive asynchronously, which means that keydown events may
+ * arrive before results do, and therefore not have the effect the user intends.
+ * That's especially likely to happen with the down arrow and enter keys, due to
+ * the one-off search buttons: if the user very quickly pastes something in the
+ * input, presses the down arrow key, and then hits enter, they are probably
+ * expecting to visit the first result. But if there are no results, then
+ * pressing down and enter will trigger the first one-off button.
+ * To prevent that undesirable behavior, certain keys are buffered and deferred
+ * until more results arrive, at which time they're replayed.
+ */
+class UrlbarEventBufferer {
+ /**
+ * Initialises the class.
+ * @param {UrlbarInput} input The urlbar input object.
+ */
+ constructor(input) {
+ this.input = input;
+ this.input.inputField.addEventListener("blur", this);
+
+ // A queue of {event, callback} objects representing deferred events.
+ // The callback is invoked when it's the right time to handle the event,
+ // but it may also never be invoked, if the context changed and the event
+ // became obsolete.
+ this._eventsQueue = [];
+ // If this timer fires, we will unconditionally replay all the deferred
+ // events so that, after a certain point, we don't keep blocking the user's
+ // actions, when nothing else has caused the events to be replayed.
+ // At that point we won't check whether it's safe to replay the events,
+ // because otherwise it may look like we ignored the user's actions.
+ this._deferringTimeout = null;
+
+ // Tracks the current or last query status.
+ this._lastQuery = {
+ // The time at which the current or last search was started. This is used
+ // to check how much time passed while deferring the user's actions. Must
+ // be set using the monotonic Cu.now() helper.
+ startDate: Cu.now(),
+ // Status of the query; one of QUERY_STATUS.*
+ status: QUERY_STATUS.UKNOWN,
+ // The query context.
+ context: null,
+ };
+
+ // Start listening for queries.
+ this.input.controller.addQueryListener(this);
+ }
+
+ // UrlbarController listener methods.
+ onQueryStarted(queryContext) {
+ this._lastQuery = {
+ startDate: Cu.now(),
+ status: QUERY_STATUS.RUNNING,
+ context: queryContext,
+ };
+ if (this._deferringTimeout) {
+ clearTimeout(this._deferringTimeout);
+ this._deferringTimeout = null;
+ }
+ }
+
+ onQueryCancelled(queryContext) {
+ this._lastQuery.status = QUERY_STATUS.COMPLETE;
+ }
+
+ onQueryFinished(queryContext) {
+ this._lastQuery.status = QUERY_STATUS.COMPLETE;
+ }
+
+ onQueryResults(queryContext) {
+ // Ensure this runs after other results handling code.
+ Services.tm.dispatchToMainThread(() => {
+ this.replayDeferredEvents(true);
+ });
+ }
+
+ /**
+ * Handles DOM events.
+ * @param {Event} event DOM event from the input.
+ */
+ handleEvent(event) {
+ if (event.type == "blur") {
+ logger.debug("Clearing queue on blur");
+ // The input field was blurred, pending events don't matter anymore.
+ // Clear the timeout and the queue.
+ this._eventsQueue.length = 0;
+ if (this._deferringTimeout) {
+ clearTimeout(this._deferringTimeout);
+ this._deferringTimeout = null;
+ }
+ }
+ }
+
+ /**
+ * Receives DOM events, eventually queues them up, and calls back when it's
+ * the right time to handle the event.
+ * @param {Event} event DOM event from the input.
+ * @param {Function} callback to be invoked when it's the right time to handle
+ * the event.
+ */
+ maybeDeferEvent(event, callback) {
+ if (!callback) {
+ throw new Error("Must provide a callback");
+ }
+ if (this.shouldDeferEvent(event)) {
+ this.deferEvent(event, callback);
+ return;
+ }
+ // If it has not been deferred, handle the callback immediately.
+ callback();
+ }
+
+ /**
+ * Adds a deferrable event to the deferred event queue.
+ * @param {Event} event The event to defer.
+ * @param {Function} callback to be invoked when it's the right time to handle
+ * the event.
+ */
+ deferEvent(event, callback) {
+ // TODO Bug 1536822: once one-off buttons are implemented, figure out if the
+ // following is true for the quantum bar as well: somehow event.defaultPrevented
+ // ends up true for deferred events. Autocomplete ignores defaultPrevented
+ // events, which means it would ignore replayed deferred events if we didn't
+ // tell it to bypass defaultPrevented through urlbarDeferred.
+ // Check we don't try to defer events more than once.
+ if (event.urlbarDeferred) {
+ throw new Error(`Event ${event.type}:${event.keyCode} already deferred!`);
+ }
+ logger.debug(`Deferring ${event.type}:${event.keyCode} event`);
+ // Mark the event as deferred.
+ event.urlbarDeferred = true;
+ // Also store the current search string, as an added safety check. If the
+ // string will differ later, the event is stale and should be dropped.
+ event.searchString = this._lastQuery.context.searchString;
+ this._eventsQueue.push({ event, callback });
+
+ if (!this._deferringTimeout) {
+ let elapsed = Cu.now() - this._lastQuery.startDate;
+ let remaining = DEFERRING_TIMEOUT_MS - elapsed;
+ this._deferringTimeout = setTimeout(() => {
+ this.replayDeferredEvents(false);
+ this._deferringTimeout = null;
+ }, Math.max(0, remaining));
+ }
+ }
+
+ /**
+ * Replays deferred key events.
+ * @param {boolean} onlyIfSafe replays only if it's a safe time to do so.
+ * Setting this to false will replay all the queue events, without any
+ * checks, that is something we want to do only if the deferring
+ * timeout elapsed, and we don't want to appear ignoring user's input.
+ */
+ replayDeferredEvents(onlyIfSafe) {
+ if (typeof onlyIfSafe != "boolean") {
+ throw new Error("Must provide a boolean argument");
+ }
+ if (!this._eventsQueue.length) {
+ return;
+ }
+
+ let { event, callback } = this._eventsQueue[0];
+ if (onlyIfSafe && !this.isSafeToPlayDeferredEvent(event)) {
+ return;
+ }
+
+ // Remove the event from the queue and play it.
+ this._eventsQueue.shift();
+ // Safety check: handle only if the search string didn't change meanwhile.
+ if (event.searchString == this._lastQuery.context.searchString) {
+ callback();
+ }
+ Services.tm.dispatchToMainThread(() => {
+ this.replayDeferredEvents(onlyIfSafe);
+ });
+ }
+
+ /**
+ * Checks whether a given event should be deferred
+ * @param {Event} event The event that should maybe be deferred.
+ * @returns {boolean} Whether the event should be deferred.
+ */
+ shouldDeferEvent(event) {
+ // If any event has been deferred for this search, then defer all subsequent
+ // events so that the user does not experience them out of order.
+ // All events will be replayed when _deferringTimeout fires.
+ if (this._eventsQueue.length) {
+ return true;
+ }
+
+ // At this point, no events have been deferred for this search; we must
+ // figure out if this event should be deferred.
+ let isMacNavigation =
+ AppConstants.platform == "macosx" &&
+ event.ctrlKey &&
+ this.input.view.isOpen &&
+ (event.key === "n" || event.key === "p");
+ if (!DEFERRED_KEY_CODES.has(event.keyCode) && !isMacNavigation) {
+ return false;
+ }
+
+ if (DEFERRED_KEY_CODES.has(event.keyCode)) {
+ // Defer while the user is composing.
+ if (this.input.editor.composing) {
+ return true;
+ }
+ if (this.input.controller.keyEventMovesCaret(event)) {
+ return false;
+ }
+ }
+
+ // This is an event that we'd defer, but if enough time has passed since the
+ // start of the search, we don't want to block the user's workflow anymore.
+ if (this._lastQuery.startDate + DEFERRING_TIMEOUT_MS <= Cu.now()) {
+ return false;
+ }
+
+ if (
+ event.keyCode == KeyEvent.DOM_VK_TAB &&
+ !this.input.view.isOpen &&
+ !this.waitingDeferUserSelectionProviders
+ ) {
+ // The view is closed and the user pressed the Tab key. The focus should
+ // move out of the urlbar immediately.
+ return false;
+ }
+
+ return !this.isSafeToPlayDeferredEvent(event);
+ }
+
+ /**
+ * Checks if the bufferer is deferring events.
+ * @returns {boolean} Whether the bufferer is deferring events.
+ */
+ get isDeferringEvents() {
+ return !!this._eventsQueue.length;
+ }
+
+ /**
+ * Checks if any of the current query provider asked to defer user selection
+ * events.
+ * @returns {boolean} Whether a provider asked to defer events.
+ */
+ get waitingDeferUserSelectionProviders() {
+ return !!this._lastQuery.context?.deferUserSelectionProviders.size;
+ }
+
+ /**
+ * Returns true if the given deferred event can be played now without possibly
+ * surprising the user. This depends on the state of the view, the results,
+ * and the type of event.
+ * Use this method only after determining that the event should be deferred,
+ * or after it has been deferred and you want to know if it can be played now.
+ * @param {Event} event The event.
+ * @returns {boolean} Whether the event can be played.
+ */
+ isSafeToPlayDeferredEvent(event) {
+ if (this._lastQuery.status != QUERY_STATUS.RUNNING) {
+ // The view can't get any more results, so there's no need to further
+ // defer events.
+ return true;
+ }
+
+ let waitingFirstResult =
+ this._lastQuery.status == QUERY_STATUS.RUNNING &&
+ !this._lastQuery.context.results.length;
+ if (event.keyCode == KeyEvent.DOM_VK_RETURN) {
+ // Check if we're waiting for providers that requested deferring.
+ if (this.waitingDeferUserSelectionProviders) {
+ return false;
+ }
+ // Play a deferred Enter if the heuristic result is not selected, or we
+ // are not waiting for the first results yet.
+ let selectedResult = this.input.view.selectedResult;
+ return (
+ (selectedResult && !selectedResult.heuristic) || !waitingFirstResult
+ );
+ }
+
+ if (
+ waitingFirstResult ||
+ !this.input.view.isOpen ||
+ this.waitingDeferUserSelectionProviders
+ ) {
+ // We're still waiting on some results, or the popup hasn't opened yet.
+ return false;
+ }
+
+ let isMacDownNavigation =
+ AppConstants.platform == "macosx" &&
+ event.ctrlKey &&
+ this.input.view.isOpen &&
+ event.key === "n";
+ if (event.keyCode == KeyEvent.DOM_VK_DOWN || isMacDownNavigation) {
+ // Don't play the event if the last result is selected so that the user
+ // doesn't accidentally arrow down into the one-off buttons when they
+ // didn't mean to. Note TAB is unaffected because it only navigates
+ // results, not one-offs.
+ return !this.lastResultIsSelected;
+ }
+
+ return true;
+ }
+
+ get lastResultIsSelected() {
+ // TODO Bug 1536818: Once one-off buttons are fully implemented, it would be
+ // nice to have a better way to check if the next down will focus one-off buttons.
+ let results = this._lastQuery.context.results;
+ return (
+ results.length &&
+ results[results.length - 1] == this.input.view.selectedResult
+ );
+ }
+}
diff --git a/browser/components/urlbar/UrlbarInput.jsm b/browser/components/urlbar/UrlbarInput.jsm
new file mode 100644
index 0000000000..b003b5ff1e
--- /dev/null
+++ b/browser/components/urlbar/UrlbarInput.jsm
@@ -0,0 +1,3443 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["UrlbarInput"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AppConstants: "resource://gre/modules/AppConstants.jsm",
+ BrowserSearchTelemetry: "resource:///modules/BrowserSearchTelemetry.jsm",
+ BrowserUtils: "resource://gre/modules/BrowserUtils.jsm",
+ ExtensionSearchHandler: "resource://gre/modules/ExtensionSearchHandler.jsm",
+ ObjectUtils: "resource://gre/modules/ObjectUtils.jsm",
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
+ PromiseUtils: "resource://gre/modules/PromiseUtils.jsm",
+ ReaderMode: "resource://gre/modules/ReaderMode.jsm",
+ PartnerLinkAttribution: "resource:///modules/PartnerLinkAttribution.jsm",
+ SearchUtils: "resource://gre/modules/SearchUtils.jsm",
+ Services: "resource://gre/modules/Services.jsm",
+ UrlbarController: "resource:///modules/UrlbarController.jsm",
+ UrlbarEventBufferer: "resource:///modules/UrlbarEventBufferer.jsm",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
+ UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm",
+ UrlbarQueryContext: "resource:///modules/UrlbarUtils.jsm",
+ UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.jsm",
+ UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.jsm",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+ UrlbarValueFormatter: "resource:///modules/UrlbarValueFormatter.jsm",
+ UrlbarView: "resource:///modules/UrlbarView.jsm",
+});
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "ClipboardHelper",
+ "@mozilla.org/widget/clipboardhelper;1",
+ "nsIClipboardHelper"
+);
+
+const DEFAULT_FORM_HISTORY_NAME = "searchbar-history";
+const SEARCH_BUTTON_ID = "urlbar-search-button";
+
+let getBoundsWithoutFlushing = element =>
+ element.ownerGlobal.windowUtils.getBoundsWithoutFlushing(element);
+let px = number => number.toFixed(2) + "px";
+
+/**
+ * Implements the text input part of the address bar UI.
+ */
+class UrlbarInput {
+ /**
+ * @param {object} options
+ * The initial options for UrlbarInput.
+ * @param {object} options.textbox
+ * The container element.
+ */
+ constructor(options = {}) {
+ this.textbox = options.textbox;
+
+ this.window = this.textbox.ownerGlobal;
+ this.isPrivate = PrivateBrowsingUtils.isWindowPrivate(this.window);
+ this.document = this.window.document;
+
+ // Create the panel to contain results.
+ this.textbox.appendChild(
+ this.window.MozXULElement.parseXULToFragment(`
+ <vbox class="urlbarView"
+ role="group"
+ tooltip="aHTMLTooltip">
+ <html:div class="urlbarView-body-outer">
+ <html:div class="urlbarView-body-inner">
+ <html:div id="urlbar-results"
+ class="urlbarView-results"
+ role="listbox"/>
+ </html:div>
+ </html:div>
+ <hbox class="search-one-offs"
+ compact="true"
+ includecurrentengine="true"
+ disabletab="true"/>
+ </vbox>
+ `)
+ );
+ this.panel = this.textbox.querySelector(".urlbarView");
+
+ this.searchButton = UrlbarPrefs.get("experimental.searchButton");
+ if (this.searchButton) {
+ this.textbox.classList.add("searchButton");
+ }
+
+ this.controller = new UrlbarController({
+ input: this,
+ eventTelemetryCategory: options.eventTelemetryCategory,
+ });
+ this.view = new UrlbarView(this);
+ this.valueIsTyped = false;
+ this.formHistoryName = DEFAULT_FORM_HISTORY_NAME;
+ this.lastQueryContextPromise = Promise.resolve();
+ this._actionOverrideKeyCount = 0;
+ this._autofillPlaceholder = "";
+ this._lastSearchString = "";
+ this._lastValidURLStr = "";
+ this._valueOnLastSearch = "";
+ this._resultForCurrentValue = null;
+ this._suppressStartQuery = false;
+ this._suppressPrimaryAdjustment = false;
+ this._untrimmedValue = "";
+
+ // Search modes are per browser and are stored in this map. For a
+ // browser, search mode can be in preview mode, confirmed, or both.
+ // Typically, search mode is entered in preview mode with a particular
+ // source and is confirmed with the same source once a query starts. It's
+ // also possible for a confirmed search mode to be replaced with a preview
+ // mode with a different source, and in those cases, we need to re-confirm
+ // search mode when preview mode is exited. In addition, only confirmed
+ // search modes should be restored across sessions. We therefore need to
+ // keep track of both the current confirmed and preview modes, per browser.
+ //
+ // For each browser with a search mode, this maps the browser to an object
+ // like this: { preview, confirmed }. Both `preview` and `confirmed` are
+ // search mode objects; see the setSearchMode documentation. Either one may
+ // be undefined if that particular mode is not active for the browser.
+ this._searchModesByBrowser = new WeakMap();
+
+ this.QueryInterface = ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsISupportsWeakReference",
+ ]);
+ this._addObservers();
+
+ // This exists only for tests.
+ this._enableAutofillPlaceholder = true;
+
+ // Forward certain methods and properties.
+ const CONTAINER_METHODS = [
+ "getAttribute",
+ "hasAttribute",
+ "querySelector",
+ "setAttribute",
+ "removeAttribute",
+ "toggleAttribute",
+ ];
+ const INPUT_METHODS = ["addEventListener", "blur", "removeEventListener"];
+ const READ_WRITE_PROPERTIES = [
+ "placeholder",
+ "readOnly",
+ "selectionStart",
+ "selectionEnd",
+ ];
+
+ for (let method of CONTAINER_METHODS) {
+ this[method] = (...args) => {
+ return this.textbox[method](...args);
+ };
+ }
+
+ for (let method of INPUT_METHODS) {
+ this[method] = (...args) => {
+ return this.inputField[method](...args);
+ };
+ }
+
+ for (let property of READ_WRITE_PROPERTIES) {
+ Object.defineProperty(this, property, {
+ enumerable: true,
+ get() {
+ return this.inputField[property];
+ },
+ set(val) {
+ return (this.inputField[property] = val);
+ },
+ });
+ }
+
+ this.inputField = this.querySelector("#urlbar-input");
+ this._inputContainer = this.querySelector("#urlbar-input-container");
+ this._identityBox = this.querySelector("#identity-box");
+ this._searchModeIndicator = this.querySelector(
+ "#urlbar-search-mode-indicator"
+ );
+ this._searchModeIndicatorTitle = this._searchModeIndicator.querySelector(
+ "#urlbar-search-mode-indicator-title"
+ );
+ this._searchModeIndicatorClose = this._searchModeIndicator.querySelector(
+ "#urlbar-search-mode-indicator-close"
+ );
+ this._searchModeLabel = this.querySelector("#urlbar-label-search-mode");
+ this._toolbar = this.textbox.closest("toolbar");
+
+ XPCOMUtils.defineLazyGetter(this, "valueFormatter", () => {
+ return new UrlbarValueFormatter(this);
+ });
+
+ // If the toolbar is not visible in this window or the urlbar is readonly,
+ // we'll stop here, so that most properties of the input object are valid,
+ // but we won't handle events.
+ if (!this.window.toolbar.visible || this.readOnly) {
+ return;
+ }
+
+ // The event bufferer can be used to defer events that may affect users
+ // muscle memory; for example quickly pressing DOWN+ENTER should end up
+ // on a predictable result, regardless of the search status. The event
+ // bufferer will invoke the handling code at the right time.
+ this.eventBufferer = new UrlbarEventBufferer(this);
+
+ this._inputFieldEvents = [
+ "compositionstart",
+ "compositionend",
+ "contextmenu",
+ "dragover",
+ "dragstart",
+ "drop",
+ "focus",
+ "blur",
+ "input",
+ "keydown",
+ "keyup",
+ "mouseover",
+ "overflow",
+ "underflow",
+ "paste",
+ "scrollend",
+ "select",
+ ];
+ for (let name of this._inputFieldEvents) {
+ this.addEventListener(name, this);
+ }
+
+ this.window.addEventListener("mousedown", this);
+ if (AppConstants.platform == "win") {
+ this.window.addEventListener("draggableregionleftmousedown", this);
+ }
+ this.textbox.addEventListener("mousedown", this);
+
+ // This listener handles clicks from our children too, included the search mode
+ // indicator close button.
+ this._inputContainer.addEventListener("click", this);
+
+ // This is used to detect commands launched from the panel, to avoid
+ // recording abandonment events when the command causes a blur event.
+ this.view.panel.addEventListener("command", this, true);
+
+ this.window.gBrowser.tabContainer.addEventListener("TabSelect", this);
+
+ this.window.addEventListener("customizationstarting", this);
+ this.window.addEventListener("aftercustomization", this);
+
+ this.updateLayoutBreakout();
+
+ this._initCopyCutController();
+ this._initPasteAndGo();
+
+ // Tracks IME composition.
+ this._compositionState = UrlbarUtils.COMPOSITION.NONE;
+ this._compositionClosedPopup = false;
+
+ this.editor.newlineHandling =
+ Ci.nsIEditor.eNewlinesStripSurroundingWhitespace;
+ }
+
+ /**
+ * Applies styling to the text in the urlbar input, depending on the text.
+ */
+ formatValue() {
+ // The editor may not exist if the toolbar is not visible.
+ if (this.editor) {
+ this.valueFormatter.update();
+ }
+ }
+
+ focus() {
+ let beforeFocus = new CustomEvent("beforefocus", {
+ bubbles: true,
+ cancelable: true,
+ });
+ this.inputField.dispatchEvent(beforeFocus);
+ if (beforeFocus.defaultPrevented) {
+ return;
+ }
+
+ this.inputField.focus();
+ }
+
+ select() {
+ let beforeSelect = new CustomEvent("beforeselect", {
+ bubbles: true,
+ cancelable: true,
+ });
+ this.inputField.dispatchEvent(beforeSelect);
+ if (beforeSelect.defaultPrevented) {
+ return;
+ }
+
+ // See _on_select(). HTMLInputElement.select() dispatches a "select"
+ // event but does not set the primary selection.
+ this._suppressPrimaryAdjustment = true;
+ this.inputField.select();
+ this._suppressPrimaryAdjustment = false;
+ }
+
+ /**
+ * Sets the URI to display in the location bar.
+ *
+ * @param {nsIURI} [uri]
+ * If this is unspecified, the current URI will be used.
+ * @param {boolean} [dueToTabSwitch]
+ * True if this is being called due to switching tabs and false
+ * otherwise.
+ */
+ setURI(uri = null, dueToTabSwitch = false) {
+ let value = this.window.gBrowser.userTypedValue;
+ let valid = false;
+
+ // Explicitly check for nulled out value. We don't want to reset the URL
+ // bar if the user has deleted the URL and we'd just put the same URL
+ // back. See bug 304198.
+ if (value === null) {
+ uri = uri || this.window.gBrowser.currentURI;
+ // Strip off usernames and passwords for the location bar
+ try {
+ uri = Services.io.createExposableURI(uri);
+ } catch (e) {}
+
+ // Replace initial page URIs with an empty string
+ // only if there's no opener (bug 370555).
+ if (
+ this.window.isInitialPage(uri) &&
+ BrowserUtils.checkEmptyPageOrigin(
+ this.window.gBrowser.selectedBrowser,
+ uri
+ )
+ ) {
+ value = "";
+ } else {
+ // We should deal with losslessDecodeURI throwing for exotic URIs
+ try {
+ value = losslessDecodeURI(uri);
+ } catch (ex) {
+ value = "about:blank";
+ }
+ }
+
+ valid =
+ !this.window.isBlankPageURL(uri.spec) || uri.schemeIs("moz-extension");
+ } else if (
+ this.window.isInitialPage(value) &&
+ BrowserUtils.checkEmptyPageOrigin(this.window.gBrowser.selectedBrowser)
+ ) {
+ value = "";
+ valid = true;
+ }
+
+ let isDifferentValidValue = valid && value != this.untrimmedValue;
+ this.value = value;
+ this.valueIsTyped = !valid;
+ this.removeAttribute("usertyping");
+ if (isDifferentValidValue) {
+ // The selection is enforced only for new values, to avoid overriding the
+ // cursor position when the user switches windows while typing.
+ this.selectionStart = this.selectionEnd = 0;
+ }
+
+ // The proxystate must be set before setting search mode below because
+ // search mode depends on it.
+ this.setPageProxyState(valid ? "valid" : "invalid", dueToTabSwitch);
+
+ // If we're switching tabs, restore the tab's search mode. Otherwise, if
+ // the URI is valid, exit search mode. This must happen after setting
+ // proxystate above because search mode depends on it.
+ if (dueToTabSwitch && !valid) {
+ this.restoreSearchModeState();
+ } else if (valid) {
+ this.searchMode = null;
+ }
+ }
+
+ /**
+ * Converts an internal URI (e.g. a URI with a username or password) into one
+ * which we can expose to the user.
+ *
+ * @param {nsIURI} uri
+ * The URI to be converted
+ * @returns {nsIURI}
+ * The converted, exposable URI
+ */
+ makeURIReadable(uri) {
+ // Avoid copying 'about:reader?url=', and always provide the original URI:
+ // Reader mode ensures we call createExposableURI itself.
+ let readerStrippedURI = ReaderMode.getOriginalUrlObjectForDisplay(
+ uri.displaySpec
+ );
+ if (readerStrippedURI) {
+ return readerStrippedURI;
+ }
+
+ try {
+ return Services.io.createExposableURI(uri);
+ } catch (ex) {}
+
+ return uri;
+ }
+
+ /**
+ * Passes DOM events to the _on_<event type> methods.
+ * @param {Event} event
+ */
+ handleEvent(event) {
+ let methodName = "_on_" + event.type;
+ if (methodName in this) {
+ this[methodName](event);
+ } else {
+ throw new Error("Unrecognized UrlbarInput event: " + event.type);
+ }
+ }
+
+ /**
+ * Handles an event which might open text or a URL. If the event requires
+ * doing so, handleCommand forwards it to handleNavigation.
+ *
+ * @param {Event} [event] The event triggering the open.
+ */
+ handleCommand(event = null) {
+ let isMouseEvent = event instanceof this.window.MouseEvent;
+ if (isMouseEvent && event.button == 2) {
+ // Do nothing for right clicks.
+ return;
+ }
+
+ // Determine whether to use the selected one-off search button. In
+ // one-off search buttons parlance, "selected" means that the button
+ // has been navigated to via the keyboard. So we want to use it if
+ // the triggering event is not a mouse click -- i.e., it's a Return
+ // key -- or if the one-off was mouse-clicked.
+ if (this.view.isOpen) {
+ let selectedOneOff = this.view.oneOffSearchButtons.selectedButton;
+ if (selectedOneOff && (!isMouseEvent || event.target == selectedOneOff)) {
+ this.view.oneOffSearchButtons.handleSearchCommand(event, {
+ engineName: selectedOneOff.engine?.name,
+ source: selectedOneOff.source,
+ entry: "oneoff",
+ });
+ return;
+ }
+ }
+
+ this.handleNavigation({ event });
+ }
+
+ /**
+ * Handles an event which would cause a URL or text to be opened.
+ *
+ * @param {Event} [event]
+ * The event triggering the open.
+ * @param {object} [oneOffParams]
+ * Optional. Pass if this navigation was triggered by a one-off. Practically
+ * speaking, UrlbarSearchOneOffs passes this when the user holds certain key
+ * modifiers while picking a one-off. In those cases, we do an immediate
+ * search using the one-off's engine instead of entering search mode.
+ * @param {string} oneOffParams.openWhere
+ * Where we expect the result to be opened.
+ * @param {object} oneOffParams.openParams
+ * The parameters related to where the result will be opened.
+ * @param {Node} oneOffParams.engine
+ * The selected one-off's engine.
+ * @param {object} [triggeringPrincipal]
+ * The principal that the action was triggered from.
+ */
+ handleNavigation({ event, oneOffParams, triggeringPrincipal }) {
+ let element = this.view.selectedElement;
+ let result = this.view.getResultFromElement(element);
+ let openParams = oneOffParams?.openParams || {};
+
+ // If the value was submitted during composition, the result may not have
+ // been updated yet, because the input event happens after composition end.
+ // We can't trust element nor _resultForCurrentValue targets in that case,
+ // so we'always generate a new heuristic to load.
+ let isComposing = this.editor.composing;
+
+ // Use the selected element if we have one; this is usually the case
+ // when the view is open.
+ let selectedPrivateResult =
+ result &&
+ result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
+ result.payload.inPrivateWindow;
+ let selectedPrivateEngineResult =
+ selectedPrivateResult && result.payload.isPrivateEngine;
+ if (
+ !isComposing &&
+ element &&
+ (!oneOffParams?.engine || selectedPrivateEngineResult)
+ ) {
+ this.pickElement(element, event);
+ return;
+ }
+
+ // We don't select a heuristic result when we're autofilling a token alias,
+ // but we want pressing Enter to behave like the first result was selected.
+ if (!result && this.value.startsWith("@")) {
+ let tokenAliasResult = this.view.getResultAtIndex(0);
+ if (tokenAliasResult?.autofill && tokenAliasResult?.payload.keyword) {
+ this.pickResult(tokenAliasResult, event);
+ return;
+ }
+ }
+
+ let url;
+ let selType = this.controller.engagementEvent.typeFromElement(element);
+ let typedValue = this.value;
+ if (oneOffParams?.engine) {
+ selType = "oneoff";
+ typedValue = this._lastSearchString;
+ // If there's a selected one-off button then load a search using
+ // the button's engine.
+ result = this._resultForCurrentValue;
+ let searchString =
+ (result && (result.payload.suggestion || result.payload.query)) ||
+ this._lastSearchString;
+ [url, openParams.postData] = UrlbarUtils.getSearchQueryUrl(
+ oneOffParams.engine,
+ searchString
+ );
+ this._recordSearch(oneOffParams.engine, event, { url });
+
+ UrlbarUtils.addToFormHistory(
+ this,
+ searchString,
+ oneOffParams.engine.name
+ ).catch(Cu.reportError);
+ } else {
+ // Use the current value if we don't have a UrlbarResult e.g. because the
+ // view is closed.
+ url = this.untrimmedValue;
+ openParams.postData = null;
+ }
+
+ if (!url) {
+ return;
+ }
+
+ // When the user hits enter in a local search mode and there's no selected
+ // result or one-off, don't do anything.
+ if (
+ this.searchMode &&
+ !this.searchMode.engineName &&
+ !result &&
+ !oneOffParams
+ ) {
+ return;
+ }
+
+ let selectedResult = result || this.view.selectedResult;
+ this.controller.recordSelectedResult(event, selectedResult);
+
+ let where = oneOffParams?.openWhere || this._whereToOpen(event);
+ if (selectedPrivateResult) {
+ where = "window";
+ openParams.private = true;
+ }
+ openParams.allowInheritPrincipal = false;
+ url = this._maybeCanonizeURL(event, url) || url.trim();
+
+ this.controller.engagementEvent.record(event, {
+ searchString: typedValue,
+ selIndex: this.view.selectedRowIndex,
+ selType,
+ provider: selectedResult?.providerName,
+ });
+
+ let isValidUrl = false;
+ try {
+ new URL(url);
+ isValidUrl = true;
+ } catch (ex) {}
+ if (isValidUrl) {
+ this._loadURL(url, event, where, openParams);
+ return;
+ }
+
+ // This is not a URL and there's no selected element, because likely the
+ // view is closed, or paste&go was used.
+ // We must act consistently here, having or not an open view should not
+ // make a difference if the search string is the same.
+
+ // If we have a result for the current value, we can just use it.
+ if (!isComposing && this._resultForCurrentValue) {
+ this.pickResult(this._resultForCurrentValue, event);
+ return;
+ }
+
+ // Otherwise, we must fetch the heuristic result for the current value.
+ // TODO (Bug 1604927): If the urlbar results are restricted to a specific
+ // engine, here we must search with that specific engine; indeed the
+ // docshell wouldn't know about our engine restriction.
+ // Also remember to invoke this._recordSearch, after replacing url with
+ // the appropriate engine submission url.
+ let browser = this.window.gBrowser.selectedBrowser;
+ let lastLocationChange = browser.lastLocationChange;
+ UrlbarUtils.getHeuristicResultFor(url)
+ .then(newResult => {
+ // Because this happens asynchronously, we must verify that the browser
+ // location did not change in the meanwhile.
+ if (
+ where != "current" ||
+ browser.lastLocationChange == lastLocationChange
+ ) {
+ this.pickResult(newResult, event, null, browser);
+ }
+ })
+ .catch(ex => {
+ if (url) {
+ // Something went wrong, we should always have a heuristic result,
+ // otherwise it means we're not able to search at all, maybe because
+ // some parts of the profile are corrupt.
+ // The urlbar should still allow to search or visit the typed string,
+ // so that the user can look for help to resolve the problem.
+ let flags =
+ Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS |
+ Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
+ if (this.isPrivate) {
+ flags |= Ci.nsIURIFixup.FIXUP_FLAG_PRIVATE_CONTEXT;
+ }
+ let {
+ preferredURI: uri,
+ postData,
+ } = Services.uriFixup.getFixupURIInfo(url, flags);
+ if (
+ where != "current" ||
+ browser.lastLocationChange == lastLocationChange
+ ) {
+ openParams.postData = postData;
+ this._loadURL(uri.spec, event, where, openParams, null, browser);
+ }
+ }
+ });
+ // Don't add further handling here, the catch above is our last resort.
+ }
+
+ handleRevert() {
+ this.window.gBrowser.userTypedValue = null;
+ // Nullify search mode before setURI so it won't try to restore it.
+ this.searchMode = null;
+ this.setURI(null, true);
+ if (this.value && this.focused) {
+ this.select();
+ }
+ }
+
+ /**
+ * Called when an element of the view is picked.
+ *
+ * @param {Element} element The element that was picked.
+ * @param {Event} event The event that picked the element.
+ */
+ pickElement(element, event) {
+ let result = this.view.getResultFromElement(element);
+ if (!result) {
+ return;
+ }
+ this.pickResult(result, event, element);
+ }
+
+ /**
+ * Called when a result is picked.
+ *
+ * @param {UrlbarResult} result The result that was picked.
+ * @param {Event} event The event that picked the result.
+ * @param {DOMElement} element the picked view element, if available.
+ * @param {object} browser The browser to use for the load.
+ */
+ pickResult(
+ result,
+ event,
+ element = null,
+ browser = this.window.gBrowser.selectedBrowser
+ ) {
+ // When a one-off is selected, we restyle heuristic results to look like
+ // search results. In the unlikely event that they are clicked, instead of
+ // picking the results as usual, we confirm search mode, same as if the user
+ // had selected them and pressed the enter key. Restyling results in this
+ // manner was agreed on as a compromise between consistent UX and
+ // engineering effort. See review discussion at bug 1667766.
+ if (
+ result.heuristic &&
+ this.searchMode?.isPreview &&
+ this.view.oneOffSearchButtons.selectedButton
+ ) {
+ this.confirmSearchMode();
+ this.search(this.value);
+ return;
+ }
+
+ let originalUntrimmedValue = this.untrimmedValue;
+ let isCanonized = this.setValueFromResult(result, event);
+ let where = this._whereToOpen(event);
+ let openParams = {
+ allowInheritPrincipal: false,
+ };
+
+ let selIndex = result.rowIndex;
+ if (!result.payload.providesSearchMode) {
+ this.view.close(/* elementPicked */ true);
+ }
+
+ this.controller.recordSelectedResult(event, result);
+
+ if (isCanonized) {
+ this.controller.engagementEvent.record(event, {
+ searchString: this._lastSearchString,
+ selIndex,
+ selType: "canonized",
+ provider: result.providerName,
+ });
+ this._loadURL(this.value, event, where, openParams, browser);
+ return;
+ }
+
+ let { url, postData } = UrlbarUtils.getUrlFromResult(result);
+ openParams.postData = postData;
+
+ switch (result.type) {
+ case UrlbarUtils.RESULT_TYPE.URL: {
+ // Bug 1578856: both the provider and the docshell run heuristics to
+ // decide how to handle a non-url string, either fixing it to a url, or
+ // searching for it.
+ // Some preferences can control the docshell behavior, for example
+ // if dns_first_for_single_words is true, the docshell looks up the word
+ // against the dns server, and either loads it as an url or searches for
+ // it, depending on the lookup result. The provider instead will always
+ // return a fixed url in this case, because URIFixup is synchronous and
+ // can't do a synchronous dns lookup. A possible long term solution
+ // would involve sharing the docshell logic with the provider, along
+ // with the dns lookup.
+ // For now, in this specific case, we'll override the result's url
+ // with the input value, and let it pass through to _loadURL(), and
+ // finally to the docshell.
+ // This also means that in some cases the heuristic result will show a
+ // Visit entry, but the docshell will instead execute a search. It's a
+ // rare case anyway, most likely to happen for enterprises customizing
+ // the urifixup prefs.
+ if (
+ result.heuristic &&
+ UrlbarPrefs.get("browser.fixup.dns_first_for_single_words") &&
+ UrlbarUtils.looksLikeSingleWordHost(originalUntrimmedValue)
+ ) {
+ url = originalUntrimmedValue;
+ }
+ break;
+ }
+ case UrlbarUtils.RESULT_TYPE.KEYWORD: {
+ // If this result comes from a bookmark keyword, let it inherit the
+ // current document's principal, otherwise bookmarklets would break.
+ openParams.allowInheritPrincipal = true;
+ break;
+ }
+ case UrlbarUtils.RESULT_TYPE.TAB_SWITCH: {
+ if (this.hasAttribute("actionoverride")) {
+ where = "current";
+ break;
+ }
+
+ this.handleRevert();
+ let prevTab = this.window.gBrowser.selectedTab;
+ let loadOpts = {
+ adoptIntoActiveWindow: UrlbarPrefs.get(
+ "switchTabs.adoptIntoActiveWindow"
+ ),
+ };
+
+ this.controller.engagementEvent.record(event, {
+ searchString: this._lastSearchString,
+ selIndex,
+ selType: "tabswitch",
+ provider: result.providerName,
+ });
+
+ let switched = this.window.switchToTabHavingURI(
+ Services.io.newURI(url),
+ false,
+ loadOpts
+ );
+ if (switched && prevTab.isEmpty) {
+ this.window.gBrowser.removeTab(prevTab);
+ }
+ return;
+ }
+ case UrlbarUtils.RESULT_TYPE.SEARCH: {
+ if (result.payload.providesSearchMode) {
+ let searchModeParams = this._searchModeForResult(result);
+ if (searchModeParams) {
+ this.searchMode = searchModeParams;
+ this.search("");
+ }
+ return;
+ }
+
+ if (
+ !this.searchMode &&
+ result.heuristic &&
+ // If we asked the DNS earlier, avoid the post-facto check.
+ !UrlbarPrefs.get("browser.fixup.dns_first_for_single_words") &&
+ // TODO (bug 1642623): for now there is no smart heuristic to skip the
+ // DNS lookup, so any value above 0 will run it.
+ UrlbarPrefs.get("dnsResolveSingleWordsAfterSearch") > 0 &&
+ this.window.gKeywordURIFixup &&
+ UrlbarUtils.looksLikeSingleWordHost(originalUntrimmedValue)
+ ) {
+ // When fixing a single word to a search, the docShell would also
+ // query the DNS and if resolved ask the user whether they would
+ // rather visit that as a host. On a positive answer, it adds the host
+ // to the list that we use to make decisions.
+ // Because we are directly asking for a search here, bypassing the
+ // docShell, we need to do the same ourselves.
+ // See also URIFixupChild.jsm and keyword-uri-fixup.
+ let fixupInfo = this._getURIFixupInfo(originalUntrimmedValue.trim());
+ if (fixupInfo) {
+ this.window.gKeywordURIFixup.check(
+ this.window.gBrowser.selectedBrowser,
+ fixupInfo
+ );
+ }
+ }
+
+ if (result.payload.inPrivateWindow) {
+ where = "window";
+ openParams.private = true;
+ }
+
+ const actionDetails = {
+ isSuggestion: !!result.payload.suggestion,
+ isFormHistory: result.source == UrlbarUtils.RESULT_SOURCE.HISTORY,
+ alias: result.payload.keyword,
+ url,
+ };
+ const engine = Services.search.getEngineByName(result.payload.engine);
+ this._recordSearch(engine, event, actionDetails);
+
+ if (!result.payload.inPrivateWindow) {
+ UrlbarUtils.addToFormHistory(
+ this,
+ result.payload.suggestion || result.payload.query,
+ engine.name
+ ).catch(Cu.reportError);
+ }
+ break;
+ }
+ case UrlbarUtils.RESULT_TYPE.TIP: {
+ let scalarName;
+ if (element.classList.contains("urlbarView-tip-help")) {
+ url = result.payload.helpUrl;
+ if (!url) {
+ Cu.reportError("helpUrl not specified");
+ return;
+ }
+ scalarName = `${result.payload.type}-help`;
+ } else {
+ scalarName = `${result.payload.type}-picked`;
+ }
+ Services.telemetry.keyedScalarAdd("urlbar.tips", scalarName, 1);
+ if (!url) {
+ this.handleRevert();
+ this.controller.engagementEvent.record(event, {
+ searchString: this._lastSearchString,
+ selIndex,
+ selType: "tip",
+ provider: result.providerName,
+ });
+ let provider = UrlbarProvidersManager.getProvider(
+ result.providerName
+ );
+ if (!provider) {
+ Cu.reportError(`Provider not found: ${result.providerName}`);
+ return;
+ }
+ provider.tryMethod("pickResult", result, element);
+ return;
+ }
+ break;
+ }
+ case UrlbarUtils.RESULT_TYPE.DYNAMIC: {
+ url = result.payload.url;
+ // Do not revert the Urlbar if we're going to navigate. We want the URL
+ // populated so we can navigate to it.
+ if (!url || !result.payload.shouldNavigate) {
+ this.handleRevert();
+ }
+
+ let provider = UrlbarProvidersManager.getProvider(result.providerName);
+ if (!provider) {
+ Cu.reportError(`Provider not found: ${result.providerName}`);
+ return;
+ }
+ provider.tryMethod("pickResult", result, element);
+
+ // If we won't be navigating, this is the end of the engagement.
+ if (!url || !result.payload.shouldNavigate) {
+ this.controller.engagementEvent.record(event, {
+ selIndex,
+ searchString: this._lastSearchString,
+ selType: this.controller.engagementEvent.typeFromElement(element),
+ provider: result.providerName,
+ });
+ return;
+ }
+ break;
+ }
+ case UrlbarUtils.RESULT_TYPE.OMNIBOX: {
+ this.controller.engagementEvent.record(event, {
+ searchString: this._lastSearchString,
+ selIndex,
+ selType: "extension",
+ provider: result.providerName,
+ });
+
+ // The urlbar needs to revert to the loaded url when a command is
+ // handled by the extension.
+ this.handleRevert();
+ // We don't directly handle a load when an Omnibox API result is picked,
+ // instead we forward the request to the WebExtension itself, because
+ // the value may not even be a url.
+ // We pass the keyword and content, that actually is the retrieved value
+ // prefixed by the keyword. ExtensionSearchHandler uses this keyword
+ // redundancy as a sanity check.
+ ExtensionSearchHandler.handleInputEntered(
+ result.payload.keyword,
+ result.payload.content,
+ where
+ );
+ return;
+ }
+ }
+
+ if (!url) {
+ throw new Error(`Invalid url for result ${JSON.stringify(result)}`);
+ }
+
+ if (!this.isPrivate && !result.heuristic) {
+ // This should not interrupt the load anyway.
+ UrlbarUtils.addToInputHistory(url, this._lastSearchString).catch(
+ Cu.reportError
+ );
+ }
+
+ this.controller.engagementEvent.record(event, {
+ searchString: this._lastSearchString,
+ selIndex,
+ selType: this.controller.engagementEvent.typeFromElement(element),
+ provider: result.providerName,
+ });
+
+ if (result.payload.sendAttributionRequest) {
+ PartnerLinkAttribution.makeRequest({
+ targetURL: result.payload.url,
+ source: "urlbar",
+ campaignID: Services.prefs.getStringPref(
+ "browser.partnerlink.campaign.topsites"
+ ),
+ });
+ }
+
+ this._loadURL(
+ url,
+ event,
+ where,
+ openParams,
+ {
+ source: result.source,
+ type: result.type,
+ },
+ browser
+ );
+ }
+
+ /**
+ * Called by the view when moving through results with the keyboard, and when
+ * picking a result. This sets the input value to the value of the result and
+ * invalidates the pageproxystate. It also sets the result that is associated
+ * with the current input value. If you need to set this result but don't
+ * want to also set the input value, then use setResultForCurrentValue.
+ *
+ * @param {UrlbarResult} [result]
+ * The result that was selected or picked, null if no result was selected.
+ * @param {Event} [event]
+ * The event that picked the result.
+ * @returns {boolean}
+ * Whether the value has been canonized
+ */
+ setValueFromResult(result = null, event = null) {
+ // Usually this is set by a previous input event, but in certain cases, like
+ // when opening Top Sites on a loaded page, it wouldn't happen. To avoid
+ // confusing the user, we always enforce it when a result changes our value.
+ this.setPageProxyState("invalid", true);
+
+ // A previous result may have previewed search mode. If we don't expect that
+ // we might stay in a search mode of some kind, exit it now.
+ if (
+ this.searchMode?.isPreview &&
+ !result?.payload.providesSearchMode &&
+ !this.view.oneOffSearchButtons.selectedButton
+ ) {
+ this.searchMode = null;
+ }
+
+ if (!result) {
+ // This happens when there's no selection, for example when moving to the
+ // one-offs search settings button, or to the input field when Top Sites
+ // are shown; then we must reset the input value.
+ // Note that for Top Sites the last search string would be empty, thus we
+ // must restore the last text value.
+ // Note that unselected autofill results will still arrive in this
+ // function with a non-null `result`. They are handled below.
+ this.value = this._lastSearchString || this._valueOnLastSearch;
+ this.setResultForCurrentValue(result);
+ return false;
+ }
+
+ // The value setter clobbers the actiontype attribute, so we need this
+ // helper to restore it afterwards.
+ const setValueAndRestoreActionType = (value, allowTrim) => {
+ this._setValue(value, allowTrim);
+
+ switch (result.type) {
+ case UrlbarUtils.RESULT_TYPE.TAB_SWITCH:
+ this.setAttribute("actiontype", "switchtab");
+ break;
+ case UrlbarUtils.RESULT_TYPE.OMNIBOX:
+ this.setAttribute("actiontype", "extension");
+ break;
+ }
+ };
+
+ // For autofilled results, the value that should be canonized is not the
+ // autofilled value but the value that the user typed.
+ let canonizedUrl = this._maybeCanonizeURL(
+ event,
+ result.autofill ? this._lastSearchString : this.value
+ );
+ if (canonizedUrl) {
+ setValueAndRestoreActionType(canonizedUrl, true);
+ this.setResultForCurrentValue(result);
+ return true;
+ }
+
+ if (result.autofill) {
+ let { value, selectionStart, selectionEnd } = result.autofill;
+ this._autofillValue(value, selectionStart, selectionEnd);
+ }
+
+ if (result.payload.providesSearchMode) {
+ let enteredSearchMode;
+ // Only preview search mode if the result is selected.
+ if (this.view.resultIsSelected(result)) {
+ // Not starting a query means we will only preview search mode.
+ enteredSearchMode = this.maybeConfirmSearchModeFromResult({
+ result,
+ checkValue: false,
+ startQuery: false,
+ });
+ }
+ if (!enteredSearchMode) {
+ setValueAndRestoreActionType(this._getValueFromResult(result), true);
+ this.searchMode = null;
+ }
+ this.setResultForCurrentValue(result);
+ return false;
+ }
+
+ // If the url is trimmed but it's invalid (for example it has an unknown
+ // single word host, or an unknown domain suffix), trimming
+ // it would end up executing a search instead of visiting it.
+ let allowTrim = true;
+ if (
+ result.type == UrlbarUtils.RESULT_TYPE.URL &&
+ UrlbarPrefs.get("trimURLs") &&
+ result.payload.url.startsWith(BrowserUtils.trimURLProtocol)
+ ) {
+ let fixupInfo = this._getURIFixupInfo(
+ BrowserUtils.trimURL(result.payload.url)
+ );
+ if (fixupInfo?.keywordAsSent) {
+ allowTrim = false;
+ }
+ }
+
+ if (!result.autofill) {
+ setValueAndRestoreActionType(this._getValueFromResult(result), allowTrim);
+ }
+ this.setResultForCurrentValue(result);
+ return false;
+ }
+
+ /**
+ * The input keeps track of the result associated with the current input
+ * value. This result can be set by calling either setValueFromResult or this
+ * method. Use this method when you need to set the result without also
+ * setting the input value. This can be the case when either the selection is
+ * cleared and no other result becomes selected, or when the result is the
+ * heuristic and we don't want to modify the value the user is typing.
+ *
+ * @param {UrlbarResult} result
+ * The result to associate with the current input value.
+ */
+ setResultForCurrentValue(result) {
+ this._resultForCurrentValue = result;
+ }
+
+ /**
+ * Called by the controller when the first result of a new search is received.
+ * If it's an autofill result, then it may need to be autofilled, subject to a
+ * few restrictions.
+ *
+ * @param {UrlbarResult} result
+ * The first result.
+ */
+ _autofillFirstResult(result) {
+ if (!result.autofill) {
+ return;
+ }
+
+ let isPlaceholderSelected =
+ this.selectionEnd == this._autofillPlaceholder.length &&
+ this.selectionStart == this._lastSearchString.length &&
+ this._autofillPlaceholder
+ .toLocaleLowerCase()
+ .startsWith(this._lastSearchString.toLocaleLowerCase());
+
+ // Don't autofill if there's already a selection (with one caveat described
+ // next) or the cursor isn't at the end of the input. But if there is a
+ // selection and it's the autofill placeholder value, then do autofill.
+ if (
+ !isPlaceholderSelected &&
+ !this._autofillIgnoresSelection &&
+ (this.selectionStart != this.selectionEnd ||
+ this.selectionEnd != this._lastSearchString.length)
+ ) {
+ return;
+ }
+
+ this.setValueFromResult(result);
+ }
+
+ /**
+ * Invoked by the controller when the first result is received.
+ *
+ * @param {UrlbarResult} firstResult
+ * The first result received.
+ * @returns {boolean}
+ * True if this method canceled the query and started a new one. False
+ * otherwise.
+ */
+ onFirstResult(firstResult) {
+ // If the heuristic result has a keyword but isn't a keyword offer, we may
+ // need to enter search mode.
+ if (
+ firstResult.heuristic &&
+ firstResult.payload.keyword &&
+ !firstResult.payload.providesSearchMode &&
+ this.maybeConfirmSearchModeFromResult({
+ result: firstResult,
+ entry: "typed",
+ checkValue: false,
+ })
+ ) {
+ return true;
+ }
+
+ // To prevent selection flickering, we apply autofill on input through a
+ // placeholder, without waiting for results. But, if the first result is
+ // not an autofill one, the autofill prediction was wrong and we should
+ // restore the original user typed string.
+ if (firstResult.autofill) {
+ this._autofillFirstResult(firstResult);
+ } else if (
+ this._autofillPlaceholder &&
+ // Avoid clobbering added spaces (for token aliases, for example).
+ !this.value.endsWith(" ")
+ ) {
+ this._setValue(this.window.gBrowser.userTypedValue, false);
+ }
+
+ return false;
+ }
+
+ /**
+ * Starts a query based on the current input value.
+ *
+ * @param {boolean} [options.allowAutofill]
+ * Whether or not to allow providers to include autofill results.
+ * @param {boolean} [options.autofillIgnoresSelection]
+ * Normally we autofill only if the cursor is at the end of the string,
+ * if this is set we'll autofill regardless of selection.
+ * @param {string} [options.searchString]
+ * The search string. If not given, the current input value is used.
+ * Otherwise, the current input value must start with this value.
+ * @param {boolean} [options.resetSearchState]
+ * If this is the first search of a user interaction with the input, set
+ * this to true (the default) so that search-related state from the previous
+ * interaction doesn't interfere with the new interaction. Otherwise set it
+ * to false so that state is maintained during a single interaction. The
+ * intended use for this parameter is that it should be set to false when
+ * this method is called due to input events.
+ * @param {event} [options.event]
+ * The user-generated event that triggered the query, if any. If given, we
+ * will record engagement event telemetry for the query.
+ */
+ startQuery({
+ allowAutofill = true,
+ autofillIgnoresSelection = false,
+ searchString = null,
+ resetSearchState = true,
+ event = null,
+ } = {}) {
+ if (!searchString) {
+ searchString =
+ this.getAttribute("pageproxystate") == "valid" ? "" : this.value;
+ } else if (!this.value.startsWith(searchString)) {
+ throw new Error("The current value doesn't start with the search string");
+ }
+
+ if (event) {
+ this.controller.engagementEvent.start(event, searchString);
+ }
+
+ if (this._suppressStartQuery) {
+ return;
+ }
+
+ this._autofillIgnoresSelection = autofillIgnoresSelection;
+ if (resetSearchState) {
+ this._resetSearchState();
+ }
+
+ this._lastSearchString = searchString;
+ this._valueOnLastSearch = this.value;
+
+ let options = {
+ allowAutofill,
+ isPrivate: this.isPrivate,
+ maxResults: UrlbarPrefs.get("maxRichResults"),
+ searchString,
+ userContextId: this.window.gBrowser.selectedBrowser.getAttribute(
+ "usercontextid"
+ ),
+ currentPage: this.window.gBrowser.currentURI.spec,
+ formHistoryName: this.formHistoryName,
+ allowSearchSuggestions:
+ !event ||
+ !UrlbarUtils.isPasteEvent(event) ||
+ !event.data ||
+ event.data.length <= UrlbarPrefs.get("maxCharsForSearchSuggestions"),
+ };
+
+ if (this.searchMode) {
+ this.confirmSearchMode();
+ options.searchMode = this.searchMode;
+ if (this.searchMode.source) {
+ options.sources = [this.searchMode.source];
+ }
+ }
+
+ // TODO (Bug 1522902): This promise is necessary for tests, because some
+ // tests are not listening for completion when starting a query through
+ // other methods than startQuery (input events for example).
+ this.lastQueryContextPromise = this.controller.startQuery(
+ new UrlbarQueryContext(options)
+ );
+ }
+
+ /**
+ * Sets the input's value, starts a search, and opens the view.
+ *
+ * @param {string} value
+ * The input's value will be set to this value, and the search will
+ * use it as its query.
+ * @param {UrlbarUtils.WEB_ENGINE_NAMES} [options.searchEngine]
+ * Search engine to use when the search is using a known alias.
+ * @param {UrlbarUtils.SEARCH_MODE_ENTRY} [options.searchModeEntry]
+ * If provided, we will record this parameter as the search mode entry point
+ * in Telemetry. Consumers should provide this if they expect their call
+ * to enter search mode.
+ * @param {boolean} [options.focus]
+ * If true, the urlbar will be focused. If false, the focus will remain
+ * unchanged.
+ */
+ search(value, { searchEngine, searchModeEntry, focus = true } = {}) {
+ if (focus) {
+ this.focus();
+ }
+
+ let tokens = value.trim().split(UrlbarTokenizer.REGEXP_SPACES);
+ // Enter search mode if the string starts with a restriction token.
+ let searchMode = UrlbarUtils.searchModeForToken(tokens[0]);
+ if (!searchMode && searchEngine) {
+ searchMode = { engineName: searchEngine.name };
+ }
+
+ if (searchMode) {
+ searchMode.entry = searchModeEntry;
+ this.searchMode = searchMode;
+ // Remove the restriction token/alias from the string to be searched for
+ // in search mode.
+ value = value.replace(tokens[0], "");
+ if (UrlbarTokenizer.REGEXP_SPACES.test(value[0])) {
+ // If there was a trailing space after the restriction token/alias,
+ // remove it.
+ value = value.slice(1);
+ }
+ this.inputField.value = value;
+ this._revertOnBlurValue = this.value;
+ } else if (Object.values(UrlbarTokenizer.RESTRICT).includes(tokens[0])) {
+ this.searchMode = null;
+ // If the entire value is a restricted token, append a space.
+ if (Object.values(UrlbarTokenizer.RESTRICT).includes(value)) {
+ value += " ";
+ }
+ this.inputField.value = value;
+ this._revertOnBlurValue = this.value;
+ } else {
+ this.inputField.value = value;
+ }
+
+ // Avoid selecting the text if this method is called twice in a row.
+ this.selectionStart = -1;
+
+ // Note: proper IME Composition handling depends on the fact this generates
+ // an input event, rather than directly invoking the controller; everything
+ // goes through _on_input, that will properly skip the search until the
+ // composition is committed. _on_input also skips the search when it's the
+ // same as the previous search, but we want to allow consecutive searches
+ // with the same string. So clear _lastSearchString first.
+ this._lastSearchString = "";
+ let event = this.document.createEvent("UIEvents");
+ event.initUIEvent("input", true, false, this.window, 0);
+ this.inputField.dispatchEvent(event);
+ }
+
+ /**
+ * Focus without the focus styles.
+ * This is used by Activity Stream and about:privatebrowsing for search hand-off.
+ */
+ setHiddenFocus() {
+ this._hideFocus = true;
+ if (this.focused) {
+ this.removeAttribute("focused");
+ } else {
+ this.focus();
+ }
+ }
+
+ /**
+ * Restore focus styles.
+ * This is used by Activity Stream and about:privatebrowsing for search hand-off.
+ */
+ removeHiddenFocus() {
+ this._hideFocus = false;
+ if (this.focused) {
+ this.setAttribute("focused", "true");
+ this.startLayoutExtend();
+ }
+ }
+
+ /**
+ * Gets the search mode for a specific browser instance.
+ *
+ * @param {Browser} browser
+ * The search mode for this browser will be returned.
+ * @param {boolean} [confirmedOnly]
+ * Normally, if the browser has both preview and confirmed modes, preview
+ * mode will be returned since it takes precedence. If this argument is
+ * true, then only confirmed search mode will be returned, or null if
+ * search mode hasn't been confirmed.
+ * @returns {object}
+ * A search mode object. See setSearchMode documentation. If the browser
+ * is not in search mode, then null is returned.
+ */
+ getSearchMode(browser, confirmedOnly = false) {
+ let modes = this._searchModesByBrowser.get(browser);
+
+ // Return copies so that callers don't modify the stored values.
+ if (!confirmedOnly && modes?.preview) {
+ return { ...modes.preview };
+ }
+ if (modes?.confirmed) {
+ return { ...modes.confirmed };
+ }
+ return null;
+ }
+
+ /**
+ * Sets search mode for a specific browser instance. If the given browser is
+ * selected, then this will also enter search mode.
+ *
+ * @param {object} searchMode
+ * A search mode object.
+ * @param {string} searchMode.engineName
+ * The name of the search engine to restrict to.
+ * @param {UrlbarUtils.RESULT_SOURCE} searchMode.source
+ * A result source to restrict to.
+ * @param {string} searchMode.entry
+ * How search mode was entered. This is recorded in event telemetry. One of
+ * the values in UrlbarUtils.SEARCH_MODE_ENTRY.
+ * @param {boolean} [searchMode.isPreview]
+ * If true, we will preview search mode. Search mode preview does not record
+ * telemetry and has slighly different UI behavior. The preview is exited in
+ * favor of full search mode when a query is executed. False should be
+ * passed if the caller needs to enter search mode but expects it will not
+ * be interacted with right away. Defaults to true.
+ * @param {Browser} browser
+ * The browser for which to set search mode.
+ */
+ async setSearchMode(searchMode, browser) {
+ let currentSearchMode = this.getSearchMode(browser);
+ let areSearchModesSame =
+ (!currentSearchMode && !searchMode) ||
+ ObjectUtils.deepEqual(currentSearchMode, searchMode);
+
+ // Exit search mode if the passed-in engine is invalid or hidden.
+ if (searchMode?.engineName) {
+ if (!Services.search.isInitialized) {
+ await Services.search.init();
+ }
+ let engine = Services.search.getEngineByName(searchMode.engineName);
+ if (!engine || engine.hidden) {
+ searchMode = null;
+ }
+ }
+
+ let { engineName, source, entry, isPreview = true } = searchMode || {};
+
+ searchMode = null;
+
+ if (engineName) {
+ searchMode = { engineName };
+ if (source) {
+ searchMode.source = source;
+ } else if (UrlbarUtils.WEB_ENGINE_NAMES.has(engineName)) {
+ // History results for general-purpose search engines are often not
+ // useful, so we hide them in search mode. See bug 1658646 for
+ // discussion.
+ searchMode.source = UrlbarUtils.RESULT_SOURCE.SEARCH;
+ }
+ } else if (source) {
+ let sourceName = UrlbarUtils.getResultSourceName(source);
+ if (sourceName) {
+ searchMode = { source };
+ } else {
+ Cu.reportError(`Unrecognized source: ${source}`);
+ }
+ }
+
+ if (searchMode) {
+ searchMode.isPreview = isPreview;
+ if (UrlbarUtils.SEARCH_MODE_ENTRY.has(entry)) {
+ searchMode.entry = entry;
+ } else {
+ // If we see this value showing up in telemetry, we should review
+ // search mode's entry points.
+ searchMode.entry = "other";
+ }
+
+ // Add the search mode to the map.
+ if (!searchMode.isPreview) {
+ this._searchModesByBrowser.set(browser, {
+ confirmed: searchMode,
+ });
+ } else {
+ let modes = this._searchModesByBrowser.get(browser) || {};
+ modes.preview = searchMode;
+ this._searchModesByBrowser.set(browser, modes);
+ }
+ } else {
+ this._searchModesByBrowser.delete(browser);
+ }
+
+ // Enter search mode if the browser is selected.
+ if (browser == this.window.gBrowser.selectedBrowser) {
+ this._updateSearchModeUI(searchMode);
+ if (searchMode) {
+ // Set userTypedValue to the query string so that it's properly restored
+ // when switching back to the current tab and across sessions.
+ this.window.gBrowser.userTypedValue = this.untrimmedValue;
+ this.valueIsTyped = true;
+ if (!searchMode.isPreview && !areSearchModesSame) {
+ try {
+ BrowserSearchTelemetry.recordSearchMode(searchMode);
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Restores the current browser search mode from a previously stored state.
+ */
+ restoreSearchModeState() {
+ let modes = this._searchModesByBrowser.get(
+ this.window.gBrowser.selectedBrowser
+ );
+ this.searchMode = modes?.confirmed;
+ }
+
+ /**
+ * Enters search mode with the default engine.
+ */
+ searchModeShortcut() {
+ // We restrict to search results when entering search mode from this
+ // shortcut to honor historical behaviour.
+ this.searchMode = {
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ engineName: UrlbarSearchUtils.getDefaultEngine(this.isPrivate).name,
+ entry: "shortcut",
+ };
+ // The searchMode setter clears the input if pageproxystate is valid, so
+ // we know at this point this.value will either be blank or the user's
+ // typed string.
+ this.search(this.value);
+ this.select();
+ }
+
+ /**
+ * Confirms the current search mode.
+ */
+ confirmSearchMode() {
+ let searchMode = this.searchMode;
+ if (searchMode?.isPreview) {
+ searchMode.isPreview = false;
+ this.searchMode = searchMode;
+
+ // Unselect the one-off search button to ensure UI consistency.
+ this.view.oneOffSearchButtons.selectedButton = null;
+ }
+ }
+
+ // Getters and Setters below.
+
+ get editor() {
+ return this.inputField.editor;
+ }
+
+ get focused() {
+ return this.document.activeElement == this.inputField;
+ }
+
+ get goButton() {
+ return this.querySelector("#urlbar-go-button");
+ }
+
+ get value() {
+ return this.inputField.value;
+ }
+
+ get untrimmedValue() {
+ return this._untrimmedValue;
+ }
+
+ set value(val) {
+ return this._setValue(val, true);
+ }
+
+ get lastSearchString() {
+ return this._lastSearchString;
+ }
+
+ get searchMode() {
+ return this.getSearchMode(this.window.gBrowser.selectedBrowser);
+ }
+
+ set searchMode(searchMode) {
+ this.setSearchMode(searchMode, this.window.gBrowser.selectedBrowser);
+ }
+
+ async updateLayoutBreakout() {
+ if (!this._toolbar) {
+ // Expanding requires a parent toolbar.
+ return;
+ }
+ if (this.document.fullscreenElement) {
+ // Toolbars are hidden in DOM fullscreen mode, so we can't get proper
+ // layout information and need to retry after leaving that mode.
+ this.window.addEventListener(
+ "fullscreen",
+ () => {
+ this.updateLayoutBreakout();
+ },
+ { once: true }
+ );
+ return;
+ }
+ await this._updateLayoutBreakoutDimensions();
+ this.startLayoutExtend();
+ }
+
+ startLayoutExtend() {
+ // Do not expand if:
+ // The Urlbar does not support being expanded or it is already expanded
+ if (
+ !this.hasAttribute("breakout") ||
+ this.hasAttribute("breakout-extend")
+ ) {
+ return;
+ }
+ // The Urlbar is unfocused or reduce motion is on and the view is closed.
+ // gReduceMotion is accurate in most cases, but it is automatically set to
+ // true when windows are loaded. We check `prefers-reduced-motion: reduce`
+ // to ensure the user actually set prefers-reduced-motion. We check
+ // gReduceMotion first to save work in the common case of having
+ // prefers-reduced-motion disabled.
+ if (
+ !this.view.isOpen &&
+ (this.getAttribute("focused") != "true" ||
+ (this.window.gReduceMotion &&
+ this.window.matchMedia("(prefers-reduced-motion: reduce)").matches))
+ ) {
+ return;
+ }
+
+ if (Cu.isInAutomation) {
+ if (UrlbarPrefs.get("disableExtendForTests")) {
+ this.setAttribute("breakout-extend-disabled", "true");
+ return;
+ }
+ this.removeAttribute("breakout-extend-disabled");
+ }
+
+ this._toolbar.setAttribute("urlbar-exceeds-toolbar-bounds", "true");
+ this.setAttribute("breakout-extend", "true");
+
+ // Enable the animation only after the first extend call to ensure it
+ // doesn't run when opening a new window.
+ if (!this.hasAttribute("breakout-extend-animate")) {
+ this.window.promiseDocumentFlushed(() => {
+ this.window.requestAnimationFrame(() => {
+ this.setAttribute("breakout-extend-animate", "true");
+ });
+ });
+ }
+ }
+
+ endLayoutExtend() {
+ // If reduce motion is enabled, we want to collapse the Urlbar here so the
+ // user sees only sees two states: not expanded, and expanded with the view
+ // open.
+ if (
+ !this.hasAttribute("breakout-extend") ||
+ this.view.isOpen ||
+ (this.getAttribute("focused") == "true" &&
+ (!this.window.gReduceMotion ||
+ !this.window.matchMedia("(prefers-reduced-motion: reduce)").matches))
+ ) {
+ return;
+ }
+ this.removeAttribute("breakout-extend");
+ this._toolbar.removeAttribute("urlbar-exceeds-toolbar-bounds");
+ }
+
+ /**
+ * Updates the user interface to indicate whether the URI in the address bar
+ * is different than the loaded page, because it's being edited or because a
+ * search result is currently selected and is displayed in the location bar.
+ *
+ * @param {string} state
+ * The string "valid" indicates that the security indicators and other
+ * related user interface elments should be shown because the URI in
+ * the location bar matches the loaded page. The string "invalid"
+ * indicates that the URI in the location bar is different than the
+ * loaded page.
+ * @param {boolean} [updatePopupNotifications]
+ * Indicates whether we should update the PopupNotifications
+ * visibility due to this change, otherwise avoid doing so as it is
+ * being handled somewhere else.
+ */
+ setPageProxyState(state, updatePopupNotifications) {
+ let prevState = this.getAttribute("pageproxystate");
+
+ this.setAttribute("pageproxystate", state);
+ this._inputContainer.setAttribute("pageproxystate", state);
+ this._identityBox.setAttribute("pageproxystate", state);
+
+ if (state == "valid") {
+ this._lastValidURLStr = this.value;
+ }
+
+ if (
+ updatePopupNotifications &&
+ prevState != state &&
+ this.window.UpdatePopupNotificationsVisibility
+ ) {
+ this.window.UpdatePopupNotificationsVisibility();
+ }
+ }
+
+ /**
+ * When switching tabs quickly, TabSelect sometimes happens before
+ * _adjustFocusAfterTabSwitch and due to the focus still being on the old
+ * tab, we end up flickering the results pane briefly.
+ */
+ afterTabSwitchFocusChange() {
+ this._gotFocusChange = true;
+ this._afterTabSelectAndFocusChange();
+ }
+
+ /**
+ * Confirms search mode and starts a new search if appropriate for the given
+ * result. See also _searchModeForResult.
+ *
+ * @param {string} entry
+ * The search mode entry point. See setSearchMode documentation for details.
+ * @param {UrlbarResult} [result]
+ * The result to confirm. Defaults to the currently selected result.
+ * @param {boolean} [checkValue]
+ * If true, the trimmed input value must equal the result's keyword in order
+ * to enter search mode.
+ * @param {boolean} [startQuery]
+ * If true, start a query after entering search mode. Defaults to true.
+ * @returns {boolean}
+ * True if we entered search mode and false if not.
+ */
+ maybeConfirmSearchModeFromResult({
+ entry,
+ result = this._resultForCurrentValue,
+ checkValue = true,
+ startQuery = true,
+ }) {
+ if (
+ !result ||
+ (checkValue && this.value.trim() != result.payload.keyword?.trim())
+ ) {
+ return false;
+ }
+
+ let searchMode = this._searchModeForResult(result, entry);
+ if (!searchMode) {
+ return false;
+ }
+
+ this.searchMode = searchMode;
+
+ let value = result.payload.query?.trimStart() || "";
+ this._setValue(value, false);
+
+ if (startQuery) {
+ this.startQuery({ allowAutofill: false });
+ }
+
+ return true;
+ }
+
+ observe(subject, topic, data) {
+ switch (topic) {
+ case SearchUtils.TOPIC_ENGINE_MODIFIED: {
+ switch (data) {
+ case SearchUtils.MODIFIED_TYPE.CHANGED:
+ case SearchUtils.MODIFIED_TYPE.REMOVED: {
+ let searchMode = this.searchMode;
+ let engine = subject.QueryInterface(Ci.nsISearchEngine);
+ if (searchMode?.engineName == engine.name) {
+ // Exit search mode if the current search mode engine was removed.
+ this.searchMode = searchMode;
+ }
+ break;
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ // Private methods below.
+
+ _addObservers() {
+ Services.obs.addObserver(this, SearchUtils.TOPIC_ENGINE_MODIFIED, true);
+ }
+
+ _getURIFixupInfo(searchString) {
+ let flags =
+ Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS |
+ Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
+ if (this.isPrivate) {
+ flags |= Ci.nsIURIFixup.FIXUP_FLAG_PRIVATE_CONTEXT;
+ }
+ try {
+ return Services.uriFixup.getFixupURIInfo(searchString, flags);
+ } catch (ex) {
+ Cu.reportError(
+ `An error occured while trying to fixup "${searchString}": ${ex}`
+ );
+ }
+ return null;
+ }
+
+ _afterTabSelectAndFocusChange() {
+ // We must have seen both events to proceed safely.
+ if (!this._gotFocusChange || !this._gotTabSelect) {
+ return;
+ }
+ this._gotFocusChange = this._gotTabSelect = false;
+
+ this._resetSearchState();
+
+ // Switching tabs doesn't always change urlbar focus, so we must try to
+ // reopen here too, not just on focus.
+ // We don't use the original TabSelect event because caching it causes
+ // leaks on MacOS.
+ if (this.view.autoOpen({ event: new CustomEvent("tabswitch") })) {
+ return;
+ }
+ // The input may retain focus when switching tabs in which case we
+ // need to close the view explicitly.
+ this.view.close();
+ }
+
+ async _updateLayoutBreakoutDimensions() {
+ // When this method gets called a second time before the first call
+ // finishes, we need to disregard the first one.
+ let updateKey = {};
+ this._layoutBreakoutUpdateKey = updateKey;
+
+ this.removeAttribute("breakout");
+ this.textbox.parentNode.removeAttribute("breakout");
+
+ await this.window.promiseDocumentFlushed(() => {});
+ await new Promise(resolve => {
+ this.window.requestAnimationFrame(() => {
+ if (this._layoutBreakoutUpdateKey != updateKey) {
+ return;
+ }
+
+ this.textbox.parentNode.style.setProperty(
+ "--urlbar-container-height",
+ px(getBoundsWithoutFlushing(this.textbox.parentNode).height)
+ );
+ this.textbox.style.setProperty(
+ "--urlbar-height",
+ px(getBoundsWithoutFlushing(this.textbox).height)
+ );
+ this.textbox.style.setProperty(
+ "--urlbar-toolbar-height",
+ px(getBoundsWithoutFlushing(this._toolbar).height)
+ );
+
+ this.setAttribute("breakout", "true");
+ this.textbox.parentNode.setAttribute("breakout", "true");
+
+ resolve();
+ });
+ });
+ }
+
+ _setValue(val, allowTrim) {
+ // Don't expose internal about:reader URLs to the user.
+ let originalUrl = ReaderMode.getOriginalUrlObjectForDisplay(val);
+ if (originalUrl) {
+ val = originalUrl.displaySpec;
+ }
+ this._untrimmedValue = val;
+
+ if (allowTrim) {
+ val = this._trimValue(val);
+ }
+
+ this.valueIsTyped = false;
+ this._resultForCurrentValue = null;
+ this.inputField.value = val;
+ this.formatValue();
+ this.removeAttribute("actiontype");
+
+ // Dispatch ValueChange event for accessibility.
+ let event = this.document.createEvent("Events");
+ event.initEvent("ValueChange", true, true);
+ this.inputField.dispatchEvent(event);
+
+ return val;
+ }
+
+ _getValueFromResult(result) {
+ switch (result.type) {
+ case UrlbarUtils.RESULT_TYPE.KEYWORD:
+ return result.payload.input;
+ case UrlbarUtils.RESULT_TYPE.SEARCH: {
+ let value = "";
+ if (result.payload.keyword) {
+ value += result.payload.keyword + " ";
+ }
+ value += result.payload.suggestion || result.payload.query;
+ return value;
+ }
+ case UrlbarUtils.RESULT_TYPE.OMNIBOX:
+ return result.payload.content;
+ }
+
+ try {
+ let uri = Services.io.newURI(result.payload.url);
+ if (uri) {
+ return losslessDecodeURI(uri);
+ }
+ } catch (ex) {}
+
+ return "";
+ }
+
+ /**
+ * Resets some state so that searches from the user's previous interaction
+ * with the input don't interfere with searches from a new interaction.
+ */
+ _resetSearchState() {
+ this._lastSearchString = this.value;
+ this._autofillPlaceholder = "";
+ }
+
+ /**
+ * Autofills the autofill placeholder string if appropriate, and determines
+ * whether autofill should be allowed for the new search started by an input
+ * event.
+ *
+ * @param {string} value
+ * The new search string.
+ * @returns {boolean}
+ * Whether autofill should be allowed in the new search.
+ */
+ _maybeAutofillOnInput(value) {
+ // We allow autofill in local but not remote search modes.
+ let allowAutofill =
+ this.selectionEnd == value.length &&
+ !this.searchMode?.engineName &&
+ this.searchMode?.source != UrlbarUtils.RESULT_SOURCE.SEARCH;
+
+ // Determine whether we can autofill the placeholder. The placeholder is a
+ // value that we autofill now, when the search starts and before we wait on
+ // its first result, in order to prevent a flicker in the input caused by
+ // the previous autofilled substring disappearing and reappearing when the
+ // first result arrives. Of course we can only autofill the placeholder if
+ // it starts with the new search string, and we shouldn't autofill anything
+ // if the caret isn't at the end of the input.
+ if (
+ !allowAutofill ||
+ this._autofillPlaceholder.length <= value.length ||
+ !this._autofillPlaceholder
+ .toLocaleLowerCase()
+ .startsWith(value.toLocaleLowerCase())
+ ) {
+ this._autofillPlaceholder = "";
+ } else if (
+ this._autofillPlaceholder &&
+ this.selectionEnd == this.value.length &&
+ this._enableAutofillPlaceholder
+ ) {
+ let autofillValue =
+ value + this._autofillPlaceholder.substring(value.length);
+ this._autofillValue(autofillValue, value.length, autofillValue.length);
+ }
+
+ return allowAutofill;
+ }
+
+ _checkForRtlText(value) {
+ let directionality = this.window.windowUtils.getDirectionFromText(value);
+ if (directionality == this.window.windowUtils.DIRECTION_RTL) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Invoked on overflow/underflow/scrollend events to update attributes
+ * related to the input text directionality. Overflow fade masks use these
+ * attributes to appear at the proper side of the urlbar.
+ */
+ _updateTextOverflow() {
+ if (!this._overflowing) {
+ this.removeAttribute("textoverflow");
+ return;
+ }
+
+ let isRTL =
+ this.getAttribute("domaindir") != "ltr" &&
+ this._checkForRtlText(this.value);
+
+ this.window.promiseDocumentFlushed(() => {
+ // Check overflow again to ensure it didn't change in the meanwhile.
+ let input = this.inputField;
+ if (input && this._overflowing) {
+ // Normally we would overflow at the final side of text direction,
+ // though RTL domains may cause us to overflow at the opposite side.
+ // This happens dynamically as a consequence of the input field contents
+ // and the call to _ensureFormattedHostVisible, this code only reports
+ // the final state of all that scrolling into an attribute, because
+ // there's no other way to capture this in css.
+ // Note it's also possible to scroll an unfocused input field using
+ // SHIFT + mousewheel on Windows, or with just the mousewheel / touchpad
+ // scroll (without modifiers) on Mac.
+ let side = "both";
+ if (isRTL) {
+ if (input.scrollLeft == 0) {
+ side = "left";
+ } else if (input.scrollLeft == input.scrollLeftMin) {
+ side = "right";
+ }
+ } else if (input.scrollLeft == 0) {
+ side = "right";
+ } else if (input.scrollLeft == input.scrollLeftMax) {
+ side = "left";
+ }
+
+ this.window.requestAnimationFrame(() => {
+ // And check once again, since we might have stopped overflowing
+ // since the promiseDocumentFlushed callback fired.
+ if (this._overflowing) {
+ this.setAttribute("textoverflow", side);
+ }
+ });
+ }
+ });
+ }
+
+ _updateUrlTooltip() {
+ if (this.focused || !this._overflowing) {
+ this.inputField.removeAttribute("title");
+ } else {
+ this.inputField.setAttribute("title", this.untrimmedValue);
+ }
+ }
+
+ _getSelectedValueForClipboard() {
+ let selection = this.editor.selection;
+ const flags =
+ Ci.nsIDocumentEncoder.OutputPreformatted |
+ Ci.nsIDocumentEncoder.OutputRaw;
+ let selectedVal = selection.toStringWithFormat("text/plain", flags, 0);
+
+ // Handle multiple-range selection as a string for simplicity.
+ if (selection.rangeCount > 1) {
+ return selectedVal;
+ }
+
+ // If the selection doesn't start at the beginning or doesn't span the
+ // full domain or the URL bar is modified or there is no text at all,
+ // nothing else to do here.
+ if (this.selectionStart > 0 || this.valueIsTyped || selectedVal == "") {
+ return selectedVal;
+ }
+
+ // The selection doesn't span the full domain if it doesn't contain a slash and is
+ // followed by some character other than a slash.
+ if (!selectedVal.includes("/")) {
+ let remainder = this.value.replace(selectedVal, "");
+ if (remainder != "" && remainder[0] != "/") {
+ return selectedVal;
+ }
+ }
+
+ let uri;
+ if (this.getAttribute("pageproxystate") == "valid") {
+ uri = this.window.gBrowser.currentURI;
+ } else {
+ // The value could be:
+ // 1. a trimmed url, set by selecting a result
+ // 2. a search string set by selecting a result
+ // 3. a url that was confirmed but didn't finish loading yet
+ // If it's an url the untrimmedValue should resolve to a valid URI,
+ // otherwise it's a search string that should be copied as-is.
+ try {
+ uri = Services.io.newURI(this._untrimmedValue);
+ } catch (ex) {
+ return selectedVal;
+ }
+ }
+ uri = this.makeURIReadable(uri);
+ let displaySpec = uri.displaySpec;
+
+ // If the entire URL is selected, just use the actual loaded URI,
+ // unless we want a decoded URI, or it's a data: or javascript: URI,
+ // since those are hard to read when encoded.
+ if (
+ this.value == selectedVal &&
+ !uri.schemeIs("javascript") &&
+ !uri.schemeIs("data") &&
+ !UrlbarPrefs.get("decodeURLsOnCopy")
+ ) {
+ return displaySpec;
+ }
+
+ // Just the beginning of the URL is selected, or we want a decoded
+ // url. First check for a trimmed value.
+
+ if (
+ !selectedVal.startsWith(BrowserUtils.trimURLProtocol) &&
+ // Note _trimValue may also trim a trailing slash, thus we can't just do
+ // a straight string compare to tell if the protocol was trimmed.
+ !displaySpec.startsWith(this._trimValue(displaySpec))
+ ) {
+ selectedVal = BrowserUtils.trimURLProtocol + selectedVal;
+ }
+
+ return selectedVal;
+ }
+
+ _toggleActionOverride(event) {
+ // Ignore repeated KeyboardEvents.
+ if (event.repeat) {
+ return;
+ }
+ if (
+ event.keyCode == KeyEvent.DOM_VK_SHIFT ||
+ event.keyCode == KeyEvent.DOM_VK_ALT ||
+ event.keyCode ==
+ (AppConstants.platform == "macosx"
+ ? KeyEvent.DOM_VK_META
+ : KeyEvent.DOM_VK_CONTROL)
+ ) {
+ if (event.type == "keydown") {
+ this._actionOverrideKeyCount++;
+ this.setAttribute("actionoverride", "true");
+ this.view.panel.setAttribute("actionoverride", "true");
+ } else if (
+ this._actionOverrideKeyCount &&
+ --this._actionOverrideKeyCount == 0
+ ) {
+ this._clearActionOverride();
+ }
+ }
+ }
+
+ _clearActionOverride() {
+ this._actionOverrideKeyCount = 0;
+ this.removeAttribute("actionoverride");
+ this.view.panel.removeAttribute("actionoverride");
+ }
+
+ /**
+ * Get the url to load for the search query and records in telemetry that it
+ * is being loaded.
+ *
+ * @param {nsISearchEngine} engine
+ * The engine to generate the query for.
+ * @param {Event} event
+ * The event that triggered this query.
+ * @param {object} searchActionDetails
+ * The details associated with this search query.
+ * @param {boolean} searchActionDetails.isSuggestion
+ * True if this query was initiated from a suggestion from the search engine.
+ * @param {boolean} searchActionDetails.alias
+ * True if this query was initiated via a search alias.
+ * @param {boolean} searchActionDetails.isFormHistory
+ * True if this query was initiated from a form history result.
+ * @param {string} searchActionDetails.url
+ * The url this query was triggered with.
+ */
+ _recordSearch(engine, event, searchActionDetails = {}) {
+ const isOneOff = this.view.oneOffSearchButtons.eventTargetIsAOneOff(event);
+
+ BrowserSearchTelemetry.recordSearch(
+ this.window.gBrowser.selectedBrowser,
+ engine,
+ // Without checking !isOneOff, we might record the string
+ // oneoff_urlbar-searchmode in the SEARCH_COUNTS probe (in addition to
+ // oneoff_urlbar and oneoff_searchbar). The extra information is not
+ // necessary; the intent is the same regardless of whether the user is
+ // in search mode when they do a key-modified click/enter on a one-off.
+ this.searchMode && !isOneOff ? "urlbar-searchmode" : "urlbar",
+ { ...searchActionDetails, isOneOff }
+ );
+ }
+
+ /**
+ * Shortens the given value, usually by removing http:// and trailing slashes.
+ *
+ * @param {string} val
+ * The string to be trimmed if it appears to be URI
+ * @returns {string}
+ * The trimmed string
+ */
+ _trimValue(val) {
+ return UrlbarPrefs.get("trimURLs") ? BrowserUtils.trimURL(val) : val;
+ }
+
+ /**
+ * If appropriate, this prefixes a search string with 'www.' and suffixes it
+ * with browser.fixup.alternate.suffix prior to navigating.
+ *
+ * @param {Event} event
+ * The event that triggered this query.
+ * @param {string} value
+ * The search string that should be canonized.
+ * @returns {string}
+ * Returns the canonized URL if available and null otherwise.
+ */
+ _maybeCanonizeURL(event, value) {
+ // Only add the suffix when the URL bar value isn't already "URL-like",
+ // and only if we get a keyboard event, to match user expectations.
+ if (
+ !(event instanceof KeyboardEvent) ||
+ event._disableCanonization ||
+ !event.ctrlKey ||
+ !UrlbarPrefs.get("ctrlCanonizesURLs") ||
+ !/^\s*[^.:\/\s]+(?:\/.*|\s*)$/i.test(value)
+ ) {
+ return null;
+ }
+
+ let suffix = Services.prefs.getCharPref("browser.fixup.alternate.suffix");
+ if (!suffix.endsWith("/")) {
+ suffix += "/";
+ }
+
+ // trim leading/trailing spaces (bug 233205)
+ value = value.trim();
+
+ // Tack www. and suffix on. If user has appended directories, insert
+ // suffix before them (bug 279035). Be careful not to get two slashes.
+ let firstSlash = value.indexOf("/");
+ if (firstSlash >= 0) {
+ value =
+ value.substring(0, firstSlash) +
+ suffix +
+ value.substring(firstSlash + 1);
+ } else {
+ value = value + suffix;
+ }
+
+ try {
+ const info = Services.uriFixup.getFixupURIInfo(
+ value,
+ Ci.nsIURIFixup.FIXUP_FLAGS_MAKE_ALTERNATE_URI
+ );
+ value = info.fixedURI.spec;
+ } catch (ex) {
+ Cu.reportError(
+ `An error occured while trying to fixup "${value}": ${ex}`
+ );
+ }
+
+ this.value = value;
+ return value;
+ }
+
+ /**
+ * Autofills a value into the input. The value will be autofilled regardless
+ * of the input's current value.
+ *
+ * @param {string} value
+ * The value to autofill.
+ * @param {integer} selectionStart
+ * The new selectionStart.
+ * @param {integer} selectionEnd
+ * The new selectionEnd.
+ */
+ _autofillValue(value, selectionStart, selectionEnd) {
+ // The autofilled value may be a URL that includes a scheme at the
+ // beginning. Do not allow it to be trimmed.
+ this._setValue(value, false);
+ this.selectionStart = selectionStart;
+ this.selectionEnd = selectionEnd;
+ this._autofillPlaceholder = value;
+ }
+
+ /**
+ * Loads the url in the appropriate place.
+ *
+ * @param {string} url
+ * The URL to open.
+ * @param {Event} event
+ * The event that triggered to load the url.
+ * @param {string} openUILinkWhere
+ * Where we expect the result to be opened.
+ * @param {object} params
+ * The parameters related to how and where the result will be opened.
+ * Further supported paramters are listed in utilityOverlay.js#openUILinkIn.
+ * @param {object} params.triggeringPrincipal
+ * The principal that the action was triggered from.
+ * @param {nsIInputStream} [params.postData]
+ * The POST data associated with a search submission.
+ * @param {boolean} [params.allowInheritPrincipal]
+ * Whether the principal can be inherited.
+ * @param {object} [resultDetails]
+ * Details of the selected result, if any.
+ * @param {UrlbarUtils.RESULT_TYPE} [result.type]
+ * Details of the result type, if any.
+ * @param {UrlbarUtils.RESULT_SOURCE} [result.source]
+ * Details of the result source, if any.
+ * @param {object} browser [optional] the browser to use for the load.
+ */
+ _loadURL(
+ url,
+ event,
+ openUILinkWhere,
+ params,
+ resultDetails = null,
+ browser = this.window.gBrowser.selectedBrowser
+ ) {
+ // No point in setting these because we'll handleRevert() a few rows below.
+ if (openUILinkWhere == "current") {
+ this.value = url;
+ browser.userTypedValue = url;
+ }
+
+ // No point in setting this if we are loading in a new window.
+ if (
+ openUILinkWhere != "window" &&
+ this.window.gInitialPages.includes(url)
+ ) {
+ browser.initialPageLoadedFromUserAction = url;
+ }
+
+ try {
+ UrlbarUtils.addToUrlbarHistory(url, this.window);
+ } catch (ex) {
+ // Things may go wrong when adding url to session history,
+ // but don't let that interfere with the loading of the url.
+ Cu.reportError(ex);
+ }
+
+ // TODO: When bug 1498553 is resolved, we should be able to
+ // remove the !triggeringPrincipal condition here.
+ if (
+ !params.triggeringPrincipal ||
+ params.triggeringPrincipal.isSystemPrincipal
+ ) {
+ // Reset DOS mitigations for the basic auth prompt.
+ delete browser.authPromptAbuseCounter;
+
+ // Reset temporary permissions on the current tab if the user reloads
+ // the tab via the urlbar.
+ if (
+ openUILinkWhere == "current" &&
+ browser.currentURI &&
+ url === browser.currentURI.spec
+ ) {
+ this.window.SitePermissions.clearTemporaryPermissions(browser);
+ }
+ }
+
+ params.allowThirdPartyFixup = true;
+
+ if (openUILinkWhere == "current") {
+ params.targetBrowser = browser;
+ params.indicateErrorPageLoad = true;
+ params.allowPinnedTabHostChange = true;
+ params.allowPopups = url.startsWith("javascript:");
+ } else {
+ params.initiatingDoc = this.window.document;
+ }
+
+ if (event?.keyCode === KeyEvent.DOM_VK_RETURN) {
+ if (openUILinkWhere === "current") {
+ params.avoidBrowserFocus = true;
+ this._keyDownEnterDeferred?.resolve(browser);
+ }
+ }
+
+ // Focus the content area before triggering loads, since if the load
+ // occurs in a new tab, we want focus to be restored to the content
+ // area when the current tab is re-selected.
+ if (!params.avoidBrowserFocus) {
+ browser.focus();
+ // Make sure the domain name stays visible for spoof protection and usability.
+ this.selectionStart = this.selectionEnd = 0;
+ }
+
+ if (openUILinkWhere != "current") {
+ this.handleRevert();
+ }
+
+ // Notify about the start of navigation.
+ this._notifyStartNavigation(resultDetails);
+
+ try {
+ this.window.openTrustedLinkIn(url, openUILinkWhere, params);
+ } catch (ex) {
+ // This load can throw an exception in certain cases, which means
+ // we'll want to replace the URL with the loaded URL:
+ if (ex.result != Cr.NS_ERROR_LOAD_SHOWED_ERRORPAGE) {
+ this.handleRevert();
+ }
+ }
+
+ this.view.close();
+ }
+
+ /**
+ * Determines where a URL/page should be opened.
+ *
+ * @param {Event} event the event triggering the opening.
+ * @returns {"current" | "tabshifted" | "tab" | "save" | "window"}
+ */
+ _whereToOpen(event) {
+ let isKeyboardEvent = event instanceof KeyboardEvent;
+ let reuseEmpty = isKeyboardEvent;
+ let where = undefined;
+ if (
+ isKeyboardEvent &&
+ (event.altKey || event.getModifierState("AltGraph"))
+ ) {
+ // We support using 'alt' to open in a tab, because ctrl/shift
+ // might be used for canonizing URLs:
+ where = event.shiftKey ? "tabshifted" : "tab";
+ } else if (
+ isKeyboardEvent &&
+ event.ctrlKey &&
+ UrlbarPrefs.get("ctrlCanonizesURLs")
+ ) {
+ // If we're allowing canonization, and this is a key event with ctrl
+ // pressed, open in current tab to allow ctrl-enter to canonize URL.
+ where = "current";
+ } else {
+ where = this.window.whereToOpenLink(event, false, false);
+ }
+ if (UrlbarPrefs.get("openintab")) {
+ if (where == "current") {
+ where = "tab";
+ } else if (where == "tab") {
+ where = "current";
+ }
+ reuseEmpty = true;
+ }
+ if (
+ where == "tab" &&
+ reuseEmpty &&
+ this.window.gBrowser.selectedTab.isEmpty
+ ) {
+ where = "current";
+ }
+ return where;
+ }
+
+ _initCopyCutController() {
+ this._copyCutController = new CopyCutController(this);
+ this.inputField.controllers.insertControllerAt(0, this._copyCutController);
+ }
+
+ _initPasteAndGo() {
+ let inputBox = this.querySelector("moz-input-box");
+ let contextMenu = inputBox.menupopup;
+ let insertLocation = contextMenu.firstElementChild;
+ while (
+ insertLocation.nextElementSibling &&
+ insertLocation.getAttribute("cmd") != "cmd_paste"
+ ) {
+ insertLocation = insertLocation.nextElementSibling;
+ }
+ if (!insertLocation) {
+ return;
+ }
+
+ let pasteAndGo = this.document.createXULElement("menuitem");
+ pasteAndGo.id = "paste-and-go";
+ let label = Services.strings
+ .createBundle("chrome://browser/locale/browser.properties")
+ .GetStringFromName("pasteAndGo.label");
+ pasteAndGo.setAttribute("label", label);
+ pasteAndGo.setAttribute("anonid", "paste-and-go");
+ pasteAndGo.addEventListener("command", () => {
+ this._suppressStartQuery = true;
+
+ this.select();
+ this.window.goDoCommand("cmd_paste");
+ this.setResultForCurrentValue(null);
+ this.handleCommand();
+
+ this._suppressStartQuery = false;
+ });
+
+ contextMenu.addEventListener("popupshowing", () => {
+ // Close the results pane when the input field contextual menu is open,
+ // because paste and go doesn't want a result selection.
+ this.view.close();
+
+ let controller = this.document.commandDispatcher.getControllerForCommand(
+ "cmd_paste"
+ );
+ let enabled = controller.isCommandEnabled("cmd_paste");
+ if (enabled) {
+ pasteAndGo.removeAttribute("disabled");
+ } else {
+ pasteAndGo.setAttribute("disabled", "true");
+ }
+ });
+
+ insertLocation.insertAdjacentElement("afterend", pasteAndGo);
+ }
+
+ /**
+ * This notifies observers that the user has entered or selected something in
+ * the URL bar which will cause navigation.
+ *
+ * We use the observer service, so that we don't need to load extra facilities
+ * if they aren't being used, e.g. WebNavigation.
+ *
+ * @param {UrlbarResult} result
+ * Details of the result that was selected, if any.
+ */
+ _notifyStartNavigation(result) {
+ Services.obs.notifyObservers({ result }, "urlbar-user-start-navigation");
+ }
+
+ /**
+ * Returns a search mode object if a result should enter search mode when
+ * selected.
+ *
+ * @param {UrlbarResult} result
+ * @param {string} [entry]
+ * If provided, this will be recorded as the entry point into search mode.
+ * See setSearchMode() documentation for details.
+ * @returns {object} A search mode object. Null if search mode should not be
+ * entered. See setSearchMode documentation for details.
+ */
+ _searchModeForResult(result, entry = null) {
+ // Search mode is determined by the result's keyword or engine.
+ if (!result.payload.keyword && !result.payload.engine) {
+ return null;
+ }
+
+ let searchMode = UrlbarUtils.searchModeForToken(result.payload.keyword);
+ // If result.originalEngine is set, then the user is Alt+Tabbing
+ // through the one-offs, so the keyword doesn't match the engine.
+ if (
+ !searchMode &&
+ result.payload.engine &&
+ (!result.payload.originalEngine ||
+ result.payload.engine == result.payload.originalEngine)
+ ) {
+ searchMode = { engineName: result.payload.engine };
+ }
+
+ if (searchMode) {
+ if (entry) {
+ searchMode.entry = entry;
+ } else {
+ switch (result.providerName) {
+ case "UrlbarProviderTopSites":
+ searchMode.entry = "topsites_urlbar";
+ break;
+ case "TabToSearch":
+ if (result.payload.dynamicType) {
+ searchMode.entry = "tabtosearch_onboard";
+ } else {
+ searchMode.entry = "tabtosearch";
+ }
+ break;
+ default:
+ searchMode.entry = "keywordoffer";
+ break;
+ }
+ }
+ }
+
+ return searchMode;
+ }
+
+ /**
+ * Updates the UI so that search mode is either entered or exited.
+ *
+ * @param {object} searchMode
+ * See setSearchMode documentation. If null, then search mode is exited.
+ */
+ _updateSearchModeUI(searchMode) {
+ let { engineName, source } = searchMode || {};
+
+ // As an optimization, bail if the given search mode is null but search mode
+ // is already inactive. Otherwise browser_preferences_usage.js fails due to
+ // accessing the browser.urlbar.placeholderName pref (via the call to
+ // BrowserSearch.initPlaceHolder below) too many times. That test does not
+ // enter search mode, but it triggers many calls to this method with a null
+ // search mode, via setURI.
+ if (!engineName && !source && !this.hasAttribute("searchmode")) {
+ return;
+ }
+
+ this._searchModeIndicatorTitle.textContent = "";
+ this._searchModeLabel.textContent = "";
+ this._searchModeIndicatorTitle.removeAttribute("data-l10n-id");
+ this._searchModeLabel.removeAttribute("data-l10n-id");
+
+ if (!engineName && !source) {
+ try {
+ // This will throw before DOMContentLoaded in
+ // PrivateBrowsingUtils.privacyContextFromWindow because
+ // aWindow.docShell is null.
+ this.window.BrowserSearch.initPlaceHolder(true);
+ } catch (ex) {}
+ this.removeAttribute("searchmode");
+ return;
+ }
+
+ if (engineName) {
+ // Set text content for the search mode indicator.
+ this._searchModeIndicatorTitle.textContent = engineName;
+ this._searchModeLabel.textContent = engineName;
+ this.document.l10n.setAttributes(
+ this.inputField,
+ UrlbarUtils.WEB_ENGINE_NAMES.has(engineName)
+ ? "urlbar-placeholder-search-mode-web-2"
+ : "urlbar-placeholder-search-mode-other-engine",
+ { name: engineName }
+ );
+ } else if (source) {
+ let sourceName = UrlbarUtils.getResultSourceName(source);
+ let l10nID = `urlbar-search-mode-${sourceName}`;
+ this.document.l10n.setAttributes(this._searchModeIndicatorTitle, l10nID);
+ this.document.l10n.setAttributes(this._searchModeLabel, l10nID);
+ this.document.l10n.setAttributes(
+ this.inputField,
+ `urlbar-placeholder-search-mode-other-${sourceName}`
+ );
+ }
+
+ this.toggleAttribute("searchmode", true);
+ // Clear autofill.
+ if (this._autofillPlaceholder && this.window.gBrowser.userTypedValue) {
+ this.value = this.window.gBrowser.userTypedValue;
+ }
+ // Search mode should only be active when pageproxystate is invalid.
+ if (this.getAttribute("pageproxystate") == "valid") {
+ this.value = "";
+ this.setPageProxyState("invalid", true);
+ }
+ }
+
+ /**
+ * Determines if we should select all the text in the Urlbar based on the
+ * Urlbar state, and whether the selection is empty.
+ */
+ _maybeSelectAll() {
+ if (
+ !this._preventClickSelectsAll &&
+ this._compositionState != UrlbarUtils.COMPOSITION.COMPOSING &&
+ this.document.activeElement == this.inputField &&
+ this.inputField.selectionStart == this.inputField.selectionEnd
+ ) {
+ this.select();
+ }
+ }
+
+ // Event handlers below.
+
+ _on_command(event) {
+ // Something is executing a command, likely causing a focus change. This
+ // should not be recorded as an abandonment. If the user is entering search
+ // mode from a one-off, then they are in the same engagement and we should
+ // not discard.
+ if (
+ !event.target.classList.contains("searchbar-engine-one-off-item") ||
+ this.searchMode?.entry != "oneoff"
+ ) {
+ this.controller.engagementEvent.discard();
+ }
+ }
+
+ _on_blur(event) {
+ this.focusedViaMousedown = false;
+ // We cannot count every blur events after a missed engagement as abandoment
+ // because the user may have clicked on some view element that executes
+ // a command causing a focus change. For example opening preferences from
+ // the oneoff settings button.
+ // For now we detect that case by discarding the event on command, but we
+ // may want to figure out a more robust way to detect abandonment.
+ this.controller.engagementEvent.record(event, {
+ searchString: this._lastSearchString,
+ });
+
+ this.removeAttribute("focused");
+ this.endLayoutExtend();
+
+ if (this._autofillPlaceholder && this.window.gBrowser.userTypedValue) {
+ // If we were autofilling, remove the autofilled portion, by restoring
+ // the value to the last typed one.
+ this.value = this.window.gBrowser.userTypedValue;
+ } else if (this.value == this._focusUntrimmedValue) {
+ // If the value was untrimmed by _on_focus and didn't change, trim it.
+ this.value = this._focusUntrimmedValue;
+ }
+ this._focusUntrimmedValue = null;
+
+ this.formatValue();
+ this._resetSearchState();
+
+ // In certain cases, like holding an override key and confirming an entry,
+ // we don't key a keyup event for the override key, thus we make this
+ // additional cleanup on blur.
+ this._clearActionOverride();
+
+ // The extension input sessions depends more on blur than on the fact we
+ // actually cancel a running query, so we do it here.
+ if (ExtensionSearchHandler.hasActiveInputSession()) {
+ ExtensionSearchHandler.handleInputCancelled();
+ }
+
+ // Respect the autohide preference for easier inspecting/debugging via
+ // the browser toolbox.
+ if (!UrlbarPrefs.get("ui.popup.disable_autohide")) {
+ this.view.close();
+ }
+
+ if (this._revertOnBlurValue == this.value) {
+ this.handleRevert();
+ }
+ this._revertOnBlurValue = null;
+
+ // We may have hidden popup notifications, show them again if necessary.
+ if (
+ this.getAttribute("pageproxystate") != "valid" &&
+ this.window.UpdatePopupNotificationsVisibility
+ ) {
+ this.window.UpdatePopupNotificationsVisibility();
+ }
+
+ // If user move the focus to another component while pressing Enter key,
+ // then keyup at that component, as we can't get the event, clear the promise.
+ if (this._keyDownEnterDeferred) {
+ this._keyDownEnterDeferred.resolve();
+ this._keyDownEnterDeferred = null;
+ }
+ this._isKeyDownWithCtrl = false;
+
+ Services.obs.notifyObservers(null, "urlbar-blur");
+ }
+
+ _on_click(event) {
+ if (
+ event.target == this.inputField ||
+ event.target == this._inputContainer ||
+ event.target.id == SEARCH_BUTTON_ID
+ ) {
+ this._maybeSelectAll();
+ }
+
+ if (event.target == this._searchModeIndicatorClose && event.button != 2) {
+ this.searchMode = null;
+ this.view.oneOffSearchButtons.selectedButton = null;
+ if (this.view.isOpen) {
+ this.startQuery({
+ event,
+ });
+ }
+ }
+ }
+
+ _on_contextmenu(event) {
+ // Context menu opened via keyboard shortcut.
+ if (!event.button) {
+ return;
+ }
+
+ this._maybeSelectAll();
+ }
+
+ _on_focus(event) {
+ if (!this._hideFocus) {
+ this.setAttribute("focused", "true");
+ }
+
+ // If the value was trimmed, check whether we should untrim it.
+ // This is necessary when a protocol was typed, but the whole url has
+ // invalid parts, like the origin, then editing and confirming the trimmed
+ // value would execute a search instead of visiting the typed url.
+ if (this.value != this._untrimmedValue) {
+ let untrim = false;
+ let fixedURI = this._getURIFixupInfo(this.value)?.preferredURI;
+ if (fixedURI) {
+ try {
+ let expectedURI = Services.io.newURI(this._untrimmedValue);
+ untrim = fixedURI.displaySpec != expectedURI.displaySpec;
+ } catch (ex) {
+ untrim = true;
+ }
+ }
+ if (untrim) {
+ this.inputField.value = this._focusUntrimmedValue = this._untrimmedValue;
+ }
+ }
+
+ this.startLayoutExtend();
+
+ if (this.focusedViaMousedown) {
+ this.view.autoOpen({ event });
+ } else if (this.inputField.hasAttribute("refocused-by-panel")) {
+ this._maybeSelectAll();
+ }
+
+ this._updateUrlTooltip();
+ this.formatValue();
+
+ // Hide popup notifications, to reduce visual noise.
+ if (
+ this.getAttribute("pageproxystate") != "valid" &&
+ this.window.UpdatePopupNotificationsVisibility
+ ) {
+ this.window.UpdatePopupNotificationsVisibility();
+ }
+
+ Services.obs.notifyObservers(null, "urlbar-focus");
+ }
+
+ _on_mouseover(event) {
+ this._updateUrlTooltip();
+ }
+
+ _on_draggableregionleftmousedown(event) {
+ if (!UrlbarPrefs.get("ui.popup.disable_autohide")) {
+ this.view.close();
+ }
+ }
+
+ _on_mousedown(event) {
+ switch (event.currentTarget) {
+ case this.textbox:
+ this._mousedownOnUrlbarDescendant = true;
+
+ if (
+ event.target != this.inputField &&
+ event.target != this._inputContainer &&
+ event.target.id != SEARCH_BUTTON_ID
+ ) {
+ break;
+ }
+
+ this.focusedViaMousedown = !this.focused;
+ this._preventClickSelectsAll = this.focused;
+
+ if (event.target != this.inputField) {
+ this.focus();
+ }
+
+ // The rest of this case only cares about left clicks.
+ if (event.button != 0) {
+ break;
+ }
+
+ // Clear any previous selection unless we are focused, to ensure it
+ // doesn't affect drag selection.
+ if (this.focusedViaMousedown) {
+ this.selectionStart = this.selectionEnd = 0;
+ }
+
+ if (event.target.id == SEARCH_BUTTON_ID) {
+ this._preventClickSelectsAll = true;
+ this.search(UrlbarTokenizer.RESTRICT.SEARCH);
+ } else {
+ this.view.autoOpen({ event });
+ }
+ break;
+ case this.window:
+ if (this._mousedownOnUrlbarDescendant) {
+ this._mousedownOnUrlbarDescendant = false;
+ break;
+ }
+ // Don't close the view when clicking on a tab; we may want to keep the
+ // view open on tab switch, and the TabSelect event arrived earlier.
+ if (event.target.closest("tab")) {
+ break;
+ }
+ // Close the view when clicking on toolbars and other UI pieces that
+ // might not automatically remove focus from the input.
+ // Respect the autohide preference for easier inspecting/debugging via
+ // the browser toolbox.
+ if (!UrlbarPrefs.get("ui.popup.disable_autohide")) {
+ this.view.close();
+ }
+ break;
+ }
+ }
+
+ _on_input(event) {
+ let value = this.value;
+ this.valueIsTyped = true;
+ this._untrimmedValue = value;
+ this._resultForCurrentValue = null;
+
+ this.window.gBrowser.userTypedValue = value;
+ // Unset userSelectionBehavior because the user is modifying the search
+ // string, thus there's no valid selection. This is also used by the view
+ // to set "aria-activedescendant", thus it should never get stale.
+ this.controller.userSelectionBehavior = "none";
+
+ let compositionState = this._compositionState;
+ let compositionClosedPopup = this._compositionClosedPopup;
+
+ // Clear composition values if we're no more composing.
+ if (this._compositionState != UrlbarUtils.COMPOSITION.COMPOSING) {
+ this._compositionState = UrlbarUtils.COMPOSITION.NONE;
+ this._compositionClosedPopup = false;
+ }
+
+ if (value) {
+ this.setAttribute("usertyping", "true");
+ } else {
+ this.removeAttribute("usertyping");
+ }
+ this.removeAttribute("actiontype");
+
+ if (
+ this.getAttribute("pageproxystate") == "valid" &&
+ this.value != this._lastValidURLStr
+ ) {
+ this.setPageProxyState("invalid", true);
+ }
+
+ if (!this.view.isOpen) {
+ this.view.clear();
+ } else if (!value && !UrlbarPrefs.get("suggest.topsites")) {
+ this.view.clear();
+ if (!this.searchMode || !this.view.oneOffSearchButtons.hasView) {
+ this.view.close();
+ return;
+ }
+ }
+
+ this.view.removeAccessibleFocus();
+
+ // During composition with an IME, the following events happen in order:
+ // 1. a compositionstart event
+ // 2. some input events
+ // 3. a compositionend event
+ // 4. an input event
+
+ // We should do nothing during composition or if composition was canceled
+ // and we didn't close the popup on composition start.
+ if (
+ UrlbarPrefs.get("imeCompositionClosesPanel") &&
+ (compositionState == UrlbarUtils.COMPOSITION.COMPOSING ||
+ (compositionState == UrlbarUtils.COMPOSITION.CANCELED &&
+ !compositionClosedPopup))
+ ) {
+ return;
+ }
+
+ // Autofill only when text is inserted (i.e., event.data is not empty) and
+ // it's not due to pasting.
+ let allowAutofill =
+ !!event.data &&
+ !UrlbarUtils.isPasteEvent(event) &&
+ this._maybeAutofillOnInput(value);
+
+ this.startQuery({
+ searchString: value,
+ allowAutofill,
+ resetSearchState: false,
+ event,
+ });
+ }
+
+ _on_select(event) {
+ // On certain user input, AutoCopyListener::OnSelectionChange() updates
+ // the primary selection with user-selected text (when supported).
+ // Selection::NotifySelectionListeners() then dispatches a "select" event
+ // under similar conditions via TextInputListener::OnSelectionChange().
+ // This event is received here in order to replace the primary selection
+ // from the editor with text having the adjustments of
+ // _getSelectedValueForClipboard(), such as adding the scheme for the url.
+ //
+ // Other "select" events are also received, however, and must be excluded.
+ if (
+ // _suppressPrimaryAdjustment is set during select(). Don't update
+ // the primary selection because that is not the intent of user input,
+ // which may be new tab or urlbar focus.
+ this._suppressPrimaryAdjustment ||
+ // The check on isHandlingUserInput filters out async "select" events
+ // from setSelectionRange(), which occur when autofill text is selected.
+ !this.window.windowUtils.isHandlingUserInput ||
+ !Services.clipboard.supportsSelectionClipboard()
+ ) {
+ return;
+ }
+
+ let val = this._getSelectedValueForClipboard();
+ if (!val) {
+ return;
+ }
+
+ ClipboardHelper.copyStringToClipboard(
+ val,
+ Services.clipboard.kSelectionClipboard
+ );
+ }
+
+ _on_overflow(event) {
+ const targetIsPlaceholder =
+ event.originalTarget.implementedPseudoElement == "::placeholder";
+ // We only care about the non-placeholder text.
+ // This shouldn't be needed, see bug 1487036.
+ if (targetIsPlaceholder) {
+ return;
+ }
+ this._overflowing = true;
+ this._updateTextOverflow();
+ }
+
+ _on_underflow(event) {
+ const targetIsPlaceholder =
+ event.originalTarget.implementedPseudoElement == "::placeholder";
+ // We only care about the non-placeholder text.
+ // This shouldn't be needed, see bug 1487036.
+ if (targetIsPlaceholder) {
+ return;
+ }
+ this._overflowing = false;
+
+ this._updateTextOverflow();
+
+ this._updateUrlTooltip();
+ }
+
+ _on_paste(event) {
+ let originalPasteData = event.clipboardData.getData("text/plain");
+ if (!originalPasteData) {
+ return;
+ }
+
+ let oldValue = this.inputField.value;
+ let oldStart = oldValue.substring(0, this.selectionStart);
+ // If there is already non-whitespace content in the URL bar
+ // preceding the pasted content, it's not necessary to check
+ // protocols used by the pasted content:
+ if (oldStart.trim()) {
+ return;
+ }
+ let oldEnd = oldValue.substring(this.selectionEnd);
+
+ let pasteData = UrlbarUtils.stripUnsafeProtocolOnPaste(originalPasteData);
+ if (originalPasteData != pasteData) {
+ // Unfortunately we're not allowed to set the bits being pasted
+ // so cancel this event:
+ event.preventDefault();
+ event.stopImmediatePropagation();
+
+ this.inputField.value = oldStart + pasteData + oldEnd;
+ // Fix up cursor/selection:
+ let newCursorPos = oldStart.length + pasteData.length;
+ this.selectionStart = newCursorPos;
+ this.selectionEnd = newCursorPos;
+ }
+ }
+
+ _on_scrollend(event) {
+ this._updateTextOverflow();
+ }
+
+ _on_TabSelect(event) {
+ this._gotTabSelect = true;
+ this._afterTabSelectAndFocusChange();
+ }
+
+ _on_keydown(event) {
+ if (event.keyCode === KeyEvent.DOM_VK_RETURN) {
+ if (this._keyDownEnterDeferred) {
+ this._keyDownEnterDeferred.reject();
+ }
+ this._keyDownEnterDeferred = PromiseUtils.defer();
+ event._disableCanonization = this._isKeyDownWithCtrl;
+ } else if (event.keyCode !== KeyEvent.DOM_VK_CONTROL && event.ctrlKey) {
+ this._isKeyDownWithCtrl = true;
+ }
+
+ // Due to event deferring, it's possible preventDefault() won't be invoked
+ // soon enough to actually prevent some of the default behaviors, thus we
+ // have to handle the event "twice". This first immediate call passes false
+ // as second argument so that handleKeyNavigation will only simulate the
+ // event handling, without actually executing actions.
+ // TODO (Bug 1541806): improve this handling, maybe by delaying actions
+ // instead of events.
+ if (this.eventBufferer.shouldDeferEvent(event)) {
+ this.controller.handleKeyNavigation(event, false);
+ }
+ this._toggleActionOverride(event);
+ this.eventBufferer.maybeDeferEvent(event, () => {
+ this.controller.handleKeyNavigation(event);
+ });
+ }
+
+ async _on_keyup(event) {
+ if (
+ event.keyCode === KeyEvent.DOM_VK_RETURN &&
+ this._keyDownEnterDeferred
+ ) {
+ try {
+ const loadingBrowser = await this._keyDownEnterDeferred.promise;
+ // Ensure the selected browser didn't change in the meanwhile.
+ if (this.window.gBrowser.selectedBrowser === loadingBrowser) {
+ loadingBrowser.focus();
+ // Make sure the domain name stays visible for spoof protection and usability.
+ this.selectionStart = this.selectionEnd = 0;
+ }
+ this._keyDownEnterDeferred = null;
+ } catch (ex) {
+ // Not all the Enter actions in the urlbar will cause a navigation, then it
+ // is normal for this to be rejected.
+ // If _keyDownEnterDeferred was rejected on keydown, we don't nullify it here
+ // to ensure not overwriting the new value created by keydown.
+ }
+ return;
+ } else if (event.keyCode === KeyEvent.DOM_VK_CONTROL) {
+ this._isKeyDownWithCtrl = false;
+ }
+
+ this._toggleActionOverride(event);
+ }
+
+ _on_compositionstart(event) {
+ if (this._compositionState == UrlbarUtils.COMPOSITION.COMPOSING) {
+ throw new Error("Trying to start a nested composition?");
+ }
+ this._compositionState = UrlbarUtils.COMPOSITION.COMPOSING;
+
+ if (!UrlbarPrefs.get("imeCompositionClosesPanel")) {
+ return;
+ }
+
+ // Close the view. This will also stop searching.
+ if (this.view.isOpen) {
+ // We're closing the view, but we want to retain search mode if the
+ // selected result was previewing it.
+ if (this.searchMode) {
+ // If we entered search mode with an empty string, clear userTypedValue,
+ // otherwise confirmSearchMode may try to set it as value.
+ // This can happen for example if we entered search mode typing a
+ // a partial engine domain and selecting a tab-to-search result.
+ if (!this.value) {
+ this.window.gBrowser.userTypedValue = null;
+ }
+ this.confirmSearchMode();
+ }
+ this._compositionClosedPopup = true;
+ this.view.close();
+ } else {
+ this._compositionClosedPopup = false;
+ }
+ }
+
+ _on_compositionend(event) {
+ if (this._compositionState != UrlbarUtils.COMPOSITION.COMPOSING) {
+ throw new Error("Trying to stop a non existing composition?");
+ }
+
+ if (UrlbarPrefs.get("imeCompositionClosesPanel")) {
+ // Clear the selection and the cached result, since they refer to the
+ // state before this composition. A new input even will be generated
+ // after this.
+ this.view.clearSelection();
+ this._resultForCurrentValue = null;
+ }
+
+ // We can't yet retrieve the committed value from the editor, since it isn't
+ // completely committed yet. We'll handle it at the next input event.
+ this._compositionState = event.data
+ ? UrlbarUtils.COMPOSITION.COMMIT
+ : UrlbarUtils.COMPOSITION.CANCELED;
+ }
+
+ _on_dragstart(event) {
+ // Drag only if the gesture starts from the input field.
+ let nodePosition = this.inputField.compareDocumentPosition(
+ event.originalTarget
+ );
+ if (
+ event.target != this.inputField &&
+ !(nodePosition & Node.DOCUMENT_POSITION_CONTAINED_BY)
+ ) {
+ return;
+ }
+
+ // Don't cover potential drop targets on the toolbars or in content.
+ this.view.close();
+
+ // Only customize the drag data if the entire value is selected and it's a
+ // loaded URI. Use default behavior otherwise.
+ if (
+ this.selectionStart != 0 ||
+ this.selectionEnd != this.inputField.textLength ||
+ this.getAttribute("pageproxystate") != "valid"
+ ) {
+ return;
+ }
+
+ let href = this.window.gBrowser.currentURI.displaySpec;
+ let title = this.window.gBrowser.contentTitle || href;
+
+ event.dataTransfer.setData("text/x-moz-url", `${href}\n${title}`);
+ event.dataTransfer.setData("text/unicode", href);
+ event.dataTransfer.setData("text/html", `<a href="${href}">${title}</a>`);
+ event.dataTransfer.effectAllowed = "copyLink";
+ event.stopPropagation();
+ }
+
+ _on_dragover(event) {
+ if (!getDroppableData(event)) {
+ event.dataTransfer.dropEffect = "none";
+ }
+ }
+
+ _on_drop(event) {
+ let droppedItem = getDroppableData(event);
+ let droppedURL =
+ droppedItem instanceof URL ? droppedItem.href : droppedItem;
+ if (droppedURL && droppedURL !== this.window.gBrowser.currentURI.spec) {
+ let principal = Services.droppedLinkHandler.getTriggeringPrincipal(event);
+ this.value = droppedURL;
+ this.setPageProxyState("invalid");
+ this.focus();
+ // To simplify tracking of events, register an initial event for event
+ // telemetry, to replace the missing input event.
+ this.controller.engagementEvent.start(event);
+ this.handleNavigation({ triggeringPrincipal: principal });
+ // For safety reasons, in the drop case we don't want to immediately show
+ // the the dropped value, instead we want to keep showing the current page
+ // url until an onLocationChange happens.
+ // See the handling in `setURI` for further details.
+ this.window.gBrowser.userTypedValue = null;
+ this.setURI(null, true);
+ }
+ }
+
+ _on_customizationstarting() {
+ this.blur();
+
+ this.inputField.controllers.removeController(this._copyCutController);
+ delete this._copyCutController;
+ }
+
+ _on_aftercustomization() {
+ this._initCopyCutController();
+ this._initPasteAndGo();
+ }
+}
+
+/**
+ * Tries to extract droppable data from a DND event.
+ * @param {Event} event The DND event to examine.
+ * @returns {URL|string|null}
+ * null if there's a security reason for which we should do nothing.
+ * A URL object if it's a value we can load.
+ * A string value otherwise.
+ */
+function getDroppableData(event) {
+ let links;
+ try {
+ links = Services.droppedLinkHandler.dropLinks(event);
+ } catch (ex) {
+ // This is either an unexpected failure or a security exception; in either
+ // case we should always return null.
+ return null;
+ }
+ // The URL bar automatically handles inputs with newline characters,
+ // so we can get away with treating text/x-moz-url flavours as text/plain.
+ if (links.length && links[0].url) {
+ event.preventDefault();
+ let href = links[0].url;
+ if (UrlbarUtils.stripUnsafeProtocolOnPaste(href) != href) {
+ // We may have stripped an unsafe protocol like javascript: and if so
+ // there's no point in handling a partial drop.
+ event.stopImmediatePropagation();
+ return null;
+ }
+
+ try {
+ // If this throws, urlSecurityCheck would also throw, as that's what it
+ // does with things that don't pass the IO service's newURI constructor
+ // without fixup. It's conceivable we may want to relax this check in
+ // the future (so e.g. www.foo.com gets fixed up), but not right now.
+ let url = new URL(href);
+ // If we succeed, try to pass security checks. If this works, return the
+ // URL object. If the *security checks* fail, return null.
+ try {
+ let principal = Services.droppedLinkHandler.getTriggeringPrincipal(
+ event
+ );
+ BrowserUtils.urlSecurityCheck(
+ url,
+ principal,
+ Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL
+ );
+ return url;
+ } catch (ex) {
+ return null;
+ }
+ } catch (ex) {
+ // We couldn't make a URL out of this. Continue on, and return text below.
+ }
+ }
+ // Handle as text.
+ return event.dataTransfer.getData("text/unicode");
+}
+
+/**
+ * Decodes the given URI for displaying it in the address bar without losing
+ * information, such that hitting Enter again will load the same URI.
+ *
+ * @param {nsIURI} aURI
+ * The URI to decode
+ * @returns {string}
+ * The decoded URI
+ */
+function losslessDecodeURI(aURI) {
+ let scheme = aURI.scheme;
+ let value = aURI.displaySpec;
+
+ // Try to decode as UTF-8 if there's no encoding sequence that we would break.
+ if (!/%25(?:3B|2F|3F|3A|40|26|3D|2B|24|2C|23)/i.test(value)) {
+ let decodeASCIIOnly = !["https", "http", "file", "ftp"].includes(scheme);
+ if (decodeASCIIOnly) {
+ // This only decodes ascii characters (hex) 20-7e, except 25 (%).
+ // This avoids both cases stipulated below (%-related issues, and \r, \n
+ // and \t, which would be %0d, %0a and %09, respectively) as well as any
+ // non-US-ascii characters.
+ value = value.replace(
+ /%(2[0-4]|2[6-9a-f]|[3-6][0-9a-f]|7[0-9a-e])/g,
+ decodeURI
+ );
+ } else {
+ try {
+ value = decodeURI(value)
+ // decodeURI decodes %25 to %, which creates unintended encoding
+ // sequences. Re-encode it, unless it's part of a sequence that
+ // survived decodeURI, i.e. one for:
+ // ';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '#'
+ // (RFC 3987 section 3.2)
+ .replace(
+ /%(?!3B|2F|3F|3A|40|26|3D|2B|24|2C|23)/gi,
+ encodeURIComponent
+ );
+ } catch (e) {}
+ }
+ }
+
+ // Encode potentially invisible characters:
+ // U+0000-001F: C0/C1 control characters
+ // U+007F-009F: commands
+ // U+00A0, U+1680, U+2000-200A, U+202F, U+205F, U+3000: other spaces
+ // U+2028-2029: line and paragraph separators
+ // U+2800: braille empty pattern
+ // U+FFFC: object replacement character
+ // Encode any trailing whitespace that may be part of a pasted URL, so that it
+ // doesn't get eaten away by the location bar (bug 410726).
+ // Encode all adjacent space chars (U+0020), to prevent spoofing attempts
+ // where they would push part of the URL to overflow the location bar
+ // (bug 1395508). A single space, or the last space if the are many, is
+ // preserved to maintain readability of certain urls. We only do this for the
+ // common space, because others may be eaten when copied to the clipboard, so
+ // it's safer to preserve them encoded.
+ value = value.replace(
+ // eslint-disable-next-line no-control-regex
+ /[\u0000-\u001f\u007f-\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u2800\u3000\ufffc]|[\r\n\t]|\u0020(?=\u0020)|\s$/g,
+ encodeURIComponent
+ );
+
+ // Encode characters that are ignorable, can't be rendered usefully, or may
+ // confuse users.
+ //
+ // Default ignorable characters; ZWNJ (U+200C) and ZWJ (U+200D) are excluded
+ // per bug 582186:
+ // U+00AD, U+034F, U+06DD, U+070F, U+115F-1160, U+17B4, U+17B5, U+180B-180E,
+ // U+2060, U+FEFF, U+200B, U+2060-206F, U+3164, U+FE00-FE0F, U+FFA0,
+ // U+FFF0-FFFB, U+1D173-1D17A (U+D834 + DD73-DD7A),
+ // U+E0000-E0FFF (U+DB40-DB43 + U+DC00-DFFF)
+ // Bidi control characters (RFC 3987 sections 3.2 and 4.1 paragraph 6):
+ // U+061C, U+200E, U+200F, U+202A-202E, U+2066-2069
+ // Other format characters in the Cf category that are unlikely to be rendered
+ // usefully:
+ // U+0600-0605, U+08E2, U+110BD (U+D804 + U+DCBD),
+ // U+110CD (U+D804 + U+DCCD), U+13430-13438 (U+D80D + U+DC30-DC38),
+ // U+1BCA0-1BCA3 (U+D82F + U+DCA0-DCA3)
+ // Mimicking UI parts:
+ // U+1F50F-1F513 (U+D83D + U+DD0F-DD13), U+1F6E1 (U+D83D + U+DEE1)
+ value = value.replace(
+ // eslint-disable-next-line no-misleading-character-class
+ /[\u00ad\u034f\u061c\u06dd\u070f\u115f\u1160\u17b4\u17b5\u180b-\u180e\u200b\u200e\u200f\u202a-\u202e\u2060-\u206f\u3164\u0600-\u0605\u08e2\ufe00-\ufe0f\ufeff\uffa0\ufff0-\ufffb]|\ud804[\udcbd\udccd]|\ud80d[\udc30-\udc38]|\ud82f[\udca0-\udca3]|\ud834[\udd73-\udd7a]|[\udb40-\udb43][\udc00-\udfff]|\ud83d[\udd0f-\udd13\udee1]/g,
+ encodeURIComponent
+ );
+ return value;
+}
+
+/**
+ * Handles copy and cut commands for the urlbar.
+ */
+class CopyCutController {
+ /**
+ * @param {UrlbarInput} urlbar
+ * The UrlbarInput instance to use this controller for.
+ */
+ constructor(urlbar) {
+ this.urlbar = urlbar;
+ }
+
+ /**
+ * @param {string} command
+ * The name of the command to handle.
+ */
+ doCommand(command) {
+ let urlbar = this.urlbar;
+ let val = urlbar._getSelectedValueForClipboard();
+ if (!val) {
+ return;
+ }
+
+ if (command == "cmd_cut" && this.isCommandEnabled(command)) {
+ let start = urlbar.selectionStart;
+ let end = urlbar.selectionEnd;
+ urlbar.inputField.value =
+ urlbar.inputField.value.substring(0, start) +
+ urlbar.inputField.value.substring(end);
+ urlbar.selectionStart = urlbar.selectionEnd = start;
+
+ let event = urlbar.document.createEvent("UIEvents");
+ event.initUIEvent("input", true, false, urlbar.window, 0);
+ urlbar.inputField.dispatchEvent(event);
+ }
+
+ ClipboardHelper.copyString(val);
+ }
+
+ /**
+ * @param {string} command
+ * @returns {boolean}
+ * Whether the command is handled by this controller.
+ */
+ supportsCommand(command) {
+ switch (command) {
+ case "cmd_copy":
+ case "cmd_cut":
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * @param {string} command
+ * @returns {boolean}
+ * Whether the command should be enabled.
+ */
+ isCommandEnabled(command) {
+ return (
+ this.supportsCommand(command) &&
+ (command != "cmd_cut" || !this.urlbar.readOnly) &&
+ this.urlbar.selectionStart < this.urlbar.selectionEnd
+ );
+ }
+
+ onEvent() {}
+}
diff --git a/browser/components/urlbar/UrlbarMuxerUnifiedComplete.jsm b/browser/components/urlbar/UrlbarMuxerUnifiedComplete.jsm
new file mode 100644
index 0000000000..dbd829f027
--- /dev/null
+++ b/browser/components/urlbar/UrlbarMuxerUnifiedComplete.jsm
@@ -0,0 +1,571 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * This module exports a component used to sort results in a UrlbarQueryContext.
+ */
+
+var EXPORTED_SYMBOLS = ["UrlbarMuxerUnifiedComplete"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+XPCOMUtils.defineLazyModuleGetters(this, {
+ Services: "resource://gre/modules/Services.jsm",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
+ UrlbarProviderTabToSearch:
+ "resource:///modules/UrlbarProviderTabToSearch.jsm",
+ UrlbarMuxer: "resource:///modules/UrlbarUtils.jsm",
+ UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.jsm",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(this, "logger", () =>
+ UrlbarUtils.getLogger({ prefix: "MuxerUnifiedComplete" })
+);
+
+function groupFromResult(result) {
+ if (result.heuristic) {
+ return UrlbarUtils.RESULT_GROUP.HEURISTIC;
+ }
+ switch (result.type) {
+ case UrlbarUtils.RESULT_TYPE.SEARCH:
+ if (result.payload.suggestion) {
+ return UrlbarUtils.RESULT_GROUP.SUGGESTION;
+ }
+ break;
+ case UrlbarUtils.RESULT_TYPE.OMNIBOX:
+ return UrlbarUtils.RESULT_GROUP.EXTENSION;
+ }
+ return UrlbarUtils.RESULT_GROUP.GENERAL;
+}
+
+// Breaks ties among heuristic results. Providers higher up the list are higher
+// priority.
+const HEURISTIC_ORDER = [
+ // Test providers are handled in sort(),
+ // Extension providers are handled in sort(),
+ "UrlbarProviderSearchTips",
+ "Omnibox",
+ "UnifiedComplete",
+ "Autofill",
+ "TokenAliasEngines",
+ "HeuristicFallback",
+];
+
+/**
+ * Class used to create a muxer.
+ * The muxer receives and sorts results in a UrlbarQueryContext.
+ */
+class MuxerUnifiedComplete extends UrlbarMuxer {
+ constructor() {
+ super();
+ }
+
+ get name() {
+ return "UnifiedComplete";
+ }
+
+ /**
+ * Sorts results in the given UrlbarQueryContext.
+ *
+ * @param {UrlbarQueryContext} context
+ * The query context.
+ */
+ sort(context) {
+ // This method is called multiple times per keystroke, so it should be as
+ // fast and efficient as possible. We do two passes through the results:
+ // one to collect state for the second pass, and then a second to build the
+ // sorted list of results. If you find yourself writing something like
+ // context.results.find(), filter(), sort(), etc., modify one or both passes
+ // instead.
+
+ // Global state we'll use to make decisions during this sort.
+ let state = {
+ context,
+ resultsByGroup: new Map(),
+ totalResultCount: 0,
+ topHeuristicRank: Infinity,
+ strippedUrlToTopPrefixAndTitle: new Map(),
+ canShowPrivateSearch: context.results.length > 1,
+ canShowTailSuggestions: true,
+ formHistorySuggestions: new Set(),
+ canAddTabToSearch: true,
+ };
+
+ let resultsWithSuggestedIndex = [];
+
+ // Do the first pass over all results to build some state.
+ for (let result of context.results) {
+ // Save results that have a suggested index for later.
+ if (result.suggestedIndex >= 0) {
+ resultsWithSuggestedIndex.push(result);
+ continue;
+ }
+
+ // Add all other results to the resultsByGroup map:
+ // group => array of results belonging to the group
+ let group = groupFromResult(result);
+ let results = state.resultsByGroup.get(group);
+ if (!results) {
+ results = [];
+ state.resultsByGroup.set(group, results);
+ }
+ results.push(result);
+
+ // Update pre-add state.
+ this._updateStatePreAdd(result, state);
+ }
+
+ if (
+ context.heuristicResult?.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
+ context.heuristicResult?.payload.query
+ ) {
+ state.heuristicResultQuery = context.heuristicResult.payload.query.toLocaleLowerCase();
+ }
+
+ // If the heuristic result is a search result, use search buckets, otherwise
+ // use normal buckets.
+ let buckets =
+ context.heuristicResult?.type == UrlbarUtils.RESULT_TYPE.SEARCH
+ ? UrlbarPrefs.get("matchBucketsSearch")
+ : UrlbarPrefs.get("matchBuckets");
+ logger.debug(`Buckets: ${buckets}`);
+
+ // Do the second pass to fill each bucket. We'll build a list where each
+ // item at index i is the array of results in the bucket at index i.
+ let resultsByBucketIndex = [];
+ for (let [group, maxResultCount] of buckets) {
+ let results = this._addResults(group, maxResultCount, state);
+ resultsByBucketIndex.push(results);
+ }
+
+ // In search mode for an engine, search suggestions should always appear
+ // before general results. Transplanting them allows us to keep history
+ // results up to the limit set in matchBuckets, while filling the space
+ // above them with suggestions.
+ if (context.searchMode?.engineName) {
+ let suggestionsIndex = resultsByBucketIndex.findIndex(
+ results =>
+ results[0] &&
+ !results[0].heuristic &&
+ results[0].type == UrlbarUtils.RESULT_TYPE.SEARCH
+ );
+ if (suggestionsIndex > 1) {
+ logger.debug(`Transplanting suggestions before general results.`);
+ let removed = resultsByBucketIndex.splice(suggestionsIndex, 1);
+ resultsByBucketIndex.splice(1, 0, ...removed);
+ }
+ }
+
+ // Build the sorted results list by concatenating each bucket's results.
+ let sortedResults = [];
+ let remainingCount = context.maxResults;
+ for (let i = 0; i < resultsByBucketIndex.length && remainingCount; i++) {
+ let results = resultsByBucketIndex[i];
+ let count = Math.min(remainingCount, results.length);
+ sortedResults.push(...results.slice(0, count));
+ remainingCount -= count;
+ }
+
+ // Finally, insert results that have a suggested index. Sort them by index
+ // in descending order so that earlier insertions don't disrupt later ones.
+ resultsWithSuggestedIndex.sort(
+ (a, b) => a.suggestedIndex - b.suggestedIndex
+ );
+ // Do a first pass to update sort state for each result.
+ for (let result of resultsWithSuggestedIndex) {
+ this._updateStatePreAdd(result, state);
+ }
+ // Now insert them.
+ for (let result of resultsWithSuggestedIndex) {
+ if (this._canAddResult(result, state)) {
+ let index =
+ result.suggestedIndex <= sortedResults.length
+ ? result.suggestedIndex
+ : sortedResults.length;
+ sortedResults.splice(index, 0, result);
+ this._updateStatePostAdd(result, state);
+ }
+ }
+
+ context.results = sortedResults;
+ }
+
+ /**
+ * Adds results to a bucket using results from the bucket's group in
+ * `state.resultsByGroup`.
+ *
+ * @param {string} group
+ * The bucket's group.
+ * @param {number} maxResultCount
+ * The maximum number of results to add to the bucket.
+ * @param {object} state
+ * Global state that we use to make decisions during this sort.
+ * @returns {array}
+ * The added results, empty if no results were added.
+ */
+ _addResults(group, maxResultCount, state) {
+ let addedResults = [];
+ let groupResults = state.resultsByGroup.get(group);
+ while (
+ groupResults?.length &&
+ addedResults.length < maxResultCount &&
+ state.totalResultCount < state.context.maxResults
+ ) {
+ // We either add or discard results in the order they appear in the
+ // groupResults array, so shift() them off. That way later buckets with
+ // the same group won't include results that earlier buckets have added or
+ // discarded.
+ let result = groupResults.shift();
+ if (this._canAddResult(result, state)) {
+ addedResults.push(result);
+ state.totalResultCount++;
+ this._updateStatePostAdd(result, state);
+ }
+ }
+ return addedResults;
+ }
+
+ /**
+ * Returns whether a result can be added to its bucket given the current sort
+ * state.
+ *
+ * @param {UrlbarResult} result
+ * The result.
+ * @param {object} state
+ * Global state that we use to make decisions during this sort.
+ * @returns {boolean}
+ * True if the result can be added and false if it should be discarded.
+ */
+ _canAddResult(result, state) {
+ // Exclude low-ranked heuristic results.
+ if (result.heuristic && result != state.context.heuristicResult) {
+ return false;
+ }
+
+ // We expect UnifiedComplete sent us the highest-ranked www. and non-www
+ // origins, if any. Now, compare them to each other and to the heuristic
+ // result.
+ //
+ // 1. If the heuristic result is lower ranked than both, discard the www
+ // origin, unless it has a different page title than the non-www
+ // origin. This is a guard against deduping when www.site.com and
+ // site.com have different content.
+ // 2. If the heuristic result is higher than either the www origin or
+ // non-www origin:
+ // 2a. If the heuristic is a www origin, discard the non-www origin.
+ // 2b. If the heuristic is a non-www origin, discard the www origin.
+ if (
+ !result.heuristic &&
+ result.type == UrlbarUtils.RESULT_TYPE.URL &&
+ result.payload.url
+ ) {
+ let [strippedUrl, prefix] = UrlbarUtils.stripPrefixAndTrim(
+ result.payload.url,
+ {
+ stripHttp: true,
+ stripHttps: true,
+ stripWww: true,
+ trimEmptyQuery: true,
+ }
+ );
+ let topPrefixData = state.strippedUrlToTopPrefixAndTitle.get(strippedUrl);
+ // We don't expect completely identical URLs in the results at this point,
+ // so if the prefixes are the same, then we're deduping a result against
+ // itself.
+ if (topPrefixData && prefix != topPrefixData.prefix) {
+ let prefixRank = UrlbarUtils.getPrefixRank(prefix);
+ if (
+ prefixRank < topPrefixData.rank &&
+ (prefix.endsWith("www.") == topPrefixData.prefix.endsWith("www.") ||
+ result.payload?.title == topPrefixData.title)
+ ) {
+ return false;
+ }
+ }
+ }
+
+ // Discard results that dupe autofill.
+ if (
+ state.context.heuristicResult &&
+ state.context.heuristicResult.providerName == "Autofill" &&
+ result.providerName != "Autofill" &&
+ state.context.heuristicResult.payload?.url == result.payload.url &&
+ state.context.heuristicResult.type == result.type
+ ) {
+ return false;
+ }
+
+ // HeuristicFallback may add non-heuristic results in some cases, but those
+ // should be retained only if the heuristic result comes from it.
+ if (
+ !result.heuristic &&
+ result.providerName == "HeuristicFallback" &&
+ state.context.heuristicResult?.providerName != "HeuristicFallback"
+ ) {
+ return false;
+ }
+
+ if (result.providerName == "TabToSearch") {
+ // Discard the result if a tab-to-search result was added already.
+ if (!state.canAddTabToSearch) {
+ return false;
+ }
+
+ if (!result.payload.satisfiesAutofillThreshold) {
+ // Discard the result if the heuristic result is not autofill.
+ if (
+ state.context.heuristicResult.type != UrlbarUtils.RESULT_TYPE.URL ||
+ !state.context.heuristicResult.autofill
+ ) {
+ return false;
+ }
+
+ let autofillHostname = new URL(
+ state.context.heuristicResult.payload.url
+ ).hostname;
+ let [autofillDomain] = UrlbarUtils.stripPrefixAndTrim(
+ autofillHostname,
+ {
+ stripWww: true,
+ }
+ );
+ // Strip the public suffix because we want to allow matching "domain.it"
+ // with "domain.com".
+ autofillDomain = UrlbarUtils.stripPublicSuffixFromHost(autofillDomain);
+ if (!autofillDomain) {
+ return false;
+ }
+
+ // For tab-to-search results, result.payload.url is the engine's domain
+ // with the public suffix already stripped, for example "www.mozilla.".
+ let [engineDomain] = UrlbarUtils.stripPrefixAndTrim(
+ result.payload.url,
+ {
+ stripWww: true,
+ }
+ );
+ // Discard if the engine domain does not end with the autofilled one.
+ if (!engineDomain.endsWith(autofillDomain)) {
+ return false;
+ }
+ }
+ }
+
+ // Discard "Search in a Private Window" if appropriate.
+ if (
+ result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
+ result.payload.inPrivateWindow &&
+ !state.canShowPrivateSearch
+ ) {
+ return false;
+ }
+
+ // Discard form history that dupes the heuristic or previous added form
+ // history (for restyleSearch).
+ if (
+ result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
+ result.source == UrlbarUtils.RESULT_SOURCE.HISTORY &&
+ (result.payload.lowerCaseSuggestion === state.heuristicResultQuery ||
+ state.formHistorySuggestions.has(result.payload.lowerCaseSuggestion))
+ ) {
+ return false;
+ }
+
+ // Discard remote search suggestions that dupe the heuristic.
+ if (
+ result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
+ result.source == UrlbarUtils.RESULT_SOURCE.SEARCH &&
+ result.payload.lowerCaseSuggestion &&
+ result.payload.lowerCaseSuggestion === state.heuristicResultQuery
+ ) {
+ return false;
+ }
+
+ // Discard tail suggestions if appropriate.
+ if (
+ result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
+ result.payload.tail &&
+ !state.canShowTailSuggestions
+ ) {
+ return false;
+ }
+
+ // Discard SERPs from browser history that dupe either the heuristic or
+ // previously added form history.
+ if (
+ result.source == UrlbarUtils.RESULT_SOURCE.HISTORY &&
+ result.type == UrlbarUtils.RESULT_TYPE.URL
+ ) {
+ let submission = Services.search.parseSubmissionURL(result.payload.url);
+ if (submission) {
+ let resultQuery = submission.terms.toLocaleLowerCase();
+ if (
+ state.heuristicResultQuery === resultQuery ||
+ state.formHistorySuggestions.has(resultQuery)
+ ) {
+ // If the result's URL is the same as a brand new SERP URL created
+ // from the query string modulo certain URL params, then treat the
+ // result as a dupe and discard it.
+ let [newSerpURL] = UrlbarUtils.getSearchQueryUrl(
+ submission.engine,
+ resultQuery
+ );
+ if (
+ UrlbarSearchUtils.serpsAreEquivalent(result.payload.url, newSerpURL)
+ ) {
+ return false;
+ }
+ }
+ }
+ }
+
+ // When in an engine search mode, discard URL results whose hostnames don't
+ // include the root domain of the search mode engine.
+ if (state.context.searchMode?.engineName && result.payload.url) {
+ let engine = Services.search.getEngineByName(
+ state.context.searchMode.engineName
+ );
+ if (engine) {
+ let searchModeRootDomain = UrlbarSearchUtils.getRootDomainFromEngine(
+ engine
+ );
+ let resultUrl = new URL(result.payload.url);
+ // Add a trailing "." to increase the stringency of the check. This
+ // check covers most general cases. Some edge cases are not covered,
+ // like `resultUrl` being ebay.mydomain.com, which would escape this
+ // check if `searchModeRootDomain` was "ebay".
+ if (!resultUrl.hostname.includes(`${searchModeRootDomain}.`)) {
+ return false;
+ }
+ }
+ }
+
+ // Include the result.
+ return true;
+ }
+
+ /**
+ * Updates the global state that we use to make decisions during sort. This
+ * should be called for results before we've decided whether to add or discard
+ * them.
+ *
+ * @param {UrlbarResult} result
+ * The result.
+ * @param {object} state
+ * Global state that we use to make decisions during this sort.
+ */
+ _updateStatePreAdd(result, state) {
+ // Determine the highest-ranking heuristic result.
+ if (result.heuristic) {
+ // + 2 to reserve the highest-priority slots for test and extension
+ // providers.
+ let heuristicRank = HEURISTIC_ORDER.indexOf(result.providerName) + 2;
+ // Extension and test provider names vary widely and aren't suitable
+ // for a static safelist like HEURISTIC_ORDER.
+ if (result.providerType == UrlbarUtils.PROVIDER_TYPE.EXTENSION) {
+ heuristicRank = 1;
+ } else if (result.providerName.startsWith("TestProvider")) {
+ heuristicRank = 0;
+ } else if (heuristicRank - 2 == -1) {
+ throw new Error(
+ `Heuristic result returned by unexpected provider: ${result.providerName}`
+ );
+ }
+ // Replace in case of ties, which would occur if a provider sent two
+ // heuristic results.
+ if (heuristicRank <= state.topHeuristicRank) {
+ state.topHeuristicRank = heuristicRank;
+ state.context.heuristicResult = result;
+ }
+ }
+
+ // Save some state we'll use later to dedupe URL results.
+ if (result.type == UrlbarUtils.RESULT_TYPE.URL && result.payload.url) {
+ let [strippedUrl, prefix] = UrlbarUtils.stripPrefixAndTrim(
+ result.payload.url,
+ {
+ stripHttp: true,
+ stripHttps: true,
+ stripWww: true,
+ trimEmptyQuery: true,
+ }
+ );
+ let prefixRank = UrlbarUtils.getPrefixRank(prefix);
+ let topPrefixData = state.strippedUrlToTopPrefixAndTitle.get(strippedUrl);
+ let topPrefixRank = topPrefixData ? topPrefixData.rank : -1;
+ if (topPrefixRank < prefixRank) {
+ // strippedUrl => { prefix, title, rank }
+ state.strippedUrlToTopPrefixAndTitle.set(strippedUrl, {
+ prefix,
+ title: result.payload.title,
+ rank: prefixRank,
+ });
+ }
+ }
+
+ // If we find results other than the heuristic, "Search in Private
+ // Window," or tail suggestions, then we should hide tail suggestions
+ // since they're a last resort.
+ if (
+ state.canShowTailSuggestions &&
+ !result.heuristic &&
+ (result.type != UrlbarUtils.RESULT_TYPE.SEARCH ||
+ (!result.payload.inPrivateWindow && !result.payload.tail))
+ ) {
+ state.canShowTailSuggestions = false;
+ }
+ }
+
+ /**
+ * Updates the global state that we use to make decisions during sort. This
+ * should be called for results after they've been added. It should not be
+ * called for discarded results.
+ *
+ * @param {UrlbarResult} result
+ * The result.
+ * @param {object} state
+ * Global state that we use to make decisions during this sort.
+ */
+ _updateStatePostAdd(result, state) {
+ // The "Search in a Private Window" result should only be shown when there
+ // are other results and all of them are searches. It should not be shown
+ // if the user typed an alias because that's an explicit engine choice.
+ if (
+ state.canShowPrivateSearch &&
+ (result.type != UrlbarUtils.RESULT_TYPE.SEARCH ||
+ result.payload.providesSearchMode ||
+ (result.heuristic && result.payload.keyword))
+ ) {
+ state.canShowPrivateSearch = false;
+ }
+
+ // Update form history suggestions.
+ if (
+ result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
+ result.source == UrlbarUtils.RESULT_SOURCE.HISTORY
+ ) {
+ state.formHistorySuggestions.add(result.payload.lowerCaseSuggestion);
+ }
+
+ // Avoid multiple tab-to-search results.
+ // TODO (Bug 1670185): figure out better strategies to manage this case.
+ if (result.providerName == "TabToSearch") {
+ state.canAddTabToSearch = false;
+ // We want to record in urlbar.tips once per engagement per engine. Since
+ // whether these results are shown is dependent on the Muxer, we must
+ // add to `onboardingEnginesShown` here.
+ if (result.payload.dynamicType) {
+ UrlbarProviderTabToSearch.onboardingEnginesShown.add(
+ result.payload.engine
+ );
+ }
+ }
+ }
+}
+
+var UrlbarMuxerUnifiedComplete = new MuxerUnifiedComplete();
diff --git a/browser/components/urlbar/UrlbarPrefs.jsm b/browser/components/urlbar/UrlbarPrefs.jsm
new file mode 100644
index 0000000000..a73d2c959e
--- /dev/null
+++ b/browser/components/urlbar/UrlbarPrefs.jsm
@@ -0,0 +1,467 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * This module exports the UrlbarPrefs singleton, which manages
+ * preferences for the urlbar.
+ */
+
+var EXPORTED_SYMBOLS = ["UrlbarPrefs", "UrlbarPrefsObserver"];
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+});
+
+const PREF_URLBAR_BRANCH = "browser.urlbar.";
+
+// Prefs are defined as [pref name, default value] or [pref name, [default
+// value, type]]. In the former case, the getter method name is inferred from
+// the typeof the default value.
+const PREF_URLBAR_DEFAULTS = new Map([
+ // Whether we announce to screen readers when tab-to-search results are
+ // inserted.
+ ["accessibility.tabToSearch.announceResults", true],
+
+ // "Autofill" is the name of the feature that automatically completes domains
+ // and URLs that the user has visited as the user is typing them in the urlbar
+ // textbox. If false, autofill will be disabled.
+ ["autoFill", true],
+
+ // If true, the domains of the user's installed search engines will be
+ // autofilled even if the user hasn't actually visited them.
+ ["autoFill.searchEngines", false],
+
+ // Affects the frecency threshold of the autofill algorithm. The threshold is
+ // the mean of all origin frecencies plus one standard deviation multiplied by
+ // this value. See UnifiedComplete.
+ ["autoFill.stddevMultiplier", [0.0, "float"]],
+
+ // Whether using `ctrl` when hitting return/enter in the URL bar
+ // (or clicking 'go') should prefix 'www.' and suffix
+ // browser.fixup.alternate.suffix to the URL bar value prior to
+ // navigating.
+ ["ctrlCanonizesURLs", true],
+
+ // Whether copying the entire URL from the location bar will put a human
+ // readable (percent-decoded) URL on the clipboard.
+ ["decodeURLsOnCopy", false],
+
+ // The amount of time (ms) to wait after the user has stopped typing before
+ // fetching results. However, we ignore this for the very first result (the
+ // "heuristic" result). We fetch it as fast as possible.
+ ["delay", 50],
+
+ // Some performance tests disable this because extending the urlbar needs
+ // layout information that we can't get before the first paint. (Or we could
+ // but this would mean flushing layout.)
+ ["disableExtendForTests", false],
+
+ // Controls when to DNS resolve single word search strings, after they were
+ // searched for. If the string is resolved as a valid host, show a
+ // "Did you mean to go to 'host'" prompt.
+ // 0 - never resolve; 1 - use heuristics (default); 2 - always resolve
+ ["dnsResolveSingleWordsAfterSearch", 1],
+
+ // Whether telemetry events should be recorded.
+ ["eventTelemetry.enabled", false],
+
+ // Whether we expand the font size when when the urlbar is
+ // focused.
+ ["experimental.expandTextOnFocus", false],
+
+ // Whether the urlbar displays a permanent search button.
+ ["experimental.searchButton", false],
+
+ // Whether we style the search mode indicator's close button on hover.
+ ["experimental.searchModeIndicatorHover", false],
+
+ // When we send events to extensions, we wait this amount of time in
+ // milliseconds for them to respond before timing out.
+ ["extension.timeout", 400],
+
+ // When true, `javascript:` URLs are not included in search results.
+ ["filter.javascript", true],
+
+ // Applies URL highlighting and other styling to the text in the urlbar input.
+ ["formatting.enabled", true],
+
+ // Whether during IME composition the results panel should be closed.
+ ["imeCompositionClosesPanel", true],
+
+ // Controls the composition of search results.
+ ["matchBuckets", "suggestion:4,general:Infinity"],
+
+ // If the heuristic result is a search engine result, we use this instead of
+ // matchBuckets.
+ ["matchBucketsSearch", ""],
+
+ // For search suggestion results, we truncate the user's search string to this
+ // number of characters before fetching results.
+ ["maxCharsForSearchSuggestions", 20],
+
+ // The maximum number of form history results to include.
+ ["maxHistoricalSearchSuggestions", 0],
+
+ // The maximum number of results in the urlbar popup.
+ ["maxRichResults", 10],
+
+ // Whether addresses and search results typed into the address bar
+ // should be opened in new tabs by default.
+ ["openintab", false],
+
+ // When true, URLs in the user's history that look like search result pages
+ // are styled to look like search engine results instead of the usual history
+ // results.
+ ["restyleSearches", false],
+
+ // If true, we show tail suggestions when available.
+ ["richSuggestions.tail", true],
+
+ // Hidden pref. Disables checks that prevent search tips being shown, thus
+ // showing them every time the newtab page or the default search engine
+ // homepage is opened.
+ ["searchTips.test.ignoreShowLimits", false],
+
+ // Whether to show each local search shortcut button in the view.
+ ["shortcuts.bookmarks", true],
+ ["shortcuts.tabs", true],
+ ["shortcuts.history", true],
+
+ // Whether speculative connections should be enabled.
+ ["speculativeConnect.enabled", true],
+
+ // Whether results will include the user's bookmarks.
+ ["suggest.bookmark", true],
+
+ // Whether results will include the user's history.
+ ["suggest.history", true],
+
+ // Whether results will include switch-to-tab results.
+ ["suggest.openpage", true],
+
+ // Whether results will include search suggestions.
+ ["suggest.searches", false],
+
+ // Whether results will include search engines (e.g. tab-to-search).
+ ["suggest.engines", true],
+
+ // Whether results will include top sites and the view will open on focus.
+ ["suggest.topsites", true],
+
+ // When using switch to tabs, if set to true this will move the tab into the
+ // active window.
+ ["switchTabs.adoptIntoActiveWindow", false],
+
+ // The number of remaining times the user can interact with tab-to-search
+ // onboarding results before we stop showing them.
+ ["tabToSearch.onboard.interactionsLeft", 3],
+
+ // The number of times the user has been shown the onboarding search tip.
+ ["tipShownCount.searchTip_onboard", 0],
+
+ // The number of times the user has been shown the redirect search tip.
+ ["tipShownCount.searchTip_redirect", 0],
+
+ // Remove redundant portions from URLs.
+ ["trimURLs", true],
+
+ // Results will include a built-in set of popular domains when this is true.
+ ["usepreloadedtopurls.enabled", false],
+
+ // After this many days from the profile creation date, the built-in set of
+ // popular domains will no longer be included in the results.
+ ["usepreloadedtopurls.expire_days", 14],
+
+ // Controls the empty search behavior in Search Mode:
+ // 0 - Show nothing
+ // 1 - Show search history
+ // 2 - Show search and browsing history
+ ["update2.emptySearchBehavior", 0],
+]);
+const PREF_OTHER_DEFAULTS = new Map([
+ ["keyword.enabled", true],
+ ["browser.search.suggest.enabled", true],
+ ["browser.search.suggest.enabled.private", false],
+ ["ui.popup.disable_autohide", false],
+ ["browser.fixup.dns_first_for_single_words", false],
+]);
+
+// Maps preferences under browser.urlbar.suggest to behavior names, as defined
+// in mozIPlacesAutoComplete.
+const SUGGEST_PREF_TO_BEHAVIOR = {
+ history: "history",
+ bookmark: "bookmark",
+ openpage: "openpage",
+ searches: "search",
+};
+
+const PREF_TYPES = new Map([
+ ["boolean", "Bool"],
+ ["float", "Float"],
+ ["number", "Int"],
+ ["string", "Char"],
+]);
+
+// Buckets for result insertion.
+// Every time a new result is returned, we go through each bucket in array order,
+// and look for the first one having available space for the given result type.
+// Each bucket is an array containing the following indices:
+// 0: The result type of the acceptable entries.
+// 1: available number of slots in this bucket.
+// There are different matchBuckets definition for different contexts, currently
+// a general one (matchBuckets) and a search one (matchBucketsSearch).
+//
+// First buckets. Anything with an Infinity frecency ends up here.
+const DEFAULT_BUCKETS_BEFORE = [
+ [UrlbarUtils.RESULT_GROUP.HEURISTIC, 1],
+ [
+ UrlbarUtils.RESULT_GROUP.EXTENSION,
+ UrlbarUtils.MAXIMUM_ALLOWED_EXTENSION_MATCHES - 1,
+ ],
+];
+// => USER DEFINED BUCKETS WILL BE INSERTED HERE <=
+//
+// Catch-all buckets. Anything remaining ends up here.
+const DEFAULT_BUCKETS_AFTER = [
+ [UrlbarUtils.RESULT_GROUP.SUGGESTION, Infinity],
+ [UrlbarUtils.RESULT_GROUP.GENERAL, Infinity],
+];
+
+/**
+ * Preferences class. The exported object is a singleton instance.
+ */
+class Preferences {
+ /**
+ * Constructor
+ */
+ constructor() {
+ this._map = new Map();
+ this.QueryInterface = ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsISupportsWeakReference",
+ ]);
+ Services.prefs.addObserver(PREF_URLBAR_BRANCH, this, true);
+ for (let pref of PREF_OTHER_DEFAULTS.keys()) {
+ Services.prefs.addObserver(pref, this, true);
+ }
+ this._observerWeakRefs = [];
+ this.addObserver(this);
+ }
+
+ /**
+ * Returns the value for the preference with the given name.
+ * For preferences in the "browser.urlbar."" branch, the passed-in name
+ * should be relative to the branch. It's also possible to get prefs from the
+ * PREF_OTHER_DEFAULTS Map, specifying their full name.
+ *
+ * @param {string} pref
+ * The name of the preference to get.
+ * @returns {*} The preference value.
+ */
+ get(pref) {
+ let value = this._map.get(pref);
+ if (value === undefined) {
+ value = this._getPrefValue(pref);
+ this._map.set(pref, value);
+ }
+ return value;
+ }
+
+ /**
+ * Sets the value for the preference with the given name.
+ * For preferences in the "browser.urlbar."" branch, the passed-in name
+ * should be relative to the branch. It's also possible to set prefs from the
+ * PREF_OTHER_DEFAULTS Map, specifying their full name.
+ *
+ * @param {string} pref
+ * The name of the preference to set.
+ * @param {*} value The preference value.
+ */
+ set(pref, value) {
+ let { defaultValue, setter } = this._getPrefDescriptor(pref);
+ if (typeof value != typeof defaultValue) {
+ throw new Error(`Invalid value type ${typeof value} for pref ${pref}`);
+ }
+ setter(pref, value);
+ }
+
+ /**
+ * Adds a preference observer. Observers are held weakly.
+ *
+ * @param {object} observer
+ * An object that must have a method named `onPrefChanged`, which will
+ * be called when a urlbar preference changes. It will be passed the
+ * pref name. For prefs in the `browser.urlbar.` branch, the name will
+ * be relative to the branch. For other prefs, the name will be the
+ * full name.
+ */
+ addObserver(observer) {
+ this._observerWeakRefs.push(Cu.getWeakReference(observer));
+ }
+
+ /**
+ * Observes preference changes.
+ *
+ * @param {nsISupports} subject
+ * @param {string} topic
+ * @param {string} data
+ */
+ observe(subject, topic, data) {
+ let pref = data.replace(PREF_URLBAR_BRANCH, "");
+ if (!PREF_URLBAR_DEFAULTS.has(pref) && !PREF_OTHER_DEFAULTS.has(pref)) {
+ return;
+ }
+ for (let i = 0; i < this._observerWeakRefs.length; ) {
+ let observer = this._observerWeakRefs[i].get();
+ if (!observer) {
+ // The observer has been GC'ed, so remove it from our list.
+ this._observerWeakRefs.splice(i, 1);
+ } else {
+ observer.onPrefChanged(pref);
+ ++i;
+ }
+ }
+ }
+
+ /**
+ * Called when a pref tracked by UrlbarPrefs changes.
+ *
+ * @param {string} pref
+ * The name of the pref, relative to `browser.urlbar.` if the pref is
+ * in that branch.
+ */
+ onPrefChanged(pref) {
+ this._map.delete(pref);
+ // Some prefs may influence others.
+ if (pref == "matchBuckets") {
+ this._map.delete("matchBucketsSearch");
+ }
+ if (pref.startsWith("suggest.")) {
+ this._map.delete("defaultBehavior");
+ }
+ }
+
+ /**
+ * Returns the raw value of the given preference straight from Services.prefs.
+ *
+ * @param {string} pref
+ * The name of the preference to get.
+ * @returns {*} The raw preference value.
+ */
+ _readPref(pref) {
+ let { defaultValue, getter } = this._getPrefDescriptor(pref);
+ return getter(pref, defaultValue);
+ }
+
+ /**
+ * Returns a validated and/or fixed-up value of the given preference. The
+ * value may be validated for correctness, or it might be converted into a
+ * different value that is easier to work with than the actual value stored in
+ * the preferences branch. Not all preferences require validation or fixup.
+ *
+ * The values returned from this method are the values that are made public by
+ * this module.
+ *
+ * @param {string} pref
+ * The name of the preference to get.
+ * @returns {*} The validated and/or fixed-up preference value.
+ */
+ _getPrefValue(pref) {
+ switch (pref) {
+ case "matchBuckets": {
+ // Convert from pref char format to an array and add the default
+ // buckets.
+ let val = this._readPref(pref);
+ try {
+ val = PlacesUtils.convertMatchBucketsStringToArray(val);
+ } catch (ex) {
+ val = PlacesUtils.convertMatchBucketsStringToArray(
+ PREF_URLBAR_DEFAULTS.get(pref)
+ );
+ }
+ return [...DEFAULT_BUCKETS_BEFORE, ...val, ...DEFAULT_BUCKETS_AFTER];
+ }
+ case "matchBucketsSearch": {
+ // Convert from pref char format to an array and add the default
+ // buckets.
+ let val = this._readPref(pref);
+ if (val) {
+ // Convert from pref char format to an array and add the default
+ // buckets.
+ try {
+ val = PlacesUtils.convertMatchBucketsStringToArray(val);
+ return [
+ ...DEFAULT_BUCKETS_BEFORE,
+ ...val,
+ ...DEFAULT_BUCKETS_AFTER,
+ ];
+ } catch (ex) {
+ /* invalid format, will just return matchBuckets */
+ }
+ }
+ return this.get("matchBuckets");
+ }
+ case "defaultBehavior": {
+ let val = 0;
+ for (let type of Object.keys(SUGGEST_PREF_TO_BEHAVIOR)) {
+ let behavior = `BEHAVIOR_${SUGGEST_PREF_TO_BEHAVIOR[
+ type
+ ].toUpperCase()}`;
+ val |=
+ this.get("suggest." + type) && Ci.mozIPlacesAutoComplete[behavior];
+ }
+ return val;
+ }
+ }
+ return this._readPref(pref);
+ }
+
+ /**
+ * Returns a descriptor of the given preference.
+ * @param {string} pref The preference to examine.
+ * @returns {object} An object describing the pref with the following shape:
+ * { defaultValue, getter, setter }
+ */
+ _getPrefDescriptor(pref) {
+ let branch = Services.prefs.getBranch(PREF_URLBAR_BRANCH);
+ let defaultValue = PREF_URLBAR_DEFAULTS.get(pref);
+ if (defaultValue === undefined) {
+ branch = Services.prefs;
+ defaultValue = PREF_OTHER_DEFAULTS.get(pref);
+ }
+ if (defaultValue === undefined) {
+ throw new Error("Trying to access an unknown pref " + pref);
+ }
+
+ let type;
+ if (!Array.isArray(defaultValue)) {
+ type = PREF_TYPES.get(typeof defaultValue);
+ } else {
+ if (defaultValue.length != 2) {
+ throw new Error("Malformed pref def: " + pref);
+ }
+ [defaultValue, type] = defaultValue;
+ type = PREF_TYPES.get(type);
+ }
+ if (!type) {
+ throw new Error("Unknown pref type: " + pref);
+ }
+ return {
+ defaultValue,
+ getter: branch[`get${type}Pref`],
+ // Float prefs are stored as Char.
+ setter: branch[`set${type == "Float" ? "Char" : type}Pref`],
+ };
+ }
+}
+
+var UrlbarPrefs = new Preferences();
diff --git a/browser/components/urlbar/UrlbarProviderAutofill.jsm b/browser/components/urlbar/UrlbarProviderAutofill.jsm
new file mode 100644
index 0000000000..25d35c79e0
--- /dev/null
+++ b/browser/components/urlbar/UrlbarProviderAutofill.jsm
@@ -0,0 +1,801 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * This module exports a provider that provides an autofill result.
+ */
+
+var EXPORTED_SYMBOLS = ["UrlbarProviderAutofill"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AboutPagesUtils: "resource://gre/modules/AboutPagesUtils.jsm",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
+ UrlbarProvider: "resource:///modules/UrlbarUtils.jsm",
+ UrlbarResult: "resource:///modules/UrlbarResult.jsm",
+ UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.jsm",
+ UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.jsm",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+});
+
+// AutoComplete query type constants.
+// Describes the various types of queries that we can process rows for.
+const QUERYTYPE = {
+ FILTERED: 0,
+ AUTOFILL_ORIGIN: 1,
+ AUTOFILL_URL: 2,
+ ADAPTIVE: 3,
+};
+
+// `WITH` clause for the autofill queries. autofill_frecency_threshold.value is
+// the mean of all moz_origins.frecency values + stddevMultiplier * one standard
+// deviation. This is inlined directly in the SQL (as opposed to being a custom
+// Sqlite function for example) in order to be as efficient as possible.
+const SQL_AUTOFILL_WITH = `
+ WITH
+ frecency_stats(count, sum, squares) AS (
+ SELECT
+ CAST((SELECT IFNULL(value, 0.0) FROM moz_meta WHERE key = 'origin_frecency_count') AS REAL),
+ CAST((SELECT IFNULL(value, 0.0) FROM moz_meta WHERE key = 'origin_frecency_sum') AS REAL),
+ CAST((SELECT IFNULL(value, 0.0) FROM moz_meta WHERE key = 'origin_frecency_sum_of_squares') AS REAL)
+ ),
+ autofill_frecency_threshold(value) AS (
+ SELECT
+ CASE count
+ WHEN 0 THEN 0.0
+ WHEN 1 THEN sum
+ ELSE (sum / count) + (:stddevMultiplier * sqrt((squares - ((sum * sum) / count)) / count))
+ END
+ FROM frecency_stats
+ )
+ `;
+
+const SQL_AUTOFILL_FRECENCY_THRESHOLD = `host_frecency >= (
+ SELECT value FROM autofill_frecency_threshold
+ )`;
+
+function originQuery(where) {
+ // `frecency`, `bookmarked` and `visited` are partitioned by the fixed host,
+ // without `www.`. `host_prefix` instead is partitioned by full host, because
+ // we assume a prefix may not work regardless of `www.`.
+ let selectVisited = where.includes("visited")
+ ? `MAX(EXISTS(
+ SELECT 1 FROM moz_places WHERE origin_id = o.id AND visit_count > 0
+ )) OVER (PARTITION BY fixup_url(host)) > 0`
+ : "0";
+ return `/* do not warn (bug no): cannot use an index to sort */
+ ${SQL_AUTOFILL_WITH},
+ origins(id, prefix, host_prefix, host, fixed, host_frecency, frecency, bookmarked, visited) AS (
+ SELECT
+ id,
+ prefix,
+ first_value(prefix) OVER (
+ PARTITION BY host ORDER BY frecency DESC, prefix = "https://" DESC, id DESC
+ ),
+ host,
+ fixup_url(host),
+ TOTAL(frecency) OVER (PARTITION BY fixup_url(host)),
+ frecency,
+ MAX(EXISTS(
+ SELECT 1 FROM moz_places WHERE origin_id = o.id AND foreign_count > 0
+ )) OVER (PARTITION BY fixup_url(host)),
+ ${selectVisited}
+ FROM moz_origins o
+ WHERE (host BETWEEN :searchString AND :searchString || X'FFFF')
+ OR (host BETWEEN 'www.' || :searchString AND 'www.' || :searchString || X'FFFF')
+ )
+ SELECT :query_type AS query_type,
+ iif(instr(host, :searchString) = 1, host, fixed) || '/' AS host_fixed,
+ ifnull(:prefix, host_prefix) || host || '/' AS url
+ FROM origins
+ ${where}
+ ORDER BY frecency DESC, prefix = "https://" DESC, id DESC
+ LIMIT 1
+ `;
+}
+
+function urlQuery(where1, where2) {
+ // We limit the search to places that are either bookmarked or have a frecency
+ // over some small, arbitrary threshold (20) in order to avoid scanning as few
+ // rows as possible. Keep in mind that we run this query every time the user
+ // types a key when the urlbar value looks like a URL with a path.
+ return `/* do not warn (bug no): cannot use an index to sort */
+ SELECT :query_type AS query_type,
+ url,
+ :strippedURL AS stripped_url,
+ frecency,
+ foreign_count > 0 AS bookmarked,
+ visit_count > 0 AS visited,
+ id
+ FROM moz_places
+ WHERE rev_host = :revHost
+ ${where1}
+ UNION ALL
+ SELECT :query_type AS query_type,
+ url,
+ :strippedURL AS stripped_url,
+ frecency,
+ foreign_count > 0 AS bookmarked,
+ visit_count > 0 AS visited,
+ id
+ FROM moz_places
+ WHERE rev_host = :revHost || 'www.'
+ ${where2}
+ ORDER BY frecency DESC, id DESC
+ LIMIT 1 `;
+}
+// Queries
+const QUERY_ORIGIN_HISTORY_BOOKMARK = originQuery(
+ `WHERE bookmarked OR ${SQL_AUTOFILL_FRECENCY_THRESHOLD}`
+);
+
+const QUERY_ORIGIN_PREFIX_HISTORY_BOOKMARK = originQuery(
+ `WHERE prefix BETWEEN :prefix AND :prefix || X'FFFF'
+ AND (bookmarked OR ${SQL_AUTOFILL_FRECENCY_THRESHOLD})`
+);
+
+const QUERY_ORIGIN_HISTORY = originQuery(
+ `WHERE visited AND ${SQL_AUTOFILL_FRECENCY_THRESHOLD}`
+);
+
+const QUERY_ORIGIN_PREFIX_HISTORY = originQuery(
+ `WHERE prefix BETWEEN :prefix AND :prefix || X'FFFF'
+ AND visited AND ${SQL_AUTOFILL_FRECENCY_THRESHOLD}`
+);
+
+const QUERY_ORIGIN_BOOKMARK = originQuery(`WHERE bookmarked`);
+
+const QUERY_ORIGIN_PREFIX_BOOKMARK = originQuery(
+ `WHERE prefix BETWEEN :prefix AND :prefix || X'FFFF' AND bookmarked`
+);
+
+const QUERY_URL_HISTORY_BOOKMARK = urlQuery(
+ `AND (bookmarked OR frecency > 20)
+ AND strip_prefix_and_userinfo(url) COLLATE NOCASE
+ BETWEEN :strippedURL AND :strippedURL || X'FFFF'`,
+ `AND (bookmarked OR frecency > 20)
+ AND strip_prefix_and_userinfo(url) COLLATE NOCASE
+ BETWEEN 'www.' || :strippedURL AND 'www.' || :strippedURL || X'FFFF'`
+);
+
+const QUERY_URL_PREFIX_HISTORY_BOOKMARK = urlQuery(
+ `AND (bookmarked OR frecency > 20)
+ AND url COLLATE NOCASE
+ BETWEEN :prefix || :strippedURL AND :prefix || :strippedURL || X'FFFF'`,
+ `AND (bookmarked OR frecency > 20)
+ AND url COLLATE NOCASE
+ BETWEEN :prefix || 'www.' || :strippedURL AND :prefix || 'www.' || :strippedURL || X'FFFF'`
+);
+
+const QUERY_URL_HISTORY = urlQuery(
+ `AND (visited OR NOT bookmarked)
+ AND frecency > 20
+ AND strip_prefix_and_userinfo(url) COLLATE NOCASE
+ BETWEEN :strippedURL AND :strippedURL || X'FFFF'`,
+ `AND (visited OR NOT bookmarked)
+ AND frecency > 20
+ AND strip_prefix_and_userinfo(url) COLLATE NOCASE
+ BETWEEN 'www.' || :strippedURL AND 'www.' || :strippedURL || X'FFFF'`
+);
+
+const QUERY_URL_PREFIX_HISTORY = urlQuery(
+ `AND (visited OR NOT bookmarked)
+ AND frecency > 20
+ AND url COLLATE NOCASE
+ BETWEEN :prefix || :strippedURL AND :prefix || :strippedURL || X'FFFF'`,
+ `AND (visited OR NOT bookmarked)
+ AND frecency > 20
+ AND url COLLATE NOCASE
+ BETWEEN :prefix || 'www.' || :strippedURL AND :prefix || 'www.' || :strippedURL || X'FFFF'`
+);
+
+const QUERY_URL_BOOKMARK = urlQuery(
+ `AND bookmarked
+ AND strip_prefix_and_userinfo(url) COLLATE NOCASE
+ BETWEEN :strippedURL AND :strippedURL || X'FFFF'`,
+ `AND bookmarked
+ AND strip_prefix_and_userinfo(url) COLLATE NOCASE
+ BETWEEN 'www.' || :strippedURL AND 'www.' || :strippedURL || X'FFFF'`
+);
+
+const QUERY_URL_PREFIX_BOOKMARK = urlQuery(
+ `AND bookmarked
+ AND url COLLATE NOCASE
+ BETWEEN :prefix || :strippedURL AND :prefix || :strippedURL || X'FFFF'`,
+ `AND bookmarked
+ AND url COLLATE NOCASE
+ BETWEEN :prefix || 'www.' || :strippedURL AND :prefix || 'www.' || :strippedURL || X'FFFF'`
+);
+
+const kProtocolsWithIcons = [
+ "chrome:",
+ "moz-extension:",
+ "about:",
+ "http:",
+ "https:",
+ "ftp:",
+];
+function iconHelper(url) {
+ if (typeof url == "string") {
+ return kProtocolsWithIcons.some(p => url.startsWith(p))
+ ? "page-icon:" + url
+ : UrlbarUtils.ICON.DEFAULT;
+ }
+ if (url && url instanceof URL && kProtocolsWithIcons.includes(url.protocol)) {
+ return "page-icon:" + url.href;
+ }
+ return UrlbarUtils.ICON.DEFAULT;
+}
+
+/**
+ * Class used to create the provider.
+ */
+class ProviderAutofill extends UrlbarProvider {
+ constructor() {
+ super();
+ }
+
+ /**
+ * Returns the name of this provider.
+ * @returns {string} the name of this provider.
+ */
+ get name() {
+ return "Autofill";
+ }
+
+ /**
+ * Returns the type of this provider.
+ * @returns {integer} one of the types from UrlbarUtils.PROVIDER_TYPE.*
+ */
+ get type() {
+ return UrlbarUtils.PROVIDER_TYPE.HEURISTIC;
+ }
+
+ /**
+ * Whether this provider should be invoked for the given context.
+ * If this method returns false, the providers manager won't start a query
+ * with this provider, to save on resources.
+ * @param {UrlbarQueryContext} queryContext The query context object
+ * @returns {boolean} Whether this provider should be invoked for the search.
+ */
+ async isActive(queryContext) {
+ let instance = this.queryInstance;
+
+ // This is usually reset on canceling or completing the query, but since we
+ // query in isActive, it may not have been canceled by the previous call.
+ // It is an object with values { result: UrlbarResult, instance: Query }.
+ // See the documentation for _getAutofillData for more information.
+ this._autofillData = null;
+
+ // First of all, check for the autoFill pref.
+ if (!UrlbarPrefs.get("autoFill")) {
+ return false;
+ }
+
+ if (!queryContext.allowAutofill) {
+ return false;
+ }
+
+ if (queryContext.tokens.length != 1) {
+ return false;
+ }
+
+ // Trying to autofill an extremely long string would be expensive, and
+ // not particularly useful since the filled part falls out of screen anyway.
+ if (queryContext.searchString.length > UrlbarUtils.MAX_TEXT_LENGTH) {
+ return false;
+ }
+
+ // autoFill can only cope with history, bookmarks, and about: entries.
+ if (
+ !queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY) &&
+ !queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS)
+ ) {
+ return false;
+ }
+
+ // Autofill doesn't search tags or titles
+ if (
+ queryContext.tokens.some(
+ t =>
+ t.type == UrlbarTokenizer.TYPE.RESTRICT_TAG ||
+ t.type == UrlbarTokenizer.TYPE.RESTRICT_TITLE
+ )
+ ) {
+ return false;
+ }
+
+ [this._strippedPrefix, this._searchString] = UrlbarUtils.stripURLPrefix(
+ queryContext.searchString
+ );
+ this._strippedPrefix = this._strippedPrefix.toLowerCase();
+
+ if (!this._searchString || !this._searchString.length) {
+ return false;
+ }
+
+ // Don't try to autofill if the search term includes any whitespace.
+ // This may confuse completeDefaultIndex cause the AUTOCOMPLETE_MATCH
+ // tokenizer ends up trimming the search string and returning a value
+ // that doesn't match it, or is even shorter.
+ if (UrlbarTokenizer.REGEXP_SPACES.test(queryContext.searchString)) {
+ return false;
+ }
+
+ // Fetch autofill result now, rather than in startQuery. We do this so the
+ // muxer doesn't have to wait on autofill for every query, since startQuery
+ // will be guaranteed to return a result very quickly using this approach.
+ // Bug 1651101 is filed to improve this behaviour.
+ let result = await this._getAutofillResult(queryContext);
+ if (!result || instance != this.queryInstance) {
+ return false;
+ }
+ this._autofillData = { result, instance };
+ return true;
+ }
+
+ /**
+ * Gets the provider's priority.
+ * @param {UrlbarQueryContext} queryContext The query context object
+ * @returns {number} The provider's priority for the given query.
+ */
+ getPriority(queryContext) {
+ // Priority search results are restricting.
+ if (
+ this._autofillData &&
+ this._autofillData.instance == this.queryInstance &&
+ this._autofillData.result.type == UrlbarUtils.RESULT_TYPE.SEARCH
+ ) {
+ return 1;
+ }
+
+ return 0;
+ }
+
+ /**
+ * Starts querying.
+ * @param {object} queryContext The query context object
+ * @param {function} addCallback Callback invoked by the provider to add a new
+ * result.
+ * @returns {Promise} resolved when the query stops.
+ */
+ async startQuery(queryContext, addCallback) {
+ // Check if the query was cancelled while the autofill result was being
+ // fetched. We don't expect this to be true since we also check the instance
+ // in isActive and clear _autofillData in cancelQuery, but we sanity check it.
+ if (
+ !this._autofillData ||
+ this._autofillData.instance != this.queryInstance
+ ) {
+ this.logger.error("startQuery invoked with an invalid _autofillData");
+ return;
+ }
+
+ this._autofillData.result.heuristic = true;
+ addCallback(this, this._autofillData.result);
+ this._autofillData = null;
+ }
+
+ /**
+ * Cancels a running query.
+ * @param {object} queryContext The query context object
+ */
+ cancelQuery(queryContext) {
+ if (this._autofillData?.instance == this.queryInstance) {
+ this._autofillData = null;
+ }
+ }
+
+ /**
+ * Filters hosts by retaining only the ones over the autofill threshold, then
+ * sorts them by their frecency, and extracts the one with the highest value.
+ * @param {UrlbarQueryContext} queryContext The current queryContext.
+ * @param {Array} hosts Array of host names to examine.
+ * @returns {Promise} Resolved when the filtering is complete.
+ * @resolves {string} The top matching host, or null if not found.
+ */
+ async getTopHostOverThreshold(queryContext, hosts) {
+ let db = await PlacesUtils.promiseLargeCacheDBConnection();
+ let conditions = [];
+ // Pay attention to the order of params, since they are not named.
+ let params = [UrlbarPrefs.get("autoFill.stddevMultiplier"), ...hosts];
+ let sources = queryContext.sources;
+ if (
+ sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY) &&
+ sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS)
+ ) {
+ conditions.push(`(bookmarked OR ${SQL_AUTOFILL_FRECENCY_THRESHOLD})`);
+ } else if (sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY)) {
+ conditions.push(`visited AND ${SQL_AUTOFILL_FRECENCY_THRESHOLD}`);
+ } else if (sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS)) {
+ conditions.push("bookmarked");
+ }
+
+ let rows = await db.executeCached(
+ `
+ ${SQL_AUTOFILL_WITH},
+ origins(id, prefix, host_prefix, host, fixed, host_frecency, frecency, bookmarked, visited) AS (
+ SELECT
+ id,
+ prefix,
+ first_value(prefix) OVER (
+ PARTITION BY host ORDER BY frecency DESC, prefix = "https://" DESC, id DESC
+ ),
+ host,
+ fixup_url(host),
+ TOTAL(frecency) OVER (PARTITION BY fixup_url(host)),
+ frecency,
+ MAX(EXISTS(
+ SELECT 1 FROM moz_places WHERE origin_id = o.id AND foreign_count > 0
+ )) OVER (PARTITION BY fixup_url(host)),
+ MAX(EXISTS(
+ SELECT 1 FROM moz_places WHERE origin_id = o.id AND visit_count > 0
+ )) OVER (PARTITION BY fixup_url(host))
+ FROM moz_origins o
+ WHERE o.host IN (${new Array(hosts.length).fill("?").join(",")})
+ )
+ SELECT host
+ FROM origins
+ ${conditions.length ? "WHERE " + conditions.join(" AND ") : ""}
+ ORDER BY frecency DESC, prefix = "https://" DESC, id DESC
+ LIMIT 1
+ `,
+ params
+ );
+ if (!rows.length) {
+ return null;
+ }
+ return rows[0].getResultByName("host");
+ }
+
+ /**
+ * Obtains the query to search for autofill origin results.
+ *
+ * @param {UrlbarQueryContext} queryContext
+ * @returns {array} consisting of the correctly optimized query to search the
+ * database with and an object containing the params to bound.
+ */
+ _getOriginQuery(queryContext) {
+ // At this point, searchString is not a URL with a path; it does not
+ // contain a slash, except for possibly at the very end. If there is
+ // trailing slash, remove it when searching here to match the rest of the
+ // string because it may be an origin.
+ let searchStr = this._searchString.endsWith("/")
+ ? this._searchString.slice(0, -1)
+ : this._searchString;
+
+ let opts = {
+ query_type: QUERYTYPE.AUTOFILL_ORIGIN,
+ searchString: searchStr.toLowerCase(),
+ stddevMultiplier: UrlbarPrefs.get("autoFill.stddevMultiplier"),
+ };
+ if (this._strippedPrefix) {
+ opts.prefix = this._strippedPrefix;
+ }
+
+ if (
+ queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY) &&
+ queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS)
+ ) {
+ return [
+ this._strippedPrefix
+ ? QUERY_ORIGIN_PREFIX_HISTORY_BOOKMARK
+ : QUERY_ORIGIN_HISTORY_BOOKMARK,
+ opts,
+ ];
+ }
+ if (queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY)) {
+ return [
+ this._strippedPrefix
+ ? QUERY_ORIGIN_PREFIX_HISTORY
+ : QUERY_ORIGIN_HISTORY,
+ opts,
+ ];
+ }
+ if (queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS)) {
+ return [
+ this._strippedPrefix
+ ? QUERY_ORIGIN_PREFIX_BOOKMARK
+ : QUERY_ORIGIN_BOOKMARK,
+ opts,
+ ];
+ }
+ throw new Error("Either history or bookmark behavior expected");
+ }
+
+ /**
+ * Obtains the query to search for autoFill url results.
+ *
+ * @param {UrlbarQueryContext} queryContext
+ * @returns {array} consisting of the correctly optimized query to search the
+ * database with and an object containing the params to bound.
+ */
+ _getUrlQuery(queryContext) {
+ // Try to get the host from the search string. The host is the part of the
+ // URL up to either the path slash, port colon, or query "?". If the search
+ // string doesn't look like it begins with a host, then return; it doesn't
+ // make sense to do a URL query with it.
+ const urlQueryHostRegexp = /^[^/:?]+/;
+ let hostMatch = urlQueryHostRegexp.exec(this._searchString);
+ if (!hostMatch) {
+ return [null, null];
+ }
+
+ let host = hostMatch[0].toLowerCase();
+ let revHost =
+ host
+ .split("")
+ .reverse()
+ .join("") + ".";
+
+ // Build a string that's the URL stripped of its prefix, i.e., the host plus
+ // everything after. Use queryContext.trimmedSearchString instead of
+ // this._searchString because this._searchString has had unEscapeURIForUI()
+ // called on it. It's therefore not necessarily the literal URL.
+ let strippedURL = queryContext.trimmedSearchString;
+ if (this._strippedPrefix) {
+ strippedURL = strippedURL.substr(this._strippedPrefix.length);
+ }
+ strippedURL = host + strippedURL.substr(host.length);
+
+ let opts = {
+ query_type: QUERYTYPE.AUTOFILL_URL,
+ revHost,
+ strippedURL,
+ };
+ if (this._strippedPrefix) {
+ opts.prefix = this._strippedPrefix;
+ }
+
+ if (
+ queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY) &&
+ queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS)
+ ) {
+ return [
+ this._strippedPrefix
+ ? QUERY_URL_PREFIX_HISTORY_BOOKMARK
+ : QUERY_URL_HISTORY_BOOKMARK,
+ opts,
+ ];
+ }
+ if (queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY)) {
+ return [
+ this._strippedPrefix ? QUERY_URL_PREFIX_HISTORY : QUERY_URL_HISTORY,
+ opts,
+ ];
+ }
+ if (queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS)) {
+ return [
+ this._strippedPrefix ? QUERY_URL_PREFIX_BOOKMARK : QUERY_URL_BOOKMARK,
+ opts,
+ ];
+ }
+ throw new Error("Either history or bookmark behavior expected");
+ }
+
+ /**
+ * Processes a matched row in the Places database.
+ * @param {object} row
+ * The matched row.
+ * @param {UrlbarQueryContext} queryContext
+ * @returns {UrlbarResult} a result generated from the matches row.
+ */
+ _processRow(row, queryContext) {
+ let queryType = row.getResultByName("query_type");
+ let autofilledValue, finalCompleteValue;
+ switch (queryType) {
+ case QUERYTYPE.AUTOFILL_ORIGIN:
+ autofilledValue = row.getResultByName("host_fixed");
+ finalCompleteValue = row.getResultByName("url");
+ break;
+ case QUERYTYPE.AUTOFILL_URL:
+ let url = row.getResultByName("url");
+ let strippedURL = row.getResultByName("stripped_url");
+ // We autofill urls to-the-next-slash.
+ // http://mozilla.org/foo/bar/baz will be autofilled to:
+ // - http://mozilla.org/f[oo/]
+ // - http://mozilla.org/foo/b[ar/]
+ // - http://mozilla.org/foo/bar/b[az]
+ // And, toLowerCase() is preferred over toLocaleLowerCase() here
+ // because "COLLATE NOCASE" in the SQL only handles ASCII characters.
+ let strippedURLIndex = url
+ .toLowerCase()
+ .indexOf(strippedURL.toLowerCase());
+ let strippedPrefix = url.substr(0, strippedURLIndex);
+ let nextSlashIndex = url.indexOf(
+ "/",
+ strippedURLIndex + strippedURL.length - 1
+ );
+ if (nextSlashIndex == -1) {
+ autofilledValue = url.substr(strippedURLIndex);
+ } else {
+ autofilledValue = url.substring(strippedURLIndex, nextSlashIndex + 1);
+ }
+ finalCompleteValue = strippedPrefix + autofilledValue;
+ break;
+ }
+
+ let [title] = UrlbarUtils.stripPrefixAndTrim(finalCompleteValue, {
+ stripHttp: true,
+ trimEmptyQuery: true,
+ trimSlash: !this._searchString.includes("/"),
+ });
+ let result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ title: [title, UrlbarUtils.HIGHLIGHT.TYPED],
+ url: [finalCompleteValue, UrlbarUtils.HIGHLIGHT.TYPED],
+ icon: iconHelper(finalCompleteValue),
+ })
+ );
+ autofilledValue =
+ queryContext.searchString +
+ autofilledValue.substring(this._searchString.length);
+ result.autofill = {
+ value: autofilledValue,
+ selectionStart: queryContext.searchString.length,
+ selectionEnd: autofilledValue.length,
+ };
+ return result;
+ }
+
+ async _getAutofillResult(queryContext) {
+ // We may be autofilling an about: link.
+ let result = this._matchAboutPageForAutofill(queryContext);
+ if (result) {
+ return result;
+ }
+
+ // It may also look like a URL we know from the database.
+ result = await this._matchKnownUrl(queryContext);
+ if (result) {
+ return result;
+ }
+
+ // Or we may want to fill a search engine domain regardless of the threshold.
+ result = await this._matchSearchEngineDomain(queryContext);
+ if (result) {
+ return result;
+ }
+
+ return null;
+ }
+
+ _matchAboutPageForAutofill(queryContext) {
+ // Check that the typed query is at least one character longer than the
+ // about: prefix.
+ if (this._strippedPrefix != "about:" || !this._searchString) {
+ return null;
+ }
+
+ for (const aboutUrl of AboutPagesUtils.visibleAboutUrls) {
+ if (aboutUrl.startsWith(`about:${this._searchString.toLowerCase()}`)) {
+ let [trimmedUrl] = UrlbarUtils.stripPrefixAndTrim(aboutUrl, {
+ stripHttp: true,
+ trimEmptyQuery: true,
+ trimSlash: !this._searchString.includes("/"),
+ });
+ let result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ title: [trimmedUrl, UrlbarUtils.HIGHLIGHT.TYPED],
+ url: [aboutUrl, UrlbarUtils.HIGHLIGHT.TYPED],
+ icon: iconHelper(aboutUrl),
+ })
+ );
+ let autofilledValue =
+ queryContext.searchString +
+ aboutUrl.substring(queryContext.searchString.length);
+ result.autofill = {
+ value: autofilledValue,
+ selectionStart: queryContext.searchString.length,
+ selectionEnd: autofilledValue.length,
+ };
+ return result;
+ }
+ }
+ return null;
+ }
+
+ async _matchKnownUrl(queryContext) {
+ let conn = await PlacesUtils.promiseLargeCacheDBConnection();
+ if (!conn) {
+ return null;
+ }
+ // If search string looks like an origin, try to autofill against origins.
+ // Otherwise treat it as a possible URL. When the string has only one slash
+ // at the end, we still treat it as an URL.
+ let query, params;
+ if (
+ UrlbarTokenizer.looksLikeOrigin(this._searchString, {
+ ignoreKnownDomains: true,
+ })
+ ) {
+ [query, params] = this._getOriginQuery(queryContext);
+ } else {
+ [query, params] = this._getUrlQuery(queryContext);
+ }
+
+ // _getUrlQuery doesn't always return a query.
+ if (query) {
+ let rows = await conn.executeCached(query, params);
+ if (rows.length) {
+ return this._processRow(rows[0], queryContext);
+ }
+ }
+ return null;
+ }
+
+ async _matchSearchEngineDomain(queryContext) {
+ if (!UrlbarPrefs.get("autoFill.searchEngines")) {
+ return null;
+ }
+
+ // enginesForDomainPrefix only matches against engine domains.
+ // Remove an eventual trailing slash from the search string (without the
+ // prefix) and check if the resulting string is worth matching.
+ // Later, we'll verify that the found result matches the original
+ // searchString and eventually discard it.
+ let searchStr = this._searchString;
+ if (searchStr.indexOf("/") == searchStr.length - 1) {
+ searchStr = searchStr.slice(0, -1);
+ }
+ // If the search string looks more like a url than a domain, bail out.
+ if (
+ !UrlbarTokenizer.looksLikeOrigin(searchStr, { ignoreKnownDomains: true })
+ ) {
+ return null;
+ }
+
+ // Since we are autofilling, we can only pick one matching engine. Use the
+ // first.
+ let engine = (await UrlbarSearchUtils.enginesForDomainPrefix(searchStr))[0];
+ if (!engine) {
+ return null;
+ }
+ let url = engine.searchForm;
+ let domain = engine.getResultDomain();
+ // Verify that the match we got is acceptable. Autofilling "example/" to
+ // "example.com/" would not be good.
+ if (
+ (this._strippedPrefix && !url.startsWith(this._strippedPrefix)) ||
+ !(domain + "/").includes(this._searchString)
+ ) {
+ return null;
+ }
+
+ // The value that's autofilled in the input is the prefix the user typed, if
+ // any, plus the portion of the engine domain that the user typed. Append a
+ // trailing slash too, as is usual with autofill.
+ let value =
+ this._strippedPrefix + domain.substr(domain.indexOf(searchStr)) + "/";
+
+ let result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ engine: [engine.name, UrlbarUtils.HIGHLIGHT.TYPED],
+ icon: engine.iconURI?.spec,
+ })
+ );
+ let autofilledValue =
+ queryContext.searchString +
+ value.substring(queryContext.searchString.length);
+ result.autofill = {
+ value: autofilledValue,
+ selectionStart: queryContext.searchString.length,
+ selectionEnd: autofilledValue.length,
+ };
+ return result;
+ }
+}
+
+var UrlbarProviderAutofill = new ProviderAutofill();
diff --git a/browser/components/urlbar/UrlbarProviderExtension.jsm b/browser/components/urlbar/UrlbarProviderExtension.jsm
new file mode 100644
index 0000000000..58b78ddb42
--- /dev/null
+++ b/browser/components/urlbar/UrlbarProviderExtension.jsm
@@ -0,0 +1,390 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * This module exports a provider class that is used for providers created by
+ * extensions.
+ */
+
+var EXPORTED_SYMBOLS = ["UrlbarProviderExtension"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+XPCOMUtils.defineLazyModuleGetters(this, {
+ Services: "resource://gre/modules/Services.jsm",
+ SkippableTimer: "resource:///modules/UrlbarUtils.jsm",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
+ UrlbarProvider: "resource:///modules/UrlbarUtils.jsm",
+ UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm",
+ UrlbarResult: "resource:///modules/UrlbarResult.jsm",
+ UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.jsm",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+});
+
+/**
+ * The browser.urlbar extension API allows extensions to create their own urlbar
+ * providers. The results from extension providers are integrated into the
+ * urlbar view just like the results from providers that are built into Firefox.
+ *
+ * This class is the interface between the provider-related parts of the
+ * browser.urlbar extension API implementation and our internal urlbar
+ * implementation. The API implementation should use this class to manage
+ * providers created by extensions. All extension providers must be instances
+ * of this class.
+ *
+ * When an extension requires a provider, the API implementation should call
+ * getOrCreate() to get or create it. When an extension adds an event listener
+ * related to a provider, the API implementation should call setEventListener()
+ * to register its own event listener with the provider.
+ */
+class UrlbarProviderExtension extends UrlbarProvider {
+ /**
+ * Returns the extension provider with the given name, creating it first if
+ * it doesn't exist.
+ *
+ * @param {string} name
+ * The provider name.
+ * @returns {UrlbarProviderExtension}
+ * The provider.
+ */
+ static getOrCreate(name) {
+ let provider = UrlbarProvidersManager.getProvider(name);
+ if (!provider) {
+ provider = new UrlbarProviderExtension(name);
+ UrlbarProvidersManager.registerProvider(provider);
+ }
+ return provider;
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param {string} name
+ * The provider's name.
+ */
+ constructor(name) {
+ super();
+ this._name = name;
+ this._eventListeners = new Map();
+ this.behavior = "inactive";
+ }
+
+ /**
+ * The provider's name.
+ */
+ get name() {
+ return this._name;
+ }
+
+ /**
+ * The provider's type. The type of extension providers is always
+ * UrlbarUtils.PROVIDER_TYPE.EXTENSION.
+ */
+ get type() {
+ return UrlbarUtils.PROVIDER_TYPE.EXTENSION;
+ }
+
+ /**
+ * Whether the provider should be invoked for the given context. If this
+ * method returns false, the providers manager won't start a query with this
+ * provider, to save on resources.
+ *
+ * @param {UrlbarQueryContext} context
+ * The query context object.
+ * @returns {boolean}
+ * Whether this provider should be invoked for the search.
+ */
+ isActive(context) {
+ return this.behavior != "inactive";
+ }
+
+ /**
+ * Gets the provider's priority.
+ *
+ * @param {UrlbarQueryContext} context
+ * The query context object.
+ * @returns {number}
+ * The provider's priority for the given query.
+ */
+ getPriority(context) {
+ // We give restricting extension providers a very high priority so that they
+ // normally override all built-in providers, but not Infinity so that we can
+ // still override them if necessary.
+ return this.behavior == "restricting" ? 999 : 0;
+ }
+
+ /**
+ * Sets the listener function for an event. The extension API implementation
+ * should call this from its EventManager.register() implementations. Since
+ * EventManager.register() is called at most only once for each extension
+ * event (the first time the extension adds a listener for the event), each
+ * provider instance needs at most only one listener per event, and that's why
+ * this method is named setEventListener instead of addEventListener.
+ *
+ * The given listener function may return a promise that's resolved once the
+ * extension responds to the event, or if the event requires no response from
+ * the extension, it may return a non-promise value (possibly nothing).
+ *
+ * To remove the previously set listener, call this method again but pass null
+ * as the listener function.
+ *
+ * The event name should be one of the following:
+ *
+ * behaviorRequested
+ * This event is fired when the provider's behavior is needed from the
+ * extension. The listener should return a behavior string.
+ * queryCanceled
+ * This event is fired when an ongoing query is canceled. The listener
+ * shouldn't return anything.
+ * resultsRequested
+ * This event is fired when the provider's results are needed from the
+ * extension. The listener should return an array of results.
+ *
+ * @param {string} eventName
+ * The name of the event to listen to.
+ * @param {function} listener
+ * The function that will be called when the event is fired.
+ */
+ setEventListener(eventName, listener) {
+ if (listener) {
+ this._eventListeners.set(eventName, listener);
+ } else {
+ this._eventListeners.delete(eventName);
+ if (!this._eventListeners.size) {
+ UrlbarProvidersManager.unregisterProvider(this);
+ }
+ }
+ }
+
+ /**
+ * This method is called by the providers manager before a query starts to
+ * update each extension provider's behavior. It fires the behaviorRequested
+ * event.
+ *
+ * @param {UrlbarQueryContext} context
+ * The query context.
+ */
+ async updateBehavior(context) {
+ let behavior = await this._notifyListener("behaviorRequested", context);
+ if (behavior) {
+ this.behavior = behavior;
+ }
+ }
+
+ /**
+ * This is called only for dynamic result types, when the urlbar view updates
+ * the view of one of the results of the provider. It should return an object
+ * describing the view update. See the base UrlbarProvider class for more.
+ *
+ * @param {UrlbarResult} result The result whose view will be updated.
+ * @param {Map} idsByName
+ * A Map from an element's name, as defined by the provider; to its ID in
+ * the DOM, as defined by the browser.
+ * @returns {object} An object describing the view update.
+ */
+ async getViewUpdate(result, idsByName) {
+ return this._notifyListener("getViewUpdate", result, idsByName);
+ }
+
+ /**
+ * This method is called by the providers manager when a query starts to fetch
+ * each extension provider's results. It fires the resultsRequested event.
+ *
+ * @param {UrlbarQueryContext} context
+ * The query context.
+ * @param {function} addCallback
+ * The callback invoked by this method to add each result.
+ */
+ async startQuery(context, addCallback) {
+ let extResults = await this._notifyListener("resultsRequested", context);
+ if (extResults) {
+ for (let extResult of extResults) {
+ let result = await this._makeUrlbarResult(context, extResult).catch(
+ Cu.reportError
+ );
+ if (result) {
+ addCallback(this, result);
+ }
+ }
+ }
+ }
+
+ /**
+ * This method is called by the providers manager when an ongoing query is
+ * canceled. It fires the queryCanceled event.
+ *
+ * @param {UrlbarQueryContext} context
+ * The query context.
+ */
+ cancelQuery(context) {
+ this._notifyListener("queryCanceled", context);
+ }
+
+ /**
+ * This method is called when a result from the provider without a URL is
+ * picked, but currently only for tip results. The provider should handle the
+ * pick.
+ *
+ * @param {UrlbarResult} result
+ * The result that was picked.
+ * @param {Element} element
+ * The element in the result's view that was picked.
+ */
+ pickResult(result, element) {
+ let dynamicElementName = "";
+ if (element && result.type == UrlbarUtils.RESULT_TYPE.DYNAMIC) {
+ dynamicElementName = element.getAttribute("name");
+ }
+ this._notifyListener("resultPicked", result.payload, dynamicElementName);
+ }
+
+ /**
+ * This method is called when the user starts and ends an engagement with the
+ * urlbar.
+ *
+ * @param {boolean} isPrivate
+ * True if the engagement is in a private context.
+ * @param {string} state
+ * The state of the engagement, one of: start, engagement, abandonment,
+ * discard.
+ */
+ onEngagement(isPrivate, state) {
+ this._notifyListener("engagement", isPrivate, state);
+ }
+
+ /**
+ * Calls a listener function set by the extension API implementation, if any.
+ *
+ * @param {string} eventName
+ * The name of the listener to call (i.e., the name of the event to fire).
+ * @param {*} args
+ * Arguments to the listener function.
+ * @returns {*}
+ * The value returned by the listener function, if any.
+ */
+ async _notifyListener(eventName, ...args) {
+ let listener = this._eventListeners.get(eventName);
+ if (!listener) {
+ return undefined;
+ }
+ let result;
+ try {
+ result = listener(...args);
+ } catch (error) {
+ Cu.reportError(error);
+ return undefined;
+ }
+ if (result.catch) {
+ // The result is a promise, so wait for it to be resolved. Set up a timer
+ // so that we're not stuck waiting forever.
+ let timer = new SkippableTimer({
+ name: "UrlbarProviderExtension notification timer",
+ time: UrlbarPrefs.get("extension.timeout"),
+ reportErrorOnTimeout: true,
+ logger: this.logger,
+ });
+ result = await Promise.race([
+ timer.promise,
+ result.catch(Cu.reportError),
+ ]);
+ timer.cancel();
+ }
+ return result;
+ }
+
+ /**
+ * Converts a plain-JS-object result created by the extension into a
+ * UrlbarResult object.
+ *
+ * @param {UrlbarQueryContext} context
+ * The query context.
+ * @param {object} extResult
+ * A plain JS object representing a result created by the extension.
+ * @returns {UrlbarResult}
+ * The UrlbarResult object.
+ */
+ async _makeUrlbarResult(context, extResult) {
+ // If the result is a search result, make sure its payload has a valid
+ // `engine` property, which is the name of an engine, and which we use later
+ // on to look up the nsISearchEngine. We allow the extension to specify the
+ // engine by its name, alias, or domain. Prefer aliases over domains since
+ // one domain can have many engines.
+ if (extResult.type == "search") {
+ let engine;
+ if (extResult.payload.engine) {
+ // Validate the engine name by looking it up.
+ engine = Services.search.getEngineByName(extResult.payload.engine);
+ } else if (extResult.payload.keyword) {
+ // Look up the engine by its alias.
+ engine = await UrlbarSearchUtils.engineForAlias(
+ extResult.payload.keyword
+ );
+ } else if (extResult.payload.url) {
+ // Look up the engine by its domain.
+ let host;
+ try {
+ host = new URL(extResult.payload.url).hostname;
+ } catch (err) {}
+ if (host) {
+ engine = (await UrlbarSearchUtils.enginesForDomainPrefix(host))[0];
+ }
+ }
+ if (!engine) {
+ // No engine found.
+ throw new Error("Invalid or missing engine specified by extension");
+ }
+ extResult.payload.engine = engine.name;
+ }
+
+ let type = UrlbarProviderExtension.RESULT_TYPES[extResult.type];
+ if (type == UrlbarUtils.RESULT_TYPE.TIP) {
+ extResult.payload.type = extResult.payload.type || "extension";
+ }
+
+ let result = new UrlbarResult(
+ UrlbarProviderExtension.RESULT_TYPES[extResult.type],
+ UrlbarProviderExtension.SOURCE_TYPES[extResult.source],
+ ...UrlbarResult.payloadAndSimpleHighlights(
+ context.tokens,
+ extResult.payload || {}
+ )
+ );
+ if (extResult.heuristic && this.behavior == "restricting") {
+ // The muxer chooses the final heuristic result by taking the first one
+ // that claims to be the heuristic. We don't want extensions to clobber
+ // UnifiedComplete's heuristic, so we allow this only if the provider is
+ // restricting.
+ result.heuristic = extResult.heuristic;
+ }
+ if (extResult.suggestedIndex !== undefined) {
+ result.suggestedIndex = extResult.suggestedIndex;
+ }
+ return result;
+ }
+}
+
+// Maps extension result type enums to internal result types.
+UrlbarProviderExtension.RESULT_TYPES = {
+ dynamic: UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ keyword: UrlbarUtils.RESULT_TYPE.KEYWORD,
+ omnibox: UrlbarUtils.RESULT_TYPE.OMNIBOX,
+ remote_tab: UrlbarUtils.RESULT_TYPE.REMOTE_TAB,
+ search: UrlbarUtils.RESULT_TYPE.SEARCH,
+ tab: UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ tip: UrlbarUtils.RESULT_TYPE.TIP,
+ url: UrlbarUtils.RESULT_TYPE.URL,
+};
+
+// Maps extension source type enums to internal source types.
+UrlbarProviderExtension.SOURCE_TYPES = {
+ bookmarks: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ history: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ local: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ network: UrlbarUtils.RESULT_SOURCE.OTHER_NETWORK,
+ search: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ tabs: UrlbarUtils.RESULT_SOURCE.TABS,
+};
diff --git a/browser/components/urlbar/UrlbarProviderHeuristicFallback.jsm b/browser/components/urlbar/UrlbarProviderHeuristicFallback.jsm
new file mode 100644
index 0000000000..3ddf7f95ec
--- /dev/null
+++ b/browser/components/urlbar/UrlbarProviderHeuristicFallback.jsm
@@ -0,0 +1,326 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * This module exports a provider that provides a heuristic result. The result
+ * either vists a URL or does a search with the current engine. This result is
+ * always the ultimate fallback for any query, so this provider is always active.
+ */
+
+var EXPORTED_SYMBOLS = ["UrlbarProviderHeuristicFallback"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+XPCOMUtils.defineLazyModuleGetters(this, {
+ Services: "resource://gre/modules/Services.jsm",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
+ UrlbarProvider: "resource:///modules/UrlbarUtils.jsm",
+ UrlbarResult: "resource:///modules/UrlbarResult.jsm",
+ UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.jsm",
+ UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.jsm",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+});
+
+/**
+ * Class used to create the provider.
+ */
+class ProviderHeuristicFallback extends UrlbarProvider {
+ constructor() {
+ super();
+ }
+
+ /**
+ * Returns the name of this provider.
+ * @returns {string} the name of this provider.
+ */
+ get name() {
+ return "HeuristicFallback";
+ }
+
+ /**
+ * Returns the type of this provider.
+ * @returns {integer} one of the types from UrlbarUtils.PROVIDER_TYPE.*
+ */
+ get type() {
+ return UrlbarUtils.PROVIDER_TYPE.HEURISTIC;
+ }
+
+ /**
+ * Whether this provider should be invoked for the given context.
+ * If this method returns false, the providers manager won't start a query
+ * with this provider, to save on resources.
+ * @param {UrlbarQueryContext} queryContext The query context object
+ * @returns {boolean} Whether this provider should be invoked for the search.
+ */
+ isActive(queryContext) {
+ return true;
+ }
+
+ /**
+ * Gets the provider's priority.
+ * @param {UrlbarQueryContext} queryContext The query context object
+ * @returns {number} The provider's priority for the given query.
+ */
+ getPriority(queryContext) {
+ return 0;
+ }
+
+ /**
+ * Starts querying.
+ * @param {object} queryContext The query context object
+ * @param {function} addCallback Callback invoked by the provider to add a new
+ * result.
+ * @returns {Promise} resolved when the query stops.
+ */
+ async startQuery(queryContext, addCallback) {
+ let instance = this.queryInstance;
+
+ let result = this._matchUnknownUrl(queryContext);
+ if (result) {
+ addCallback(this, result);
+ // Since we can't tell if this is a real URL and whether the user wants
+ // to visit or search for it, we provide an alternative searchengine
+ // match if the string looks like an alphanumeric origin or an e-mail.
+ let str = queryContext.searchString;
+ try {
+ new URL(str);
+ } catch (ex) {
+ if (
+ UrlbarPrefs.get("keyword.enabled") &&
+ (UrlbarTokenizer.looksLikeOrigin(str, {
+ noIp: true,
+ noPort: true,
+ }) ||
+ UrlbarTokenizer.REGEXP_COMMON_EMAIL.test(str))
+ ) {
+ let searchResult = this._engineSearchResult(queryContext);
+ if (instance != this.queryInstance) {
+ return;
+ }
+ addCallback(this, searchResult);
+ }
+ }
+ return;
+ }
+
+ result = this._searchModeKeywordResult(queryContext);
+ if (result) {
+ addCallback(this, result);
+ return;
+ }
+
+ result = this._engineSearchResult(queryContext);
+ if (instance != this.queryInstance) {
+ return;
+ }
+ if (result) {
+ result.heuristic = true;
+ addCallback(this, result);
+ }
+ }
+
+ // TODO (bug 1054814): Use visited URLs to inform which scheme to use, if the
+ // scheme isn't specificed.
+ _matchUnknownUrl(queryContext) {
+ // The user may have typed something like "word?" to run a search. We
+ // should not convert that to a URL. We should also never convert actual
+ // URLs into URL results when search mode is active or a search mode
+ // restriction token was typed.
+ if (
+ queryContext.restrictSource == UrlbarUtils.RESULT_SOURCE.SEARCH ||
+ UrlbarTokenizer.SEARCH_MODE_RESTRICT.has(
+ queryContext.restrictToken?.value
+ ) ||
+ queryContext.searchMode
+ ) {
+ return null;
+ }
+
+ let unescapedSearchString = Services.textToSubURI.unEscapeURIForUI(
+ queryContext.searchString
+ );
+ let [prefix, suffix] = UrlbarUtils.stripURLPrefix(unescapedSearchString);
+ if (!suffix && prefix) {
+ // The user just typed a stripped protocol, don't build a non-sense url
+ // like http://http/ for it.
+ return null;
+ }
+
+ let searchUrl = queryContext.trimmedSearchString;
+
+ if (queryContext.fixupError) {
+ if (
+ queryContext.fixupError == Cr.NS_ERROR_MALFORMED_URI &&
+ !UrlbarPrefs.get("keyword.enabled")
+ ) {
+ let result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ title: [searchUrl, UrlbarUtils.HIGHLIGHT.NONE],
+ url: [searchUrl, UrlbarUtils.HIGHLIGHT.NONE],
+ })
+ );
+ result.heuristic = true;
+ return result;
+ }
+
+ return null;
+ }
+
+ // If the URI cannot be fixed or the preferred URI would do a keyword search,
+ // that basically means this isn't useful to us. Note that
+ // fixupInfo.keywordAsSent will never be true if the keyword.enabled pref
+ // is false or there are no engines, so in that case we will always return
+ // a "visit".
+ if (!queryContext.fixupInfo?.href || queryContext.fixupInfo?.isSearch) {
+ return null;
+ }
+
+ let uri = new URL(queryContext.fixupInfo.href);
+ // Check the host, as "http:///" is a valid nsIURI, but not useful to us.
+ // But, some schemes are expected to have no host. So we check just against
+ // schemes we know should have a host. This allows new schemes to be
+ // implemented without us accidentally blocking access to them.
+ let hostExpected = ["http:", "https:", "ftp:", "chrome:"].includes(
+ uri.protocol
+ );
+ if (hostExpected && !uri.host) {
+ return null;
+ }
+
+ // getFixupURIInfo() escaped the URI, so it may not be pretty. Embed the
+ // escaped URL in the result since that URL should be "canonical". But
+ // pass the pretty, unescaped URL as the result's title, since it is
+ // displayed to the user.
+ let escapedURL = uri.toString();
+ let displayURL = decodeURI(uri);
+
+ // We don't know if this url is in Places or not, and checking that would
+ // be expensive. Thus we also don't know if we may have an icon.
+ // If we'd just try to fetch the icon for the typed string, we'd cause icon
+ // flicker, since the url keeps changing while the user types.
+ // By default we won't provide an icon, but for the subset of urls with a
+ // host we'll check for a typed slash and set favicon for the host part.
+ let iconUri;
+ if (hostExpected && (searchUrl.endsWith("/") || uri.pathname.length > 1)) {
+ // Look for an icon with the entire URL except for the pathname, including
+ // scheme, usernames, passwords, hostname, and port.
+ let pathIndex = uri.toString().lastIndexOf(uri.pathname);
+ let prePath = uri.toString().slice(0, pathIndex);
+ iconUri = `page-icon:${prePath}/`;
+ }
+
+ let result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ title: [displayURL, UrlbarUtils.HIGHLIGHT.NONE],
+ url: [escapedURL, UrlbarUtils.HIGHLIGHT.NONE],
+ icon: iconUri,
+ })
+ );
+ result.heuristic = true;
+ return result;
+ }
+
+ _searchModeKeywordResult(queryContext) {
+ if (!queryContext.tokens.length) {
+ return null;
+ }
+
+ let firstToken = queryContext.tokens[0].value;
+ if (!UrlbarTokenizer.SEARCH_MODE_RESTRICT.has(firstToken)) {
+ return null;
+ }
+
+ // At this point, the search string starts with a token that can be
+ // converted into search mode.
+ // Now we need to determine what to do based on the remainder of the search
+ // string. If the remainder starts with a space, then we should enter
+ // search mode, so we should continue below and create the result.
+ // Otherwise, we should not enter search mode, and in that case, the search
+ // string will look like one of the following:
+ //
+ // * The search string ends with the restriction token (e.g., the user
+ // has typed only the token by itself, with no trailing spaces).
+ // * More tokens exist, but there's no space between the restriction
+ // token and the following token. This is possible because the tokenizer
+ // does not require spaces between a restriction token and the remainder
+ // of the search string. In this case, we should not enter search mode.
+ //
+ // If we return null here and thereby do not enter search mode, then we'll
+ // continue on to _engineSearchResult, and the heuristic will be a
+ // default engine search result.
+ let query = UrlbarUtils.substringAfter(
+ queryContext.searchString,
+ firstToken
+ );
+ if (!UrlbarTokenizer.REGEXP_SPACES_START.test(query)) {
+ return null;
+ }
+
+ let result;
+ if (queryContext.restrictSource == UrlbarUtils.RESULT_SOURCE.SEARCH) {
+ result = this._engineSearchResult(queryContext, firstToken);
+ } else {
+ result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ query: [query.trimStart(), UrlbarUtils.HIGHLIGHT.NONE],
+ keyword: [firstToken, UrlbarUtils.HIGHLIGHT.NONE],
+ })
+ );
+ }
+ result.heuristic = true;
+ return result;
+ }
+
+ _engineSearchResult(queryContext, keyword = null) {
+ let engine;
+ if (queryContext.searchMode?.engineName) {
+ engine = Services.search.getEngineByName(
+ queryContext.searchMode.engineName
+ );
+ } else {
+ engine = UrlbarSearchUtils.getDefaultEngine(queryContext.isPrivate);
+ }
+
+ if (!engine) {
+ return null;
+ }
+
+ // Strip a leading search restriction char, because we prepend it to text
+ // when the search shortcut is used and it's not user typed. Don't strip
+ // other restriction chars, so that it's possible to search for things
+ // including one of those (e.g. "c#").
+ let query = queryContext.searchString;
+ if (
+ queryContext.tokens[0] &&
+ queryContext.tokens[0].value === UrlbarTokenizer.RESTRICT.SEARCH
+ ) {
+ query = UrlbarUtils.substringAfter(
+ query,
+ queryContext.tokens[0].value
+ ).trim();
+ }
+
+ return new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ engine: [engine.name, UrlbarUtils.HIGHLIGHT.TYPED],
+ icon: engine.iconURI?.spec,
+ query: [query, UrlbarUtils.HIGHLIGHT.NONE],
+ keyword: keyword ? [keyword, UrlbarUtils.HIGHLIGHT.NONE] : undefined,
+ })
+ );
+ }
+}
+
+var UrlbarProviderHeuristicFallback = new ProviderHeuristicFallback();
diff --git a/browser/components/urlbar/UrlbarProviderInterventions.jsm b/browser/components/urlbar/UrlbarProviderInterventions.jsm
new file mode 100644
index 0000000000..d303da0f49
--- /dev/null
+++ b/browser/components/urlbar/UrlbarProviderInterventions.jsm
@@ -0,0 +1,810 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["UrlbarProviderInterventions", "QueryScorer"];
+var gGlobalScope = this;
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AppUpdater: "resource:///modules/AppUpdater.jsm",
+ BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
+ NLP: "resource://gre/modules/NLP.jsm",
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
+ ResetProfile: "resource://gre/modules/ResetProfile.jsm",
+ Sanitizer: "resource:///modules/Sanitizer.jsm",
+ Services: "resource://gre/modules/Services.jsm",
+ UrlbarProvider: "resource:///modules/UrlbarUtils.jsm",
+ UrlbarResult: "resource:///modules/UrlbarResult.jsm",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(this, "appUpdater", () => new AppUpdater());
+
+// The possible tips to show. These names (except NONE) are used in the names
+// of keys in the `urlbar.tips` keyed scalar telemetry (see telemetry.rst).
+// Don't modify them unless you've considered that. If you do modify them or
+// add new tips, then you are also adding new `urlbar.tips` keys and therefore
+// need an expanded data collection review.
+const TIPS = {
+ NONE: "",
+ CLEAR: "intervention_clear",
+ REFRESH: "intervention_refresh",
+
+ // There's an update available, but the user's pref says we should ask them to
+ // download and apply it.
+ UPDATE_ASK: "intervention_update_ask",
+
+ // The updater is currently checking. We don't actually show a tip for this,
+ // but we use it to tell whether we should wait for the check to complete in
+ // startQuery. See startQuery for details.
+ UPDATE_CHECKING: "intervention_update_checking",
+
+ // The user's browser is up to date, but they triggered the update
+ // intervention. We show this special refresh intervention instead.
+ UPDATE_REFRESH: "intervention_update_refresh",
+
+ // There's an update and it's been downloaded and applied. The user needs to
+ // restart to finish.
+ UPDATE_RESTART: "intervention_update_restart",
+
+ // We can't update the browser or possibly even check for updates for some
+ // reason, so the user should download the latest version from the web.
+ UPDATE_WEB: "intervention_update_web",
+};
+
+const EN_LOCALE_MATCH = /^en(-.*)$/;
+
+// The search "documents" corresponding to each tip type.
+const DOCUMENTS = {
+ clear: [
+ "cache firefox",
+ "clear cache firefox",
+ "clear cache in firefox",
+ "clear cookies firefox",
+ "clear firefox cache",
+ "clear history firefox",
+ "cookies firefox",
+ "delete cookies firefox",
+ "delete history firefox",
+ "firefox cache",
+ "firefox clear cache",
+ "firefox clear cookies",
+ "firefox clear history",
+ "firefox cookie",
+ "firefox cookies",
+ "firefox delete cookies",
+ "firefox delete history",
+ "firefox history",
+ "firefox not loading pages",
+ "history firefox",
+ "how to clear cache",
+ "how to clear history",
+ ],
+ refresh: [
+ "firefox crashing",
+ "firefox keeps crashing",
+ "firefox not responding",
+ "firefox not working",
+ "firefox refresh",
+ "firefox slow",
+ "how to reset firefox",
+ "refresh firefox",
+ "reset firefox",
+ ],
+ update: [
+ "download firefox",
+ "download mozilla",
+ "firefox browser",
+ "firefox download",
+ "firefox for mac",
+ "firefox for windows",
+ "firefox free download",
+ "firefox install",
+ "firefox installer",
+ "firefox latest version",
+ "firefox mac",
+ "firefox quantum",
+ "firefox update",
+ "firefox version",
+ "firefox windows",
+ "get firefox",
+ "how to update firefox",
+ "install firefox",
+ "mozilla download",
+ "mozilla firefox 2019",
+ "mozilla firefox 2020",
+ "mozilla firefox download",
+ "mozilla firefox for mac",
+ "mozilla firefox for windows",
+ "mozilla firefox free download",
+ "mozilla firefox mac",
+ "mozilla firefox update",
+ "mozilla firefox windows",
+ "mozilla update",
+ "update firefox",
+ "update mozilla",
+ "www.firefox.com",
+ ],
+};
+
+// In order to determine whether we should show an update tip, we check for app
+// updates, but only once per this time period.
+const UPDATE_CHECK_PERIOD_MS = 12 * 60 * 60 * 1000; // 12 hours
+
+/**
+ * A node in the QueryScorer's phrase tree.
+ */
+class Node {
+ constructor(word) {
+ this.word = word;
+ this.documents = new Set();
+ this.childrenByWord = new Map();
+ }
+}
+
+/**
+ * This class scores a query string against sets of phrases. To refer to a
+ * single set of phrases, we borrow the term "document" from search engine
+ * terminology. To use this class, first add your documents with `addDocument`,
+ * and then call `score` with a query string. `score` returns a sorted array of
+ * document-score pairs.
+ *
+ * The scoring method is fairly simple and is based on Levenshtein edit
+ * distance. Therefore, lower scores indicate a better match than higher
+ * scores. In summary, a query matches a phrase if the query starts with the
+ * phrase. So a query "firefox update foo bar" matches the phrase "firefox
+ * update" for example. A query matches a document if it matches any phrase in
+ * the document. The query and phrases are compared word for word, and we allow
+ * fuzzy matching by computing the Levenshtein edit distance in each comparison.
+ * The amount of fuzziness allowed is controlled with `distanceThreshold`. If
+ * the distance in a comparison is greater than this threshold, then the phrase
+ * does not match the query. The final score for a document is the minimum edit
+ * distance between its phrases and the query.
+ *
+ * As mentioned, `score` returns a sorted array of document-score pairs. It's
+ * up to you to filter the array to exclude scores above a certain threshold, or
+ * to take the top scorer, etc.
+ */
+class QueryScorer {
+ /**
+ * @param {number} distanceThreshold
+ * Edit distances no larger than this value are considered matches.
+ * @param {Map} variations
+ * For convenience, the scorer can augment documents by replacing certain
+ * words with other words and phrases. This mechanism is called variations.
+ * This keys of this map are words that should be replaced, and the values
+ * are the replacement words or phrases. For example, if you add a document
+ * whose only phrase is "firefox update", normally the scorer will register
+ * only this single phrase for the document. However, if you pass the value
+ * `new Map(["firefox", ["fire fox", "fox fire", "foxfire"]])` for this
+ * parameter, it will register 4 total phrases for the document: "fire fox
+ * update", "fox fire update", "foxfire update", and the original "firefox
+ * update".
+ */
+ constructor({ distanceThreshold = 1, variations = new Map() } = {}) {
+ this._distanceThreshold = distanceThreshold;
+ this._variations = variations;
+ this._documents = new Set();
+ this._rootNode = new Node();
+ }
+
+ /**
+ * Adds a document to the scorer.
+ *
+ * @param {object} doc
+ * The document.
+ * @param {string} doc.id
+ * The document's ID.
+ * @param {array} doc.phrases
+ * The set of phrases in the document. Each phrase should be a string.
+ */
+ addDocument(doc) {
+ this._documents.add(doc);
+
+ for (let phraseStr of doc.phrases) {
+ // Split the phrase and lowercase the words.
+ let phrase = phraseStr
+ .trim()
+ .split(/\s+/)
+ .map(word => word.toLocaleLowerCase());
+
+ // Build a phrase list that contains the original phrase plus its
+ // variations, if any.
+ let phrases = [phrase];
+ for (let [triggerWord, variations] of this._variations) {
+ let index = phrase.indexOf(triggerWord);
+ if (index >= 0) {
+ for (let variation of variations) {
+ let variationPhrase = Array.from(phrase);
+ variationPhrase.splice(index, 1, ...variation.split(/\s+/));
+ phrases.push(variationPhrase);
+ }
+ }
+ }
+
+ // Finally, add the phrases to the phrase tree.
+ for (let completedPhrase of phrases) {
+ this._buildPhraseTree(this._rootNode, doc, completedPhrase, 0);
+ }
+ }
+ }
+
+ /**
+ * Scores a query string against the documents in the scorer.
+ *
+ * @param {string} queryString
+ * The query string to score.
+ * @returns {array}
+ * An array of objects: { document, score }. Each element in the array is a
+ * a document and its score against the query string. The elements are
+ * ordered by score from low to high. Scores represent edit distance, so
+ * lower scores are better.
+ */
+ score(queryString) {
+ let queryWords = queryString
+ .trim()
+ .split(/\s+/)
+ .map(word => word.toLocaleLowerCase());
+ let minDistanceByDoc = this._traverse({ queryWords });
+ let results = [];
+ for (let doc of this._documents) {
+ let distance = minDistanceByDoc.get(doc);
+ results.push({
+ document: doc,
+ score: distance === undefined ? Infinity : distance,
+ });
+ }
+ results.sort((a, b) => a.score - b.score);
+ return results;
+ }
+
+ /**
+ * Builds the phrase tree based on the current documents.
+ *
+ * The phrase tree lets us efficiently match queries against phrases. Each
+ * path through the tree starting from the root and ending at a leaf
+ * represents a complete phrase in a document (or more than one document, if
+ * the same phrase is present in multiple documents). Each node in the path
+ * represents a word in the phrase. To match a query, we start at the root,
+ * and in the root we look up the query's first word. If the word matches the
+ * first word of any phrase, then the root will have a child node representing
+ * that word, and we move on to the child node. Then we look up the query's
+ * second word in the child node, and so on, until either a lookup fails or we
+ * reach a leaf node.
+ *
+ * @param {Node} node
+ * The current node being visited.
+ * @param {object} doc
+ * The document whose phrases are being added to the tree.
+ * @param {array} phrase
+ * The phrase to add to the tree.
+ * @param {number} wordIndex
+ * The index in the phrase of the current word.
+ */
+ _buildPhraseTree(node, doc, phrase, wordIndex) {
+ if (phrase.length == wordIndex) {
+ // We're done with this phrase.
+ return;
+ }
+
+ let word = phrase[wordIndex].toLocaleLowerCase();
+ let child = node.childrenByWord.get(word);
+ if (!child) {
+ child = new Node(word);
+ node.childrenByWord.set(word, child);
+ }
+ child.documents.add(doc);
+
+ // Recurse with the next word in the phrase.
+ this._buildPhraseTree(child, doc, phrase, wordIndex + 1);
+ }
+
+ /**
+ * Traverses a path in the phrase tree in order to score a query. See
+ * `_buildPhraseTree` for a description of how this works.
+ *
+ * @param {array} queryWords
+ * The query being scored, split into words.
+ * @param {Node} node
+ * The node currently being visited.
+ * @param {Map} minDistanceByDoc
+ * Keeps track of the minimum edit distance for each document as the
+ * traversal continues.
+ * @param {number} queryWordsIndex
+ * The current index in the query words array.
+ * @param {number} phraseDistance
+ * The total edit distance between the query and the path in the tree that's
+ * been traversed so far.
+ * @returns {Map} minDistanceByDoc
+ */
+ _traverse({
+ queryWords,
+ node = this._rootNode,
+ minDistanceByDoc = new Map(),
+ queryWordsIndex = 0,
+ phraseDistance = 0,
+ } = {}) {
+ if (!node.childrenByWord.size) {
+ // We reached a leaf node. The query has matched a phrase. If the query
+ // and the phrase have the same number of words, then queryWordsIndex ==
+ // queryWords.length also. Otherwise the query contains more words than
+ // the phrase. We still count that as a match.
+ for (let doc of node.documents) {
+ minDistanceByDoc.set(
+ doc,
+ Math.min(
+ phraseDistance,
+ minDistanceByDoc.has(doc) ? minDistanceByDoc.get(doc) : Infinity
+ )
+ );
+ }
+ return minDistanceByDoc;
+ }
+
+ if (queryWordsIndex == queryWords.length) {
+ // We exhausted all the words in the query but have not reached a leaf
+ // node. No match; the query has matched a phrase(s) up to this point,
+ // but it doesn't have enough words.
+ return minDistanceByDoc;
+ }
+
+ // Compare each word in the node to the current query word.
+ let queryWord = queryWords[queryWordsIndex];
+ for (let [childWord, child] of node.childrenByWord) {
+ let distance = NLP.levenshtein(queryWord, childWord);
+ if (distance <= this._distanceThreshold) {
+ // The word represented by this child node matches the current query
+ // word. Recurse into the child node.
+ this._traverse({
+ node: child,
+ queryWords,
+ queryWordsIndex: queryWordsIndex + 1,
+ phraseDistance: phraseDistance + distance,
+ minDistanceByDoc,
+ });
+ }
+ // Else, the path that continues at the child node can't possibly match
+ // the query, so don't recurse into it.
+ }
+
+ return minDistanceByDoc;
+ }
+}
+
+/**
+ * Gets appropriate l10n values for each tip's payload.
+ * @param {string} tip a value from the TIPS enum
+ * @returns {object} an Object shaped as { textData, buttonTextData, helpUrl }
+ */
+function getL10nPropertiesForTip(tip) {
+ const baseURL = "https://support.mozilla.org/kb/";
+ switch (tip) {
+ case TIPS.CLEAR:
+ return {
+ textData: { id: "intervention-clear-data" },
+ buttonTextData: { id: "intervention-clear-data-confirm" },
+ helpUrl: baseURL + "delete-browsing-search-download-history-firefox",
+ };
+ case TIPS.REFRESH:
+ return {
+ textData: { id: "intervention-refresh-profile" },
+ buttonTextData: { id: "intervention-refresh-profile-confirm" },
+ helpUrl: baseURL + "refresh-firefox-reset-add-ons-and-settings",
+ };
+ case TIPS.UPDATE_ASK:
+ return {
+ textData: { id: "intervention-update-ask" },
+ buttonTextData: { id: "intervention-update-ask-confirm" },
+ helpUrl: baseURL + "update-firefox-latest-release",
+ };
+ case TIPS.UPDATE_REFRESH:
+ return {
+ textData: { id: "intervention-update-refresh" },
+ buttonTextData: { id: "intervention-update-refresh-confirm" },
+ helpUrl: baseURL + "refresh-firefox-reset-add-ons-and-settings",
+ };
+ case TIPS.UPDATE_RESTART:
+ return {
+ textData: { id: "intervention-update-restart" },
+ buttonTextData: { id: "intervention-update-restart-confirm" },
+ helpUrl: baseURL + "update-firefox-latest-release",
+ };
+ case TIPS.UPDATE_WEB:
+ return {
+ textData: { id: "intervention-update-web" },
+ buttonTextData: { id: "intervention-update-web-confirm" },
+ helpUrl: baseURL + "update-firefox-latest-release",
+ };
+ default:
+ throw new Error("Unknown TIP type.");
+ }
+}
+
+/**
+ * A provider that returns actionable tip results when the user is performing
+ * a search related to those actions.
+ */
+class ProviderInterventions extends UrlbarProvider {
+ constructor() {
+ super();
+ // The tip we should currently show.
+ this.currentTip = TIPS.NONE;
+
+ this.tipsShownInCurrentEngagement = new Set();
+
+ // This object is used to match the user's queries to tips.
+ XPCOMUtils.defineLazyGetter(this, "queryScorer", () => {
+ let queryScorer = new QueryScorer({
+ variations: new Map([
+ // Recognize "fire fox", "fox fire", and "foxfire" as "firefox".
+ ["firefox", ["fire fox", "fox fire", "foxfire"]],
+ // Recognize "mozila" as "mozilla". This will catch common mispellings
+ // "mozila", "mozzila", and "mozzilla" (among others) due to the edit
+ // distance threshold of 1.
+ ["mozilla", ["mozila"]],
+ ]),
+ });
+ for (let [id, phrases] of Object.entries(DOCUMENTS)) {
+ queryScorer.addDocument({ id, phrases });
+ }
+ return queryScorer;
+ });
+ }
+
+ /**
+ * Enum of the types of intervention tips.
+ */
+ get TIP_TYPE() {
+ return TIPS;
+ }
+
+ /**
+ * Unique name for the provider, used by the context to filter on providers.
+ */
+ get name() {
+ return "UrlbarProviderInterventions";
+ }
+
+ /**
+ * The type of the provider, must be one of UrlbarUtils.PROVIDER_TYPE.
+ */
+ get type() {
+ return UrlbarUtils.PROVIDER_TYPE.PROFILE;
+ }
+
+ /**
+ * Whether this provider should be invoked for the given context.
+ * If this method returns false, the providers manager won't start a query
+ * with this provider, to save on resources.
+ * @param {UrlbarQueryContext} queryContext The query context object
+ * @returns {boolean} Whether this provider should be invoked for the search.
+ */
+ isActive(queryContext) {
+ if (
+ !queryContext.searchString ||
+ !EN_LOCALE_MATCH.test(Services.locale.appLocaleAsBCP47) ||
+ !Services.policies.isAllowed("urlbarinterventions")
+ ) {
+ return false;
+ }
+
+ this.currentTip = TIPS.NONE;
+
+ // Get the scores and the top score.
+ let docScores = this.queryScorer.score(queryContext.searchString);
+ let topDocScore = docScores[0];
+
+ // Multiple docs may have the top score, so collect them all.
+ let topDocIDs = new Set();
+ if (topDocScore.score != Infinity) {
+ for (let { score, document } of docScores) {
+ if (score != topDocScore.score) {
+ break;
+ }
+ topDocIDs.add(document.id);
+ }
+ }
+
+ // Determine the tip to show, if any. If there are multiple top-score docs,
+ // prefer them in the following order.
+ if (topDocIDs.has("update")) {
+ this._setCurrentTipFromAppUpdaterStatus();
+ } else if (topDocIDs.has("clear")) {
+ let window = BrowserWindowTracker.getTopWindow();
+ if (!PrivateBrowsingUtils.isWindowPrivate(window)) {
+ this.currentTip = TIPS.CLEAR;
+ }
+ } else if (topDocIDs.has("refresh")) {
+ // Note that the "update" case can set currentTip to TIPS.REFRESH too.
+ this.currentTip = TIPS.REFRESH;
+ }
+
+ return (
+ this.currentTip != TIPS.NONE &&
+ (this.currentTip != TIPS.REFRESH ||
+ Services.policies.isAllowed("profileRefresh"))
+ );
+ }
+
+ async _setCurrentTipFromAppUpdaterStatus(waitForCheck) {
+ // The update tips depend on the app's update status, so check for updates
+ // now (if we haven't already checked within the update-check period). If
+ // we're running in an xpcshell test, then checkForBrowserUpdate's attempt
+ // to use appUpdater will throw an exception because it won't be available.
+ // In that case, return false to disable the provider.
+ //
+ // This causes synchronous IO within the updater the first time it's called
+ // (at least) so be careful not to do it the first time the urlbar is used.
+ try {
+ this.checkForBrowserUpdate();
+ } catch (ex) {
+ return;
+ }
+
+ // There are several update tips. Figure out which one to show.
+ switch (appUpdater.status) {
+ case AppUpdater.STATUS.READY_FOR_RESTART:
+ // Prompt the user to restart.
+ this.currentTip = TIPS.UPDATE_RESTART;
+ break;
+ case AppUpdater.STATUS.DOWNLOAD_AND_INSTALL:
+ // There's an update available, but the user's pref says we should ask
+ // them to download and apply it.
+ this.currentTip = TIPS.UPDATE_ASK;
+ break;
+ case AppUpdater.STATUS.NO_UPDATES_FOUND:
+ // We show a special refresh tip when the browser is up to date.
+ this.currentTip = TIPS.UPDATE_REFRESH;
+ break;
+ case AppUpdater.STATUS.CHECKING:
+ // This will be the case the first time we check. See startQuery for
+ // how this special tip is handled.
+ this.currentTip = TIPS.UPDATE_CHECKING;
+ break;
+ default:
+ // Give up and ask the user to download the latest version from the
+ // web. We default to this case when the update is still downloading
+ // because an update doesn't actually occur if the user were to
+ // restart the browser. See bug 1625241.
+ this.currentTip = TIPS.UPDATE_WEB;
+ break;
+ }
+ }
+
+ /**
+ * Starts querying.
+ * @param {UrlbarQueryContext} queryContext The query context object
+ * @param {function} addCallback Callback invoked by the provider to add a new
+ * result. A UrlbarResult should be passed to it.
+ */
+ async startQuery(queryContext, addCallback) {
+ let instance = this.queryInstance;
+
+ // TIPS.UPDATE_CHECKING is special, and we never actually show a tip that
+ // reflects a "checking" status. Instead it's handled like this. We call
+ // appUpdater.check() to start an update check. If we haven't called it
+ // before, then when it returns, appUpdater.status will be
+ // AppUpdater.STATUS.CHECKING, and it will remain CHECKING until the check
+ // finishes. We can add a listener to appUpdater to be notified when the
+ // check finishes. We don't want to wait for it to finish in isActive
+ // because that would block other providers from adding their results, so
+ // instead we wait here in startQuery. The results from other providers
+ // will be added while we're waiting. When the check finishes, we call
+ // addCallback and add our result. It doesn't matter how long the check
+ // takes because if another query starts, the view is closed, or the user
+ // changes the selection, the query will be canceled.
+ if (this.currentTip == TIPS.UPDATE_CHECKING) {
+ // First check the status because it may have changed between the time
+ // isActive was called and now.
+ this._setCurrentTipFromAppUpdaterStatus();
+ if (this.currentTip == TIPS.UPDATE_CHECKING) {
+ // The updater is still checking, so wait for it to finish.
+ await new Promise(resolve => {
+ this._appUpdaterListener = () => {
+ appUpdater.removeListener(this._appUpdaterListener);
+ delete this._appUpdaterListener;
+ resolve();
+ };
+ appUpdater.addListener(this._appUpdaterListener);
+ });
+ if (instance != this.queryInstance) {
+ // The query was canceled before the check finished.
+ return;
+ }
+ // Finally, set the tip from the updater status. The updater should no
+ // longer be checking, but guard against it just in case by returning
+ // early.
+ this._setCurrentTipFromAppUpdaterStatus();
+ if (this.currentTip == TIPS.UPDATE_CHECKING) {
+ return;
+ }
+ }
+ }
+ // At this point, this.currentTip != TIPS.UPDATE_CHECKING because we
+ // returned early above if it was.
+
+ let result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TIP,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ type: this.currentTip,
+ }
+ );
+
+ result.suggestedIndex = 1;
+
+ Object.assign(result.payload, getL10nPropertiesForTip(this.currentTip));
+
+ if (instance != this.queryInstance) {
+ return;
+ }
+
+ this.tipsShownInCurrentEngagement.add(this.currentTip);
+
+ addCallback(this, result);
+ }
+
+ /**
+ * Cancels a running query,
+ * @param {UrlbarQueryContext} queryContext the query context object to cancel
+ * query for.
+ */
+ cancelQuery(queryContext) {
+ // If we're waiting for appUpdater to finish its update check,
+ // this._appUpdaterListener will be defined. We can stop listening now.
+ if (this._appUpdaterListener) {
+ appUpdater.removeListener(this._appUpdaterListener);
+ delete this._appUpdaterListener;
+ }
+ }
+
+ /**
+ * Called when a result from the provider without a URL is picked, but
+ * currently only for tip results. The provider should handle the pick.
+ * @param {UrlbarResult} result
+ * The result that was picked.
+ */
+ pickResult(result) {
+ let tip = result.payload.type;
+
+ // Do the tip action.
+ switch (tip) {
+ case TIPS.CLEAR:
+ openClearHistoryDialog();
+ break;
+ case TIPS.REFRESH:
+ case TIPS.UPDATE_REFRESH:
+ resetBrowser();
+ break;
+ case TIPS.UPDATE_ASK:
+ installBrowserUpdateAndRestart();
+ break;
+ case TIPS.UPDATE_RESTART:
+ restartBrowser();
+ break;
+ case TIPS.UPDATE_WEB:
+ let window = BrowserWindowTracker.getTopWindow();
+ window.gBrowser.selectedTab = window.gBrowser.addWebTab(
+ "https://www.mozilla.org/firefox/new/"
+ );
+ break;
+ }
+ }
+
+ onEngagement(isPrivate, state) {
+ if (["engagement", "abandonment"].includes(state)) {
+ for (let tip of this.tipsShownInCurrentEngagement) {
+ Services.telemetry.keyedScalarAdd("urlbar.tips", `${tip}-shown`, 1);
+ }
+ }
+ this.tipsShownInCurrentEngagement.clear();
+ }
+
+ /**
+ * Checks for app updates.
+ *
+ * @param {boolean} force If false, this only checks for updates if we haven't
+ * already checked within the update-check period. If true, we check
+ * regardless.
+ */
+ checkForBrowserUpdate(force = false) {
+ if (
+ force ||
+ !this._lastUpdateCheckTime ||
+ Date.now() - this._lastUpdateCheckTime >= UPDATE_CHECK_PERIOD_MS
+ ) {
+ this._lastUpdateCheckTime = Date.now();
+ appUpdater.check();
+ }
+ }
+
+ /**
+ * Resets the provider's app updater state by making a new app updater. This
+ * is intended to be used by tests.
+ */
+ resetAppUpdater() {
+ // Reset only if the object has already been initialized.
+ if (!Object.getOwnPropertyDescriptor(gGlobalScope, "appUpdater").get) {
+ appUpdater = new AppUpdater();
+ }
+ }
+}
+
+var UrlbarProviderInterventions = new ProviderInterventions();
+
+/**
+ * Tip callbacks follow.
+ */
+
+function installBrowserUpdateAndRestart() {
+ if (appUpdater.status != AppUpdater.STATUS.DOWNLOAD_AND_INSTALL) {
+ return Promise.resolve();
+ }
+ return new Promise(resolve => {
+ let listener = () => {
+ // Once we call startDownload, there are two possible end
+ // states: DOWNLOAD_FAILED and READY_FOR_RESTART.
+ if (
+ appUpdater.status != AppUpdater.STATUS.READY_FOR_RESTART &&
+ appUpdater.status != AppUpdater.STATUS.DOWNLOAD_FAILED
+ ) {
+ return;
+ }
+ appUpdater.removeListener(listener);
+ if (appUpdater.status == AppUpdater.STATUS.READY_FOR_RESTART) {
+ restartBrowser();
+ }
+ resolve();
+ };
+ appUpdater.addListener(listener);
+ appUpdater.startDownload();
+ });
+}
+
+function openClearHistoryDialog() {
+ let window = BrowserWindowTracker.getTopWindow();
+ // The behaviour of the Clear Recent History dialog in PBM does
+ // not have the expected effect (bug 463607).
+ if (PrivateBrowsingUtils.isWindowPrivate(window)) {
+ return;
+ }
+ Sanitizer.showUI(window);
+}
+
+function restartBrowser() {
+ // Notify all windows that an application quit has been requested.
+ let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(
+ Ci.nsISupportsPRBool
+ );
+ Services.obs.notifyObservers(
+ cancelQuit,
+ "quit-application-requested",
+ "restart"
+ );
+ // Something aborted the quit process.
+ if (cancelQuit.data) {
+ return;
+ }
+ // If already in safe mode restart in safe mode.
+ if (Services.appinfo.inSafeMode) {
+ Services.startup.restartInSafeMode(Ci.nsIAppStartup.eAttemptQuit);
+ } else {
+ Services.startup.quit(
+ Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart
+ );
+ }
+}
+
+function resetBrowser() {
+ if (!ResetProfile.resetSupported()) {
+ return;
+ }
+ let window = BrowserWindowTracker.getTopWindow();
+ ResetProfile.openConfirmationDialog(window);
+}
diff --git a/browser/components/urlbar/UrlbarProviderOmnibox.jsm b/browser/components/urlbar/UrlbarProviderOmnibox.jsm
new file mode 100644
index 0000000000..86399316f4
--- /dev/null
+++ b/browser/components/urlbar/UrlbarProviderOmnibox.jsm
@@ -0,0 +1,178 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * This module exports a provider class that is used for providers created by
+ * extensions using the `omnibox` API.
+ */
+
+var EXPORTED_SYMBOLS = ["UrlbarProviderOmnibox"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+XPCOMUtils.defineLazyModuleGetters(this, {
+ ExtensionSearchHandler: "resource://gre/modules/ExtensionSearchHandler.jsm",
+ SkippableTimer: "resource:///modules/UrlbarUtils.jsm",
+ UrlbarProvider: "resource:///modules/UrlbarUtils.jsm",
+ UrlbarResult: "resource:///modules/UrlbarResult.jsm",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+});
+
+// After this time, we'll give up waiting for the extension to return matches.
+const MAXIMUM_ALLOWED_EXTENSION_TIME_MS = 3000;
+
+/**
+ * This provider handles results returned by extensions using the WebExtensions
+ * Omnibox API. If the user types a registered keyword, we send subsequent
+ * keystrokes to the extension.
+ */
+class ProviderOmnibox extends UrlbarProvider {
+ constructor() {
+ super();
+ }
+
+ /**
+ * Returns the name of this provider.
+ * @returns {string} the name of this provider.
+ */
+ get name() {
+ return "Omnibox";
+ }
+
+ /**
+ * Returns the type of this provider.
+ * @returns {integer} one of the types from UrlbarUtils.PROVIDER_TYPE.*
+ */
+ get type() {
+ return UrlbarUtils.PROVIDER_TYPE.HEURISTIC;
+ }
+
+ /**
+ * Whether the provider should be invoked for the given context. If this
+ * method returns false, the providers manager won't start a query with this
+ * provider, to save on resources.
+ *
+ * @param {UrlbarQueryContext} queryContext
+ * The query context object.
+ * @returns {boolean}
+ * Whether this provider should be invoked for the search.
+ */
+ isActive(queryContext) {
+ if (
+ queryContext.tokens[0] &&
+ queryContext.tokens[0].value.length &&
+ ExtensionSearchHandler.isKeywordRegistered(
+ queryContext.tokens[0].value
+ ) &&
+ UrlbarUtils.substringAfter(
+ queryContext.searchString,
+ queryContext.tokens[0].value
+ )
+ ) {
+ return true;
+ }
+
+ // We need to handle cancellation here since isActive is called once per
+ // query but cancelQuery can be called multiple times per query.
+ // The frequent cancels can cause the extension's state to drift from the
+ // provider's state.
+ if (ExtensionSearchHandler.hasActiveInputSession()) {
+ ExtensionSearchHandler.handleInputCancelled();
+ }
+
+ return false;
+ }
+
+ /**
+ * Gets the provider's priority.
+ *
+ * @param {UrlbarQueryContext} queryContext
+ * The query context object.
+ * @returns {number}
+ * The provider's priority for the given query.
+ */
+ getPriority(queryContext) {
+ return 0;
+ }
+
+ /**
+ * This method is called by the providers manager when a query starts to fetch
+ * each extension provider's results. It fires the resultsRequested event.
+ *
+ * @param {UrlbarQueryContext} queryContext
+ * The query context object.
+ * @param {function} addCallback
+ * The callback invoked by this method to add each result.
+ */
+ async startQuery(queryContext, addCallback) {
+ let instance = this.queryInstance;
+
+ // Fetch heuristic result.
+ let keyword = queryContext.tokens[0].value;
+ let description = ExtensionSearchHandler.getDescription(keyword);
+ let heuristicResult = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.OMNIBOX,
+ UrlbarUtils.RESULT_SOURCE.OTHER_NETWORK,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ title: [description, UrlbarUtils.HIGHLIGHT.TYPED],
+ content: [queryContext.searchString, UrlbarUtils.HIGHLIGHT.TYPED],
+ keyword: [queryContext.tokens[0].value, UrlbarUtils.HIGHLIGHT.TYPED],
+ icon: UrlbarUtils.ICON.EXTENSION,
+ })
+ );
+ heuristicResult.heuristic = true;
+ addCallback(this, heuristicResult);
+
+ // Fetch non-heuristic results.
+ let data = {
+ keyword,
+ text: queryContext.searchString,
+ inPrivateWindow: queryContext.isPrivate,
+ };
+ this._resultsPromise = ExtensionSearchHandler.handleSearch(
+ data,
+ suggestions => {
+ if (instance != this.queryInstance) {
+ return;
+ }
+ for (let suggestion of suggestions) {
+ let content = `${queryContext.tokens[0].value} ${suggestion.content}`;
+ if (content == heuristicResult.payload.content) {
+ continue;
+ }
+ let result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.OMNIBOX,
+ UrlbarUtils.RESULT_SOURCE.OTHER_NETWORK,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ title: [suggestion.description, UrlbarUtils.HIGHLIGHT.TYPED],
+ content: [content, UrlbarUtils.HIGHLIGHT.TYPED],
+ keyword: [
+ queryContext.tokens[0].value,
+ UrlbarUtils.HIGHLIGHT.TYPED,
+ ],
+ icon: UrlbarUtils.ICON.EXTENSION,
+ })
+ );
+ addCallback(this, result);
+ }
+ }
+ );
+
+ // Since the extension has no way to signal when it's done pushing results,
+ // we add a timer racing with the addition.
+ let timeoutPromise = new SkippableTimer({
+ name: "ProviderOmnibox",
+ time: MAXIMUM_ALLOWED_EXTENSION_TIME_MS,
+ logger: this.logger,
+ }).promise;
+ await Promise.race([timeoutPromise, this._resultsPromise]).catch(
+ Cu.reportError
+ );
+ }
+}
+
+var UrlbarProviderOmnibox = new ProviderOmnibox();
diff --git a/browser/components/urlbar/UrlbarProviderOpenTabs.jsm b/browser/components/urlbar/UrlbarProviderOpenTabs.jsm
new file mode 100644
index 0000000000..8a870bb2c1
--- /dev/null
+++ b/browser/components/urlbar/UrlbarProviderOpenTabs.jsm
@@ -0,0 +1,215 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * This module exports a provider, returning open tabs matches for the urlbar.
+ * It is also used to register and unregister open tabs.
+ */
+
+var EXPORTED_SYMBOLS = ["UrlbarProviderOpenTabs"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+XPCOMUtils.defineLazyModuleGetters(this, {
+ PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
+ UrlbarProvider: "resource:///modules/UrlbarUtils.jsm",
+ UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm",
+ UrlbarResult: "resource:///modules/UrlbarResult.jsm",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+});
+
+/**
+ * Class used to create the provider.
+ */
+class UrlbarProviderOpenTabs extends UrlbarProvider {
+ constructor() {
+ super();
+ }
+
+ /**
+ * Returns the name of this provider.
+ * @returns {string} the name of this provider.
+ */
+ get name() {
+ return "OpenTabs";
+ }
+
+ /**
+ * Returns the type of this provider.
+ * @returns {integer} one of the types from UrlbarUtils.PROVIDER_TYPE.*
+ */
+ get type() {
+ return UrlbarUtils.PROVIDER_TYPE.PROFILE;
+ }
+
+ /**
+ * Whether this provider should be invoked for the given context.
+ * If this method returns false, the providers manager won't start a query
+ * with this provider, to save on resources.
+ * @param {UrlbarQueryContext} queryContext The query context object
+ * @returns {boolean} Whether this provider should be invoked for the search.
+ */
+ isActive(queryContext) {
+ // For now we don't actually use this provider to query open tabs, instead
+ // we join the temp table in UnifiedComplete.
+ return false;
+ }
+
+ /**
+ * Tracks whether the memory tables have been initialized yet. Until this
+ * happens tabs are only stored in openTabs and later copied over to the
+ * memory table.
+ */
+ static memoryTableInitialized = false;
+
+ /**
+ * Maps the open tabs by userContextId.
+ */
+ static openTabs = new Map();
+
+ /**
+ * Copy over cached open tabs to the memory table once the Urlbar
+ * connection has been initialized.
+ */
+ static promiseDBPopulated = PlacesUtils.largeCacheDBConnDeferred.promise.then(
+ async () => {
+ // Must be set before populating.
+ UrlbarProviderOpenTabs.memoryTableInitialized = true;
+ // Populate the table with the current cached tabs.
+ for (let [userContextId, urls] of UrlbarProviderOpenTabs.openTabs) {
+ for (let url of urls) {
+ await addToMemoryTable(url, userContextId).catch(Cu.reportError);
+ }
+ }
+ }
+ );
+
+ /**
+ * Registers a tab as open.
+ * @param {string} url Address of the tab
+ * @param {integer} userContextId Containers user context id
+ */
+ static async registerOpenTab(url, userContextId = 0) {
+ if (!UrlbarProviderOpenTabs.openTabs.has(userContextId)) {
+ UrlbarProviderOpenTabs.openTabs.set(userContextId, []);
+ }
+ UrlbarProviderOpenTabs.openTabs.get(userContextId).push(url);
+ await addToMemoryTable(url, userContextId).catch(Cu.reportError);
+ }
+
+ /**
+ * Unregisters a previously registered open tab.
+ * @param {string} url Address of the tab
+ * @param {integer} userContextId Containers user context id
+ */
+ static async unregisterOpenTab(url, userContextId = 0) {
+ let openTabs = UrlbarProviderOpenTabs.openTabs.get(userContextId);
+ if (openTabs) {
+ let index = openTabs.indexOf(url);
+ if (index != -1) {
+ openTabs.splice(index, 1);
+ await removeFromMemoryTable(url, userContextId).catch(Cu.reportError);
+ }
+ }
+ }
+
+ /**
+ * Starts querying.
+ * @param {object} queryContext The query context object
+ * @param {function} addCallback Callback invoked by the provider to add a new
+ * match.
+ * @returns {Promise} resolved when the query stops.
+ */
+ async startQuery(queryContext, addCallback) {
+ // Note: this is not actually expected to be used as an internal provider,
+ // because normal history search will already coalesce with the open tabs
+ // temp table to return proper frecency.
+ // TODO:
+ // * properly search and handle tokens, this is just a mock for now.
+ let instance = this.queryInstance;
+ let conn = await PlacesUtils.promiseLargeCacheDBConnection();
+ await UrlbarProviderOpenTabs.promiseDBPopulated;
+ await conn.executeCached(
+ `
+ SELECT url, userContextId
+ FROM moz_openpages_temp
+ `,
+ {},
+ (row, cancel) => {
+ if (instance != this.queryInstance) {
+ cancel();
+ return;
+ }
+ addCallback(
+ this,
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ {
+ url: row.getResultByName("url"),
+ userContextId: row.getResultByName("userContextId"),
+ }
+ )
+ );
+ }
+ );
+ }
+}
+
+/**
+ * Adds an open page to the memory table.
+ * @param {string} url Address of the page
+ * @param {number} userContextId Containers user context id
+ * @returns {Promise} resolved after the addition.
+ */
+async function addToMemoryTable(url, userContextId) {
+ if (!UrlbarProviderOpenTabs.memoryTableInitialized) {
+ return;
+ }
+ await UrlbarProvidersManager.runInCriticalSection(async () => {
+ let conn = await PlacesUtils.promiseLargeCacheDBConnection();
+ await conn.executeCached(
+ `
+ INSERT OR REPLACE INTO moz_openpages_temp (url, userContextId, open_count)
+ VALUES ( :url,
+ :userContextId,
+ IFNULL( ( SELECT open_count + 1
+ FROM moz_openpages_temp
+ WHERE url = :url
+ AND userContextId = :userContextId ),
+ 1
+ )
+ )
+ `,
+ { url, userContextId }
+ );
+ });
+}
+
+/**
+ * Removes an open page from the memory table.
+ * @param {string} url Address of the page
+ * @param {number} userContextId Containers user context id
+ * @returns {Promise} resolved after the removal.
+ */
+async function removeFromMemoryTable(url, userContextId) {
+ if (!UrlbarProviderOpenTabs.memoryTableInitialized) {
+ return;
+ }
+ await UrlbarProvidersManager.runInCriticalSection(async () => {
+ let conn = await PlacesUtils.promiseLargeCacheDBConnection();
+ await conn.executeCached(
+ `
+ UPDATE moz_openpages_temp
+ SET open_count = open_count - 1
+ WHERE url = :url
+ AND userContextId = :userContextId
+ `,
+ { url, userContextId }
+ );
+ });
+}
diff --git a/browser/components/urlbar/UrlbarProviderPrivateSearch.jsm b/browser/components/urlbar/UrlbarProviderPrivateSearch.jsm
new file mode 100644
index 0000000000..713fb02ae9
--- /dev/null
+++ b/browser/components/urlbar/UrlbarProviderPrivateSearch.jsm
@@ -0,0 +1,132 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * This module exports a provider returning a private search entry.
+ */
+
+var EXPORTED_SYMBOLS = ["UrlbarProviderPrivateSearch"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+XPCOMUtils.defineLazyModuleGetters(this, {
+ Services: "resource://gre/modules/Services.jsm",
+ SkippableTimer: "resource:///modules/UrlbarUtils.jsm",
+ UrlbarProvider: "resource:///modules/UrlbarUtils.jsm",
+ UrlbarResult: "resource:///modules/UrlbarResult.jsm",
+ UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.jsm",
+ UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.jsm",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+});
+
+/**
+ * Class used to create the provider.
+ */
+class ProviderPrivateSearch extends UrlbarProvider {
+ constructor() {
+ super();
+ // Maps the open tabs by userContextId.
+ this.openTabs = new Map();
+ }
+
+ /**
+ * Returns the name of this provider.
+ * @returns {string} the name of this provider.
+ */
+ get name() {
+ return "PrivateSearch";
+ }
+
+ /**
+ * Returns the type of this provider.
+ * @returns {integer} one of the types from UrlbarUtils.PROVIDER_TYPE.*
+ */
+ get type() {
+ return UrlbarUtils.PROVIDER_TYPE.PROFILE;
+ }
+
+ /**
+ * Whether this provider should be invoked for the given context.
+ * If this method returns false, the providers manager won't start a query
+ * with this provider, to save on resources.
+ * @param {UrlbarQueryContext} queryContext The query context object
+ * @returns {boolean} Whether this provider should be invoked for the search.
+ */
+ isActive(queryContext) {
+ return (
+ UrlbarSearchUtils.separatePrivateDefaultUIEnabled &&
+ !queryContext.isPrivate &&
+ queryContext.tokens.length
+ );
+ }
+
+ /**
+ * Starts querying.
+ * @param {object} queryContext The query context object
+ * @param {function} addCallback Callback invoked by the provider to add a new
+ * match.
+ * @returns {Promise} resolved when the query stops.
+ */
+ async startQuery(queryContext, addCallback) {
+ let searchString = queryContext.trimmedSearchString;
+ if (
+ queryContext.tokens.some(
+ t => t.type == UrlbarTokenizer.TYPE.RESTRICT_SEARCH
+ )
+ ) {
+ if (queryContext.tokens.length == 1) {
+ // There's only the restriction token, bail out.
+ return;
+ }
+ // Remove the restriction char from the search string.
+ searchString = queryContext.tokens
+ .filter(t => t.type != UrlbarTokenizer.TYPE.RESTRICT_SEARCH)
+ .map(t => t.value)
+ .join(" ");
+ }
+
+ let instance = this.queryInstance;
+
+ let engine = queryContext.searchMode?.engineName
+ ? Services.search.getEngineByName(queryContext.searchMode.engineName)
+ : await Services.search.getDefaultPrivate();
+ let isPrivateEngine =
+ UrlbarSearchUtils.separatePrivateDefault &&
+ engine != (await Services.search.getDefault());
+ this.logger.info(`isPrivateEngine: ${isPrivateEngine}`);
+
+ // This is a delay added before returning results, to avoid flicker.
+ // Our result must appear only when all results are searches, but if search
+ // results arrive first, then the muxer would insert our result and then
+ // immediately remove it when non-search results arrive.
+ await new SkippableTimer({
+ name: "ProviderPrivateSearch",
+ time: 100,
+ logger: this.logger,
+ }).promise;
+
+ if (instance != this.queryInstance) {
+ return;
+ }
+
+ let result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ engine: [engine.name, UrlbarUtils.HIGHLIGHT.TYPED],
+ query: [searchString, UrlbarUtils.HIGHLIGHT.NONE],
+ icon: engine.iconURI?.spec,
+ inPrivateWindow: true,
+ isPrivateEngine,
+ })
+ );
+ result.suggestedIndex = 1;
+ addCallback(this, result);
+ }
+}
+
+var UrlbarProviderPrivateSearch = new ProviderPrivateSearch();
diff --git a/browser/components/urlbar/UrlbarProviderSearchSuggestions.jsm b/browser/components/urlbar/UrlbarProviderSearchSuggestions.jsm
new file mode 100644
index 0000000000..4a2cbb5e78
--- /dev/null
+++ b/browser/components/urlbar/UrlbarProviderSearchSuggestions.jsm
@@ -0,0 +1,524 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * This module exports a provider that offers search engine suggestions.
+ */
+
+var EXPORTED_SYMBOLS = ["UrlbarProviderSearchSuggestions"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+XPCOMUtils.defineLazyModuleGetters(this, {
+ SearchSuggestionController:
+ "resource://gre/modules/SearchSuggestionController.jsm",
+ Services: "resource://gre/modules/Services.jsm",
+ SkippableTimer: "resource:///modules/UrlbarUtils.jsm",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
+ UrlbarProvider: "resource:///modules/UrlbarUtils.jsm",
+ UrlbarResult: "resource:///modules/UrlbarResult.jsm",
+ UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.jsm",
+ UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.jsm",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+});
+
+/**
+ * Returns whether the passed in string looks like a url.
+ * @param {string} str
+ * @param {boolean} [ignoreAlphanumericHosts]
+ * @returns {boolean}
+ * True if the query looks like a URL.
+ */
+function looksLikeUrl(str, ignoreAlphanumericHosts = false) {
+ // Single word including special chars.
+ return (
+ !UrlbarTokenizer.REGEXP_SPACES.test(str) &&
+ (["/", "@", ":", "["].some(c => str.includes(c)) ||
+ (ignoreAlphanumericHosts
+ ? /^([\[\]A-Z0-9-]+\.){3,}[^.]+$/i.test(str)
+ : str.includes(".")))
+ );
+}
+
+/**
+ * Class used to create the provider.
+ */
+class ProviderSearchSuggestions extends UrlbarProvider {
+ constructor() {
+ super();
+ }
+
+ /**
+ * Returns the name of this provider.
+ * @returns {string} the name of this provider.
+ */
+ get name() {
+ return "SearchSuggestions";
+ }
+
+ /**
+ * Returns the type of this provider.
+ * @returns {integer} one of the types from UrlbarUtils.PROVIDER_TYPE.*
+ */
+ get type() {
+ return UrlbarUtils.PROVIDER_TYPE.NETWORK;
+ }
+
+ /**
+ * Whether this provider should be invoked for the given context.
+ * If this method returns false, the providers manager won't start a query
+ * with this provider, to save on resources.
+ * @param {UrlbarQueryContext} queryContext The query context object
+ * @returns {boolean} Whether this provider should be invoked for the search.
+ */
+ isActive(queryContext) {
+ // If the sources don't include search or the user used a restriction
+ // character other than search, don't allow any suggestions.
+ if (
+ !queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.SEARCH) ||
+ (queryContext.restrictSource &&
+ queryContext.restrictSource != UrlbarUtils.RESULT_SOURCE.SEARCH)
+ ) {
+ return false;
+ }
+
+ // No suggestions for empty search strings, unless we are restricting to
+ // search.
+ if (
+ !queryContext.trimmedSearchString &&
+ !this._isTokenOrRestrictionPresent(queryContext)
+ ) {
+ return false;
+ }
+
+ if (!this._allowSuggestions(queryContext)) {
+ return false;
+ }
+
+ let wantsLocalSuggestions =
+ UrlbarPrefs.get("maxHistoricalSearchSuggestions") &&
+ (queryContext.trimmedSearchString ||
+ UrlbarPrefs.get("update2.emptySearchBehavior") != 0);
+
+ return wantsLocalSuggestions || this._allowRemoteSuggestions(queryContext);
+ }
+
+ /**
+ * Returns whether the user typed a token alias or restriction token, or is in
+ * search mode. We use this value to override the pref to disable search
+ * suggestions in the Urlbar.
+ * @param {UrlbarQueryContext} queryContext The query context object.
+ * @returns {boolean} True if the user typed a token alias or search
+ * restriction token.
+ */
+ _isTokenOrRestrictionPresent(queryContext) {
+ return (
+ queryContext.searchString.startsWith("@") ||
+ (queryContext.restrictSource &&
+ queryContext.restrictSource == UrlbarUtils.RESULT_SOURCE.SEARCH) ||
+ queryContext.tokens.some(
+ t => t.type == UrlbarTokenizer.TYPE.RESTRICT_SEARCH
+ ) ||
+ (queryContext.searchMode &&
+ queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.SEARCH))
+ );
+ }
+
+ /**
+ * Returns whether suggestions in general are allowed for a given query
+ * context. If this returns false, then we shouldn't fetch either form
+ * history or remote suggestions.
+ *
+ * @param {object} queryContext The query context object
+ * @returns {boolean} True if suggestions in general are allowed and false if
+ * not.
+ */
+ _allowSuggestions(queryContext) {
+ if (
+ !queryContext.allowSearchSuggestions ||
+ // If the user typed a restriction token or token alias, we ignore the
+ // pref to disable suggestions in the Urlbar.
+ (!UrlbarPrefs.get("suggest.searches") &&
+ !this._isTokenOrRestrictionPresent(queryContext)) ||
+ !UrlbarPrefs.get("browser.search.suggest.enabled") ||
+ (queryContext.isPrivate &&
+ !UrlbarPrefs.get("browser.search.suggest.enabled.private"))
+ ) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Returns whether remote suggestions are allowed for a given query context.
+ *
+ * @param {object} queryContext The query context object
+ * @param {string} [searchString] The effective search string without
+ * restriction tokens or aliases. Defaults to the context searchString.
+ * @returns {boolean} True if remote suggestions are allowed and false if not.
+ */
+ _allowRemoteSuggestions(
+ queryContext,
+ searchString = queryContext.searchString
+ ) {
+ // TODO (Bug 1626964): Support zero prefix suggestions.
+ if (!searchString.trim()) {
+ return false;
+ }
+
+ // Skip all remaining checks and allow remote suggestions at this point if
+ // the user used a token alias or restriction token. We want "@engine query"
+ // to return suggestions from the engine. We'll return early from startQuery
+ // if the query doesn't match an alias.
+ if (this._isTokenOrRestrictionPresent(queryContext)) {
+ return true;
+ }
+
+ // If the user is just adding on to a query that previously didn't return
+ // many remote suggestions, we are unlikely to get any more results.
+ if (
+ !!this._lastLowResultsSearchSuggestion &&
+ searchString.length > this._lastLowResultsSearchSuggestion.length &&
+ searchString.startsWith(this._lastLowResultsSearchSuggestion)
+ ) {
+ return false;
+ }
+
+ // We're unlikely to get useful remote suggestions for a single character.
+ if (searchString.length < 2) {
+ return false;
+ }
+
+ // Disallow remote suggestions if only an origin is typed to avoid
+ // disclosing information about sites the user visits. This also catches
+ // partially-typed origins, like mozilla.o, because the URIFixup check
+ // below can't validate those.
+ if (
+ queryContext.tokens.length == 1 &&
+ queryContext.tokens[0].type == UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN
+ ) {
+ return false;
+ }
+
+ // Disallow remote suggestions for strings containing tokens that look like
+ // URIs, to avoid disclosing information about networks or passwords.
+ if (queryContext.fixupInfo?.href && !queryContext.fixupInfo?.isSearch) {
+ return false;
+ }
+
+ // Allow remote suggestions.
+ return true;
+ }
+
+ /**
+ * Starts querying.
+ * @param {object} queryContext The query context object
+ * @param {function} addCallback Callback invoked by the provider to add a new
+ * result.
+ * @returns {Promise} resolved when the query stops.
+ */
+ async startQuery(queryContext, addCallback) {
+ let instance = this.queryInstance;
+
+ let aliasEngine = await this._maybeGetAlias(queryContext);
+ if (!aliasEngine) {
+ // Autofill matches queries starting with "@" to token alias engines.
+ // If the string starts with "@", but an alias engine is not yet
+ // matched, then autofill might still be filtering token alias
+ // engine results. We don't want to mix search suggestions with those
+ // engine results, so we return early. See bug 1551049 comment 1 for
+ // discussion on how to improve this behavior.
+ if (queryContext.searchString.startsWith("@")) {
+ return;
+ }
+ }
+
+ let query = aliasEngine
+ ? aliasEngine.query
+ : UrlbarUtils.substringAt(
+ queryContext.searchString,
+ queryContext.tokens[0]?.value || ""
+ ).trim();
+
+ let leadingRestrictionToken = null;
+ if (
+ UrlbarTokenizer.isRestrictionToken(queryContext.tokens[0]) &&
+ (queryContext.tokens.length > 1 ||
+ queryContext.tokens[0].type == UrlbarTokenizer.TYPE.RESTRICT_SEARCH)
+ ) {
+ leadingRestrictionToken = queryContext.tokens[0].value;
+ }
+
+ // Strip a leading search restriction char, because we prepend it to text
+ // when the search shortcut is used and it's not user typed. Don't strip
+ // other restriction chars, so that it's possible to search for things
+ // including one of those (e.g. "c#").
+ if (leadingRestrictionToken === UrlbarTokenizer.RESTRICT.SEARCH) {
+ query = UrlbarUtils.substringAfter(query, leadingRestrictionToken).trim();
+ }
+
+ // Find our search engine. It may have already been set with an alias.
+ let engine;
+ if (aliasEngine) {
+ engine = aliasEngine.engine;
+ } else if (queryContext.searchMode?.engineName) {
+ engine = Services.search.getEngineByName(
+ queryContext.searchMode.engineName
+ );
+ } else {
+ engine = UrlbarSearchUtils.getDefaultEngine(queryContext.isPrivate);
+ }
+
+ if (!engine) {
+ return;
+ }
+
+ let alias = (aliasEngine && aliasEngine.alias) || "";
+ let results = await this._fetchSearchSuggestions(
+ queryContext,
+ engine,
+ query,
+ alias
+ );
+
+ if (!results || instance != this.queryInstance) {
+ return;
+ }
+
+ for (let result of results) {
+ addCallback(this, result);
+ }
+ }
+
+ /**
+ * Gets the provider's priority.
+ * @param {UrlbarQueryContext} queryContext The query context object
+ * @returns {number} The provider's priority for the given query.
+ */
+ getPriority(queryContext) {
+ return 0;
+ }
+
+ /**
+ * Cancels a running query.
+ * @param {object} queryContext The query context object
+ */
+ cancelQuery(queryContext) {
+ if (this._suggestionsController) {
+ this._suggestionsController.stop();
+ this._suggestionsController = null;
+ }
+ }
+
+ async _fetchSearchSuggestions(queryContext, engine, searchString, alias) {
+ if (!engine) {
+ return null;
+ }
+
+ this._suggestionsController = new SearchSuggestionController();
+ this._suggestionsController.formHistoryParam = queryContext.formHistoryName;
+
+ // If there's a form history entry that equals the search string, the search
+ // suggestions controller will include it, and we'll make a result for it.
+ // If the heuristic result ends up being a search result, the muxer will
+ // discard the form history result since it dupes the heuristic, and the
+ // final list of results would be left with `count` - 1 form history results
+ // instead of `count`. Therefore we request `count` + 1 entries. The muxer
+ // will dedupe and limit the final form history count as appropriate.
+ this._suggestionsController.maxLocalResults = queryContext.maxResults + 1;
+
+ // Request maxResults + 1 remote suggestions for the same reason we request
+ // maxHistoricalSearchSuggestions + 1 form history entries.
+ let allowRemote = this._allowRemoteSuggestions(queryContext, searchString);
+ this._suggestionsController.maxRemoteResults = allowRemote
+ ? queryContext.maxResults + 1
+ : 0;
+
+ this._suggestionsFetchCompletePromise = this._suggestionsController.fetch(
+ searchString,
+ queryContext.isPrivate,
+ engine,
+ queryContext.userContextId,
+ this._isTokenOrRestrictionPresent(queryContext),
+ false
+ );
+
+ // See `SearchSuggestionsController.fetch` documentation for a description
+ // of `fetchData`.
+ let fetchData = await this._suggestionsFetchCompletePromise;
+ // The fetch was canceled.
+ if (!fetchData) {
+ return null;
+ }
+
+ let results = [];
+
+ // We use this to discard remote suggestions that duplicate form history.
+ let seenSuggestions = new Set();
+
+ // Add maxHistoricalSearchSuggestions form history results to the beginning
+ // of the array. Below we'll add the remainder after remote suggestions.
+ // Any excess results that are not discarded by the muxer will appear below
+ // the remote suggestions in the final results.
+ let maxInitialFormHistory = UrlbarPrefs.get(
+ "maxHistoricalSearchSuggestions"
+ );
+ while (results.length < maxInitialFormHistory && fetchData.local.length) {
+ let entry = fetchData.local.shift();
+ results.push(makeFormHistoryResult(queryContext, engine, entry));
+ seenSuggestions.add(entry.value);
+ }
+
+ // If we don't return many results, then keep track of the query. If the
+ // user just adds on to the query, we won't fetch more suggestions if the
+ // query is very long since we are unlikely to get any.
+ if (
+ allowRemote &&
+ !fetchData.remote.length &&
+ searchString.length > UrlbarPrefs.get("maxCharsForSearchSuggestions")
+ ) {
+ this._lastLowResultsSearchSuggestion = searchString;
+ }
+
+ // If we have only tail suggestions, we only show them if we have no other
+ // results. We need to wait for other results to arrive to avoid flickering.
+ // We will wait for this timer unless we have suggestions that don't have a
+ // tail.
+ let tailTimer = new SkippableTimer({
+ name: "ProviderSearchSuggestions",
+ time: 100,
+ logger: this.logger,
+ });
+
+ for (let entry of fetchData.remote) {
+ if (looksLikeUrl(entry.value) || seenSuggestions.has(entry.value)) {
+ continue;
+ }
+
+ if (entry.tail && entry.tailOffsetIndex < 0) {
+ Cu.reportError(
+ `Error in tail suggestion parsing. Value: ${entry.value}, tail: ${entry.tail}.`
+ );
+ continue;
+ }
+
+ let tail = entry.tail;
+ let tailPrefix = entry.matchPrefix;
+
+ // Skip tail suggestions if the pref is disabled.
+ if (tail && !UrlbarPrefs.get("richSuggestions.tail")) {
+ continue;
+ }
+
+ if (!tail) {
+ await tailTimer.fire().catch(Cu.reportError);
+ }
+
+ try {
+ results.push(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ engine: [engine.name, UrlbarUtils.HIGHLIGHT.TYPED],
+ suggestion: [entry.value, UrlbarUtils.HIGHLIGHT.SUGGESTED],
+ lowerCaseSuggestion: entry.value.toLocaleLowerCase(),
+ tailPrefix,
+ tail: [tail, UrlbarUtils.HIGHLIGHT.SUGGESTED],
+ tailOffsetIndex: tail ? entry.tailOffsetIndex : undefined,
+ keyword: [alias ? alias : undefined, UrlbarUtils.HIGHLIGHT.TYPED],
+ query: [searchString.trim(), UrlbarUtils.HIGHLIGHT.NONE],
+ icon: !entry.value ? engine.iconURI?.spec : undefined,
+ })
+ )
+ );
+ seenSuggestions.add(entry.value);
+ } catch (err) {
+ Cu.reportError(err);
+ continue;
+ }
+ }
+
+ // Add the remaining form history results. maxHistoricalSearchSuggestions
+ // == 0 is an opt-out mechanism, so do this only if it's non-zero.
+ while (
+ maxInitialFormHistory &&
+ results.length < queryContext.maxResults + 1 &&
+ fetchData.local.length
+ ) {
+ let entry = fetchData.local.shift();
+ if (!seenSuggestions.has(entry.value)) {
+ results.push(makeFormHistoryResult(queryContext, engine, entry));
+ }
+ }
+
+ await tailTimer.promise;
+ return results;
+ }
+
+ /**
+ * Searches for an engine alias given the queryContext.
+ * @param {UrlbarQueryContext} queryContext
+ * @returns {object} aliasEngine
+ * A representation of the aliased engine. Null if there's no match.
+ * @returns {nsISearchEngine} aliasEngine.engine
+ * @returns {string} aliasEngine.alias
+ * @returns {string} aliasEngine.query
+ * @returns {object} { engine, alias, query }
+ *
+ */
+ async _maybeGetAlias(queryContext) {
+ if (queryContext.searchMode) {
+ // If we're in search mode, don't try to parse an alias at all.
+ return null;
+ }
+
+ let possibleAlias = queryContext.tokens[0]?.value;
+ // "@" on its own is handled by UrlbarProviderTokenAliasEngines and returns
+ // a list of every available token alias.
+ if (!possibleAlias || possibleAlias == "@") {
+ return null;
+ }
+
+ let query = UrlbarUtils.substringAfter(
+ queryContext.searchString,
+ possibleAlias
+ );
+
+ // Match an alias only when it has a space after it. If there's no trailing
+ // space, then continue to treat it as part of the search string.
+ if (!UrlbarTokenizer.REGEXP_SPACES_START.test(query)) {
+ return null;
+ }
+
+ // Check if the user entered an engine alias directly.
+ let engineMatch = await UrlbarSearchUtils.engineForAlias(possibleAlias);
+ if (engineMatch) {
+ return {
+ engine: engineMatch,
+ alias: possibleAlias,
+ query: query.trim(),
+ };
+ }
+
+ return null;
+ }
+}
+
+function makeFormHistoryResult(queryContext, engine, entry) {
+ return new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ engine: engine.name,
+ suggestion: [entry.value, UrlbarUtils.HIGHLIGHT.SUGGESTED],
+ lowerCaseSuggestion: entry.value.toLocaleLowerCase(),
+ })
+ );
+}
+
+var UrlbarProviderSearchSuggestions = new ProviderSearchSuggestions();
diff --git a/browser/components/urlbar/UrlbarProviderSearchTips.jsm b/browser/components/urlbar/UrlbarProviderSearchTips.jsm
new file mode 100644
index 0000000000..3ee8194f2d
--- /dev/null
+++ b/browser/components/urlbar/UrlbarProviderSearchTips.jsm
@@ -0,0 +1,505 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * This module exports a provider that might show a tip when the user opens
+ * the newtab or starts an organic search with their default search engine.
+ */
+
+var EXPORTED_SYMBOLS = ["UrlbarProviderSearchTips"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AppMenuNotifications: "resource://gre/modules/AppMenuNotifications.jsm",
+ DefaultBrowserCheck: "resource:///modules/BrowserGlue.jsm",
+ BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
+ ProfileAge: "resource://gre/modules/ProfileAge.jsm",
+ Services: "resource://gre/modules/Services.jsm",
+ setTimeout: "resource://gre/modules/Timer.jsm",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
+ UrlbarProvider: "resource:///modules/UrlbarUtils.jsm",
+ UrlbarProviderTopSites: "resource:///modules/UrlbarProviderTopSites.jsm",
+ UrlbarResult: "resource:///modules/UrlbarResult.jsm",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(this, "updateManager", () => {
+ return (
+ Cc["@mozilla.org/updates/update-manager;1"] &&
+ Cc["@mozilla.org/updates/update-manager;1"].getService(Ci.nsIUpdateManager)
+ );
+});
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "cfrFeaturesUserPref",
+ "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features",
+ true
+);
+
+// The possible tips to show. These names (except NONE) are used in the names
+// of keys in the `urlbar.tips` keyed scalar telemetry (see telemetry.rst).
+// Don't modify them unless you've considered that. If you do modify them or
+// add new tips, then you are also adding new `urlbar.tips` keys and therefore
+// need an expanded data collection review.
+const TIPS = {
+ NONE: "",
+ ONBOARD: "searchTip_onboard",
+ REDIRECT: "searchTip_redirect",
+};
+
+// This maps engine names to regexes matching their homepages. We show the
+// redirect tip on these pages. The Google domains are taken from
+// https://ipfs.io/ipfs/QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco/wiki/List_of_Google_domains.html.
+const SUPPORTED_ENGINES = new Map([
+ ["Bing", { domainPath: /^www\.bing\.com\/$/ }],
+ [
+ "DuckDuckGo",
+ {
+ domainPath: /^(start\.)?duckduckgo\.com\/$/,
+ prohibitedSearchParams: ["q"],
+ },
+ ],
+ [
+ "Google",
+ {
+ domainPath: /^www\.google\.(com|ac|ad|ae|com\.af|com\.ag|com\.ai|al|am|co\.ao|com\.ar|as|at|com\.au|az|ba|com\.bd|be|bf|bg|com\.bh|bi|bj|com\.bn|com\.bo|com\.br|bs|bt|co\.bw|by|com\.bz|ca|com\.kh|cc|cd|cf|cat|cg|ch|ci|co\.ck|cl|cm|cn|com\.co|co\.cr|com\.cu|cv|com\.cy|cz|de|dj|dk|dm|com\.do|dz|com\.ec|ee|com\.eg|es|com\.et|fi|com\.fj|fm|fr|ga|ge|gf|gg|com\.gh|com\.gi|gl|gm|gp|gr|com\.gt|gy|com\.hk|hn|hr|ht|hu|co\.id|iq|ie|co\.il|im|co\.in|io|is|it|je|com\.jm|jo|co\.jp|co\.ke|ki|kg|co\.kr|com\.kw|kz|la|com\.lb|com\.lc|li|lk|co\.ls|lt|lu|lv|com\.ly|co\.ma|md|me|mg|mk|ml|com\.mm|mn|ms|com\.mt|mu|mv|mw|com\.mx|com\.my|co\.mz|com\.na|ne|com\.nf|com\.ng|com\.ni|nl|no|com\.np|nr|nu|co\.nz|com\.om|com\.pk|com\.pa|com\.pe|com\.ph|pl|com\.pg|pn|com\.pr|ps|pt|com\.py|com\.qa|ro|rs|ru|rw|com\.sa|com\.sb|sc|se|com\.sg|sh|si|sk|com\.sl|sn|sm|so|st|sr|com\.sv|td|tg|co\.th|com\.tj|tk|tl|tm|to|tn|com\.tr|tt|com\.tw|co\.tz|com\.ua|co\.ug|co\.uk|com\.uy|co\.uz|com\.vc|co\.ve|vg|co\.vi|com\.vn|vu|ws|co\.za|co\.zm|co\.zw)\/(webhp)?$/,
+ },
+ ],
+]);
+
+// The maximum number of times we'll show a tip across all sessions.
+const MAX_SHOWN_COUNT = 4;
+
+// Amount of time to wait before showing a tip after selecting a tab or
+// navigating to a page where we should show a tip.
+const SHOW_TIP_DELAY_MS = 200;
+
+// We won't show a tip if the browser has been updated in the past
+// LAST_UPDATE_THRESHOLD_MS.
+const LAST_UPDATE_THRESHOLD_MS = 24 * 60 * 60 * 1000;
+
+/**
+ * A provider that sometimes returns a tip result when the user visits the
+ * newtab page or their default search engine's homepage.
+ */
+class ProviderSearchTips extends UrlbarProvider {
+ constructor() {
+ super();
+
+ // Whether we should disable tips for the current browser session, for
+ // example because a tip was already shown.
+ this.disableTipsForCurrentSession = true;
+ for (let tip of Object.values(TIPS)) {
+ if (tip && UrlbarPrefs.get(`tipShownCount.${tip}`) < MAX_SHOWN_COUNT) {
+ this.disableTipsForCurrentSession = false;
+ break;
+ }
+ }
+
+ // Whether and what kind of tip we've shown in the current engagement.
+ this.showedTipTypeInCurrentEngagement = TIPS.NONE;
+
+ // Used to track browser windows we've seen.
+ this._seenWindows = new WeakSet();
+ }
+
+ /**
+ * Enum of the types of search tips.
+ */
+ get TIP_TYPE() {
+ return TIPS;
+ }
+
+ get PRIORITY() {
+ // Search tips are prioritized over the UnifiedComplete and top sites
+ // providers.
+ return UrlbarProviderTopSites.PRIORITY + 1;
+ }
+
+ /**
+ * Unique name for the provider, used by the context to filter on providers.
+ * Not using a unique name will cause the newest registration to win.
+ */
+ get name() {
+ return "UrlbarProviderSearchTips";
+ }
+
+ /**
+ * The type of the provider.
+ */
+ get type() {
+ return UrlbarUtils.PROVIDER_TYPE.PROFILE;
+ }
+
+ /**
+ * Whether this provider should be invoked for the given context.
+ * If this method returns false, the providers manager won't start a query
+ * with this provider, to save on resources.
+ * @param {UrlbarQueryContext} queryContext The query context object
+ * @returns {boolean} Whether this provider should be invoked for the search.
+ */
+ isActive(queryContext) {
+ return this.currentTip && cfrFeaturesUserPref;
+ }
+
+ /**
+ * Gets the provider's priority.
+ * @param {UrlbarQueryContext} queryContext The query context object
+ * @returns {number} The provider's priority for the given query.
+ */
+ getPriority(queryContext) {
+ return this.PRIORITY;
+ }
+
+ /**
+ * Starts querying.
+ * @param {UrlbarQueryContext} queryContext The query context object
+ * @param {function} addCallback Callback invoked by the provider to add a new
+ * result. A UrlbarResult should be passed to it.
+ * @note Extended classes should return a Promise resolved when the provider
+ * is done searching AND returning results.
+ */
+ async startQuery(queryContext, addCallback) {
+ let instance = this.queryInstance;
+
+ let tip = this.currentTip;
+ this.showedTipTypeInCurrentEngagement = this.currentTip;
+ this.currentTip = TIPS.NONE;
+
+ let defaultEngine = await Services.search.getDefault();
+
+ let result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TIP,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ type: tip,
+ buttonTextData: { id: "urlbar-search-tips-confirm" },
+ icon: defaultEngine.iconURI?.spec,
+ }
+ );
+
+ switch (tip) {
+ case TIPS.ONBOARD:
+ result.heuristic = true;
+ result.payload.textData = {
+ id: "urlbar-search-tips-onboard",
+ args: {
+ engineName: defaultEngine.name,
+ },
+ };
+ break;
+ case TIPS.REDIRECT:
+ result.heuristic = false;
+ result.payload.textData = {
+ id: "urlbar-search-tips-redirect-2",
+ args: {
+ engineName: defaultEngine.name,
+ },
+ };
+ break;
+ }
+
+ if (instance != this.queryInstance) {
+ return;
+ }
+
+ Services.telemetry.keyedScalarAdd("urlbar.tips", `${tip}-shown`, 1);
+
+ addCallback(this, result);
+ }
+
+ /**
+ * Called when the tip is selected.
+ * @param {UrlbarResult} result
+ * The result that was picked.
+ */
+ pickResult(result) {
+ let window = BrowserWindowTracker.getTopWindow();
+ window.gURLBar.value = "";
+ window.gURLBar.setPageProxyState("invalid");
+ window.gURLBar.focus();
+ }
+
+ /**
+ * Called when the user starts and ends an engagement with the urlbar.
+ *
+ * @param {boolean} isPrivate True if the engagement is in a private context.
+ * @param {string} state The state of the engagement, one of: start,
+ * engagement, abandonment, discard.
+ */
+ onEngagement(isPrivate, state) {
+ if (
+ this.showedTipTypeInCurrentEngagement != TIPS.NONE &&
+ state == "engagement"
+ ) {
+ // The user either clicked the tip's "Okay, Got It" button, or they
+ // engaged with the urlbar while the tip was showing. We treat both as the
+ // user's acknowledgment of the tip, and we don't show tips again in any
+ // session. Set the shown count to the max.
+ UrlbarPrefs.set(
+ `tipShownCount.${this.showedTipTypeInCurrentEngagement}`,
+ MAX_SHOWN_COUNT
+ );
+ }
+ this.showedTipTypeInCurrentEngagement = TIPS.NONE;
+ }
+
+ /**
+ * Called from `onLocationChange` in browser.js.
+ * @param {window} window
+ * The browser window where the location change happened.
+ * @param {URL} uri
+ * The URI being navigated to.
+ * @param {nsIWebProgress} webProgress
+ * @param {number} flags
+ * Load flags. See nsIWebProgressListener.idl for possible values.
+ */
+ async onLocationChange(window, uri, webProgress, flags) {
+ let instance = (this._onLocationChangeInstance = {});
+
+ // If this is the first time we've seen this browser window, we take some
+ // precautions to avoid impacting ts_paint.
+ if (!this._seenWindows.has(window)) {
+ this._seenWindows.add(window);
+
+ // First, wait until MozAfterPaint is fired in the current content window.
+ await window.gBrowserInit.firstContentWindowPaintPromise;
+ if (instance != this._onLocationChangeInstance) {
+ return;
+ }
+
+ // Second, wait 500ms. ts_paint waits at most 500ms after MozAfterPaint
+ // before ending. We use XPCOM directly instead of Timer.jsm to avoid the
+ // perf impact of loading Timer.jsm, in case it's not already loaded.
+ await new Promise(resolve => {
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.initWithCallback(resolve, 500, Ci.nsITimer.TYPE_ONE_SHOT);
+ });
+ if (instance != this._onLocationChangeInstance) {
+ return;
+ }
+ }
+
+ // Ignore events that don't change the document. Google is known to do this.
+ // Also ignore changes in sub-frames. See bug 1623978.
+ if (
+ flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT ||
+ !webProgress.isTopLevel
+ ) {
+ return;
+ }
+
+ // The UrlbarView is usually closed on location change when the input is
+ // blurred. Since we open the view to show the redirect tip without focusing
+ // the input, the view won't close in that case. We need to close it
+ // manually.
+ if (this.showedTipTypeInCurrentEngagement != TIPS.NONE) {
+ window.gURLBar.view.close();
+ }
+
+ // Check if we are supposed to show a tip for the current session.
+ if (
+ !cfrFeaturesUserPref ||
+ (this.disableTipsForCurrentSession &&
+ !UrlbarPrefs.get("searchTips.test.ignoreShowLimits"))
+ ) {
+ return;
+ }
+
+ this._maybeShowTipForUrl(uri.spec).catch(Cu.reportError);
+ }
+
+ /**
+ * Determines whether we should show a tip for the current tab, sets
+ * this.currentTip, and starts a search on an empty string.
+ * @param {number} urlStr
+ * The URL of the page being loaded, in string form.
+ */
+ async _maybeShowTipForUrl(urlStr) {
+ let instance = {};
+ this._maybeShowTipForUrlInstance = instance;
+
+ // Determine which tip we should show for the tab. Do this check first
+ // before the others below. It has less of a performance impact than the
+ // others, so in the common case where the URL is not one we're interested
+ // in, we can return immediately.
+ let tip;
+ let isNewtab = ["about:newtab", "about:home"].includes(urlStr);
+ let isSearchHomepage = !isNewtab && (await isDefaultEngineHomepage(urlStr));
+ if (isNewtab) {
+ tip = TIPS.ONBOARD;
+ } else if (isSearchHomepage) {
+ tip = TIPS.REDIRECT;
+ } else {
+ // No tip.
+ return;
+ }
+
+ let ignoreShowLimits = UrlbarPrefs.get("searchTips.test.ignoreShowLimits");
+
+ // If we've shown this type of tip the maximum number of times over all
+ // sessions, don't show it again.
+ let shownCount = UrlbarPrefs.get(`tipShownCount.${tip}`);
+ if (shownCount >= MAX_SHOWN_COUNT && !ignoreShowLimits) {
+ return;
+ }
+
+ // Don't show a tip if the browser is already showing some other
+ // notification.
+ if ((await isBrowserShowingNotification()) && !ignoreShowLimits) {
+ return;
+ }
+
+ // Don't show a tip if the browser has been updated recently.
+ let date = await lastBrowserUpdateDate();
+ if (Date.now() - date <= LAST_UPDATE_THRESHOLD_MS && !ignoreShowLimits) {
+ return;
+ }
+
+ // At this point, we're showing a tip.
+ this.disableTipsForCurrentSession = true;
+
+ // Store the new shown count.
+ UrlbarPrefs.set(`tipShownCount.${tip}`, shownCount + 1);
+
+ // Start a search.
+ setTimeout(() => {
+ if (this._maybeShowTipForUrlInstance != instance) {
+ return;
+ }
+
+ let window = BrowserWindowTracker.getTopWindow();
+ // We don't want to interrupt a user's typed query with a Search Tip.
+ // See bugs 1613662 and 1619547.
+ if (
+ window.gURLBar.getAttribute("pageproxystate") == "invalid" &&
+ window.gURLBar.value != ""
+ ) {
+ return;
+ }
+
+ this.currentTip = tip;
+ window.gURLBar.search("", { focus: tip == TIPS.ONBOARD });
+ }, SHOW_TIP_DELAY_MS);
+ }
+}
+
+async function isBrowserShowingNotification() {
+ let window = BrowserWindowTracker.getTopWindow();
+
+ // urlbar view and notification box (info bar)
+ if (
+ window.gURLBar.view.isOpen ||
+ window.gHighPriorityNotificationBox.currentNotification ||
+ window.gBrowser.getNotificationBox().currentNotification
+ ) {
+ return true;
+ }
+
+ // app menu notification doorhanger
+ if (
+ AppMenuNotifications.activeNotification &&
+ !AppMenuNotifications.activeNotification.dismissed &&
+ !AppMenuNotifications.activeNotification.options.badgeOnly
+ ) {
+ return true;
+ }
+
+ // tracking protection and identity box doorhangers
+ if (
+ ["tracking-protection-icon-container", "identity-box"].some(
+ id => window.document.getElementById(id).getAttribute("open") == "true"
+ )
+ ) {
+ return true;
+ }
+
+ // page action button panels
+ let pageActions = window.document.getElementById("page-action-buttons");
+ if (pageActions) {
+ for (let child of pageActions.childNodes) {
+ if (child.getAttribute("open") == "true") {
+ return true;
+ }
+ }
+ }
+
+ // toolbar button panels
+ let navbar = window.document.getElementById("nav-bar-customization-target");
+ for (let node of navbar.querySelectorAll("toolbarbutton")) {
+ if (node.getAttribute("open") == "true") {
+ return true;
+ }
+ }
+
+ // On startup, the default browser check normally opens after the Search Tip.
+ // As a result, we can't check for the prompt's presence, but we can check if
+ // it plans on opening.
+ const willPrompt = await DefaultBrowserCheck.willCheckDefaultBrowser(
+ /* isStartupCheck */ false
+ );
+ if (willPrompt) {
+ return true;
+ }
+
+ return false;
+}
+
+/**
+ * Checks if the given URL is the homepage of the current default search engine.
+ * Returns false if the default engine is not listed in SUPPORTED_ENGINES.
+ * @param {string} urlStr
+ * The URL to check, in string form.
+ *
+ * @returns {boolean}
+ */
+async function isDefaultEngineHomepage(urlStr) {
+ let defaultEngine = await Services.search.getDefault();
+ if (!defaultEngine) {
+ return false;
+ }
+
+ let homepageMatches = SUPPORTED_ENGINES.get(defaultEngine.name);
+ if (!homepageMatches) {
+ return false;
+ }
+
+ // The URL object throws if the string isn't a valid URL.
+ let url;
+ try {
+ url = new URL(urlStr);
+ } catch (e) {
+ return false;
+ }
+
+ if (url.searchParams.has(homepageMatches.prohibitedSearchParams)) {
+ return false;
+ }
+
+ // Strip protocol and query params.
+ urlStr = url.hostname.concat(url.pathname);
+
+ return homepageMatches.domainPath.test(urlStr);
+}
+
+async function lastBrowserUpdateDate() {
+ // Get the newest update in the update history. This isn't perfect
+ // because these dates are when updates are applied, not when the
+ // user restarts with the update. See bug 1595328.
+ if (updateManager && updateManager.getUpdateCount()) {
+ let update = updateManager.getUpdateAt(0);
+ return update.installDate;
+ }
+ // Fall back to the profile age.
+ let age = await ProfileAge();
+ return (await age.firstUse) || age.created;
+}
+
+var UrlbarProviderSearchTips = new ProviderSearchTips();
diff --git a/browser/components/urlbar/UrlbarProviderTabToSearch.jsm b/browser/components/urlbar/UrlbarProviderTabToSearch.jsm
new file mode 100644
index 0000000000..d7f2c49f22
--- /dev/null
+++ b/browser/components/urlbar/UrlbarProviderTabToSearch.jsm
@@ -0,0 +1,435 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * This module exports a provider that offers a search engine when the user is
+ * typing a search engine domain.
+ */
+
+var EXPORTED_SYMBOLS = ["UrlbarProviderTabToSearch"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+XPCOMUtils.defineLazyModuleGetters(this, {
+ Services: "resource://gre/modules/Services.jsm",
+ UrlbarView: "resource:///modules/UrlbarView.jsm",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
+ UrlbarProvider: "resource:///modules/UrlbarUtils.jsm",
+ UrlbarProviderAutofill: "resource:///modules/UrlbarProviderAutofill.jsm",
+ UrlbarResult: "resource:///modules/UrlbarResult.jsm",
+ UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.jsm",
+ UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.jsm",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+});
+
+const DYNAMIC_RESULT_TYPE = "onboardTabToSearch";
+const VIEW_TEMPLATE = {
+ attributes: {
+ role: "group",
+ selectable: "true",
+ },
+ children: [
+ {
+ name: "no-wrap",
+ tag: "span",
+ classList: ["urlbarView-no-wrap"],
+ children: [
+ {
+ name: "icon",
+ tag: "img",
+ classList: ["urlbarView-favicon"],
+ },
+ {
+ name: "text-container",
+ tag: "span",
+ children: [
+ {
+ name: "first-row-container",
+ tag: "span",
+ children: [
+ {
+ name: "title",
+ tag: "span",
+ classList: ["urlbarView-title"],
+ children: [
+ {
+ name: "titleStrong",
+ tag: "strong",
+ },
+ ],
+ },
+ {
+ name: "title-separator",
+ tag: "span",
+ classList: ["urlbarView-title-separator"],
+ },
+ {
+ name: "action",
+ tag: "span",
+ classList: ["urlbarView-action"],
+ attributes: {
+ "slide-in": true,
+ },
+ },
+ ],
+ },
+ {
+ name: "description",
+ tag: "span",
+ },
+ ],
+ },
+ ],
+ },
+ ],
+};
+
+/**
+ * Initializes this provider's dynamic result. To be called after the creation
+ * of the provider singleton.
+ */
+function initializeDynamicResult() {
+ UrlbarResult.addDynamicResultType(DYNAMIC_RESULT_TYPE);
+ UrlbarView.addDynamicViewTemplate(DYNAMIC_RESULT_TYPE, VIEW_TEMPLATE);
+}
+
+/**
+ * Class used to create the provider.
+ */
+class ProviderTabToSearch extends UrlbarProvider {
+ constructor() {
+ super();
+ this.onboardingEnginesShown = new Set();
+ }
+
+ /**
+ * Returns the name of this provider.
+ * @returns {string} the name of this provider.
+ */
+ get name() {
+ return "TabToSearch";
+ }
+
+ /**
+ * Returns the type of this provider.
+ * @returns {integer} one of the types from UrlbarUtils.PROVIDER_TYPE.*
+ */
+ get type() {
+ return UrlbarUtils.PROVIDER_TYPE.PROFILE;
+ }
+
+ /**
+ * Whether this provider should be invoked for the given context.
+ * If this method returns false, the providers manager won't start a query
+ * with this provider, to save on resources.
+ * @param {UrlbarQueryContext} queryContext The query context object
+ * @returns {boolean} Whether this provider should be invoked for the search.
+ */
+ async isActive(queryContext) {
+ return (
+ queryContext.searchString &&
+ queryContext.tokens.length == 1 &&
+ !queryContext.searchMode &&
+ UrlbarPrefs.get("suggest.engines")
+ );
+ }
+
+ /**
+ * Gets the provider's priority.
+ * @param {UrlbarQueryContext} queryContext The query context object
+ * @returns {number} The provider's priority for the given query.
+ */
+ getPriority(queryContext) {
+ return 0;
+ }
+
+ /**
+ * This is called only for dynamic result types, when the urlbar view updates
+ * the view of one of the results of the provider. It should return an object
+ * describing the view update.
+ *
+ * @param {UrlbarResult} result The result whose view will be updated.
+ * @param {Map} idsByName
+ * A Map from an element's name, as defined by the provider; to its ID in
+ * the DOM, as defined by the browser.
+ * @returns {object} An object describing the view update.
+ */
+ getViewUpdate(result, idsByName) {
+ return {
+ icon: {
+ attributes: {
+ src: result.payload.icon,
+ },
+ },
+ titleStrong: {
+ l10n: {
+ id: "urlbar-result-action-search-w-engine",
+ args: {
+ engine: result.payload.engine,
+ },
+ },
+ },
+ action: {
+ l10n: {
+ id: UrlbarUtils.WEB_ENGINE_NAMES.has(result.payload.engine)
+ ? "urlbar-result-action-tabtosearch-web"
+ : "urlbar-result-action-tabtosearch-other-engine",
+ args: {
+ engine: result.payload.engine,
+ },
+ },
+ },
+ description: {
+ l10n: {
+ id: "urlbar-tabtosearch-onboard",
+ },
+ },
+ };
+ }
+
+ /**
+ * Called when any selectable element in a dynamic result's view is picked.
+ *
+ * @param {UrlbarResult} result
+ * The result that was picked.
+ * @param {Element} element
+ * The element in the result's view that was picked.
+ */
+ pickResult(result, element) {
+ element.ownerGlobal.gURLBar.maybeConfirmSearchModeFromResult({
+ result,
+ checkValue: false,
+ });
+ }
+
+ /**
+ * Called when a result from the provider is selected. "Selected" refers to
+ * the user highlighing the result with the arrow keys/Tab, before it is
+ * picked. onSelection is also called when a user clicks a result. In the
+ * event of a click, onSelection is called just before pickResult.
+ *
+ * @param {UrlbarResult} result
+ * The result that was selected.
+ * @param {Element} element
+ * The element in the result's view that was selected.
+ */
+ onSelection(result, element) {
+ // We keep track of the number of times the user interacts with
+ // tab-to-search onboarding results so we stop showing them after
+ // `tabToSearch.onboard.interactionsLeft` interactions.
+ // Also do not increment the counter if the result was interacted with less
+ // than 5 minutes ago. This is a guard against the user running up the
+ // counter by interacting with the same result repeatedly.
+ if (
+ result.payload.dynamicType &&
+ (!this.onboardingInteractionAtTime ||
+ this.onboardingInteractionAtTime < Date.now() - 1000 * 60 * 5)
+ ) {
+ let interactionsLeft = UrlbarPrefs.get(
+ "tabToSearch.onboard.interactionsLeft"
+ );
+
+ if (interactionsLeft > 0) {
+ UrlbarPrefs.set(
+ "tabToSearch.onboard.interactionsLeft",
+ --interactionsLeft
+ );
+ }
+
+ this.onboardingInteractionAtTime = Date.now();
+ }
+ }
+
+ /**
+ * Called when the user starts and ends an engagement with the urlbar. We
+ * clear onboardingEnginesShown on engagement because we want to record in
+ * urlbar.tips once per engagement per engine. This has the unfortunate side
+ * effect of recording again when the user re-opens a view with a retained
+ * tab-to-search result. This is an acceptable tradeoff for not recording
+ * multiple times if the user backspaces autofill but then retypes the engine
+ * hostname, yielding the same tab-to-search result.
+ *
+ * @param {boolean} isPrivate True if the engagement is in a private context.
+ * @param {string} state The state of the engagement, one of: start,
+ * engagement, abandonment, discard.
+ */
+ onEngagement(isPrivate, state) {
+ if (!this.onboardingEnginesShown.size) {
+ return;
+ }
+
+ Services.telemetry.keyedScalarAdd(
+ "urlbar.tips",
+ "tabtosearch_onboard-shown",
+ this.onboardingEnginesShown.size
+ );
+ this.onboardingEnginesShown.clear();
+ }
+
+ /**
+ * Defines whether the view should defer user selection events while waiting
+ * for the first result from this provider.
+ * @returns {boolean} Whether the provider wants to defer user selection
+ * events.
+ */
+ get deferUserSelection() {
+ return true;
+ }
+
+ /**
+ * Starts querying.
+ * @param {object} queryContext The query context object
+ * @param {function} addCallback Callback invoked by the provider to add a new
+ * result.
+ * @returns {Promise} resolved when the query stops.
+ */
+ async startQuery(queryContext, addCallback) {
+ // enginesForDomainPrefix only matches against engine domains.
+ // Remove trailing slashes and www. from the search string and check if the
+ // resulting string is worth matching.
+ let [searchStr] = UrlbarUtils.stripPrefixAndTrim(
+ queryContext.searchString,
+ {
+ stripWww: true,
+ trimSlash: true,
+ }
+ );
+ // Skip any string that cannot be an origin.
+ if (
+ !UrlbarTokenizer.looksLikeOrigin(searchStr, {
+ ignoreKnownDomains: true,
+ noIp: true,
+ })
+ ) {
+ return;
+ }
+
+ // Also remove the public suffix, if present, to allow for partial matches.
+ if (searchStr.includes(".")) {
+ searchStr = UrlbarUtils.stripPublicSuffixFromHost(searchStr);
+ }
+
+ // Add all matching engines.
+ let engines = await UrlbarSearchUtils.enginesForDomainPrefix(searchStr, {
+ matchAllDomainLevels: true,
+ onlyEnabled: true,
+ });
+ if (!engines.length) {
+ return;
+ }
+
+ const onboardingInteractionsLeft = UrlbarPrefs.get(
+ "tabToSearch.onboard.interactionsLeft"
+ );
+
+ // If the engine host begins with the search string, autofill may happen
+ // for it, and the Muxer will retain the result only if there's a matching
+ // autofill heuristic result.
+ // Otherwise, we may have a partial match, where the search string is at
+ // the boundary of a host part, for example "wiki" in "en.wikipedia.org".
+ // We put those engines apart, and later we check if their host satisfies
+ // the autofill threshold. If they do, we mark them with the
+ // "satisfiesAutofillThreshold" payload property, so the muxer can avoid
+ // filtering them out.
+ let partialMatchEnginesByHost = new Map();
+
+ for (let engine of engines) {
+ // Trim the engine host. This will also be set as the result url, so the
+ // Muxer can use it to filter.
+ let [host] = UrlbarUtils.stripPrefixAndTrim(engine.getResultDomain(), {
+ stripWww: true,
+ });
+ // Check if the host may be autofilled.
+ if (host.startsWith(searchStr.toLocaleLowerCase())) {
+ if (onboardingInteractionsLeft > 0) {
+ addCallback(this, makeOnboardingResult(engine));
+ } else {
+ addCallback(this, makeResult(queryContext, engine));
+ }
+ continue;
+ }
+
+ // Otherwise it may be a partial match that would not be autofilled.
+ if (host.includes("." + searchStr.toLocaleLowerCase())) {
+ partialMatchEnginesByHost.set(engine.getResultDomain(), engine);
+ // Don't continue here, we are looking for more partial matches.
+ }
+ // We also try to match the searchForm domain, because otherwise for an
+ // engine like ebay, we'd check rover.ebay.com, when the user is likely
+ // to visit ebay.LANG. The searchForm URL often points to the main host.
+ let searchFormHost;
+ try {
+ searchFormHost = new URL(engine.searchForm).host;
+ } catch (ex) {
+ // Invalid url or no searchForm.
+ }
+ if (searchFormHost?.includes("." + searchStr)) {
+ partialMatchEnginesByHost.set(searchFormHost, engine);
+ }
+ }
+ if (partialMatchEnginesByHost.size) {
+ let host = await UrlbarProviderAutofill.getTopHostOverThreshold(
+ queryContext,
+ Array.from(partialMatchEnginesByHost.keys())
+ );
+ if (host) {
+ let engine = partialMatchEnginesByHost.get(host);
+ if (onboardingInteractionsLeft > 0) {
+ addCallback(this, makeOnboardingResult(engine, true));
+ } else {
+ addCallback(this, makeResult(queryContext, engine, true));
+ }
+ }
+ }
+ }
+}
+
+function makeOnboardingResult(engine, satisfiesAutofillThreshold = false) {
+ let [url] = UrlbarUtils.stripPrefixAndTrim(engine.getResultDomain(), {
+ stripWww: true,
+ });
+ url = url.substr(0, url.length - engine.searchUrlPublicSuffix.length);
+ let result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ {
+ engine: engine.name,
+ url,
+ providesSearchMode: true,
+ icon: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED,
+ dynamicType: DYNAMIC_RESULT_TYPE,
+ satisfiesAutofillThreshold,
+ }
+ );
+ result.resultSpan = 2;
+ result.suggestedIndex = 1;
+ return result;
+}
+
+function makeResult(context, engine, satisfiesAutofillThreshold = false) {
+ let [url] = UrlbarUtils.stripPrefixAndTrim(engine.getResultDomain(), {
+ stripWww: true,
+ });
+ url = url.substr(0, url.length - engine.searchUrlPublicSuffix.length);
+ let result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ ...UrlbarResult.payloadAndSimpleHighlights(context.tokens, {
+ engine: engine.name,
+ url,
+ providesSearchMode: true,
+ icon: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED,
+ query: "",
+ satisfiesAutofillThreshold,
+ })
+ );
+ result.suggestedIndex = 1;
+ return result;
+}
+
+var UrlbarProviderTabToSearch = new ProviderTabToSearch();
+initializeDynamicResult();
diff --git a/browser/components/urlbar/UrlbarProviderTokenAliasEngines.jsm b/browser/components/urlbar/UrlbarProviderTokenAliasEngines.jsm
new file mode 100644
index 0000000000..42e3adeb86
--- /dev/null
+++ b/browser/components/urlbar/UrlbarProviderTokenAliasEngines.jsm
@@ -0,0 +1,228 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * This module exports a provider that offers token alias engines.
+ */
+
+var EXPORTED_SYMBOLS = ["UrlbarProviderTokenAliasEngines"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+XPCOMUtils.defineLazyModuleGetters(this, {
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
+ UrlbarProvider: "resource:///modules/UrlbarUtils.jsm",
+ UrlbarResult: "resource:///modules/UrlbarResult.jsm",
+ UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.jsm",
+ UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.jsm",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+});
+
+/**
+ * Class used to create the provider.
+ */
+class ProviderTokenAliasEngines extends UrlbarProvider {
+ constructor() {
+ super();
+ this._engines = [];
+ }
+
+ /**
+ * Returns the name of this provider.
+ * @returns {string} the name of this provider.
+ */
+ get name() {
+ return "TokenAliasEngines";
+ }
+
+ /**
+ * Returns the type of this provider.
+ * @returns {integer} one of the types from UrlbarUtils.PROVIDER_TYPE.*
+ */
+ get type() {
+ return UrlbarUtils.PROVIDER_TYPE.HEURISTIC;
+ }
+
+ get PRIORITY() {
+ // Beats UrlbarProviderSearchSuggestions and UnifiedComplete.
+ return 1;
+ }
+
+ /**
+ * Whether this provider should be invoked for the given context.
+ * If this method returns false, the providers manager won't start a query
+ * with this provider, to save on resources.
+ * @param {UrlbarQueryContext} queryContext The query context object
+ * @returns {boolean} Whether this provider should be invoked for the search.
+ */
+ async isActive(queryContext) {
+ let instance = this.queryInstance;
+
+ // This is usually reset on canceling or completing the query, but since we
+ // query in isActive, it may not have been canceled by the previous call.
+ // It is an object with values { result: UrlbarResult, instance: Query }.
+ this._autofillData = null;
+
+ // Once the user starts typing a search string after the token, we hand off
+ // suggestions to UrlbarProviderSearchSuggestions.
+ if (
+ !queryContext.searchString.startsWith("@") ||
+ queryContext.tokens.length != 1
+ ) {
+ return false;
+ }
+
+ // Do not show token alias results in search mode.
+ if (queryContext.searchMode) {
+ return false;
+ }
+
+ this._engines = await UrlbarSearchUtils.tokenAliasEngines();
+ if (!this._engines.length) {
+ return false;
+ }
+
+ // Check the query was not canceled while this executed.
+ if (instance != this.queryInstance) {
+ return false;
+ }
+
+ if (queryContext.trimmedSearchString == "@") {
+ return true;
+ }
+
+ // If the user is typing a potential engine name, autofill it.
+ if (UrlbarPrefs.get("autoFill") && queryContext.allowAutofill) {
+ let result = this._getAutofillResult(queryContext);
+ if (result) {
+ this._autofillData = { result, instance };
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Starts querying.
+ * @param {object} queryContext The query context object
+ * @param {function} addCallback Callback invoked by the provider to add a new
+ * result.
+ */
+ async startQuery(queryContext, addCallback) {
+ if (!this._engines || !this._engines.length) {
+ return;
+ }
+
+ if (
+ this._autofillData &&
+ this._autofillData.instance == this.queryInstance
+ ) {
+ addCallback(this, this._autofillData.result);
+ }
+
+ for (let { engine, tokenAliases } of this._engines) {
+ if (
+ tokenAliases[0].startsWith(queryContext.trimmedSearchString) &&
+ engine.name != this._autofillData?.result.payload.engine
+ ) {
+ let result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ engine: [engine.name, UrlbarUtils.HIGHLIGHT.TYPED],
+ keyword: [tokenAliases[0], UrlbarUtils.HIGHLIGHT.TYPED],
+ query: ["", UrlbarUtils.HIGHLIGHT.TYPED],
+ icon: engine.iconURI?.spec,
+ providesSearchMode: true,
+ })
+ );
+ addCallback(this, result);
+ }
+ }
+
+ this._autofillData = null;
+ }
+
+ /**
+ * Gets the provider's priority.
+ * @param {UrlbarQueryContext} queryContext The query context object
+ * @returns {number} The provider's priority for the given query.
+ */
+ getPriority(queryContext) {
+ return this.PRIORITY;
+ }
+
+ /**
+ * Cancels a running query.
+ * @param {object} queryContext The query context object
+ */
+ cancelQuery(queryContext) {
+ if (this._autofillData?.instance == this.queryInstance) {
+ this._autofillData = null;
+ }
+ }
+
+ _getAutofillResult(queryContext) {
+ let lowerCaseSearchString = queryContext.searchString.toLowerCase();
+
+ // The user is typing a specific engine. We should show a heuristic result.
+ for (let { engine, tokenAliases } of this._engines) {
+ for (let alias of tokenAliases) {
+ if (alias.startsWith(lowerCaseSearchString)) {
+ // We found the engine.
+
+ // Stop adding an autofill result once the user has typed the full
+ // alias followed by a space. UrlbarProviderUnifiedComplete will take
+ // over at this point.
+ if (
+ lowerCaseSearchString.startsWith(alias) &&
+ UrlbarTokenizer.REGEXP_SPACES_START.test(
+ lowerCaseSearchString.substring(alias.length)
+ )
+ ) {
+ return null;
+ }
+
+ // Add an autofill result. Append a space so the user can hit enter
+ // or the right arrow key and immediately start typing their query.
+ let aliasPreservingUserCase =
+ queryContext.searchString +
+ alias.substr(queryContext.searchString.length);
+ let value = aliasPreservingUserCase + " ";
+ let result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ engine: [engine.name, UrlbarUtils.HIGHLIGHT.TYPED],
+ keyword: [aliasPreservingUserCase, UrlbarUtils.HIGHLIGHT.TYPED],
+ query: ["", UrlbarUtils.HIGHLIGHT.TYPED],
+ icon: engine.iconURI?.spec,
+ providesSearchMode: true,
+ })
+ );
+
+ // We set suggestedIndex = 0 instead of the heuristic because we
+ // don't want this result to be automatically selected. That way,
+ // users can press Tab to select the result, building on their
+ // muscle memory from tab-to-search.
+ result.suggestedIndex = 0;
+
+ result.autofill = {
+ value,
+ selectionStart: queryContext.searchString.length,
+ selectionEnd: value.length,
+ };
+ return result;
+ }
+ }
+ }
+ return null;
+ }
+}
+
+var UrlbarProviderTokenAliasEngines = new ProviderTokenAliasEngines();
diff --git a/browser/components/urlbar/UrlbarProviderTopSites.jsm b/browser/components/urlbar/UrlbarProviderTopSites.jsm
new file mode 100644
index 0000000000..37d3e3c873
--- /dev/null
+++ b/browser/components/urlbar/UrlbarProviderTopSites.jsm
@@ -0,0 +1,249 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["UrlbarProviderTopSites"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AboutNewTab: "resource:///modules/AboutNewTab.jsm",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
+ Services: "resource://gre/modules/Services.jsm",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
+ UrlbarProvider: "resource:///modules/UrlbarUtils.jsm",
+ UrlbarProviderOpenTabs: "resource:///modules/UrlbarProviderOpenTabs.jsm",
+ UrlbarResult: "resource:///modules/UrlbarResult.jsm",
+ UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.jsm",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+ TOP_SITES_MAX_SITES_PER_ROW: "resource://activity-stream/common/Reducers.jsm",
+ TOP_SITES_DEFAULT_ROWS: "resource://activity-stream/common/Reducers.jsm",
+});
+
+/**
+ * This module exports a provider returning the user's newtab Top Sites.
+ */
+
+/**
+ * A provider that returns the Top Sites shown on about:newtab.
+ */
+class ProviderTopSites extends UrlbarProvider {
+ constructor() {
+ super();
+ }
+
+ get PRIORITY() {
+ // Top sites are prioritized over the UnifiedComplete provider.
+ return 1;
+ }
+
+ /**
+ * Unique name for the provider, used by the context to filter on providers.
+ * Not using a unique name will cause the newest registration to win.
+ */
+ get name() {
+ return "UrlbarProviderTopSites";
+ }
+
+ /**
+ * The type of the provider.
+ */
+ get type() {
+ return UrlbarUtils.PROVIDER_TYPE.PROFILE;
+ }
+
+ /**
+ * Whether this provider should be invoked for the given context.
+ * If this method returns false, the providers manager won't start a query
+ * with this provider, to save on resources.
+ * @param {UrlbarQueryContext} queryContext The query context object
+ * @returns {boolean} Whether this provider should be invoked for the search.
+ */
+ isActive(queryContext) {
+ return (
+ !queryContext.restrictSource &&
+ !queryContext.searchString &&
+ !queryContext.searchMode
+ );
+ }
+
+ /**
+ * Gets the provider's priority.
+ * @param {UrlbarQueryContext} queryContext The query context object
+ * @returns {number} The provider's priority for the given query.
+ */
+ getPriority(queryContext) {
+ return this.PRIORITY;
+ }
+
+ /**
+ * Starts querying.
+ * @param {UrlbarQueryContext} queryContext The query context object
+ * @param {function} addCallback Callback invoked by the provider to add a new
+ * result. A UrlbarResult should be passed to it.
+ * @note Extended classes should return a Promise resolved when the provider
+ * is done searching AND returning results.
+ */
+ async startQuery(queryContext, addCallback) {
+ // If system.topsites is disabled, we would get stale or empty Top Sites
+ // data. We check this condition here instead of in isActive because we
+ // still want this provider to be restricting even if this is not true. If
+ // it wasn't restricting, we would show the results from UnifiedComplete's
+ // empty search behaviour. We aren't interested in those since they are very
+ // similar to Top Sites and thus might be confusing, especially since users
+ // can configure Top Sites but cannot configure the default empty search
+ // results. See bug 1623666.
+ if (
+ !UrlbarPrefs.get("suggest.topsites") ||
+ !Services.prefs.getBoolPref(
+ "browser.newtabpage.activity-stream.feeds.system.topsites",
+ false
+ )
+ ) {
+ return;
+ }
+
+ let sites = AboutNewTab.getTopSites();
+
+ let instance = this.queryInstance;
+
+ // Filter out empty values. Site is empty when there's a gap between tiles
+ // on about:newtab.
+ sites = sites.filter(site => site);
+
+ // This is done here, rather than in the global scope, because
+ // TOP_SITES_DEFAULT_ROWS causes the import of Reducers.jsm, and we want to
+ // do that only when actually querying for Top Sites.
+ if (this.topSitesRows === undefined) {
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "topSitesRows",
+ "browser.newtabpage.activity-stream.topSitesRows",
+ TOP_SITES_DEFAULT_ROWS
+ );
+ }
+
+ // We usually respect maxRichResults, though we never show a number of Top
+ // Sites greater than what is visible in the New Tab Page, because the
+ // additional ones couldn't be managed from the page.
+ let numTopSites = Math.min(
+ UrlbarPrefs.get("maxRichResults"),
+ TOP_SITES_MAX_SITES_PER_ROW * this.topSitesRows
+ );
+ sites = sites.slice(0, numTopSites);
+
+ sites = sites.map(link => ({
+ type: link.searchTopSite ? "search" : "url",
+ url: link.url_urlbar || link.url,
+ isPinned: !!link.isPinned,
+ isSponsored: !!link.sponsored_position,
+ // The newtab page allows the user to set custom site titles, which
+ // are stored in `label`, so prefer it. Search top sites currently
+ // don't have titles but `hostname` instead.
+ title: link.label || link.title || link.hostname || "",
+ favicon: link.smallFavicon || link.favicon || undefined,
+ sendAttributionRequest: !!link.sendAttributionRequest,
+ }));
+
+ for (let site of sites) {
+ switch (site.type) {
+ case "url": {
+ let result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ title: site.title,
+ url: site.url,
+ icon: site.favicon,
+ isPinned: site.isPinned,
+ isSponsored: site.isSponsored,
+ sendAttributionRequest: site.sendAttributionRequest,
+ })
+ );
+
+ let allowTabSwitch =
+ !queryContext.isPrivate ||
+ PrivateBrowsingUtils.permanentPrivateBrowsing;
+
+ let tabs;
+ if (allowTabSwitch && UrlbarPrefs.get("suggest.openpage")) {
+ tabs = UrlbarProviderOpenTabs.openTabs.get(
+ queryContext.userContextId || 0
+ );
+ }
+
+ if (tabs && tabs.includes(site.url.replace(/#.*$/, ""))) {
+ result.type = UrlbarUtils.RESULT_TYPE.TAB_SWITCH;
+ result.source = UrlbarUtils.RESULT_SOURCE.TABS;
+ } else if (UrlbarPrefs.get("suggest.bookmark")) {
+ let bookmark = await PlacesUtils.bookmarks.fetch({
+ url: new URL(result.payload.url),
+ });
+ if (bookmark) {
+ result.source = UrlbarUtils.RESULT_SOURCE.BOOKMARKS;
+ }
+ }
+
+ // Our query has been cancelled.
+ if (instance != this.queryInstance) {
+ break;
+ }
+
+ addCallback(this, result);
+ break;
+ }
+ case "search": {
+ let engine = await UrlbarSearchUtils.engineForAlias(site.title);
+
+ if (!engine && site.url) {
+ // Look up the engine by its domain.
+ let host;
+ try {
+ host = new URL(site.url).hostname;
+ } catch (err) {}
+ if (host) {
+ engine = (
+ await UrlbarSearchUtils.enginesForDomainPrefix(host)
+ )[0];
+ }
+ }
+
+ if (!engine) {
+ // No engine found. We skip this Top Site.
+ break;
+ }
+
+ if (instance != this.queryInstance) {
+ break;
+ }
+
+ let result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ title: site.title,
+ keyword: site.title,
+ providesSearchMode: true,
+ engine: engine.name,
+ query: "",
+ icon: site.favicon,
+ isPinned: site.isPinned,
+ })
+ );
+ addCallback(this, result);
+ break;
+ }
+ default:
+ Cu.reportError(`Unknown Top Site type: ${site.type}`);
+ break;
+ }
+ }
+ }
+}
+
+var UrlbarProviderTopSites = new ProviderTopSites();
diff --git a/browser/components/urlbar/UrlbarProviderUnifiedComplete.jsm b/browser/components/urlbar/UrlbarProviderUnifiedComplete.jsm
new file mode 100644
index 0000000000..006105d303
--- /dev/null
+++ b/browser/components/urlbar/UrlbarProviderUnifiedComplete.jsm
@@ -0,0 +1,359 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * This module exports a provider that wraps the existing UnifiedComplete
+ * component, it is supposed to be used as an interim solution while we rewrite
+ * the model providers in a more modular way.
+ */
+
+var EXPORTED_SYMBOLS = ["UrlbarProviderUnifiedComplete"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+XPCOMUtils.defineLazyModuleGetters(this, {
+ PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
+ Services: "resource://gre/modules/Services.jsm",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
+ UrlbarProvider: "resource:///modules/UrlbarUtils.jsm",
+ UrlbarResult: "resource:///modules/UrlbarResult.jsm",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+});
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "unifiedComplete",
+ "@mozilla.org/autocomplete/search;1?name=unifiedcomplete",
+ "nsIAutoCompleteSearch"
+);
+
+/**
+ * Class used to create the provider.
+ */
+class ProviderUnifiedComplete extends UrlbarProvider {
+ constructor() {
+ super();
+ }
+
+ /**
+ * Returns the name of this provider.
+ * @returns {string} the name of this provider.
+ */
+ get name() {
+ return "UnifiedComplete";
+ }
+
+ /**
+ * Returns the type of this provider.
+ * @returns {integer} one of the types from UrlbarUtils.PROVIDER_TYPE.*
+ */
+ get type() {
+ return UrlbarUtils.PROVIDER_TYPE.HEURISTIC;
+ }
+
+ /**
+ * Whether this provider should be invoked for the given context.
+ * If this method returns false, the providers manager won't start a query
+ * with this provider, to save on resources.
+ * @param {UrlbarQueryContext} queryContext The query context object
+ * @returns {boolean} Whether this provider should be invoked for the search.
+ */
+ isActive(queryContext) {
+ if (
+ !queryContext.trimmedSearchString &&
+ queryContext.searchMode?.engineName &&
+ UrlbarPrefs.get("update2.emptySearchBehavior") < 2
+ ) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Starts querying.
+ * @param {object} queryContext The query context object
+ * @param {function} addCallback Callback invoked by the provider to add a new
+ * result.
+ * @returns {Promise} resolved when the query stops.
+ */
+ async startQuery(queryContext, addCallback) {
+ let instance = this.queryInstance;
+ let urls = new Set();
+ await unifiedComplete.wrappedJSObject.startQuery(queryContext, acResult => {
+ if (instance != this.queryInstance) {
+ return;
+ }
+ let results = convertLegacyAutocompleteResult(
+ queryContext,
+ acResult,
+ urls
+ );
+ for (let result of results) {
+ addCallback(this, result);
+ }
+ });
+ }
+
+ /**
+ * Cancels a running query.
+ * @param {object} queryContext The query context object
+ */
+ cancelQuery(queryContext) {
+ unifiedComplete.stopSearch();
+ }
+}
+
+var UrlbarProviderUnifiedComplete = new ProviderUnifiedComplete();
+
+/**
+ * Convert from a nsIAutocompleteResult to a list of results.
+ * Note that at every call we get the full set of results, included the
+ * previously returned ones, and new results may be inserted in the middle.
+ * This means we could sort these wrongly, the muxer should take care of it.
+ * In any case at least we're sure there's just one heuristic result and it
+ * comes first.
+ *
+ * @param {UrlbarQueryContext} context the query context.
+ * @param {object} acResult an nsIAutocompleteResult
+ * @param {set} urls a Set containing all the found urls, used to discard
+ * already added results.
+ * @returns {array} converted results
+ */
+function convertLegacyAutocompleteResult(context, acResult, urls) {
+ let results = [];
+ for (let i = 0; i < acResult.matchCount; ++i) {
+ // First, let's check if we already added this result.
+ // nsIAutocompleteResult always contains all of the results, includes ones
+ // we may have added already. This means we'll end up adding things in the
+ // wrong order here, but that's a task for the UrlbarMuxer.
+ let url = acResult.getFinalCompleteValueAt(i);
+ if (urls.has(url)) {
+ continue;
+ }
+ urls.add(url);
+ let style = acResult.getStyleAt(i);
+ let isHeuristic = i == 0 && style.includes("heuristic");
+ let result = makeUrlbarResult(context.tokens, {
+ url,
+ // getImageAt returns an empty string if there is no icon. Use undefined
+ // instead so that tests can be simplified by not including `icon: ""` in
+ // all their payloads.
+ icon: acResult.getImageAt(i) || undefined,
+ style,
+ comment: acResult.getCommentAt(i),
+ firstToken: context.tokens[0],
+ isHeuristic,
+ });
+ // Should not happen, but better safe than sorry.
+ if (!result) {
+ continue;
+ }
+ // Manage autofill for the first result.
+ if (
+ isHeuristic &&
+ style.includes("autofill") &&
+ acResult.defaultIndex == 0
+ ) {
+ let autofillValue = acResult.getValueAt(i);
+ if (
+ autofillValue
+ .toLocaleLowerCase()
+ .startsWith(context.searchString.toLocaleLowerCase())
+ ) {
+ result.autofill = {
+ value:
+ context.searchString +
+ autofillValue.substring(context.searchString.length),
+ selectionStart: context.searchString.length,
+ selectionEnd: autofillValue.length,
+ };
+ }
+ }
+ result.heuristic = isHeuristic;
+ results.push(result);
+ }
+ return results;
+}
+
+/**
+ * Creates a new UrlbarResult from the provided data.
+ * @param {array} tokens the search tokens.
+ * @param {object} info includes properties from the legacy result.
+ * @returns {object} an UrlbarResult
+ */
+function makeUrlbarResult(tokens, info) {
+ let action = PlacesUtils.parseActionUrl(info.url);
+ if (action) {
+ switch (action.type) {
+ case "searchengine": {
+ if (action.params.isSearchHistory) {
+ // Return a form history result.
+ return new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ ...UrlbarResult.payloadAndSimpleHighlights(tokens, {
+ engine: action.params.engineName,
+ suggestion: [
+ action.params.searchSuggestion,
+ UrlbarUtils.HIGHLIGHT.SUGGESTED,
+ ],
+ lowerCaseSuggestion: action.params.searchSuggestion.toLocaleLowerCase(),
+ })
+ );
+ }
+
+ return new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ ...UrlbarResult.payloadAndSimpleHighlights(tokens, {
+ engine: [action.params.engineName, UrlbarUtils.HIGHLIGHT.TYPED],
+ suggestion: [
+ action.params.searchSuggestion,
+ UrlbarUtils.HIGHLIGHT.SUGGESTED,
+ ],
+ lowerCaseSuggestion: action.params.searchSuggestion?.toLocaleLowerCase(),
+ keyword: action.params.alias,
+ query: [
+ action.params.searchQuery.trim(),
+ UrlbarUtils.HIGHLIGHT.NONE,
+ ],
+ icon: info.icon,
+ })
+ );
+ }
+ case "keyword": {
+ let title = info.comment;
+ if (!title) {
+ // If the url doesn't have an host (e.g. javascript urls), comment
+ // will be empty, and we can't build the usual title. Thus use the url.
+ title = Services.textToSubURI.unEscapeURIForUI(action.params.url);
+ } else if (tokens && tokens.length > 1) {
+ title = UrlbarUtils.strings.formatStringFromName(
+ "bookmarkKeywordSearch",
+ [
+ info.comment,
+ tokens
+ .slice(1)
+ .map(t => t.value)
+ .join(" "),
+ ]
+ );
+ }
+ return new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.KEYWORD,
+ UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ ...UrlbarResult.payloadAndSimpleHighlights(tokens, {
+ title: [title, UrlbarUtils.HIGHLIGHT.TYPED],
+ url: [action.params.url, UrlbarUtils.HIGHLIGHT.TYPED],
+ keyword: [info.firstToken.value, UrlbarUtils.HIGHLIGHT.TYPED],
+ input: [action.params.input],
+ postData: [action.params.postData],
+ icon: info.icon,
+ })
+ );
+ }
+ case "remotetab":
+ return new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.REMOTE_TAB,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ ...UrlbarResult.payloadAndSimpleHighlights(tokens, {
+ url: [action.params.url, UrlbarUtils.HIGHLIGHT.TYPED],
+ title: [info.comment, UrlbarUtils.HIGHLIGHT.TYPED],
+ device: [action.params.deviceName, UrlbarUtils.HIGHLIGHT.TYPED],
+ icon: info.icon,
+ })
+ );
+ case "switchtab":
+ return new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ ...UrlbarResult.payloadAndSimpleHighlights(tokens, {
+ url: [action.params.url, UrlbarUtils.HIGHLIGHT.TYPED],
+ title: [info.comment, UrlbarUtils.HIGHLIGHT.TYPED],
+ icon: info.icon,
+ })
+ );
+ case "visiturl":
+ return new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ ...UrlbarResult.payloadAndSimpleHighlights(tokens, {
+ title: [info.comment, UrlbarUtils.HIGHLIGHT.TYPED],
+ url: [action.params.url, UrlbarUtils.HIGHLIGHT.TYPED],
+ icon: info.icon,
+ })
+ );
+ default:
+ Cu.reportError(`Unexpected action type: ${action.type}`);
+ return null;
+ }
+ }
+
+ if (info.style.includes("priority-search")) {
+ return new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ ...UrlbarResult.payloadAndSimpleHighlights(tokens, {
+ engine: [info.comment, UrlbarUtils.HIGHLIGHT.TYPED],
+ icon: info.icon,
+ })
+ );
+ }
+
+ // This is a normal url/title tuple.
+ let source;
+ let tags = [];
+ let comment = info.comment;
+
+ // UnifiedComplete may return "bookmark", "bookmark-tag" or "tag". In the last
+ // case it should not be considered a bookmark, but an history item with tags.
+ // We don't show tags for non bookmarked items though.
+ if (info.style.includes("bookmark")) {
+ source = UrlbarUtils.RESULT_SOURCE.BOOKMARKS;
+ } else if (info.style.includes("preloaded-top-sites")) {
+ source = UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL;
+ } else {
+ source = UrlbarUtils.RESULT_SOURCE.HISTORY;
+ }
+
+ // If the style indicates that the result is tagged, then the tags are
+ // included in the title, and we must extract them.
+ if (info.style.includes("tag")) {
+ [comment, tags] = info.comment.split(UrlbarUtils.TITLE_TAGS_SEPARATOR);
+
+ // However, as mentioned above, we don't want to show tags for non-
+ // bookmarked items, so we include tags in the final result only if it's
+ // bookmarked, and we drop the tags otherwise.
+ if (source != UrlbarUtils.RESULT_SOURCE.BOOKMARKS) {
+ tags = "";
+ }
+
+ // Tags are separated by a comma and in a random order.
+ // We should also just include tags that match the searchString.
+ tags = tags
+ .split(",")
+ .map(t => t.trim())
+ .filter(tag => {
+ let lowerCaseTag = tag.toLocaleLowerCase();
+ return tokens.some(token =>
+ lowerCaseTag.includes(token.lowerCaseValue)
+ );
+ })
+ .sort();
+ }
+
+ return new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ source,
+ ...UrlbarResult.payloadAndSimpleHighlights(tokens, {
+ url: [info.url, UrlbarUtils.HIGHLIGHT.TYPED],
+ icon: info.icon,
+ title: [comment, UrlbarUtils.HIGHLIGHT.TYPED],
+ tags: [tags, UrlbarUtils.HIGHLIGHT.TYPED],
+ })
+ );
+}
diff --git a/browser/components/urlbar/UrlbarProvidersManager.jsm b/browser/components/urlbar/UrlbarProvidersManager.jsm
new file mode 100644
index 0000000000..e848666566
--- /dev/null
+++ b/browser/components/urlbar/UrlbarProvidersManager.jsm
@@ -0,0 +1,714 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * This module exports a component used to register search providers and manage
+ * the connection between such providers and a UrlbarController.
+ */
+
+var EXPORTED_SYMBOLS = ["UrlbarProvidersManager"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+XPCOMUtils.defineLazyModuleGetters(this, {
+ ObjectUtils: "resource://gre/modules/ObjectUtils.jsm",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
+ SkippableTimer: "resource:///modules/UrlbarUtils.jsm",
+ UrlbarMuxer: "resource:///modules/UrlbarUtils.jsm",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
+ UrlbarProvider: "resource:///modules/UrlbarUtils.jsm",
+ UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.jsm",
+ UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.jsm",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(this, "logger", () =>
+ UrlbarUtils.getLogger({ prefix: "ProvidersManager" })
+);
+
+// List of available local providers, each is implemented in its own jsm module
+// and will track different queries internally by queryContext.
+var localProviderModules = {
+ UrlbarProviderUnifiedComplete:
+ "resource:///modules/UrlbarProviderUnifiedComplete.jsm",
+ UrlbarProviderAutofill: "resource:///modules/UrlbarProviderAutofill.jsm",
+ UrlbarProviderHeuristicFallback:
+ "resource:///modules/UrlbarProviderHeuristicFallback.jsm",
+ UrlbarProviderInterventions:
+ "resource:///modules/UrlbarProviderInterventions.jsm",
+ UrlbarProviderOmnibox: "resource:///modules/UrlbarProviderOmnibox.jsm",
+ UrlbarProviderPrivateSearch:
+ "resource:///modules/UrlbarProviderPrivateSearch.jsm",
+ UrlbarProviderSearchTips: "resource:///modules/UrlbarProviderSearchTips.jsm",
+ UrlbarProviderSearchSuggestions:
+ "resource:///modules/UrlbarProviderSearchSuggestions.jsm",
+ UrlbarProviderTabToSearch:
+ "resource:///modules/UrlbarProviderTabToSearch.jsm",
+ UrlbarProviderTokenAliasEngines:
+ "resource:///modules/UrlbarProviderTokenAliasEngines.jsm",
+ UrlbarProviderTopSites: "resource:///modules/UrlbarProviderTopSites.jsm",
+};
+
+// List of available local muxers, each is implemented in its own jsm module.
+var localMuxerModules = {
+ UrlbarMuxerUnifiedComplete:
+ "resource:///modules/UrlbarMuxerUnifiedComplete.jsm",
+};
+
+// To improve dataflow and reduce UI work, when a result is added by a
+// non-heuristic provider, we notify it to the controller after a delay, so
+// that we can chunk results coming in that timeframe into a single call.
+const CHUNK_RESULTS_DELAY_MS = 16;
+
+const DEFAULT_MUXER = "UnifiedComplete";
+
+/**
+ * Class used to create a manager.
+ * The manager is responsible to keep a list of providers, instantiate query
+ * objects and pass those to the providers.
+ */
+class ProvidersManager {
+ constructor() {
+ // Tracks the available providers. This is a sorted array, with HEURISTIC
+ // providers at the front.
+ this.providers = [];
+ for (let [symbol, module] of Object.entries(localProviderModules)) {
+ let { [symbol]: provider } = ChromeUtils.import(module, {});
+ this.registerProvider(provider);
+ }
+ // Tracks ongoing Query instances by queryContext.
+ this.queries = new Map();
+
+ // Interrupt() allows to stop any running SQL query, some provider may be
+ // running a query that shouldn't be interrupted, and if so it should
+ // bump this through disableInterrupt and enableInterrupt.
+ this.interruptLevel = 0;
+
+ // This maps muxer names to muxers.
+ this.muxers = new Map();
+ for (let [symbol, module] of Object.entries(localMuxerModules)) {
+ let { [symbol]: muxer } = ChromeUtils.import(module, {});
+ this.registerMuxer(muxer);
+ }
+ }
+
+ /**
+ * Registers a provider object with the manager.
+ * @param {object} provider
+ */
+ registerProvider(provider) {
+ if (!provider || !(provider instanceof UrlbarProvider)) {
+ throw new Error(`Trying to register an invalid provider`);
+ }
+ if (!Object.values(UrlbarUtils.PROVIDER_TYPE).includes(provider.type)) {
+ throw new Error(`Unknown provider type ${provider.type}`);
+ }
+ logger.info(`Registering provider ${provider.name}`);
+ let index = -1;
+ if (provider.type == UrlbarUtils.PROVIDER_TYPE.HEURISTIC) {
+ // Keep heuristic providers in order at the front of the array. Find the
+ // first non-heuristic provider and insert the new provider there.
+ index = this.providers.findIndex(
+ p => p.type != UrlbarUtils.PROVIDER_TYPE.HEURISTIC
+ );
+ }
+ if (index < 0) {
+ index = this.providers.length;
+ }
+ this.providers.splice(index, 0, provider);
+ }
+
+ /**
+ * Unregisters a previously registered provider object.
+ * @param {object} provider
+ */
+ unregisterProvider(provider) {
+ logger.info(`Unregistering provider ${provider.name}`);
+ let index = this.providers.findIndex(p => p.name == provider.name);
+ if (index != -1) {
+ this.providers.splice(index, 1);
+ }
+ }
+
+ /**
+ * Returns the provider with the given name.
+ * @param {string} name The provider name.
+ * @returns {UrlbarProvider} The provider.
+ */
+ getProvider(name) {
+ return this.providers.find(p => p.name == name);
+ }
+
+ /**
+ * Registers a muxer object with the manager.
+ * @param {object} muxer a UrlbarMuxer object
+ */
+ registerMuxer(muxer) {
+ if (!muxer || !(muxer instanceof UrlbarMuxer)) {
+ throw new Error(`Trying to register an invalid muxer`);
+ }
+ logger.info(`Registering muxer ${muxer.name}`);
+ this.muxers.set(muxer.name, muxer);
+ }
+
+ /**
+ * Unregisters a previously registered muxer object.
+ * @param {object} muxer a UrlbarMuxer object or name.
+ */
+ unregisterMuxer(muxer) {
+ let muxerName = typeof muxer == "string" ? muxer : muxer.name;
+ logger.info(`Unregistering muxer ${muxerName}`);
+ this.muxers.delete(muxerName);
+ }
+
+ /**
+ * Starts querying.
+ * @param {object} queryContext The query context object
+ * @param {object} [controller] a UrlbarController instance
+ */
+ async startQuery(queryContext, controller = null) {
+ logger.info(`Query start ${queryContext.searchString}`);
+
+ // Define the muxer to use.
+ let muxerName = queryContext.muxer || DEFAULT_MUXER;
+ logger.info(`Using muxer ${muxerName}`);
+ let muxer = this.muxers.get(muxerName);
+ if (!muxer) {
+ throw new Error(`Muxer with name ${muxerName} not found`);
+ }
+
+ // If the queryContext specifies a list of providers to use, filter on it,
+ // otherwise just pass the full list of providers.
+ let providers = queryContext.providers
+ ? this.providers.filter(p => queryContext.providers.includes(p.name))
+ : this.providers;
+
+ // Apply tokenization.
+ UrlbarTokenizer.tokenize(queryContext);
+
+ // If there's a single source, we are in restriction mode.
+ if (queryContext.sources && queryContext.sources.length == 1) {
+ queryContext.restrictSource = queryContext.sources[0];
+ }
+ // Providers can use queryContext.sources to decide whether they want to be
+ // invoked or not.
+ // The sources may be defined in the context, then the whole search string
+ // can be used for searching. Otherwise sources are extracted from prefs and
+ // restriction tokens, then restriction tokens must be filtered out of the
+ // search string.
+ let restrictToken = updateSourcesIfEmpty(queryContext);
+ if (restrictToken) {
+ queryContext.restrictToken = restrictToken;
+ // If the restriction token has an equivalent source, then set it as
+ // restrictSource.
+ if (UrlbarTokenizer.SEARCH_MODE_RESTRICT.has(restrictToken.value)) {
+ queryContext.restrictSource = queryContext.sources[0];
+ }
+ }
+ logger.debug(`Context sources ${queryContext.sources}`);
+
+ let query = new Query(queryContext, controller, muxer, providers);
+ this.queries.set(queryContext, query);
+
+ // The muxer and many providers depend on the search service and our search
+ // utils. Make sure they're initialized now (via UrlbarSearchUtils) so that
+ // all query-related urlbar modules don't need to do it.
+ await UrlbarSearchUtils.init();
+ if (query.canceled) {
+ return;
+ }
+
+ // Update the behavior of extension providers.
+ let updateBehaviorPromises = [];
+ for (let provider of this.providers) {
+ if (
+ provider.type == UrlbarUtils.PROVIDER_TYPE.EXTENSION &&
+ provider.name != "Omnibox"
+ ) {
+ updateBehaviorPromises.push(
+ provider.tryMethod("updateBehavior", queryContext)
+ );
+ }
+ }
+ if (updateBehaviorPromises.length) {
+ await Promise.all(updateBehaviorPromises);
+ if (query.canceled) {
+ return;
+ }
+ }
+
+ await query.start();
+ }
+
+ /**
+ * Cancels a running query.
+ * @param {object} queryContext
+ */
+ cancelQuery(queryContext) {
+ logger.info(`Query cancel "${queryContext.searchString}"`);
+ let query = this.queries.get(queryContext);
+ if (!query) {
+ throw new Error("Couldn't find a matching query for the given context");
+ }
+ query.cancel();
+ if (!this.interruptLevel) {
+ try {
+ let db = PlacesUtils.promiseLargeCacheDBConnection();
+ db.interrupt();
+ } catch (ex) {}
+ }
+ this.queries.delete(queryContext);
+ }
+
+ /**
+ * A provider can use this util when it needs to run a SQL query that can't
+ * be interrupted. Otherwise, when a query is canceled any running SQL query
+ * is interrupted abruptly.
+ * @param {function} taskFn a Task to execute in the critical section.
+ */
+ async runInCriticalSection(taskFn) {
+ this.interruptLevel++;
+ try {
+ await taskFn();
+ } finally {
+ this.interruptLevel--;
+ }
+ }
+
+ /**
+ * Notifies all providers when the user starts and ends an engagement with the
+ * urlbar.
+ *
+ * @param {boolean} isPrivate True if the engagement is in a private context.
+ * @param {string} state The state of the engagement, one of: start,
+ * engagement, abandonment, discard.
+ */
+ notifyEngagementChange(isPrivate, state) {
+ for (let provider of this.providers) {
+ provider.tryMethod("onEngagement", isPrivate, state);
+ }
+ }
+}
+
+var UrlbarProvidersManager = new ProvidersManager();
+
+/**
+ * Tracks a query status.
+ * Multiple queries can potentially be executed at the same time by different
+ * controllers. Each query has to track its own status and delays separately,
+ * to avoid conflicting with other ones.
+ */
+class Query {
+ /**
+ * Initializes the query object.
+ * @param {object} queryContext
+ * The query context
+ * @param {object} controller
+ * The controller to be notified
+ * @param {object} muxer
+ * The muxer to sort results
+ * @param {Array} providers
+ * Array of all the providers.
+ */
+ constructor(queryContext, controller, muxer, providers) {
+ this.context = queryContext;
+ this.context.results = [];
+ // Clear any state in the context object, since it could be reused by the
+ // caller and we don't want to port previous query state over.
+ this.context.pendingHeuristicProviders.clear();
+ this.context.deferUserSelectionProviders.clear();
+ this.muxer = muxer;
+ this.controller = controller;
+ this.providers = providers;
+ this.started = false;
+ this.canceled = false;
+
+ // This is used as a last safety filter in add(), thus we keep an unmodified
+ // copy of it.
+ this.acceptableSources = queryContext.sources.slice();
+ }
+
+ /**
+ * Starts querying.
+ */
+ async start() {
+ if (this.started) {
+ throw new Error("This Query has been started already");
+ }
+ this.started = true;
+
+ // Check which providers should be queried by calling isActive on them.
+ let activeProviders = [];
+ let activePromises = [];
+ let maxPriority = -1;
+ for (let provider of this.providers) {
+ // This can be used by the provider to check the query is still running
+ // after executing async tasks:
+ // let instance = this.queryInstance;
+ // await ...
+ // if (instance != this.queryInstance) {
+ // // Query was canceled or a new one started.
+ // return;
+ // }
+ provider.queryInstance = this;
+ activePromises.push(
+ // Not all isActive implementations are async, so wrap the call in a
+ // promise so we can be sure we can call `then` on it. Note that
+ // Promise.resolve returns its arg directly if it's already a promise.
+ Promise.resolve(provider.tryMethod("isActive", this.context))
+ .then(isActive => {
+ if (isActive && !this.canceled) {
+ let priority = provider.tryMethod("getPriority", this.context);
+ if (priority >= maxPriority) {
+ // The provider's priority is at least as high as the max.
+ if (priority > maxPriority) {
+ // The provider's priority is higher than the max. Remove all
+ // previously added providers, since their priority is
+ // necessarily lower, by setting length to zero.
+ activeProviders.length = 0;
+ maxPriority = priority;
+ }
+ activeProviders.push(provider);
+ if (provider.deferUserSelection) {
+ this.context.deferUserSelectionProviders.add(provider.name);
+ }
+ }
+ }
+ })
+ .catch(Cu.reportError)
+ );
+ }
+
+ // We have to wait for all isActive calls to finish because we want to query
+ // only the highest priority active providers as determined by the priority
+ // logic above.
+ await Promise.all(activePromises);
+
+ if (this.canceled) {
+ this.controller = null;
+ return;
+ }
+
+ // Start querying active providers.
+ let startQuery = async provider => {
+ provider.logger.info(`Starting query for "${this.context.searchString}"`);
+ let addedResult = false;
+ await provider.tryMethod("startQuery", this.context, (...args) => {
+ addedResult = true;
+ this.add(...args);
+ });
+ if (!addedResult) {
+ this.context.deferUserSelectionProviders.delete(provider.name);
+ }
+ };
+
+ let queryPromises = [];
+ for (let provider of activeProviders) {
+ if (provider.type == UrlbarUtils.PROVIDER_TYPE.HEURISTIC) {
+ this.context.pendingHeuristicProviders.add(provider.name);
+ queryPromises.push(startQuery(provider));
+ continue;
+ }
+ if (!this._sleepTimer) {
+ // Tracks the delay timer. We will fire (in this specific case, cancel
+ // would do the same, since the callback is empty) the timer when the
+ // search is canceled, unblocking start().
+ this._sleepTimer = new SkippableTimer({
+ name: "Query provider timer",
+ time: UrlbarPrefs.get("delay"),
+ logger: provider.logger,
+ });
+ }
+ queryPromises.push(
+ this._sleepTimer.promise.then(() =>
+ this.canceled ? undefined : startQuery(provider)
+ )
+ );
+ }
+
+ logger.info(`Queried ${queryPromises.length} providers`);
+ await Promise.all(queryPromises);
+
+ // All the providers are done returning results, so we can stop chunking.
+ if (!this.canceled) {
+ if (this._heuristicProviderTimer) {
+ await this._heuristicProviderTimer.fire();
+ }
+ if (this._chunkTimer) {
+ await this._chunkTimer.fire();
+ }
+ }
+
+ // Break cycles with the controller to avoid leaks.
+ this.controller = null;
+ }
+
+ /**
+ * Cancels this query.
+ * @note Invoking cancel multiple times is a no-op.
+ */
+ cancel() {
+ if (this.canceled) {
+ return;
+ }
+ this.canceled = true;
+ this.context.deferUserSelectionProviders.clear();
+ for (let provider of this.providers) {
+ provider.logger.info(
+ `Canceling query for "${this.context.searchString}"`
+ );
+ // Mark the instance as no more valid, see start() for details.
+ provider.queryInstance = null;
+ provider.tryMethod("cancelQuery", this.context);
+ }
+ if (this._heuristicProviderTimer) {
+ this._heuristicProviderTimer.cancel().catch(Cu.reportError);
+ }
+ if (this._chunkTimer) {
+ this._chunkTimer.cancel().catch(Cu.reportError);
+ }
+ if (this._sleepTimer) {
+ this._sleepTimer.fire().catch(Cu.reportError);
+ }
+ }
+
+ /**
+ * Adds a result returned from a provider to the results set.
+ * @param {object} provider
+ * @param {object} result
+ */
+ add(provider, result) {
+ if (!(provider instanceof UrlbarProvider)) {
+ throw new Error("Invalid provider passed to the add callback");
+ }
+
+ // When this set is empty, we can display heuristic results early. We remove
+ // the provider from the list without checking result.heuristic since
+ // heuristic providers don't necessarily have to return heuristic results.
+ // We expect a provider with type HEURISTIC will return its heuristic
+ // result(s) first.
+ this.context.pendingHeuristicProviders.delete(provider.name);
+
+ // Stop returning results as soon as we've been canceled.
+ if (this.canceled) {
+ return;
+ }
+
+ // In search mode, don't allow heuristic results in the following cases
+ // since they don't make sense:
+ // * When the search string is empty, or
+ // * In local search mode, except for autofill results
+ if (
+ result.heuristic &&
+ this.context.searchMode &&
+ (!this.context.trimmedSearchString ||
+ (!this.context.searchMode.engineName && !result.autofill))
+ ) {
+ return;
+ }
+
+ // Check if the result source should be filtered out. Pay attention to the
+ // heuristic result though, that is supposed to be added regardless.
+ if (
+ !this.acceptableSources.includes(result.source) &&
+ !result.heuristic &&
+ // Treat form history as searches for the purpose of acceptableSources.
+ (result.type != UrlbarUtils.RESULT_TYPE.SEARCH ||
+ result.source != UrlbarUtils.RESULT_SOURCE.HISTORY ||
+ !this.acceptableSources.includes(UrlbarUtils.RESULT_SOURCE.SEARCH))
+ ) {
+ return;
+ }
+
+ // Filter out javascript results for safety. The provider is supposed to do
+ // it, but we don't want to risk leaking these out.
+ if (
+ result.type != UrlbarUtils.RESULT_TYPE.KEYWORD &&
+ result.payload.url &&
+ result.payload.url.startsWith("javascript:") &&
+ !this.context.searchString.startsWith("javascript:") &&
+ UrlbarPrefs.get("filter.javascript")
+ ) {
+ return;
+ }
+
+ result.providerName = provider.name;
+ result.providerType = provider.type;
+ this.context.results.push(result);
+
+ this._notifyResultsFromProvider(provider);
+ }
+
+ _notifyResultsFromProvider(provider) {
+ // We create two chunking timers: one for heuristic results, and one for
+ // other results. We expect heuristic providers to return their heuristic
+ // results before other results/providers in most cases. When all heuristic
+ // providers have returned some results, we fire the heuristic timer early.
+ // If the timer fires first, we stop waiting on the remaining heuristic
+ // providers.
+ // Both timers are used to reduce UI flicker.
+ if (provider.type == UrlbarUtils.PROVIDER_TYPE.HEURISTIC) {
+ if (!this._heuristicProviderTimer) {
+ this._heuristicProviderTimer = new SkippableTimer({
+ name: "Heuristic provider timer",
+ callback: () => this._notifyResults(),
+ time: CHUNK_RESULTS_DELAY_MS,
+ logger: provider.logger,
+ });
+ }
+ } else if (!this._chunkTimer) {
+ this._chunkTimer = new SkippableTimer({
+ name: "Query chunk timer",
+ callback: () => this._notifyResults(),
+ time: CHUNK_RESULTS_DELAY_MS,
+ logger: provider.logger,
+ });
+ }
+ // If all active heuristic providers have returned results, we can skip the
+ // heuristic results timer and start showing results immediately.
+ if (
+ this._heuristicProviderTimer &&
+ !this.context.pendingHeuristicProviders.size
+ ) {
+ this._heuristicProviderTimer.fire().catch(Cu.reportError);
+ }
+ }
+
+ _notifyResults() {
+ this.muxer.sort(this.context);
+
+ if (this._heuristicProviderTimer) {
+ this._heuristicProviderTimer.cancel().catch(Cu.reportError);
+ this._heuristicProviderTimer = null;
+ }
+
+ if (this._chunkTimer) {
+ this._chunkTimer.cancel().catch(Cu.reportError);
+ this._chunkTimer = null;
+ }
+
+ // Before the muxer.sort call above, this.context.results should never be
+ // empty since this method is called when results are added. But the muxer
+ // may have excluded one or more results from the final sorted list. For
+ // example, it excludes the "Search in a Private Window" result if it's the
+ // only result that's been added so far. We don't want to notify consumers
+ // if there are no results since they generally expect at least one result
+ // when notified, so bail, but only after nulling out the chunk timer above
+ // so that it will be restarted the next time results are added.
+ if (!this.context.results.length) {
+ return;
+ }
+
+ // Crop results to the requested number, taking their result spans into
+ // account.
+ let resultCount = this.context.maxResults;
+ for (let i = 0; i < this.context.results.length; i++) {
+ resultCount -= UrlbarUtils.getSpanForResult(this.context.results[i]);
+ if (resultCount < 0) {
+ logger.debug(
+ `Splicing results from ${i} to crop results to ${this.context.maxResults}`
+ );
+ this.context.results.splice(i, this.context.results.length - i);
+ break;
+ }
+ }
+
+ this.context.firstResultChanged = !ObjectUtils.deepEqual(
+ this.context.firstResult,
+ this.context.results[0]
+ );
+ this.context.firstResult = this.context.results[0];
+
+ if (this.controller) {
+ this.controller.receiveResults(this.context);
+ }
+ }
+}
+
+/**
+ * Updates in place the sources for a given UrlbarQueryContext.
+ * @param {UrlbarQueryContext} context The query context to examine
+ * @returns {object} The restriction token that was used to set sources, or
+ * undefined if there's no restriction token.
+ */
+function updateSourcesIfEmpty(context) {
+ if (context.sources && context.sources.length) {
+ return false;
+ }
+ let acceptedSources = [];
+ // There can be only one restrict token about sources.
+ let restrictToken = context.tokens.find(t =>
+ [
+ UrlbarTokenizer.TYPE.RESTRICT_HISTORY,
+ UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK,
+ UrlbarTokenizer.TYPE.RESTRICT_TAG,
+ UrlbarTokenizer.TYPE.RESTRICT_OPENPAGE,
+ UrlbarTokenizer.TYPE.RESTRICT_SEARCH,
+ ].includes(t.type)
+ );
+ let restrictTokenType = restrictToken ? restrictToken.type : undefined;
+ for (let source of Object.values(UrlbarUtils.RESULT_SOURCE)) {
+ // Skip sources that the context doesn't care about.
+ if (context.sources && !context.sources.includes(source)) {
+ continue;
+ }
+ // Check prefs and restriction tokens.
+ switch (source) {
+ case UrlbarUtils.RESULT_SOURCE.BOOKMARKS:
+ if (
+ restrictTokenType === UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK ||
+ restrictTokenType === UrlbarTokenizer.TYPE.RESTRICT_TAG ||
+ (!restrictTokenType && UrlbarPrefs.get("suggest.bookmark"))
+ ) {
+ acceptedSources.push(source);
+ }
+ break;
+ case UrlbarUtils.RESULT_SOURCE.HISTORY:
+ if (
+ restrictTokenType === UrlbarTokenizer.TYPE.RESTRICT_HISTORY ||
+ (!restrictTokenType && UrlbarPrefs.get("suggest.history"))
+ ) {
+ acceptedSources.push(source);
+ }
+ break;
+ case UrlbarUtils.RESULT_SOURCE.SEARCH:
+ if (
+ restrictTokenType === UrlbarTokenizer.TYPE.RESTRICT_SEARCH ||
+ !restrictTokenType
+ ) {
+ // We didn't check browser.urlbar.suggest.searches here, because it
+ // just controls search suggestions. If a search suggestion arrives
+ // here, we lost already, because we broke user's privacy by hitting
+ // the network. Thus, it's better to leave things go through and
+ // notice the bug, rather than hiding it with a filter.
+ acceptedSources.push(source);
+ }
+ break;
+ case UrlbarUtils.RESULT_SOURCE.TABS:
+ if (
+ restrictTokenType === UrlbarTokenizer.TYPE.RESTRICT_OPENPAGE ||
+ (!restrictTokenType && UrlbarPrefs.get("suggest.openpage"))
+ ) {
+ acceptedSources.push(source);
+ }
+ break;
+ case UrlbarUtils.RESULT_SOURCE.OTHER_NETWORK:
+ if (!context.isPrivate && !restrictTokenType) {
+ acceptedSources.push(source);
+ }
+ break;
+ case UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL:
+ default:
+ if (!restrictTokenType) {
+ acceptedSources.push(source);
+ }
+ break;
+ }
+ }
+ context.sources = acceptedSources;
+ return restrictToken;
+}
diff --git a/browser/components/urlbar/UrlbarResult.jsm b/browser/components/urlbar/UrlbarResult.jsm
new file mode 100644
index 0000000000..fe333f7442
--- /dev/null
+++ b/browser/components/urlbar/UrlbarResult.jsm
@@ -0,0 +1,339 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * This module exports a urlbar result class, each representing a single result
+ * found by a provider that can be passed from the model to the view through
+ * the controller. It is mainly defined by a result type, and a payload,
+ * containing the data. A few getters allow to retrieve information common to all
+ * the result types.
+ */
+
+var EXPORTED_SYMBOLS = ["UrlbarResult"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+XPCOMUtils.defineLazyModuleGetters(this, {
+ BrowserUtils: "resource://gre/modules/BrowserUtils.jsm",
+ JsonSchemaValidator:
+ "resource://gre/modules/components-utils/JsonSchemaValidator.jsm",
+ Services: "resource://gre/modules/Services.jsm",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+});
+
+/**
+ * Class used to create a single result.
+ */
+class UrlbarResult {
+ /**
+ * Creates a result.
+ * @param {integer} resultType one of UrlbarUtils.RESULT_TYPE.* values
+ * @param {integer} resultSource one of UrlbarUtils.RESULT_SOURCE.* values
+ * @param {object} payload data for this result. A payload should always
+ * contain a way to extract a final url to visit. The url getter
+ * should have a case for each of the types.
+ * @param {object} [payloadHighlights] payload highlights, if any. Each
+ * property in the payload may have a corresponding property in this
+ * object. The value of each property should be an array of [index,
+ * length] tuples. Each tuple indicates a substring in the correspoding
+ * payload property.
+ */
+ constructor(resultType, resultSource, payload, payloadHighlights = {}) {
+ // Type describes the payload and visualization that should be used for
+ // this result.
+ if (!Object.values(UrlbarUtils.RESULT_TYPE).includes(resultType)) {
+ throw new Error("Invalid result type");
+ }
+ this.type = resultType;
+
+ // Source describes which data has been used to derive this result. In case
+ // multiple sources are involved, use the more privacy restricted.
+ if (!Object.values(UrlbarUtils.RESULT_SOURCE).includes(resultSource)) {
+ throw new Error("Invalid result source");
+ }
+ this.source = resultSource;
+
+ // UrlbarView is responsible for updating this.
+ this.rowIndex = -1;
+
+ // This is an optional hint to the Muxer that can be set by a provider to
+ // suggest a specific position among the results.
+ this.suggestedIndex = -1;
+
+ // May be used to indicate an heuristic result. Heuristic results can bypass
+ // source filters in the ProvidersManager, that otherwise may skip them.
+ this.heuristic = false;
+
+ // The payload contains result data. Some of the data is common across
+ // multiple types, but most of it will vary.
+ if (!payload || typeof payload != "object") {
+ throw new Error("Invalid result payload");
+ }
+ this.payload = this.validatePayload(payload);
+
+ if (!payloadHighlights || typeof payloadHighlights != "object") {
+ throw new Error("Invalid result payload highlights");
+ }
+ this.payloadHighlights = payloadHighlights;
+
+ // Make sure every property in the payload has an array of highlights. If a
+ // payload property does not have a highlights array, then give it one now.
+ // That way the consumer doesn't need to check whether it exists.
+ for (let name in payload) {
+ if (!(name in this.payloadHighlights)) {
+ this.payloadHighlights[name] = [];
+ }
+ }
+ }
+
+ /**
+ * Returns a title that could be used as a label for this result.
+ * @returns {string} The label to show in a simplified title / url view.
+ */
+ get title() {
+ return this._titleAndHighlights[0];
+ }
+
+ /**
+ * Returns an array of highlights for the title.
+ * @returns {array} The array of highlights.
+ */
+ get titleHighlights() {
+ return this._titleAndHighlights[1];
+ }
+
+ /**
+ * Returns an array [title, highlights].
+ * @returns {array} The title and array of highlights.
+ */
+ get _titleAndHighlights() {
+ switch (this.type) {
+ case UrlbarUtils.RESULT_TYPE.KEYWORD:
+ case UrlbarUtils.RESULT_TYPE.TAB_SWITCH:
+ case UrlbarUtils.RESULT_TYPE.URL:
+ case UrlbarUtils.RESULT_TYPE.OMNIBOX:
+ case UrlbarUtils.RESULT_TYPE.REMOTE_TAB:
+ return this.payload.title
+ ? [this.payload.title, this.payloadHighlights.title]
+ : [this.payload.url || "", this.payloadHighlights.url || []];
+ case UrlbarUtils.RESULT_TYPE.SEARCH:
+ if (this.payload.providesSearchMode) {
+ return ["", []];
+ }
+ if (this.payload.tail && this.payload.tailOffsetIndex >= 0) {
+ return [this.payload.tail, this.payloadHighlights.tail];
+ } else if (this.payload.suggestion) {
+ return [this.payload.suggestion, this.payloadHighlights.suggestion];
+ }
+ return [this.payload.query, this.payloadHighlights.query];
+ default:
+ return ["", []];
+ }
+ }
+
+ /**
+ * Returns an icon url.
+ * @returns {string} url of the icon.
+ */
+ get icon() {
+ return this.payload.icon;
+ }
+
+ /**
+ * Returns the given payload if it's valid or throws an error if it's not.
+ * The schemas in UrlbarUtils.RESULT_PAYLOAD_SCHEMA are used for validation.
+ *
+ * @param {object} payload The payload object.
+ * @returns {object} `payload` if it's valid.
+ */
+ validatePayload(payload) {
+ let schema = UrlbarUtils.getPayloadSchema(this.type);
+ if (!schema) {
+ throw new Error(`Unrecognized result type: ${this.type}`);
+ }
+ let result = JsonSchemaValidator.validate(payload, schema, {
+ allowExplicitUndefinedProperties: true,
+ allowNullAsUndefinedProperties: true,
+ allowExtraProperties: this.type == UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ });
+ if (!result.valid) {
+ throw result.error;
+ }
+ return payload;
+ }
+
+ /**
+ * A convenience function that takes a payload annotated with
+ * UrlbarUtils.HIGHLIGHT enums and returns the payload and the payload's
+ * highlights. Use this function when the highlighting required by your
+ * payload is based on simple substring matching, as done by
+ * UrlbarUtils.getTokenMatches(). Pass the return values as the `payload` and
+ * `payloadHighlights` params of the UrlbarResult constructor.
+ * `payloadHighlights` is optional. If omitted, payload will not be
+ * highlighted.
+ *
+ * If the payload doesn't have a title or has an empty title, and it also has
+ * a URL, then this function also sets the title to the URL's domain.
+ *
+ * @param {array} tokens The tokens that should be highlighted in each of the
+ * payload properties.
+ * @param {object} payloadInfo An object that looks like this:
+ * { payloadPropertyName: payloadPropertyInfo }
+ *
+ * Each payloadPropertyInfo may be either a string or an array. If
+ * it's a string, then the property value will be that string, and no
+ * highlighting will be applied to it. If it's an array, then it
+ * should look like this: [payloadPropertyValue, highlightType].
+ * payloadPropertyValue may be a string or an array of strings. If
+ * it's a string, then the payloadHighlights in the return value will
+ * be an array of match highlights as described in
+ * UrlbarUtils.getTokenMatches(). If it's an array, then
+ * payloadHighlights will be an array of arrays of match highlights,
+ * one element per element in payloadPropertyValue.
+ * @returns {array} An array [payload, payloadHighlights].
+ */
+ static payloadAndSimpleHighlights(tokens, payloadInfo) {
+ // Convert scalar values in payloadInfo to [value] arrays.
+ for (let [name, info] of Object.entries(payloadInfo)) {
+ if (!Array.isArray(info)) {
+ payloadInfo[name] = [info];
+ }
+ }
+
+ if (
+ (!payloadInfo.title || !payloadInfo.title[0]) &&
+ payloadInfo.url &&
+ typeof payloadInfo.url[0] == "string"
+ ) {
+ // If there's no title, show the domain as the title. Not all valid URLs
+ // have a domain.
+ payloadInfo.title = payloadInfo.title || [
+ "",
+ UrlbarUtils.HIGHLIGHT.TYPED,
+ ];
+ try {
+ payloadInfo.title[0] = new URL(payloadInfo.url[0]).host;
+ } catch (e) {}
+ }
+
+ if (payloadInfo.url) {
+ // For display purposes we need to unescape the url.
+ payloadInfo.displayUrl = [...payloadInfo.url];
+ let url = payloadInfo.displayUrl[0];
+ if (url && UrlbarPrefs.get("trimURLs")) {
+ url = BrowserUtils.removeSingleTrailingSlashFromURL(url);
+ if (url.startsWith("https://")) {
+ url = url.substring(8);
+ if (url.startsWith("www.")) {
+ url = url.substring(4);
+ }
+ }
+ }
+ payloadInfo.displayUrl[0] = Services.textToSubURI.unEscapeURIForUI(url);
+ }
+
+ // For performance reasons limit excessive string lengths, to reduce the
+ // amount of string matching we do here, and avoid wasting resources to
+ // handle long textruns that the user would never see anyway.
+ for (let prop of ["displayUrl", "title", "suggestion"]) {
+ let val = payloadInfo[prop]?.[0];
+ if (typeof val == "string") {
+ payloadInfo[prop][0] = val.substring(0, UrlbarUtils.MAX_TEXT_LENGTH);
+ }
+ }
+
+ let entries = Object.entries(payloadInfo);
+ return [
+ entries.reduce((payload, [name, [val, _]]) => {
+ payload[name] = val;
+ return payload;
+ }, {}),
+ entries.reduce((highlights, [name, [val, highlightType]]) => {
+ if (highlightType) {
+ highlights[name] = !Array.isArray(val)
+ ? UrlbarUtils.getTokenMatches(tokens, val || "", highlightType)
+ : val.map(subval =>
+ UrlbarUtils.getTokenMatches(tokens, subval, highlightType)
+ );
+ }
+ return highlights;
+ }, {}),
+ ];
+ }
+
+ static _dynamicResultTypesByName = new Map();
+
+ /**
+ * Registers a dynamic result type. Dynamic result types are types that are
+ * created at runtime, for example by an extension. A particular type should
+ * be added only once; if this method is called for a type more than once, the
+ * `type` in the last call overrides those in previous calls.
+ *
+ * @param {string} name
+ * The name of the type. This is used in CSS selectors, so it shouldn't
+ * contain any spaces or punctuation except for -, _, etc.
+ * @param {object} type
+ * An object that describes the type. Currently types do not have any
+ * associated metadata, so this object should be empty.
+ */
+ static addDynamicResultType(name, type = {}) {
+ if (/[^a-z0-9_-]/i.test(name)) {
+ Cu.reportError(`Illegal dynamic type name: ${name}`);
+ return;
+ }
+ this._dynamicResultTypesByName.set(name, type);
+ }
+
+ /**
+ * Unregisters a dynamic result type.
+ *
+ * @param {string} name
+ * The name of the type.
+ */
+ static removeDynamicResultType(name) {
+ let type = this._dynamicResultTypesByName.get(name);
+ if (type) {
+ this._dynamicResultTypesByName.delete(name);
+ }
+ }
+
+ /**
+ * Returns an object describing a registered dynamic result type.
+ *
+ * @param {string} name
+ * The name of the type.
+ * @returns {object}
+ * Currently types do not have any associated metadata, so the return value
+ * is an empty object if the type exists. If the type doesn't exist,
+ * undefined is returned.
+ */
+ static getDynamicResultType(name) {
+ return this._dynamicResultTypesByName.get(name);
+ }
+
+ /**
+ * This is useful for logging results. If you need the full payload, then it's
+ * better to JSON.stringify the result object itself.
+ * @returns {string} string representation of the result.
+ */
+ toString() {
+ if (this.payload.url) {
+ return this.payload.title + " - " + this.payload.url.substr(0, 100);
+ }
+ if (this.payload.keyword) {
+ return this.payload.keyword + " - " + this.payload.query;
+ }
+ if (this.payload.suggestion) {
+ return this.payload.engine + " - " + this.payload.suggestion;
+ }
+ if (this.payload.engine) {
+ return this.payload.engine + " - " + this.payload.query;
+ }
+ return JSON.stringify(this);
+ }
+}
diff --git a/browser/components/urlbar/UrlbarSearchOneOffs.jsm b/browser/components/urlbar/UrlbarSearchOneOffs.jsm
new file mode 100644
index 0000000000..508e87f564
--- /dev/null
+++ b/browser/components/urlbar/UrlbarSearchOneOffs.jsm
@@ -0,0 +1,374 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["UrlbarSearchOneOffs"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+XPCOMUtils.defineLazyModuleGetters(this, {
+ SearchOneOffs: "resource:///modules/SearchOneOffs.jsm",
+ Services: "resource://gre/modules/Services.jsm",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+});
+
+/**
+ * The one-off search buttons in the urlbar.
+ */
+class UrlbarSearchOneOffs extends SearchOneOffs {
+ /**
+ * Constructor.
+ *
+ * @param {UrlbarView} view
+ * The parent UrlbarView.
+ */
+ constructor(view) {
+ super(view.panel.querySelector(".search-one-offs"));
+ this.view = view;
+ this.input = view.input;
+ UrlbarPrefs.addObserver(this);
+ // Override the SearchOneOffs.jsm value for the Address Bar.
+ this.disableOneOffsHorizontalKeyNavigation = true;
+ }
+
+ /**
+ * Returns the local search mode one-off buttons.
+ *
+ * @returns {array}
+ * The local one-off buttons.
+ */
+ get localButtons() {
+ return this.getSelectableButtons(false).filter(b => b.source);
+ }
+
+ /**
+ * Enables (shows) or disables (hides) the one-offs.
+ *
+ * @param {boolean} enable
+ * True to enable, false to disable.
+ */
+ enable(enable) {
+ if (enable) {
+ this.telemetryOrigin = "urlbar";
+ this.style.display = "";
+ this.textbox = this.view.input.inputField;
+ if (this.view.isOpen) {
+ this._rebuild();
+ }
+ this.view.controller.addQueryListener(this);
+ } else {
+ this.telemetryOrigin = null;
+ this.style.display = "none";
+ this.textbox = null;
+ this.view.controller.removeQueryListener(this);
+ }
+ }
+
+ /**
+ * Query listener method. Delegates to the superclass.
+ */
+ onViewOpen() {
+ this._on_popupshowing();
+ }
+
+ /**
+ * Query listener method. Delegates to the superclass.
+ */
+ onViewClose() {
+ this._on_popuphidden();
+ }
+
+ /**
+ * @returns {boolean}
+ * True if the one-offs are connected to a view.
+ */
+ get hasView() {
+ // Return true if the one-offs are enabled. We set style.display = "none"
+ // when they're disabled, and we hide the container when there are no
+ // engines to show.
+ return this.style.display != "none" && !this.container.hidden;
+ }
+
+ /**
+ * @returns {boolean}
+ * True if the view is open.
+ */
+ get isViewOpen() {
+ return this.view.isOpen;
+ }
+
+ /**
+ * The selected one-off, a xul:button, including the search-settings button.
+ *
+ * @param {DOMElement|null} button
+ * The selected one-off button. Null if no one-off is selected.
+ */
+ set selectedButton(button) {
+ if (this.selectedButton == button) {
+ return;
+ }
+
+ super.selectedButton = button;
+
+ let expectedSearchMode;
+ if (
+ button &&
+ button != this.view.oneOffSearchButtons.settingsButtonCompact
+ ) {
+ expectedSearchMode = {
+ engineName: button.engine?.name,
+ source: button.source,
+ entry: "oneoff",
+ };
+ this.input.searchMode = expectedSearchMode;
+ } else if (this.input.searchMode) {
+ // Restore the previous state. We do this only if we're in search mode, as
+ // an optimization in the common case of cycling through normal results.
+ this.input.restoreSearchModeState();
+ }
+ }
+
+ get selectedButton() {
+ return super.selectedButton;
+ }
+
+ /**
+ * @returns {number}
+ * The selected index in the view.
+ */
+ get selectedViewIndex() {
+ return this.view.selectedElementIndex;
+ }
+
+ /**
+ * Sets the selected index in the view.
+ *
+ * @param {number} val
+ * The selected index or -1 if no selection.
+ */
+ set selectedViewIndex(val) {
+ this.view.selectedElementIndex = val;
+ }
+
+ /**
+ * Closes the view.
+ */
+ closeView() {
+ if (this.view) {
+ this.view.close();
+ }
+ }
+
+ /**
+ * Called when a one-off is clicked. This is not called for the settings
+ * button.
+ *
+ * @param {event} event
+ * The event that triggered the pick.
+ * @param {object} searchMode
+ * Used by UrlbarInput.setSearchMode to enter search mode. See setSearchMode
+ * documentation for details.
+ */
+ handleSearchCommand(event, searchMode) {
+ // The settings button is a special case. Its action should be executed
+ // immediately.
+ if (
+ this.selectedButton == this.view.oneOffSearchButtons.settingsButtonCompact
+ ) {
+ this.input.controller.engagementEvent.discard();
+ this.selectedButton.doCommand();
+ return;
+ }
+
+ // We allow autofill in local but not remote search modes.
+ let startQueryParams = {
+ allowAutofill:
+ !searchMode.engineName &&
+ searchMode.source != UrlbarUtils.RESULT_SOURCE.SEARCH,
+ event,
+ };
+
+ let userTypedSearchString =
+ this.input.value && this.input.getAttribute("pageproxystate") != "valid";
+ let engine = Services.search.getEngineByName(searchMode.engineName);
+
+ let { where, params } = this._whereToOpen(event);
+
+ // Some key combinations should execute a search immediately. We handle
+ // these here, outside the switch statement.
+ if (
+ userTypedSearchString &&
+ engine &&
+ (event.shiftKey || where != "current")
+ ) {
+ this.input.handleNavigation({
+ event,
+ oneOffParams: {
+ openWhere: where,
+ openParams: params,
+ engine: this.selectedButton.engine,
+ },
+ });
+ this.selectedButton = null;
+ return;
+ }
+
+ // Handle opening search mode in either the current tab or in a new tab.
+ switch (where) {
+ case "current": {
+ this.input.searchMode = searchMode;
+ this.input.startQuery(startQueryParams);
+ break;
+ }
+ case "tab": {
+ // We set this.selectedButton when switching tabs. If we entered search
+ // mode preview here, it could be cleared when this.selectedButton calls
+ // setSearchMode.
+ searchMode.isPreview = false;
+
+ let newTab = this.input.window.gBrowser.addTrustedTab("about:newtab");
+ this.input.setSearchMode(searchMode, newTab.linkedBrowser);
+ if (userTypedSearchString) {
+ // Set the search string for the new tab.
+ newTab.linkedBrowser.userTypedValue = this.input.value;
+ }
+ if (!params?.inBackground) {
+ this.input.window.gBrowser.selectedTab = newTab;
+ newTab.ownerGlobal.gURLBar.startQuery(startQueryParams);
+ }
+ break;
+ }
+ default: {
+ this.input.searchMode = searchMode;
+ this.input.startQuery(startQueryParams);
+ this.input.select();
+ break;
+ }
+ }
+
+ this.selectedButton = null;
+ }
+
+ /**
+ * Sets the tooltip for a one-off button with an engine. This should set
+ * either the `tooltiptext` attribute or the relevant l10n ID.
+ *
+ * @param {element} button
+ * The one-off button.
+ */
+ setTooltipForEngineButton(button) {
+ let aliases = button.engine.aliases;
+ if (!aliases.length) {
+ super.setTooltipForEngineButton(button);
+ return;
+ }
+ this.document.l10n.setAttributes(
+ button,
+ "search-one-offs-engine-with-alias",
+ {
+ engineName: button.engine.name,
+ alias: aliases[0],
+ }
+ );
+ }
+
+ /**
+ * Overrides the willHide method in the superclass to account for the local
+ * search mode buttons.
+ *
+ * @returns {boolean}
+ * True if we will hide the one-offs when they are requested.
+ */
+ async willHide() {
+ // We need to call super.willHide() even when we return false below because
+ // it has the necessary side effect of creating this._engineInfo.
+ let superWillHide = await super.willHide();
+ if (UrlbarUtils.LOCAL_SEARCH_MODES.some(m => UrlbarPrefs.get(m.pref))) {
+ return false;
+ }
+ return superWillHide;
+ }
+
+ /**
+ * Called when a pref tracked by UrlbarPrefs changes.
+ *
+ * @param {string} changedPref
+ * The name of the pref, relative to `browser.urlbar.` if the pref is in
+ * that branch.
+ */
+ onPrefChanged(changedPref) {
+ // Invalidate the engine cache when the local-one-offs-related prefs change
+ // so that the one-offs rebuild themselves the next time the view opens.
+ if (
+ [...UrlbarUtils.LOCAL_SEARCH_MODES.map(m => m.pref)].includes(changedPref)
+ ) {
+ this.invalidateCache();
+ }
+ }
+
+ /**
+ * Overrides _rebuildEngineList to add the local one-offs.
+ *
+ * @param {array} engines
+ * The search engines to add.
+ */
+ _rebuildEngineList(engines) {
+ super._rebuildEngineList(engines);
+
+ for (let { source, pref, restrict } of UrlbarUtils.LOCAL_SEARCH_MODES) {
+ if (!UrlbarPrefs.get(pref)) {
+ continue;
+ }
+ let name = UrlbarUtils.getResultSourceName(source);
+ let button = this.document.createXULElement("button");
+ button.id = `urlbar-engine-one-off-item-${name}`;
+ button.setAttribute("class", "searchbar-engine-one-off-item");
+ this.document.l10n.setAttributes(button, `search-one-offs-${name}`, {
+ restrict,
+ });
+ button.source = source;
+ this.buttons.appendChild(button);
+ }
+ }
+
+ /**
+ * Overrides the superclass's click listener to handle clicks on local
+ * one-offs in addition to engine one-offs.
+ *
+ * @param {event} event
+ * The click event.
+ */
+ _on_click(event) {
+ // Ignore right clicks.
+ if (event.button == 2) {
+ return;
+ }
+
+ let button = event.originalTarget;
+ if (!button.engine && !button.source) {
+ return;
+ }
+
+ this.selectedButton = button;
+ this.handleSearchCommand(event, {
+ engineName: button.engine?.name,
+ source: button.source,
+ entry: "oneoff",
+ });
+ }
+
+ /**
+ * Overrides the superclass's contextmenu listener to handle the context menu.
+ *
+ * @param {event} event
+ * The contextmenu event.
+ */
+ _on_contextmenu(event) {
+ // Prevent the context menu from appearing.
+ event.preventDefault();
+ }
+}
diff --git a/browser/components/urlbar/UrlbarSearchUtils.jsm b/browser/components/urlbar/UrlbarSearchUtils.jsm
new file mode 100644
index 0000000000..0a2a1d30e2
--- /dev/null
+++ b/browser/components/urlbar/UrlbarSearchUtils.jsm
@@ -0,0 +1,293 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Search service utilities for urlbar. The only reason these functions aren't
+ * a part of UrlbarUtils is that we want O(1) case-insensitive lookup for search
+ * aliases, and to do that we need to observe the search service, persistent
+ * state, and an init method. A separate object is easier.
+ */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["UrlbarSearchUtils"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const SEARCH_ENGINE_TOPIC = "browser-search-engine-modified";
+
+/**
+ * Search service utilities for urlbar.
+ */
+class SearchUtils {
+ constructor() {
+ this._refreshEnginesByAliasPromise = Promise.resolve();
+ this.QueryInterface = ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsISupportsWeakReference",
+ ]);
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "separatePrivateDefaultUIEnabled",
+ "browser.search.separatePrivateDefault.ui.enabled",
+ false
+ );
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "separatePrivateDefault",
+ "browser.search.separatePrivateDefault",
+ false
+ );
+ }
+
+ /**
+ * Initializes the instance and also Services.search.
+ */
+ async init() {
+ if (!this._initPromise) {
+ this._initPromise = this._initInternal();
+ }
+ await this._initPromise;
+ }
+
+ /**
+ * Gets the engines whose domains match a given prefix.
+ *
+ * @param {string} prefix
+ * String containing the first part of the matching domain name(s).
+ * @param {object} [options]
+ * @param {boolean} [options.matchAllDomainLevels]
+ * Match at each sub domain, for example "a.b.c.com" will be matched at
+ * "a.b.c.com", "b.c.com", and "c.com". Partial matches are always returned
+ * after perfect matches.
+ * @param {boolean} [options.onlyEnabled]
+ * Match only engines that have not been disabled on the Search Preferences
+ * list.
+ * @returns {Array<nsISearchEngine>}
+ * An array of all matching engines. An empty array if there are none.
+ */
+ async enginesForDomainPrefix(
+ prefix,
+ { matchAllDomainLevels = false, onlyEnabled = false } = {}
+ ) {
+ await this.init();
+ prefix = prefix.toLowerCase();
+
+ let disabledEngines = onlyEnabled
+ ? Services.prefs
+ .getStringPref("browser.search.hiddenOneOffs", "")
+ .split(",")
+ .filter(e => !!e)
+ : [];
+
+ // Array of partially matched engines, added through matchPrefix().
+ let partialMatchEngines = [];
+ function matchPrefix(engine, engineHost) {
+ let parts = engineHost.split(".");
+ for (let i = 1; i < parts.length - 1; ++i) {
+ if (
+ parts
+ .slice(i)
+ .join(".")
+ .startsWith(prefix)
+ ) {
+ partialMatchEngines.push(engine);
+ }
+ }
+ }
+
+ // Array of fully matched engines.
+ let engines = [];
+ for (let engine of await Services.search.getVisibleEngines()) {
+ if (disabledEngines.includes(engine.name)) {
+ continue;
+ }
+ let domain = engine.getResultDomain();
+ if (domain.startsWith(prefix) || domain.startsWith("www." + prefix)) {
+ engines.push(engine);
+ }
+
+ if (matchAllDomainLevels) {
+ // The prefix may or may not contain part of the public suffix. If
+ // it contains a dot, we must match with and without the public suffix,
+ // otherwise it's sufficient to just match without it.
+ if (prefix.includes(".")) {
+ matchPrefix(engine, domain);
+ }
+ matchPrefix(
+ engine,
+ domain.substr(0, domain.length - engine.searchUrlPublicSuffix.length)
+ );
+ }
+ }
+
+ // Partial matches come after perfect matches.
+ return [...engines, ...partialMatchEngines];
+ }
+
+ /**
+ * Gets the engine with a given alias.
+ *
+ * @param {string} alias
+ * A search engine alias. The alias string comparison is case insensitive.
+ * @returns {nsISearchEngine}
+ * The matching engine or null if there isn't one.
+ */
+ async engineForAlias(alias) {
+ await Promise.all([this.init(), this._refreshEnginesByAliasPromise]);
+ return this._enginesByAlias.get(alias.toLocaleLowerCase()) || null;
+ }
+
+ /**
+ * The list of engines with token ("@") aliases.
+ *
+ * @returns {array}
+ * Array of objects { engine, tokenAliases } for token alias engines.
+ */
+ async tokenAliasEngines() {
+ await this.init();
+ let tokenAliasEngines = [];
+ for (let engine of await Services.search.getVisibleEngines()) {
+ let tokenAliases = this._aliasesForEngine(engine).filter(a =>
+ a.startsWith("@")
+ );
+ if (tokenAliases.length) {
+ tokenAliasEngines.push({ engine, tokenAliases });
+ }
+ }
+ return tokenAliasEngines;
+ }
+
+ /**
+ * @param {nsISearchEngine} engine
+ * @returns {string}
+ * The root domain of a search engine. e.g. If `engine` has the domain
+ * www.subdomain.rootdomain.com, `rootdomain` is returned. Returns the
+ * engine's domain if the engine's URL does not have a valid TLD.
+ */
+ getRootDomainFromEngine(engine) {
+ let domain = engine.getResultDomain();
+ let suffix = engine.searchUrlPublicSuffix;
+ if (!suffix) {
+ if (domain.endsWith(".test")) {
+ suffix = "test";
+ } else {
+ return domain;
+ }
+ }
+ domain = domain.substr(
+ 0,
+ // -1 to remove the trailing dot.
+ domain.length - suffix.length - 1
+ );
+ let domainParts = domain.split(".");
+ return domainParts.pop();
+ }
+
+ getDefaultEngine(isPrivate = false) {
+ return this.separatePrivateDefaultUIEnabled &&
+ this.separatePrivateDefault &&
+ isPrivate
+ ? Services.search.defaultPrivateEngine
+ : Services.search.defaultEngine;
+ }
+
+ async _initInternal() {
+ await Services.search.init();
+ await this._refreshEnginesByAlias();
+ Services.obs.addObserver(this, SEARCH_ENGINE_TOPIC, true);
+ }
+
+ async _refreshEnginesByAlias() {
+ // See the comment at the top of this file. The only reason we need this
+ // class is for O(1) case-insensitive lookup for search aliases, which is
+ // facilitated by _enginesByAlias.
+ this._enginesByAlias = new Map();
+ for (let engine of await Services.search.getVisibleEngines()) {
+ if (!engine.hidden) {
+ for (let alias of this._aliasesForEngine(engine)) {
+ this._enginesByAlias.set(alias, engine);
+ }
+ }
+ }
+ }
+
+ /**
+ * Compares the query parameters of two SERPs to see if one is equivalent to
+ * the other. URL `x` is equivalent to URL `y` if
+ * (a) `y` contains at least all the query parameters contained in `x`, and
+ * (b) The values of the query parameters contained in both `x` and `y `are
+ * the same.
+ *
+ * @param {string} historySerp
+ * The SERP from history whose params should be contained in
+ * `generatedSerp`.
+ * @param {string} generatedSerp
+ * The search URL we would generate for a search result with the same search
+ * string used in `historySerp`.
+ * @param {array} [ignoreParams]
+ * A list of params to ignore in the matching, i.e. params that can be
+ * contained in `historySerp` but not be in `generatedSerp`.
+ * @returns {boolean} True if `historySerp` can be deduped by `generatedSerp`.
+ *
+ * @note This function does not compare the SERPs' origins or pathnames.
+ * `historySerp` can have a different origin and/or pathname than
+ * `generatedSerp` and still be considered equivalent.
+ */
+ serpsAreEquivalent(historySerp, generatedSerp, ignoreParams = []) {
+ let historyParams = new URL(historySerp).searchParams;
+ let generatedParams = new URL(generatedSerp).searchParams;
+ if (
+ !Array.from(historyParams.entries()).every(
+ ([key, value]) =>
+ ignoreParams.includes(key) || value === generatedParams.get(key)
+ )
+ ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Gets the aliases of an engine. For the user's convenience, we recognize
+ * token versions of all non-token aliases. For example, if the user has an
+ * alias of "foo", then we recognize both "foo" and "@foo" as aliases for
+ * foo's engine. The returned list is therefore a superset of
+ * `engine.aliases`. Additionally, the returned aliases will be lower-cased
+ * to make lookups and comparisons easier.
+ *
+ * @param {nsISearchEngine} engine
+ * The aliases of this search engine will be returned.
+ * @returns {array}
+ * An array of lower-cased string aliases as described above.
+ */
+ _aliasesForEngine(engine) {
+ return engine.aliases.reduce((aliases, aliasWithCase) => {
+ // We store lower-cased aliases to make lookups and comparisons easier.
+ let alias = aliasWithCase.toLocaleLowerCase();
+ aliases.push(alias);
+ if (!alias.startsWith("@")) {
+ aliases.push("@" + alias);
+ }
+ return aliases;
+ }, []);
+ }
+
+ observe(subject, topic, data) {
+ switch (data) {
+ case "engine-added":
+ case "engine-changed":
+ case "engine-removed":
+ case "engine-default":
+ this._refreshEnginesByAliasPromise = this._refreshEnginesByAlias();
+ break;
+ }
+ }
+}
+
+var UrlbarSearchUtils = new SearchUtils();
diff --git a/browser/components/urlbar/UrlbarTokenizer.jsm b/browser/components/urlbar/UrlbarTokenizer.jsm
new file mode 100644
index 0000000000..4991f87fac
--- /dev/null
+++ b/browser/components/urlbar/UrlbarTokenizer.jsm
@@ -0,0 +1,407 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * This module exports a tokenizer to be used by the urlbar model.
+ * Emitted tokens are objects in the shape { type, value }, where type is one
+ * of UrlbarTokenizer.TYPE.
+ */
+
+var EXPORTED_SYMBOLS = ["UrlbarTokenizer"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+XPCOMUtils.defineLazyModuleGetters(this, {
+ Services: "resource://gre/modules/Services.jsm",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(this, "logger", () =>
+ UrlbarUtils.getLogger({ prefix: "Tokenizer" })
+);
+
+var UrlbarTokenizer = {
+ // Regex matching on whitespaces.
+ REGEXP_SPACES: /\s+/,
+ REGEXP_SPACES_START: /^\s+/,
+
+ // Regex used to guess url-like strings.
+ // These are not expected to be 100% correct, we accept some user mistypes
+ // and we're unlikely to be able to cover 100% of the cases.
+ REGEXP_LIKE_PROTOCOL: /^[A-Z+.-]+:\/*(?!\/)/i,
+ REGEXP_USERINFO_INVALID_CHARS: /[^\w.~%!$&'()*+,;=:-]/,
+ REGEXP_HOSTPORT_INVALID_CHARS: /[^\[\]A-Z0-9.:-]/i,
+ REGEXP_SINGLE_WORD_HOST: /^[^.:]+$/i,
+ REGEXP_HOSTPORT_IP_LIKE: /^(?=(.*[.:].*){2})[a-f0-9\.\[\]:]+$/i,
+ // This accepts partial IPv4.
+ REGEXP_HOSTPORT_INVALID_IP: /\.{2,}|\d{5,}|\d{4,}(?![:\]])|^\.|^(\d+\.){4,}\d+$|^\d{4,}$/,
+ // This only accepts complete IPv4.
+ REGEXP_HOSTPORT_IPV4: /^(\d{1,3}\.){3,}\d{1,3}(:\d+)?$/,
+ // This accepts partial IPv6.
+ REGEXP_HOSTPORT_IPV6: /^\[([0-9a-f]{0,4}:){0,7}[0-9a-f]{0,4}\]?$/i,
+ REGEXP_COMMON_EMAIL: /^[\w!#$%&'*+/=?^`{|}~.-]+@[\[\]A-Z0-9.-]+$/i,
+ REGEXP_HAS_PORT: /:\d+$/,
+ // Regex matching a percent encoded char at the beginning of a string.
+ REGEXP_PERCENT_ENCODED_START: /^(%[0-9a-f]{2}){2,}/i,
+
+ TYPE: {
+ TEXT: 1,
+ POSSIBLE_ORIGIN: 2, // It may be an ip, a domain, but even just a single word used as host.
+ POSSIBLE_URL: 3, // Consumers should still check this with a fixup.
+ RESTRICT_HISTORY: 4,
+ RESTRICT_BOOKMARK: 5,
+ RESTRICT_TAG: 6,
+ RESTRICT_OPENPAGE: 7,
+ RESTRICT_SEARCH: 8,
+ RESTRICT_TITLE: 9,
+ RESTRICT_URL: 10,
+ },
+
+ // The special characters below can be typed into the urlbar to restrict
+ // the search to a certain category, like history, bookmarks or open pages; or
+ // to force a match on just the title or url.
+ // These restriction characters can be typed alone, or at word boundaries,
+ // provided their meaning cannot be confused, for example # could be present
+ // in a valid url, and thus it should not be interpreted as a restriction.
+ RESTRICT: {
+ HISTORY: "^",
+ BOOKMARK: "*",
+ TAG: "+",
+ OPENPAGE: "%",
+ SEARCH: "?",
+ TITLE: "#",
+ URL: "$",
+ },
+
+ // The keys of characters in RESTRICT that will enter search mode.
+ get SEARCH_MODE_RESTRICT() {
+ return new Set([
+ this.RESTRICT.HISTORY,
+ this.RESTRICT.BOOKMARK,
+ this.RESTRICT.OPENPAGE,
+ this.RESTRICT.SEARCH,
+ ]);
+ },
+
+ /**
+ * Returns whether the passed in token looks like a URL.
+ * This is based on guessing and heuristics, that means if this function
+ * returns false, it's surely not a URL, if it returns true, the result must
+ * still be verified through URIFixup.
+ *
+ * @param {string} token
+ * The string token to verify
+ * @param {boolean} [requirePath] The url must have a path
+ * @returns {boolean} whether the token looks like a URL.
+ */
+ looksLikeUrl(token, { requirePath = false } = {}) {
+ if (token.length < 2) {
+ return false;
+ }
+ // Ignore spaces and require path for the data: protocol.
+ if (token.startsWith("data:")) {
+ return token.length > 5;
+ }
+ if (this.REGEXP_SPACES.test(token)) {
+ return false;
+ }
+ // If it starts with something that looks like a protocol, it's likely a url.
+ if (this.REGEXP_LIKE_PROTOCOL.test(token)) {
+ return true;
+ }
+ // Guess path and prePath. At this point we should be analyzing strings not
+ // having a protocol.
+ let slashIndex = token.indexOf("/");
+ let prePath = slashIndex != -1 ? token.slice(0, slashIndex) : token;
+ if (!this.looksLikeOrigin(prePath, { ignoreKnownDomains: true })) {
+ return false;
+ }
+
+ let path = slashIndex != -1 ? token.slice(slashIndex) : "";
+ logger.debug("path", path);
+ if (requirePath && !path) {
+ return false;
+ }
+ // If there are both path and userinfo, it's likely a url.
+ let atIndex = prePath.indexOf("@");
+ let userinfo = atIndex != -1 ? prePath.slice(0, atIndex) : "";
+ if (path.length && userinfo.length) {
+ return true;
+ }
+
+ // If the first character after the slash in the path is a letter, then the
+ // token may be an "abc/def" url.
+ if (/^\/[a-z]/i.test(path)) {
+ return true;
+ }
+ // If the path contains special chars, it is likely a url.
+ if (["%", "?", "#"].some(c => path.includes(c))) {
+ return true;
+ }
+
+ // The above looksLikeOrigin call told us the prePath looks like an origin,
+ // now we go into details checking some common origins.
+ let hostPort = atIndex != -1 ? prePath.slice(atIndex + 1) : prePath;
+ if (this.REGEXP_HOSTPORT_IPV4.test(hostPort)) {
+ return true;
+ }
+ // ipv6 is very complex to support, just check for a few chars.
+ if (
+ this.REGEXP_HOSTPORT_IPV6.test(hostPort) &&
+ ["[", "]", ":"].some(c => hostPort.includes(c))
+ ) {
+ return true;
+ }
+ if (Services.uriFixup.isDomainKnown(hostPort)) {
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Returns whether the passed in token looks like an origin.
+ * This is based on guessing and heuristics, that means if this function
+ * returns false, it's surely not an origin, if it returns true, the result
+ * must still be verified through URIFixup.
+ *
+ * @param {string} token
+ * The string token to verify
+ * @param {boolean} [ignoreKnownDomains] If true, the origin doesn't have to be
+ * in the known domain list
+ * @param {boolean} [noIp] If true, the origin cannot be an IP address
+ * @param {boolean} [noPort] If true, the origin cannot have a port number
+ * @returns {boolean} whether the token looks like an origin.
+ */
+ looksLikeOrigin(
+ token,
+ { ignoreKnownDomains = false, noIp = false, noPort = false } = {}
+ ) {
+ if (!token.length) {
+ return false;
+ }
+ let atIndex = token.indexOf("@");
+ if (atIndex != -1 && this.REGEXP_COMMON_EMAIL.test(token)) {
+ // We prefer handling it as an email rather than an origin with userinfo.
+ return false;
+ }
+ let userinfo = atIndex != -1 ? token.slice(0, atIndex) : "";
+ let hostPort = atIndex != -1 ? token.slice(atIndex + 1) : token;
+ let hasPort = this.REGEXP_HAS_PORT.test(hostPort);
+ logger.debug("userinfo", userinfo);
+ logger.debug("hostPort", hostPort);
+ if (noPort && hasPort) {
+ return false;
+ }
+ if (
+ this.REGEXP_HOSTPORT_IPV4.test(hostPort) ||
+ this.REGEXP_HOSTPORT_IPV6.test(hostPort)
+ ) {
+ return !noIp;
+ }
+
+ // Check for invalid chars.
+ if (
+ this.REGEXP_LIKE_PROTOCOL.test(hostPort) ||
+ this.REGEXP_USERINFO_INVALID_CHARS.test(userinfo) ||
+ this.REGEXP_HOSTPORT_INVALID_CHARS.test(hostPort) ||
+ (!this.REGEXP_SINGLE_WORD_HOST.test(hostPort) &&
+ this.REGEXP_HOSTPORT_IP_LIKE.test(hostPort) &&
+ this.REGEXP_HOSTPORT_INVALID_IP.test(hostPort))
+ ) {
+ return false;
+ }
+
+ // If it looks like a single word host, check the known domains.
+ if (
+ !ignoreKnownDomains &&
+ !userinfo &&
+ !hasPort &&
+ this.REGEXP_SINGLE_WORD_HOST.test(hostPort)
+ ) {
+ return Services.uriFixup.isDomainKnown(hostPort);
+ }
+
+ return true;
+ },
+
+ /**
+ * Tokenizes the searchString from a UrlbarQueryContext.
+ * @param {UrlbarQueryContext} queryContext
+ * The query context object to tokenize
+ * @returns {UrlbarQueryContext} the same query context object with a new
+ * tokens property.
+ */
+ tokenize(queryContext) {
+ logger.info("Tokenizing", queryContext);
+ if (!queryContext.trimmedSearchString) {
+ queryContext.tokens = [];
+ return queryContext;
+ }
+ let unfiltered = splitString(queryContext.searchString);
+ let tokens = filterTokens(unfiltered);
+ queryContext.tokens = tokens;
+ return queryContext;
+ },
+
+ /**
+ * Given a token, tells if it's a restriction token.
+ * @param {string} token
+ * @returns {boolean} Whether the token is a restriction character.
+ */
+ isRestrictionToken(token) {
+ return (
+ token &&
+ token.type >= this.TYPE.RESTRICT_HISTORY &&
+ token.type <= this.TYPE.RESTRICT_URL
+ );
+ },
+};
+
+const CHAR_TO_TYPE_MAP = new Map(
+ Object.entries(UrlbarTokenizer.RESTRICT).map(([type, char]) => [
+ char,
+ UrlbarTokenizer.TYPE[`RESTRICT_${type}`],
+ ])
+);
+
+/**
+ * Given a search string, splits it into string tokens.
+ * @param {string} searchString
+ * The search string to split
+ * @returns {array} An array of string tokens.
+ */
+function splitString(searchString) {
+ // The first step is splitting on unicode whitespaces. We ignore whitespaces
+ // if the search string starts with "data:", to better support Web developers
+ // and compatiblity with other browsers.
+ let trimmed = searchString.trim();
+ let tokens = trimmed.startsWith("data:")
+ ? [trimmed]
+ : trimmed.split(UrlbarTokenizer.REGEXP_SPACES);
+ let accumulator = [];
+ let hasRestrictionToken = tokens.some(t => CHAR_TO_TYPE_MAP.has(t));
+ let chars = Array.from(CHAR_TO_TYPE_MAP.keys()).join("");
+ logger.debug("Restriction chars", chars);
+ for (let i = 0; i < tokens.length; ++i) {
+ // If there is no separate restriction token, it's possible we have to split
+ // a token, if it's the first one and it includes a leading restriction char
+ // or it's the last one and it includes a trailing restriction char.
+ // This allows to not require the user to add artificial whitespaces to
+ // enforce restrictions, for example typing questions would restrict to
+ // search results.
+ let token = tokens[i];
+ if (!hasRestrictionToken && token.length > 1) {
+ // Check for an unambiguous restriction char at the beginning of the
+ // first token, or at the end of the last token. We only count trailing
+ // restriction chars if they are the search restriction char, which is
+ // "?". This is to allow for a typed question to yield only search results.
+ if (
+ i == 0 &&
+ chars.includes(token[0]) &&
+ !UrlbarTokenizer.REGEXP_PERCENT_ENCODED_START.test(token)
+ ) {
+ hasRestrictionToken = true;
+ accumulator.push(token[0]);
+ accumulator.push(token.slice(1));
+ continue;
+ } else if (
+ i == tokens.length - 1 &&
+ token[token.length - 1] == UrlbarTokenizer.RESTRICT.SEARCH &&
+ !UrlbarTokenizer.looksLikeUrl(token, { requirePath: true })
+ ) {
+ hasRestrictionToken = true;
+ accumulator.push(token.slice(0, token.length - 1));
+ accumulator.push(token[token.length - 1]);
+ continue;
+ }
+ }
+ accumulator.push(token);
+ }
+ logger.info("Found tokens", accumulator);
+ return accumulator;
+}
+
+/**
+ * Given an array of unfiltered tokens, this function filters them and converts
+ * to token objects with a type.
+ *
+ * @param {array} tokens
+ * An array of strings, representing search tokens.
+ * @returns {array} An array of token objects.
+ * @note restriction characters are only considered if they appear at the start
+ * or at the end of the tokens list. In case of restriction characters
+ * conflict, the most external ones win. Leading ones win over trailing
+ * ones. Discarded restriction characters are considered text.
+ */
+function filterTokens(tokens) {
+ let filtered = [];
+ let restrictions = [];
+ for (let i = 0; i < tokens.length; ++i) {
+ let token = tokens[i];
+ let tokenObj = {
+ value: token,
+ lowerCaseValue: token.toLocaleLowerCase(),
+ type: UrlbarTokenizer.TYPE.TEXT,
+ };
+ let restrictionType = CHAR_TO_TYPE_MAP.get(token);
+ if (restrictionType) {
+ restrictions.push({ index: i, type: restrictionType });
+ } else if (UrlbarTokenizer.looksLikeOrigin(token)) {
+ tokenObj.type = UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN;
+ } else if (UrlbarTokenizer.looksLikeUrl(token, { requirePath: true })) {
+ tokenObj.type = UrlbarTokenizer.TYPE.POSSIBLE_URL;
+ }
+ filtered.push(tokenObj);
+ }
+
+ // Handle restriction characters.
+ if (restrictions.length) {
+ // We can apply two kind of restrictions: type (bookmark, search, ...) and
+ // matching (url, title). These kind of restrictions can be combined, but we
+ // can only have one restriction per kind.
+ let matchingRestrictionFound = false;
+ let typeRestrictionFound = false;
+ function assignRestriction(r) {
+ if (r && !(matchingRestrictionFound && typeRestrictionFound)) {
+ if (
+ [
+ UrlbarTokenizer.TYPE.RESTRICT_TITLE,
+ UrlbarTokenizer.TYPE.RESTRICT_URL,
+ ].includes(r.type)
+ ) {
+ if (!matchingRestrictionFound) {
+ matchingRestrictionFound = true;
+ filtered[r.index].type = r.type;
+ return true;
+ }
+ } else if (!typeRestrictionFound) {
+ typeRestrictionFound = true;
+ filtered[r.index].type = r.type;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // Look at the first token.
+ let found = assignRestriction(restrictions.find(r => r.index == 0));
+ if (found) {
+ // If the first token was assigned, look at the next one.
+ assignRestriction(restrictions.find(r => r.index == 1));
+ }
+ // Then look at the last token.
+ let lastIndex = tokens.length - 1;
+ found = assignRestriction(restrictions.find(r => r.index == lastIndex));
+ if (found) {
+ // If the last token was assigned, look at the previous one.
+ assignRestriction(restrictions.find(r => r.index == lastIndex - 1));
+ }
+ }
+
+ logger.info("Filtered Tokens", tokens);
+ return filtered;
+}
diff --git a/browser/components/urlbar/UrlbarUtils.jsm b/browser/components/urlbar/UrlbarUtils.jsm
new file mode 100644
index 0000000000..b8fd02a5fe
--- /dev/null
+++ b/browser/components/urlbar/UrlbarUtils.jsm
@@ -0,0 +1,1758 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * This module exports the UrlbarUtils singleton, which contains constants and
+ * helper functions that are useful to all components of the urlbar.
+ */
+
+var EXPORTED_SYMBOLS = [
+ "UrlbarMuxer",
+ "UrlbarProvider",
+ "UrlbarQueryContext",
+ "UrlbarUtils",
+ "SkippableTimer",
+];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+XPCOMUtils.defineLazyModuleGetters(this, {
+ BrowserUtils: "resource://gre/modules/BrowserUtils.jsm",
+ BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
+ FormHistory: "resource://gre/modules/FormHistory.jsm",
+ Log: "resource://gre/modules/Log.jsm",
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
+ PlacesUIUtils: "resource:///modules/PlacesUIUtils.jsm",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
+ SearchSuggestionController:
+ "resource://gre/modules/SearchSuggestionController.jsm",
+ Services: "resource://gre/modules/Services.jsm",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
+ UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm",
+ UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.jsm",
+ UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.jsm",
+});
+
+var UrlbarUtils = {
+ // Extensions are allowed to add suggestions if they have registered a keyword
+ // with the omnibox API. This is the maximum number of suggestions an extension
+ // is allowed to add for a given search string.
+ // This value includes the heuristic result.
+ MAXIMUM_ALLOWED_EXTENSION_MATCHES: 6,
+
+ // This is used by UnifiedComplete, the new implementation will use
+ // PROVIDER_TYPE and RESULT_TYPE
+ RESULT_GROUP: {
+ HEURISTIC: "heuristic",
+ GENERAL: "general",
+ SUGGESTION: "suggestion",
+ EXTENSION: "extension",
+ },
+
+ // Defines provider types.
+ PROVIDER_TYPE: {
+ // Should be executed immediately, because it returns heuristic results
+ // that must be handed to the user asap.
+ HEURISTIC: 1,
+ // Can be delayed, contains results coming from the session or the profile.
+ PROFILE: 2,
+ // Can be delayed, contains results coming from the network.
+ NETWORK: 3,
+ // Can be delayed, contains results coming from unknown sources.
+ EXTENSION: 4,
+ },
+
+ // Defines UrlbarResult types.
+ RESULT_TYPE: {
+ // An open tab.
+ TAB_SWITCH: 1,
+ // A search suggestion or engine.
+ SEARCH: 2,
+ // A common url/title tuple, may be a bookmark with tags.
+ URL: 3,
+ // A bookmark keyword.
+ KEYWORD: 4,
+ // A WebExtension Omnibox result.
+ OMNIBOX: 5,
+ // A tab from another synced device.
+ REMOTE_TAB: 6,
+ // An actionable message to help the user with their query.
+ TIP: 7,
+ // A type of result created at runtime, for example by an extension.
+ DYNAMIC: 8,
+
+ // When you add a new type, also add its schema to
+ // UrlbarUtils.RESULT_PAYLOAD_SCHEMA below. Also consider checking if
+ // consumers of "urlbar-user-start-navigation" need updating.
+ },
+
+ // This defines the source of results returned by a provider. Each provider
+ // can return results from more than one source. This is used by the
+ // ProvidersManager to decide which providers must be queried and which
+ // results can be returned.
+ // If you add new source types, consider checking if consumers of
+ // "urlbar-user-start-navigation" need update as well.
+ RESULT_SOURCE: {
+ BOOKMARKS: 1,
+ HISTORY: 2,
+ SEARCH: 3,
+ TABS: 4,
+ OTHER_LOCAL: 5,
+ OTHER_NETWORK: 6,
+ },
+
+ /**
+ * Buckets used for logging telemetry to the FX_URLBAR_SELECTED_RESULT_TYPE_2
+ * histogram.
+ */
+ SELECTED_RESULT_TYPES: {
+ autofill: 0,
+ bookmark: 1,
+ history: 2,
+ keyword: 3,
+ searchengine: 4,
+ searchsuggestion: 5,
+ switchtab: 6,
+ tag: 7,
+ visiturl: 8,
+ remotetab: 9,
+ extension: 10,
+ "preloaded-top-site": 11, // This is currently unused.
+ tip: 12,
+ topsite: 13,
+ formhistory: 14,
+ dynamic: 15,
+ tabtosearch: 16,
+ // n_values = 32, so you'll need to create a new histogram if you need more.
+ },
+
+ // This defines icon locations that are commonly used in the UI.
+ ICON: {
+ // DEFAULT is defined lazily so it doesn't eagerly initialize PlacesUtils.
+ EXTENSION: "chrome://browser/content/extension.svg",
+ HISTORY: "chrome://browser/skin/history.svg",
+ SEARCH_GLASS: "chrome://browser/skin/search-glass.svg",
+ SEARCH_GLASS_INVERTED: "chrome://browser/skin/search-glass-inverted.svg",
+ TIP: "chrome://browser/skin/tip.svg",
+ },
+
+ // The number of results by which Page Up/Down move the selection.
+ PAGE_UP_DOWN_DELTA: 5,
+
+ // IME composition states.
+ COMPOSITION: {
+ NONE: 1,
+ COMPOSING: 2,
+ COMMIT: 3,
+ CANCELED: 4,
+ },
+
+ // Limit the length of titles and URLs we display so layout doesn't spend too
+ // much time building text runs.
+ MAX_TEXT_LENGTH: 255,
+
+ // Whether a result should be highlighted up to the point the user has typed
+ // or after that point.
+ HIGHLIGHT: {
+ NONE: 0,
+ TYPED: 1,
+ SUGGESTED: 2,
+ },
+
+ // UnifiedComplete's autocomplete results store their titles and tags together
+ // in their comments. This separator is used to separate them. When we
+ // rewrite UnifiedComplete for quantumbar, we should stop using this old hack
+ // and store titles and tags separately. It's important that this be a
+ // character that no title would ever have. We use \x1F, the non-printable
+ // unit separator.
+ TITLE_TAGS_SEPARATOR: "\x1F",
+
+ // Regex matching single word hosts with an optional port; no spaces, auth or
+ // path-like chars are admitted.
+ REGEXP_SINGLE_WORD: /^[^\s@:/?#]+(:\d+)?$/,
+
+ // Names of engines shipped in Firefox that search the web in general. These
+ // are used to update the input placeholder when entering search mode.
+ // TODO (Bug 1658661): Don't hardcode this list; store search engine category
+ // information someplace better.
+ WEB_ENGINE_NAMES: new Set([
+ "百度", // Baidu
+ "百度搜索", // "Baidu Search", the name of Baidu's OpenSearch engine.
+ "Bing",
+ "DuckDuckGo",
+ "Ecosia",
+ "Google",
+ "Qwant",
+ "Yandex",
+ "Яндекс", // Yandex, non-EN
+ ]),
+
+ // Valid entry points for search mode. If adding a value here, please update
+ // telemetry documentation and Scalars.yaml.
+ SEARCH_MODE_ENTRY: new Set([
+ "bookmarkmenu",
+ "handoff",
+ "keywordoffer",
+ "oneoff",
+ "other",
+ "shortcut",
+ "tabmenu",
+ "tabtosearch",
+ "tabtosearch_onboard",
+ "topsites_newtab",
+ "topsites_urlbar",
+ "touchbar",
+ "typed",
+ ]),
+
+ // Search mode objects corresponding to the local shortcuts in the view, in
+ // order they appear. Pref names are relative to the `browser.urlbar` branch.
+ get LOCAL_SEARCH_MODES() {
+ return [
+ {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ restrict: UrlbarTokenizer.RESTRICT.BOOKMARK,
+ icon: "chrome://browser/skin/bookmark.svg",
+ pref: "shortcuts.bookmarks",
+ },
+ {
+ source: UrlbarUtils.RESULT_SOURCE.TABS,
+ restrict: UrlbarTokenizer.RESTRICT.OPENPAGE,
+ icon: "chrome://browser/skin/tab.svg",
+ pref: "shortcuts.tabs",
+ },
+ {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ restrict: UrlbarTokenizer.RESTRICT.HISTORY,
+ icon: "chrome://browser/skin/history.svg",
+ pref: "shortcuts.history",
+ },
+ ];
+ },
+
+ /**
+ * Returns the payload schema for the given type of result.
+ *
+ * @param {number} type One of the UrlbarUtils.RESULT_TYPE values.
+ * @returns {object} The schema for the given type.
+ */
+ getPayloadSchema(type) {
+ return UrlbarUtils.RESULT_PAYLOAD_SCHEMA[type];
+ },
+
+ /**
+ * Adds a url to history as long as it isn't in a private browsing window,
+ * and it is valid.
+ *
+ * @param {string} url The url to add to history.
+ * @param {nsIDomWindow} window The window from where the url is being added.
+ */
+ addToUrlbarHistory(url, window) {
+ if (
+ !PrivateBrowsingUtils.isWindowPrivate(window) &&
+ url &&
+ !url.includes(" ") &&
+ // eslint-disable-next-line no-control-regex
+ !/[\x00-\x1F]/.test(url)
+ ) {
+ PlacesUIUtils.markPageAsTyped(url);
+ }
+ },
+
+ /**
+ * Given a string, will generate a more appropriate urlbar value if a Places
+ * keyword or a search alias is found at the beginning of it.
+ *
+ * @param {string} url
+ * A string that may begin with a keyword or an alias.
+ *
+ * @returns {Promise}
+ * @resolves { url, postData, mayInheritPrincipal }. If it's not possible
+ * to discern a keyword or an alias, url will be the input string.
+ */
+ async getShortcutOrURIAndPostData(url) {
+ let mayInheritPrincipal = false;
+ let postData = null;
+ // Split on the first whitespace.
+ let [keyword, param = ""] = url.trim().split(/\s(.+)/, 2);
+
+ if (!keyword) {
+ return { url, postData, mayInheritPrincipal };
+ }
+
+ let engine = await Services.search.getEngineByAlias(keyword);
+ if (engine) {
+ let submission = engine.getSubmission(param, null, "keyword");
+ return {
+ url: submission.uri.spec,
+ postData: submission.postData,
+ mayInheritPrincipal,
+ };
+ }
+
+ // A corrupt Places database could make this throw, breaking navigation
+ // from the location bar.
+ let entry = null;
+ try {
+ entry = await PlacesUtils.keywords.fetch(keyword);
+ } catch (ex) {
+ Cu.reportError(`Unable to fetch Places keyword "${keyword}": ${ex}`);
+ }
+ if (!entry || !entry.url) {
+ // This is not a Places keyword.
+ return { url, postData, mayInheritPrincipal };
+ }
+
+ try {
+ [url, postData] = await BrowserUtils.parseUrlAndPostData(
+ entry.url.href,
+ entry.postData,
+ param
+ );
+ if (postData) {
+ postData = this.getPostDataStream(postData);
+ }
+
+ // Since this URL came from a bookmark, it's safe to let it inherit the
+ // current document's principal.
+ mayInheritPrincipal = true;
+ } catch (ex) {
+ // It was not possible to bind the param, just use the original url value.
+ }
+
+ return { url, postData, mayInheritPrincipal };
+ },
+
+ /**
+ * Returns an input stream wrapper for the given post data.
+ *
+ * @param {string} postDataString The string to wrap.
+ * @param {string} [type] The encoding type.
+ * @returns {nsIInputStream} An input stream of the wrapped post data.
+ */
+ getPostDataStream(
+ postDataString,
+ type = "application/x-www-form-urlencoded"
+ ) {
+ let dataStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ dataStream.data = postDataString;
+
+ let mimeStream = Cc[
+ "@mozilla.org/network/mime-input-stream;1"
+ ].createInstance(Ci.nsIMIMEInputStream);
+ mimeStream.addHeader("Content-Type", type);
+ mimeStream.setData(dataStream);
+ return mimeStream.QueryInterface(Ci.nsIInputStream);
+ },
+
+ _compareIgnoringDiacritics: null,
+
+ /**
+ * Returns a list of all the token substring matches in a string. Matching is
+ * case insensitive. Each match in the returned list is a tuple: [matchIndex,
+ * matchLength]. matchIndex is the index in the string of the match, and
+ * matchLength is the length of the match.
+ *
+ * @param {array} tokens The tokens to search for.
+ * @param {string} str The string to match against.
+ * @param {boolean} highlightType
+ * One of the HIGHLIGHT values:
+ * TYPED: match ranges matching the tokens; or
+ * SUGGESTED: match ranges for words not matching the tokens and the
+ * endings of words that start with a token.
+ * @returns {array} An array: [
+ * [matchIndex_0, matchLength_0],
+ * [matchIndex_1, matchLength_1],
+ * ...
+ * [matchIndex_n, matchLength_n]
+ * ].
+ * The array is sorted by match indexes ascending.
+ */
+ getTokenMatches(tokens, str, highlightType) {
+ // Only search a portion of the string, because not more than a certain
+ // amount of characters are visible in the UI, matching over what is visible
+ // would be expensive and pointless.
+ str = str.substring(0, UrlbarUtils.MAX_TEXT_LENGTH).toLocaleLowerCase();
+ // To generate non-overlapping ranges, we start from a 0-filled array with
+ // the same length of the string, and use it as a collision marker, setting
+ // 1 where the text should be highlighted.
+ let hits = new Array(str.length).fill(
+ highlightType == this.HIGHLIGHT.SUGGESTED ? 1 : 0
+ );
+ let compareIgnoringDiacritics;
+ for (let { lowerCaseValue: needle } of tokens) {
+ // Ideally we should never hit the empty token case, but just in case
+ // the `needle` check protects us from an infinite loop.
+ if (!needle) {
+ continue;
+ }
+ let index = 0;
+ let found = false;
+ // First try a diacritic-sensitive search.
+ for (;;) {
+ index = str.indexOf(needle, index);
+ if (index < 0) {
+ break;
+ }
+
+ if (highlightType == UrlbarUtils.HIGHLIGHT.SUGGESTED) {
+ // We de-emphasize the match only if it's preceded by a space, thus
+ // it's a perfect match or the beginning of a longer word.
+ let previousSpaceIndex = str.lastIndexOf(" ", index) + 1;
+ if (index != previousSpaceIndex) {
+ index += needle.length;
+ // We found the token but we won't de-emphasize it, because it's not
+ // after a word boundary.
+ found = true;
+ continue;
+ }
+ }
+
+ hits.fill(
+ highlightType == this.HIGHLIGHT.SUGGESTED ? 0 : 1,
+ index,
+ index + needle.length
+ );
+ index += needle.length;
+ found = true;
+ }
+ // If that fails to match anything, try a (computationally intensive)
+ // diacritic-insensitive search.
+ if (!found) {
+ if (!compareIgnoringDiacritics) {
+ if (!this._compareIgnoringDiacritics) {
+ // Diacritic insensitivity in the search engine follows a set of
+ // general rules that are not locale-dependent, so use a generic
+ // English collator for highlighting matching words instead of a
+ // collator for the user's particular locale.
+ this._compareIgnoringDiacritics = new Intl.Collator("en", {
+ sensitivity: "base",
+ }).compare;
+ }
+ compareIgnoringDiacritics = this._compareIgnoringDiacritics;
+ }
+ index = 0;
+ while (index < str.length) {
+ let hay = str.substr(index, needle.length);
+ if (compareIgnoringDiacritics(needle, hay) === 0) {
+ if (highlightType == UrlbarUtils.HIGHLIGHT.SUGGESTED) {
+ let previousSpaceIndex = str.lastIndexOf(" ", index) + 1;
+ if (index != previousSpaceIndex) {
+ index += needle.length;
+ continue;
+ }
+ }
+ hits.fill(
+ highlightType == this.HIGHLIGHT.SUGGESTED ? 0 : 1,
+ index,
+ index + needle.length
+ );
+ index += needle.length;
+ } else {
+ index++;
+ }
+ }
+ }
+ }
+ // Starting from the collision array, generate [start, len] tuples
+ // representing the ranges to be highlighted.
+ let ranges = [];
+ for (let index = hits.indexOf(1); index >= 0 && index < hits.length; ) {
+ let len = 0;
+ // eslint-disable-next-line no-empty
+ for (let j = index; j < hits.length && hits[j]; ++j, ++len) {}
+ ranges.push([index, len]);
+ // Move to the next 1.
+ index = hits.indexOf(1, index + len);
+ }
+ return ranges;
+ },
+
+ /**
+ * Extracts an url from a result, if possible.
+ * @param {UrlbarResult} result The result to extract from.
+ * @returns {object} a {url, postData} object, or null if a url can't be built
+ * from this result.
+ */
+ getUrlFromResult(result) {
+ switch (result.type) {
+ case UrlbarUtils.RESULT_TYPE.URL:
+ case UrlbarUtils.RESULT_TYPE.REMOTE_TAB:
+ case UrlbarUtils.RESULT_TYPE.TAB_SWITCH:
+ return { url: result.payload.url, postData: null };
+ case UrlbarUtils.RESULT_TYPE.KEYWORD:
+ return {
+ url: result.payload.url,
+ postData: result.payload.postData
+ ? this.getPostDataStream(result.payload.postData)
+ : null,
+ };
+ case UrlbarUtils.RESULT_TYPE.SEARCH: {
+ if (result.payload.engine) {
+ const engine = Services.search.getEngineByName(result.payload.engine);
+ let [url, postData] = this.getSearchQueryUrl(
+ engine,
+ result.payload.suggestion || result.payload.query
+ );
+ return { url, postData };
+ }
+ break;
+ }
+ case UrlbarUtils.RESULT_TYPE.TIP: {
+ // Return the button URL. Consumers must check payload.helpUrl
+ // themselves if they need the tip's help link.
+ return { url: result.payload.buttonUrl, postData: null };
+ }
+ }
+ return { url: null, postData: null };
+ },
+
+ /**
+ * Get the url to load for the search query.
+ *
+ * @param {nsISearchEngine} engine
+ * The engine to generate the query for.
+ * @param {string} query
+ * The query string to search for.
+ * @returns {array}
+ * Returns an array containing the query url (string) and the
+ * post data (object).
+ */
+ getSearchQueryUrl(engine, query) {
+ let submission = engine.getSubmission(query, null, "keyword");
+ return [submission.uri.spec, submission.postData];
+ },
+
+ // Ranks a URL prefix from 3 - 0 with the following preferences:
+ // https:// > https://www. > http:// > http://www.
+ // Higher is better for the purposes of deduping URLs.
+ // Returns -1 if the prefix does not match any of the above.
+ getPrefixRank(prefix) {
+ return ["http://www.", "http://", "https://www.", "https://"].indexOf(
+ prefix
+ );
+ },
+
+ /**
+ * Get the number of rows a result should span in the autocomplete dropdown.
+ *
+ * @param {UrlbarResult} result The result being created.
+ * @returns {number}
+ * The number of rows the result should span in the autocomplete
+ * dropdown.
+ */
+ getSpanForResult(result) {
+ if (result.resultSpan) {
+ return result.resultSpan;
+ }
+ switch (result.type) {
+ case UrlbarUtils.RESULT_TYPE.URL:
+ case UrlbarUtils.RESULT_TYPE.BOOKMARKS:
+ case UrlbarUtils.RESULT_TYPE.REMOTE_TAB:
+ case UrlbarUtils.RESULT_TYPE.TAB_SWITCH:
+ case UrlbarUtils.RESULT_TYPE.KEYWORD:
+ case UrlbarUtils.RESULT_TYPE.SEARCH:
+ case UrlbarUtils.RESULT_TYPE.OMNIBOX:
+ return 1;
+ case UrlbarUtils.RESULT_TYPE.TIP:
+ return 3;
+ }
+ return 1;
+ },
+
+ /**
+ * Returns a search mode object if a token should enter search mode when
+ * typed. This does not handle engine aliases.
+ *
+ * @param {UrlbarUtils.RESTRICT} token
+ * A restriction token to convert to search mode.
+ * @returns {object}
+ * A search mode object. Null if search mode should not be entered. See
+ * setSearchMode documentation for details.
+ */
+ searchModeForToken(token) {
+ if (token == UrlbarTokenizer.RESTRICT.SEARCH) {
+ return {
+ engineName: UrlbarSearchUtils.getDefaultEngine(this.isPrivate).name,
+ };
+ }
+
+ let mode = UrlbarUtils.LOCAL_SEARCH_MODES.find(m => m.restrict == token);
+ if (!mode) {
+ return null;
+ }
+
+ // Return a copy so callers don't modify the object in LOCAL_SEARCH_MODES.
+ return { ...mode };
+ },
+
+ /**
+ * Tries to initiate a speculative connection to a given url.
+ * @param {nsISearchEngine|nsIURI|URL|string} urlOrEngine entity to initiate
+ * a speculative connection for.
+ * @param {window} window the window from where the connection is initialized.
+ * @note This is not infallible, if a speculative connection cannot be
+ * initialized, it will be a no-op.
+ */
+ setupSpeculativeConnection(urlOrEngine, window) {
+ if (!UrlbarPrefs.get("speculativeConnect.enabled")) {
+ return;
+ }
+ if (urlOrEngine instanceof Ci.nsISearchEngine) {
+ try {
+ urlOrEngine.speculativeConnect({
+ window,
+ originAttributes: window.gBrowser.contentPrincipal.originAttributes,
+ });
+ } catch (ex) {
+ // Can't setup speculative connection for this url, just ignore it.
+ }
+ return;
+ }
+
+ if (urlOrEngine instanceof URL) {
+ urlOrEngine = urlOrEngine.href;
+ }
+
+ try {
+ let uri =
+ urlOrEngine instanceof Ci.nsIURI
+ ? urlOrEngine
+ : Services.io.newURI(urlOrEngine);
+ Services.io.speculativeConnect(
+ uri,
+ window.gBrowser.contentPrincipal,
+ null
+ );
+ } catch (ex) {
+ // Can't setup speculative connection for this url, just ignore it.
+ }
+ },
+
+ /**
+ * Strips parts of a URL defined in `options`.
+ *
+ * @param {string} spec
+ * The text to modify.
+ * @param {object} options
+ * @param {boolean} options.stripHttp
+ * Whether to strip http.
+ * @param {boolean} options.stripHttps
+ * Whether to strip https.
+ * @param {boolean} options.stripWww
+ * Whether to strip `www.`.
+ * @param {boolean} options.trimSlash
+ * Whether to trim the trailing slash.
+ * @param {boolean} options.trimEmptyQuery
+ * Whether to trim a trailing `?`.
+ * @param {boolean} options.trimEmptyHash
+ * Whether to trim a trailing `#`.
+ * @returns {array} [modified, prefix, suffix]
+ * modified: {string} The modified spec.
+ * prefix: {string} The parts stripped from the prefix, if any.
+ * suffix: {string} The parts trimmed from the suffix, if any.
+ */
+ stripPrefixAndTrim(spec, options = {}) {
+ let prefix = "";
+ let suffix = "";
+ if (options.stripHttp && spec.startsWith("http://")) {
+ spec = spec.slice(7);
+ prefix = "http://";
+ } else if (options.stripHttps && spec.startsWith("https://")) {
+ spec = spec.slice(8);
+ prefix = "https://";
+ }
+ if (options.stripWww && spec.startsWith("www.")) {
+ spec = spec.slice(4);
+ prefix += "www.";
+ }
+ if (options.trimEmptyHash && spec.endsWith("#")) {
+ spec = spec.slice(0, -1);
+ suffix = "#" + suffix;
+ }
+ if (options.trimEmptyQuery && spec.endsWith("?")) {
+ spec = spec.slice(0, -1);
+ suffix = "?" + suffix;
+ }
+ if (options.trimSlash && spec.endsWith("/")) {
+ spec = spec.slice(0, -1);
+ suffix = "/" + suffix;
+ }
+ return [spec, prefix, suffix];
+ },
+
+ /**
+ * Strips a PSL verified public suffix from an hostname.
+ * @param {string} host A host name.
+ * @returns {string} Host name without the public suffix.
+ * @note Because stripping the full suffix requires to verify it against the
+ * Public Suffix List, this call is not the cheapest, and thus it should
+ * not be used in hot paths.
+ */
+ stripPublicSuffixFromHost(host) {
+ try {
+ return host.substring(
+ 0,
+ host.length - Services.eTLD.getKnownPublicSuffixFromHost(host).length
+ );
+ } catch (ex) {
+ if (ex.result != Cr.NS_ERROR_HOST_IS_IP_ADDRESS) {
+ throw ex;
+ }
+ }
+ return host;
+ },
+
+ /**
+ * Used to filter out the javascript protocol from URIs, since we don't
+ * support LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL for those.
+ * @param {string} pasteData The data to check for javacript protocol.
+ * @returns {string} The modified paste data.
+ */
+ stripUnsafeProtocolOnPaste(pasteData) {
+ while (true) {
+ let scheme = "";
+ try {
+ scheme = Services.io.extractScheme(pasteData);
+ } catch (ex) {
+ // If it throws, this is not a javascript scheme.
+ }
+ if (scheme != "javascript") {
+ break;
+ }
+
+ pasteData = pasteData.substring(pasteData.indexOf(":") + 1);
+ }
+ return pasteData;
+ },
+
+ async addToInputHistory(url, input) {
+ await PlacesUtils.withConnectionWrapper("addToInputHistory", db => {
+ // use_count will asymptotically approach the max of 10.
+ return db.executeCached(
+ `
+ INSERT OR REPLACE INTO moz_inputhistory
+ SELECT h.id, IFNULL(i.input, :input), IFNULL(i.use_count, 0) * .9 + 1
+ FROM moz_places h
+ LEFT JOIN moz_inputhistory i ON i.place_id = h.id AND i.input = :input
+ WHERE url_hash = hash(:url) AND url = :url
+ `,
+ { url, input }
+ );
+ });
+ },
+
+ /**
+ * Whether the passed-in input event is paste event.
+ * @param {DOMEvent} event an input DOM event.
+ * @returns {boolean} Whether the event is a paste event.
+ */
+ isPasteEvent(event) {
+ return (
+ event.inputType &&
+ (event.inputType.startsWith("insertFromPaste") ||
+ event.inputType == "insertFromYank")
+ );
+ },
+
+ /**
+ * Given a string, checks if it looks like a single word host, not containing
+ * spaces nor dots (apart from a possible trailing one).
+ * @note This matching should stay in sync with the related code in
+ * URIFixup::KeywordURIFixup
+ * @param {string} value
+ * @returns {boolean} Whether the value looks like a single word host.
+ */
+ looksLikeSingleWordHost(value) {
+ let str = value.trim();
+ return this.REGEXP_SINGLE_WORD.test(str);
+ },
+
+ /**
+ * Returns the portion of a string starting at the index where another string
+ * begins.
+ *
+ * @param {string} sourceStr
+ * The string to search within.
+ * @param {string} targetStr
+ * The string to search for.
+ * @returns {string} The substring within sourceStr starting at targetStr, or
+ * the empty string if targetStr does not occur in sourceStr.
+ */
+ substringAt(sourceStr, targetStr) {
+ let index = sourceStr.indexOf(targetStr);
+ return index < 0 ? "" : sourceStr.substr(index);
+ },
+
+ /**
+ * Returns the portion of a string starting at the index where another string
+ * ends.
+ *
+ * @param {string} sourceStr
+ * The string to search within.
+ * @param {string} targetStr
+ * The string to search for.
+ * @returns {string} The substring within sourceStr where targetStr ends, or
+ * the empty string if targetStr does not occur in sourceStr.
+ */
+ substringAfter(sourceStr, targetStr) {
+ let index = sourceStr.indexOf(targetStr);
+ return index < 0 ? "" : sourceStr.substr(index + targetStr.length);
+ },
+
+ /**
+ * Strips the prefix from a URL and returns the prefix and the remainder of the
+ * URL. "Prefix" is defined to be the scheme and colon, plus, if present, two
+ * slashes. If the given string is not actually a URL, then an empty prefix and
+ * the string itself is returned.
+ *
+ * @param {string} str The possible URL to strip.
+ * @returns {array} If `str` is a URL, then [prefix, remainder]. Otherwise, ["", str].
+ */
+ stripURLPrefix(str) {
+ const REGEXP_STRIP_PREFIX = /^[a-z]+:(?:\/){0,2}/i;
+ let match = REGEXP_STRIP_PREFIX.exec(str);
+ if (!match) {
+ return ["", str];
+ }
+ let prefix = match[0];
+ if (prefix.length < str.length && str[prefix.length] == " ") {
+ return ["", str];
+ }
+ return [prefix, str.substr(prefix.length)];
+ },
+
+ /**
+ * Runs a search for the given string, and returns the heuristic result.
+ * @param {string} searchString The string to search for.
+ * @param {nsIDOMWindow} window The window requesting it.
+ * @returns {UrlbarResult} an heuristic result.
+ */
+ async getHeuristicResultFor(
+ searchString,
+ window = BrowserWindowTracker.getTopWindow()
+ ) {
+ if (!searchString) {
+ throw new Error("Must pass a non-null search string");
+ }
+
+ let options = {
+ allowAutofill: false,
+ isPrivate: PrivateBrowsingUtils.isWindowPrivate(window),
+ maxResults: 1,
+ searchString,
+ userContextId: window.gBrowser.selectedBrowser.getAttribute(
+ "usercontextid"
+ ),
+ allowSearchSuggestions: false,
+ providers: ["UnifiedComplete", "HeuristicFallback"],
+ };
+ if (window.gURLBar.searchMode) {
+ let searchMode = window.gURLBar.searchMode;
+ options.searchMode = searchMode;
+ if (searchMode.source) {
+ options.sources = [searchMode.source];
+ }
+ }
+ let context = new UrlbarQueryContext(options);
+ await UrlbarProvidersManager.startQuery(context);
+ if (!context.heuristicResult) {
+ throw new Error("There should always be an heuristic result");
+ }
+ return context.heuristicResult;
+ },
+
+ /**
+ * Creates a logger.
+ * Logging level can be controlled through browser.urlbar.loglevel.
+ * @param {string} [prefix] Prefix to use for the logged messages, "::" will
+ * be appended automatically to the prefix.
+ * @returns {object} The logger.
+ */
+ getLogger({ prefix = "" } = {}) {
+ if (!this._logger) {
+ this._logger = Log.repository.getLogger("urlbar");
+ this._logger.manageLevelFromPref("browser.urlbar.loglevel");
+ this._logger.addAppender(
+ new Log.ConsoleAppender(new Log.BasicFormatter())
+ );
+ }
+ if (prefix) {
+ // This is not an early return because it is necessary to invoke getLogger
+ // at least once before getLoggerWithMessagePrefix; it replaces a
+ // method of the original logger, rather than using an actual Proxy.
+ return Log.repository.getLoggerWithMessagePrefix("urlbar", prefix + "::");
+ }
+ return this._logger;
+ },
+
+ /**
+ * Returns the name of a result source. The name is the lowercase name of the
+ * corresponding property in the RESULT_SOURCE object.
+ *
+ * @param {string} source A UrlbarUtils.RESULT_SOURCE value.
+ * @returns {string} The token's name, a lowercased name in the RESULT_SOURCE
+ * object.
+ */
+ getResultSourceName(source) {
+ if (!this._resultSourceNamesBySource) {
+ this._resultSourceNamesBySource = new Map();
+ for (let [name, src] of Object.entries(this.RESULT_SOURCE)) {
+ this._resultSourceNamesBySource.set(src, name.toLowerCase());
+ }
+ }
+ return this._resultSourceNamesBySource.get(source);
+ },
+
+ /**
+ * Add the search to form history. This also updates any existing form
+ * history for the search.
+ * @param {UrlbarInput} input The UrlbarInput object requesting the addition.
+ * @param {string} value The value to add.
+ * @param {string} [source] The source of the addition, usually
+ * the name of the engine the search was made with.
+ * @returns {Promise} resolved once the operation is complete
+ */
+ addToFormHistory(input, value, source) {
+ // If the user types a search engine alias without a search string,
+ // we have an empty search string and we can't bump it.
+ // We also don't want to add history in private browsing mode.
+ // Finally we don't want to store extremely long strings that would not be
+ // particularly useful to the user.
+ if (
+ !value ||
+ input.isPrivate ||
+ value.length > SearchSuggestionController.SEARCH_HISTORY_MAX_VALUE_LENGTH
+ ) {
+ return Promise.resolve();
+ }
+ return new Promise((resolve, reject) => {
+ FormHistory.update(
+ {
+ op: "bump",
+ fieldname: input.formHistoryName,
+ value,
+ source,
+ },
+ {
+ handleError: reject,
+ handleCompletion: resolve,
+ }
+ );
+ });
+ },
+
+ /**
+ * Extracts a telemetry type from a result, used by scalars and event
+ * telemetry.
+ *
+ * @param {UrlbarResult} result The result to analyze.
+ * @returns {string} A string type for telemetry.
+ * @note New types should be added to Scalars.yaml under the urlbar.picked
+ * category and documented in the in-tree documentation. A data-review
+ * is always necessary.
+ */
+ telemetryTypeFromResult(result) {
+ if (!result) {
+ return "unknown";
+ }
+ switch (result.type) {
+ case UrlbarUtils.RESULT_TYPE.TAB_SWITCH:
+ return "switchtab";
+ case UrlbarUtils.RESULT_TYPE.SEARCH:
+ if (result.source == UrlbarUtils.RESULT_SOURCE.HISTORY) {
+ return "formhistory";
+ }
+ if (result.providerName == "TabToSearch") {
+ return "tabtosearch";
+ }
+ return result.payload.suggestion ? "searchsuggestion" : "searchengine";
+ case UrlbarUtils.RESULT_TYPE.URL:
+ if (result.autofill) {
+ return "autofill";
+ }
+ if (
+ result.source == UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL &&
+ result.heuristic
+ ) {
+ return "visiturl";
+ }
+ return result.source == UrlbarUtils.RESULT_SOURCE.BOOKMARKS
+ ? "bookmark"
+ : "history";
+ case UrlbarUtils.RESULT_TYPE.KEYWORD:
+ return "keyword";
+ case UrlbarUtils.RESULT_TYPE.OMNIBOX:
+ return "extension";
+ case UrlbarUtils.RESULT_TYPE.REMOTE_TAB:
+ return "remotetab";
+ case UrlbarUtils.RESULT_TYPE.TIP:
+ return "tip";
+ case UrlbarUtils.RESULT_TYPE.DYNAMIC:
+ if (result.providerName == "TabToSearch") {
+ // This is the onboarding result.
+ return "tabtosearch";
+ }
+ return "dynamic";
+ }
+ return "unknown";
+ },
+};
+
+XPCOMUtils.defineLazyGetter(UrlbarUtils.ICON, "DEFAULT", () => {
+ return PlacesUtils.favicons.defaultFavicon.spec;
+});
+
+XPCOMUtils.defineLazyGetter(UrlbarUtils, "strings", () => {
+ return Services.strings.createBundle(
+ "chrome://global/locale/autocomplete.properties"
+ );
+});
+
+/**
+ * Payload JSON schemas for each result type. Payloads are validated against
+ * these schemas using JsonSchemaValidator.jsm.
+ */
+UrlbarUtils.RESULT_PAYLOAD_SCHEMA = {
+ [UrlbarUtils.RESULT_TYPE.TAB_SWITCH]: {
+ type: "object",
+ required: ["url"],
+ properties: {
+ displayUrl: {
+ type: "string",
+ },
+ icon: {
+ type: "string",
+ },
+ title: {
+ type: "string",
+ },
+ url: {
+ type: "string",
+ },
+ userContextId: {
+ type: "number",
+ },
+ },
+ },
+ [UrlbarUtils.RESULT_TYPE.SEARCH]: {
+ type: "object",
+ properties: {
+ displayUrl: {
+ type: "string",
+ },
+ engine: {
+ type: "string",
+ },
+ icon: {
+ type: "string",
+ },
+ inPrivateWindow: {
+ type: "boolean",
+ },
+ isPinned: {
+ type: "boolean",
+ },
+ isPrivateEngine: {
+ type: "boolean",
+ },
+ keyword: {
+ type: "string",
+ },
+ lowerCaseSuggestion: {
+ type: "string",
+ },
+ providesSearchMode: {
+ type: "boolean",
+ },
+ query: {
+ type: "string",
+ },
+ satisfiesAutofillThreshold: {
+ type: "boolean",
+ },
+ suggestion: {
+ type: "string",
+ },
+ tail: {
+ type: "string",
+ },
+ tailPrefix: {
+ type: "string",
+ },
+ tailOffsetIndex: {
+ type: "number",
+ },
+ title: {
+ type: "string",
+ },
+ url: {
+ type: "string",
+ },
+ },
+ },
+ [UrlbarUtils.RESULT_TYPE.URL]: {
+ type: "object",
+ required: ["url"],
+ properties: {
+ displayUrl: {
+ type: "string",
+ },
+ icon: {
+ type: "string",
+ },
+ isPinned: {
+ type: "boolean",
+ },
+ isSponsored: {
+ type: "boolean",
+ },
+ sendAttributionRequest: {
+ type: "boolean",
+ },
+ tags: {
+ type: "array",
+ items: {
+ type: "string",
+ },
+ },
+ title: {
+ type: "string",
+ },
+ url: {
+ type: "string",
+ },
+ },
+ },
+ [UrlbarUtils.RESULT_TYPE.KEYWORD]: {
+ type: "object",
+ required: ["keyword", "url"],
+ properties: {
+ displayUrl: {
+ type: "string",
+ },
+ icon: {
+ type: "string",
+ },
+ input: {
+ type: "string",
+ },
+ keyword: {
+ type: "string",
+ },
+ postData: {
+ type: "string",
+ },
+ title: {
+ type: "string",
+ },
+ url: {
+ type: "string",
+ },
+ },
+ },
+ [UrlbarUtils.RESULT_TYPE.OMNIBOX]: {
+ type: "object",
+ required: ["keyword"],
+ properties: {
+ content: {
+ type: "string",
+ },
+ icon: {
+ type: "string",
+ },
+ keyword: {
+ type: "string",
+ },
+ title: {
+ type: "string",
+ },
+ },
+ },
+ [UrlbarUtils.RESULT_TYPE.REMOTE_TAB]: {
+ type: "object",
+ required: ["device", "url"],
+ properties: {
+ device: {
+ type: "string",
+ },
+ displayUrl: {
+ type: "string",
+ },
+ icon: {
+ type: "string",
+ },
+ title: {
+ type: "string",
+ },
+ url: {
+ type: "string",
+ },
+ },
+ },
+ [UrlbarUtils.RESULT_TYPE.TIP]: {
+ type: "object",
+ required: ["type"],
+ properties: {
+ // Prefer `buttonTextData` if your string is translated. This is for
+ // untranslated strings.
+ buttonText: {
+ type: "string",
+ },
+ // l10n { id, args }
+ buttonTextData: {
+ type: "object",
+ required: ["id"],
+ properties: {
+ id: {
+ type: "string",
+ },
+ args: {
+ type: "array",
+ },
+ },
+ },
+ buttonUrl: {
+ type: "string",
+ },
+ helpUrl: {
+ type: "string",
+ },
+ icon: {
+ type: "string",
+ },
+ // Prefer `text` if your string is translated. This is for untranslated
+ // strings.
+ text: {
+ type: "string",
+ },
+ // l10n { id, args }
+ textData: {
+ type: "object",
+ required: ["id"],
+ properties: {
+ id: {
+ type: "string",
+ },
+ args: {
+ type: "array",
+ },
+ },
+ },
+ // `type` is used in the names of keys in the `urlbar.tips` keyed scalar
+ // telemetry (see telemetry.rst). If you add a new type, then you are
+ // also adding new `urlbar.tips` keys and therefore need an expanded data
+ // collection review.
+ type: {
+ type: "string",
+ enum: [
+ "extension",
+ "intervention_clear",
+ "intervention_refresh",
+ "intervention_update_ask",
+ "intervention_update_refresh",
+ "intervention_update_restart",
+ "intervention_update_web",
+ "searchTip_onboard",
+ "searchTip_redirect",
+ "test", // for tests only
+ ],
+ },
+ },
+ },
+ [UrlbarUtils.RESULT_TYPE.DYNAMIC]: {
+ type: "object",
+ required: ["dynamicType"],
+ properties: {
+ dynamicType: {
+ type: "string",
+ },
+ // If `shouldNavigate` is `true` and the payload contains a `url`
+ // property, when the result is selected the browser will navigate to the
+ // `url`.
+ shouldNavigate: {
+ type: "boolean",
+ },
+ },
+ },
+};
+
+/**
+ * UrlbarQueryContext defines a user's autocomplete input from within the urlbar.
+ * It supplements it with details of how the search results should be obtained
+ * and what they consist of.
+ */
+class UrlbarQueryContext {
+ /**
+ * Constructs the UrlbarQueryContext instance.
+ *
+ * @param {object} options
+ * The initial options for UrlbarQueryContext.
+ * @param {string} options.searchString
+ * The string the user entered in autocomplete. Could be the empty string
+ * in the case of the user opening the popup via the mouse.
+ * @param {boolean} options.isPrivate
+ * Set to true if this query was started from a private browsing window.
+ * @param {number} options.maxResults
+ * The maximum number of results that will be displayed for this query.
+ * @param {boolean} options.allowAutofill
+ * Whether or not to allow providers to include autofill results.
+ * @param {number} options.userContextId
+ * The container id where this context was generated, if any.
+ * @param {array} [options.sources]
+ * A list of acceptable UrlbarUtils.RESULT_SOURCE for the context.
+ * @param {object} [options.searchMode]
+ * The input's current search mode. See UrlbarInput.setSearchMode for a
+ * description.
+ * @param {boolean} [options.allowSearchSuggestions]
+ * Whether to allow search suggestions. This is a veto, meaning that when
+ * false, suggestions will not be fetched, but when true, some other
+ * condition may still prohibit suggestions, like private browsing mode.
+ * Defaults to true.
+ * @param {string} [options.formHistoryName]
+ * The name under which the local form history is registered.
+ */
+ constructor(options = {}) {
+ this._checkRequiredOptions(options, [
+ "allowAutofill",
+ "isPrivate",
+ "maxResults",
+ "searchString",
+ ]);
+
+ if (isNaN(parseInt(options.maxResults))) {
+ throw new Error(
+ `Invalid maxResults property provided to UrlbarQueryContext`
+ );
+ }
+
+ // Manage optional properties of options.
+ for (let [prop, checkFn, defaultValue] of [
+ ["allowSearchSuggestions", v => true, true],
+ ["currentPage", v => typeof v == "string" && !!v.length],
+ ["formHistoryName", v => typeof v == "string" && !!v.length],
+ ["providers", v => Array.isArray(v) && v.length],
+ ["searchMode", v => v && typeof v == "object"],
+ ["sources", v => Array.isArray(v) && v.length],
+ ]) {
+ if (prop in options) {
+ if (!checkFn(options[prop])) {
+ throw new Error(`Invalid value for option "${prop}"`);
+ }
+ this[prop] = options[prop];
+ } else if (defaultValue !== undefined) {
+ this[prop] = defaultValue;
+ }
+ }
+
+ this.lastResultCount = 0;
+ // Note that Set is not serializable through JSON, so these may not be
+ // easily shared with add-ons.
+ this.pendingHeuristicProviders = new Set();
+ this.deferUserSelectionProviders = new Set();
+ this.trimmedSearchString = this.searchString.trim();
+ this.userContextId =
+ options.userContextId ||
+ Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID;
+ }
+
+ /**
+ * Checks the required options, saving them as it goes.
+ *
+ * @param {object} options The options object to check.
+ * @param {array} optionNames The names of the options to check for.
+ * @throws {Error} Throws if there is a missing option.
+ */
+ _checkRequiredOptions(options, optionNames) {
+ for (let optionName of optionNames) {
+ if (!(optionName in options)) {
+ throw new Error(
+ `Missing or empty ${optionName} provided to UrlbarQueryContext`
+ );
+ }
+ this[optionName] = options[optionName];
+ }
+ }
+
+ /**
+ * Caches and returns fixup info from URIFixup for the current search string.
+ * Only returns a subset of the properties from URIFixup. This is both to
+ * reduce the memory footprint of UrlbarQueryContexts and to keep them
+ * serializable so they can be sent to extensions.
+ */
+ get fixupInfo() {
+ if (this.trimmedSearchString && !this._fixupInfo) {
+ let flags =
+ Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS |
+ Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
+ if (this.isPrivate) {
+ flags |= Ci.nsIURIFixup.FIXUP_FLAG_PRIVATE_CONTEXT;
+ }
+
+ try {
+ let info = Services.uriFixup.getFixupURIInfo(
+ this.trimmedSearchString,
+ flags
+ );
+ this._fixupInfo = {
+ href: info.fixedURI.spec,
+ isSearch: !!info.keywordAsSent,
+ };
+ } catch (ex) {
+ this._fixupError = ex.result;
+ }
+ }
+
+ return this._fixupInfo || null;
+ }
+
+ /**
+ * Returns the error that was thrown when fixupInfo was fetched, if any. If
+ * fixupInfo has not yet been fetched for this queryContext, it is fetched
+ * here.
+ */
+ get fixupError() {
+ if (!this.fixupInfo) {
+ return this._fixupError;
+ }
+
+ return null;
+ }
+}
+
+/**
+ * Base class for a muxer.
+ * The muxer scope is to sort a given list of results.
+ */
+class UrlbarMuxer {
+ /**
+ * Unique name for the muxer, used by the context to sort results.
+ * Not using a unique name will cause the newest registration to win.
+ * @abstract
+ */
+ get name() {
+ return "UrlbarMuxerBase";
+ }
+
+ /**
+ * Sorts queryContext results in-place.
+ * @param {UrlbarQueryContext} queryContext the context to sort results for.
+ * @abstract
+ */
+ sort(queryContext) {
+ throw new Error("Trying to access the base class, must be overridden");
+ }
+}
+
+/**
+ * Base class for a provider.
+ * The provider scope is to query a datasource and return results from it.
+ */
+class UrlbarProvider {
+ constructor() {
+ XPCOMUtils.defineLazyGetter(this, "logger", () =>
+ UrlbarUtils.getLogger({ prefix: `Provider.${this.name}` })
+ );
+ }
+
+ /**
+ * Unique name for the provider, used by the context to filter on providers.
+ * Not using a unique name will cause the newest registration to win.
+ * @abstract
+ */
+ get name() {
+ return "UrlbarProviderBase";
+ }
+
+ /**
+ * The type of the provider, must be one of UrlbarUtils.PROVIDER_TYPE.
+ * @abstract
+ */
+ get type() {
+ throw new Error("Trying to access the base class, must be overridden");
+ }
+
+ /**
+ * Calls a method on the provider in a try-catch block and reports any error.
+ * Unlike most other provider methods, `tryMethod` is not intended to be
+ * overridden.
+ *
+ * @param {string} methodName The name of the method to call.
+ * @param {*} args The method arguments.
+ * @returns {*} The return value of the method, or undefined if the method
+ * throws an error.
+ * @abstract
+ */
+ tryMethod(methodName, ...args) {
+ try {
+ return this[methodName](...args);
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ return undefined;
+ }
+
+ /**
+ * Whether this provider should be invoked for the given context.
+ * If this method returns false, the providers manager won't start a query
+ * with this provider, to save on resources.
+ * @param {UrlbarQueryContext} queryContext The query context object
+ * @returns {boolean} Whether this provider should be invoked for the search.
+ * @abstract
+ */
+ isActive(queryContext) {
+ throw new Error("Trying to access the base class, must be overridden");
+ }
+
+ /**
+ * Gets the provider's priority. Priorities are numeric values starting at
+ * zero and increasing in value. Smaller values are lower priorities, and
+ * larger values are higher priorities. For a given query, `startQuery` is
+ * called on only the active and highest-priority providers.
+ * @param {UrlbarQueryContext} queryContext The query context object
+ * @returns {number} The provider's priority for the given query.
+ * @abstract
+ */
+ getPriority(queryContext) {
+ // By default, all providers share the lowest priority.
+ return 0;
+ }
+
+ /**
+ * Starts querying.
+ * @param {UrlbarQueryContext} queryContext The query context object
+ * @param {function} addCallback Callback invoked by the provider to add a new
+ * result. A UrlbarResult should be passed to it.
+ * @note Extended classes should return a Promise resolved when the provider
+ * is done searching AND returning results.
+ * @abstract
+ */
+ startQuery(queryContext, addCallback) {
+ throw new Error("Trying to access the base class, must be overridden");
+ }
+
+ /**
+ * Cancels a running query,
+ * @param {UrlbarQueryContext} queryContext the query context object to cancel
+ * query for.
+ * @abstract
+ */
+ cancelQuery(queryContext) {
+ // Override this with your clean-up on cancel code.
+ }
+
+ /**
+ * Called when a result from the provider is picked, but currently only for
+ * tip and dynamic results. The provider should handle the pick. For tip
+ * results, this is called only when the tip's payload doesn't have a URL.
+ * For dynamic results, this is called when any selectable element in the
+ * result's view is picked.
+ *
+ * @param {UrlbarResult} result
+ * The result that was picked.
+ * @param {Element} element
+ * The element in the result's view that was picked.
+ * @abstract
+ */
+ pickResult(result, element) {}
+
+ /**
+ * Called when the user starts and ends an engagement with the urlbar.
+ *
+ * @param {boolean} isPrivate True if the engagement is in a private context.
+ * @param {string} state The state of the engagement, one of: start,
+ * engagement, abandonment, discard.
+ */
+ onEngagement(isPrivate, state) {}
+
+ /**
+ * Called when a result from the provider is selected. "Selected" refers to
+ * the user highlighing the result with the arrow keys/Tab, before it is
+ * picked. onSelection is also called when a user clicks a result. In the
+ * event of a click, onSelection is called just before pickResult. Note that
+ * this is called when heuristic results are pre-selected.
+ *
+ * @param {UrlbarResult} result
+ * The result that was selected.
+ * @param {Element} element
+ * The element in the result's view that was selected.
+ * @abstract
+ */
+ onSelection(result, element) {}
+
+ /**
+ * This is called only for dynamic result types, when the urlbar view updates
+ * the view of one of the results of the provider. It should return an object
+ * describing the view update that looks like this:
+ *
+ * {
+ * nodeNameFoo: {
+ * attributes: {
+ * someAttribute: someValue,
+ * },
+ * style: {
+ * someStyleProperty: someValue,
+ * },
+ * l10n: {
+ * id: someL10nId,
+ * args: someL10nArgs,
+ * },
+ * textContent: "some text content",
+ * },
+ * nodeNameBar: {
+ * ...
+ * },
+ * nodeNameBaz: {
+ * ...
+ * },
+ * }
+ *
+ * The object should contain a property for each element to update in the
+ * dynamic result type view. The names of these properties are the names
+ * declared in the view template of the dynamic result type; see
+ * UrlbarView.addDynamicViewTemplate(). The values are similar to the nested
+ * objects specified in the view template but not quite the same; see below.
+ * For each property, the element in the view subtree with the specified name
+ * is updated according to the object in the property's value. If an
+ * element's name is not specified, then it will not be updated and will
+ * retain its current state.
+ *
+ * @param {UrlbarResult} result
+ * The result whose view will be updated.
+ * @param {Map} idsByName
+ * A Map from an element's name, as defined by the provider; to its ID in
+ * the DOM, as defined by the browser. The browser manages element IDs for
+ * dynamic results to prevent collisions. However, a provider may need to
+ * access the IDs of the elements created for its results. For example, to
+ * set various `aria` attributes.
+ * @returns {object}
+ * A view update object as described above. The names of properties are the
+ * the names of elements declared in the view template. The values of
+ * properties are objects that describe how to update each element, and
+ * these objects may include the following properties, all of which are
+ * optional:
+ *
+ * {object} [attributes]
+ * A mapping from attribute names to values. Each name-value pair results
+ * in an attribute being added to the element. The `id` attribute is
+ * reserved and cannot be set by the provider.
+ * {object} [style]
+ * A plain object that can be used to add inline styles to the element,
+ * like `display: none`. `element.style` is updated for each name-value
+ * pair in this object.
+ * {object} [l10n]
+ * An { id, args } object that will be passed to
+ * document.l10n.setAttributes().
+ * {string} [textContent]
+ * A string that will be set as `element.textContent`.
+ */
+ getViewUpdate(result, idsByName) {
+ return null;
+ }
+
+ /**
+ * Defines whether the view should defer user selection events while waiting
+ * for the first result from this provider.
+ *
+ * @returns {boolean} Whether the provider wants to defer user selection
+ * events.
+ * @see UrlbarEventBufferer
+ * @note UrlbarEventBufferer has a timeout after which user events will be
+ * processed regardless.
+ */
+ get deferUserSelection() {
+ return false;
+ }
+}
+
+/**
+ * Class used to create a timer that can be manually fired, to immediately
+ * invoke the callback, or canceled, as necessary.
+ * Examples:
+ * let timer = new SkippableTimer();
+ * // Invokes the callback immediately without waiting for the delay.
+ * await timer.fire();
+ * // Cancel the timer, the callback won't be invoked.
+ * await timer.cancel();
+ * // Wait for the timer to have elapsed.
+ * await timer.promise;
+ */
+class SkippableTimer {
+ /**
+ * Creates a skippable timer for the given callback and time.
+ * @param {object} options An object that configures the timer
+ * @param {string} options.name The name of the timer, logged when necessary
+ * @param {function} options.callback To be invoked when requested
+ * @param {number} options.time A delay in milliseconds to wait for
+ * @param {boolean} options.reportErrorOnTimeout If true and the timer times
+ * out, an error will be logged with Cu.reportError
+ * @param {logger} options.logger An optional logger
+ */
+ constructor({
+ name = "<anonymous timer>",
+ callback = null,
+ time = 0,
+ reportErrorOnTimeout = false,
+ logger = null,
+ } = {}) {
+ this.name = name;
+ this.logger = logger;
+
+ let timerPromise = new Promise(resolve => {
+ this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this._timer.initWithCallback(
+ () => {
+ this._log(`Timed out!`, reportErrorOnTimeout);
+ resolve();
+ },
+ time,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+ this._log(`Started`);
+ });
+
+ let firePromise = new Promise(resolve => {
+ this.fire = () => {
+ this._log(`Skipped`);
+ resolve();
+ return this.promise;
+ };
+ });
+
+ this.promise = Promise.race([timerPromise, firePromise]).then(() => {
+ // If we've been canceled, don't call back.
+ if (this._timer && callback) {
+ callback();
+ }
+ });
+ }
+
+ /**
+ * Allows to cancel the timer and the callback won't be invoked.
+ * It is not strictly necessary to await for this, the promise can just be
+ * used to ensure all the internal work is complete.
+ * @returns {promise} Resolved once all the cancelation work is complete.
+ */
+ cancel() {
+ this._log(`Canceling`);
+ this._timer.cancel();
+ delete this._timer;
+ return this.fire();
+ }
+
+ _log(msg, isError = false) {
+ let line = `SkippableTimer :: ${this.name} :: ${msg}`;
+ if (this.logger) {
+ this.logger.debug(line);
+ }
+ if (isError) {
+ Cu.reportError(line);
+ }
+ }
+}
diff --git a/browser/components/urlbar/UrlbarValueFormatter.jsm b/browser/components/urlbar/UrlbarValueFormatter.jsm
new file mode 100644
index 0000000000..3708521c8e
--- /dev/null
+++ b/browser/components/urlbar/UrlbarValueFormatter.jsm
@@ -0,0 +1,498 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["UrlbarValueFormatter"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AppConstants: "resource://gre/modules/AppConstants.jsm",
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
+ Services: "resource://gre/modules/Services.jsm",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+});
+
+/**
+ * Applies URL highlighting and other styling to the text in the urlbar input,
+ * depending on the text.
+ */
+class UrlbarValueFormatter {
+ /**
+ * @param {UrlbarInput} urlbarInput
+ */
+ constructor(urlbarInput) {
+ this.urlbarInput = urlbarInput;
+ this.window = this.urlbarInput.window;
+ this.document = this.window.document;
+
+ // This is used only as an optimization to avoid removing formatting in
+ // the _remove* format methods when no formatting is actually applied.
+ this._formattingApplied = false;
+
+ this.window.addEventListener("resize", this);
+ }
+
+ get inputField() {
+ return this.urlbarInput.inputField;
+ }
+
+ get scheme() {
+ return this.urlbarInput.querySelector("#urlbar-scheme");
+ }
+
+ async update() {
+ // _getUrlMetaData does URI fixup, which depends on the search service, so
+ // make sure it's initialized. It can be uninitialized here on session
+ // restore. Skip this if the service is already initialized in order to
+ // avoid the async call in the common case. However, we can't access
+ // Service.search before first paint (delayed startup) because there's a
+ // performance test that prohibits it, so first bail if delayed startup
+ // isn't finished.
+ if (!this.window.gBrowserInit.delayedStartupFinished) {
+ return;
+ }
+ if (!Services.search.isInitialized) {
+ let instance = (this._updateInstance = {});
+ await Services.search.init();
+ if (this._updateInstance != instance) {
+ return;
+ }
+ delete this._updateInstance;
+ }
+
+ // If this window is being torn down, stop here
+ if (!this.window.docShell) {
+ return;
+ }
+
+ // Cleanup that must be done in any case, even if there's no value.
+ this.urlbarInput.removeAttribute("domaindir");
+ this.scheme.value = "";
+
+ if (!this.inputField.value) {
+ return;
+ }
+
+ // Remove the current formatting.
+ this._removeURLFormat();
+ this._removeSearchAliasFormat();
+
+ // Apply new formatting. Formatter methods should return true if they
+ // successfully formatted the value and false if not. We apply only
+ // one formatter at a time, so we stop at the first successful one.
+ this._formattingApplied = this._formatURL() || this._formatSearchAlias();
+ }
+
+ _ensureFormattedHostVisible(urlMetaData) {
+ // Used to avoid re-entrance in the requestAnimationFrame callback.
+ let instance = (this._formatURLInstance = {});
+
+ // Make sure the host is always visible. Since it is aligned on
+ // the first strong directional character, we set scrollLeft
+ // appropriately to ensure the domain stays visible in case of an
+ // overflow.
+ this.window.requestAnimationFrame(() => {
+ // Check for re-entrance. On focus change this formatting code is
+ // invoked regardless, thus this should be enough.
+ if (this._formatURLInstance != instance) {
+ return;
+ }
+
+ // In the future, for example in bug 525831, we may add a forceRTL
+ // char just after the domain, and in such a case we should not
+ // scroll to the left.
+ urlMetaData = urlMetaData || this._getUrlMetaData();
+ if (!urlMetaData) {
+ this.urlbarInput.removeAttribute("domaindir");
+ return;
+ }
+ let { url, preDomain, domain } = urlMetaData;
+ let directionality = this.window.windowUtils.getDirectionFromText(domain);
+ if (
+ directionality == this.window.windowUtils.DIRECTION_RTL &&
+ url[preDomain.length + domain.length] != "\u200E"
+ ) {
+ this.urlbarInput.setAttribute("domaindir", "rtl");
+ this.inputField.scrollLeft = this.inputField.scrollLeftMax;
+ } else {
+ this.urlbarInput.setAttribute("domaindir", "ltr");
+ this.inputField.scrollLeft = 0;
+ }
+ });
+ }
+
+ _getUrlMetaData() {
+ if (this.urlbarInput.focused) {
+ return null;
+ }
+
+ let url = this.inputField.value;
+ let browser = this.window.gBrowser.selectedBrowser;
+
+ // Since doing a full URIFixup and offset calculations is expensive, we
+ // keep the metadata cached in the browser itself, so when switching tabs
+ // we can skip most of this.
+ if (browser._urlMetaData && browser._urlMetaData.url == url) {
+ return browser._urlMetaData.data;
+ }
+ browser._urlMetaData = { url, data: null };
+
+ // Get the URL from the fixup service:
+ let flags =
+ Services.uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS |
+ Services.uriFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
+ if (PrivateBrowsingUtils.isWindowPrivate(this.window)) {
+ flags |= Services.uriFixup.FIXUP_FLAG_PRIVATE_CONTEXT;
+ }
+ let uriInfo;
+ try {
+ uriInfo = Services.uriFixup.getFixupURIInfo(url, flags);
+ } catch (ex) {}
+ // Ignore if we couldn't make a URI out of this, the URI resulted in a search,
+ // or the URI has a non-http(s)/ftp protocol.
+ if (
+ !uriInfo ||
+ !uriInfo.fixedURI ||
+ uriInfo.keywordProviderName ||
+ !["http", "https", "ftp"].includes(uriInfo.fixedURI.scheme)
+ ) {
+ return null;
+ }
+
+ // If we trimmed off the http scheme, ensure we stick it back on before
+ // trying to figure out what domain we're accessing, so we don't get
+ // confused by user:pass@host http URLs. We later use
+ // trimmedLength to ensure we don't count the length of a trimmed protocol
+ // when determining which parts of the URL to highlight as "preDomain".
+ let trimmedLength = 0;
+ if (uriInfo.fixedURI.scheme == "http" && !url.startsWith("http://")) {
+ url = "http://" + url;
+ trimmedLength = "http://".length;
+ }
+
+ // This RegExp is not a perfect match, and for specially crafted URLs it may
+ // get the host wrong; for safety reasons we will later compare the found
+ // host with the one that will actually be loaded.
+ let matchedURL = url.match(
+ /^(([a-z]+:\/\/)(?:[^\/#?]+@)?)(\S+?)(?::\d+)?\s*(?:[\/#?]|$)/
+ );
+ if (!matchedURL) {
+ return null;
+ }
+ let [, preDomain, schemeWSlashes, domain] = matchedURL;
+
+ // If the found host differs from the fixed URI one, we can't properly
+ // highlight it. To stay on the safe side, we clobber user's input with
+ // the fixed URI and apply highlight to that one instead.
+ let replaceUrl = false;
+ try {
+ replaceUrl =
+ Services.io.newURI("http://" + domain).displayHost !=
+ uriInfo.fixedURI.displayHost;
+ } catch (ex) {
+ return null;
+ }
+ if (replaceUrl) {
+ if (this._inGetUrlMetaData) {
+ // Protect from infinite recursion.
+ return null;
+ }
+ try {
+ this._inGetUrlMetaData = true;
+ this.window.gBrowser.userTypedValue = null;
+ this.urlbarInput.setURI(uriInfo.fixedURI);
+ return this._getUrlMetaData();
+ } finally {
+ this._inGetUrlMetaData = false;
+ }
+ }
+
+ return (browser._urlMetaData.data = {
+ domain,
+ origin: uriInfo.fixedURI.host,
+ preDomain,
+ schemeWSlashes,
+ trimmedLength,
+ url,
+ });
+ }
+
+ _removeURLFormat() {
+ if (!this._formattingApplied) {
+ return;
+ }
+ let controller = this.urlbarInput.editor.selectionController;
+ let strikeOut = controller.getSelection(controller.SELECTION_URLSTRIKEOUT);
+ strikeOut.removeAllRanges();
+ let selection = controller.getSelection(controller.SELECTION_URLSECONDARY);
+ selection.removeAllRanges();
+ this._formatScheme(controller.SELECTION_URLSTRIKEOUT, true);
+ this._formatScheme(controller.SELECTION_URLSECONDARY, true);
+ this.inputField.style.setProperty("--urlbar-scheme-size", "0px");
+ }
+
+ /**
+ * If the input value is a URL and the input is not focused, this
+ * formatter method highlights the domain, and if mixed content is present,
+ * it crosses out the https scheme. It also ensures that the host is
+ * visible (not scrolled out of sight).
+ *
+ * @returns {boolean}
+ * True if formatting was applied and false if not.
+ */
+ _formatURL() {
+ let urlMetaData = this._getUrlMetaData();
+ if (!urlMetaData) {
+ return false;
+ }
+
+ let {
+ domain,
+ origin,
+ preDomain,
+ schemeWSlashes,
+ trimmedLength,
+ url,
+ } = urlMetaData;
+ // We strip http, so we should not show the scheme box for it.
+ if (!UrlbarPrefs.get("trimURLs") || schemeWSlashes != "http://") {
+ this.scheme.value = schemeWSlashes;
+ this.inputField.style.setProperty(
+ "--urlbar-scheme-size",
+ schemeWSlashes.length + "ch"
+ );
+ }
+
+ this._ensureFormattedHostVisible(urlMetaData);
+
+ if (!UrlbarPrefs.get("formatting.enabled")) {
+ return false;
+ }
+
+ let editor = this.urlbarInput.editor;
+ let controller = editor.selectionController;
+
+ this._formatScheme(controller.SELECTION_URLSECONDARY);
+
+ let textNode = editor.rootElement.firstChild;
+
+ // Strike out the "https" part if mixed active content is loaded.
+ if (
+ this.urlbarInput.getAttribute("pageproxystate") == "valid" &&
+ url.startsWith("https:") &&
+ this.window.gBrowser.securityUI.state &
+ Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT
+ ) {
+ let range = this.document.createRange();
+ range.setStart(textNode, 0);
+ range.setEnd(textNode, 5);
+ let strikeOut = controller.getSelection(
+ controller.SELECTION_URLSTRIKEOUT
+ );
+ strikeOut.addRange(range);
+ this._formatScheme(controller.SELECTION_URLSTRIKEOUT);
+ }
+
+ let baseDomain = domain;
+ let subDomain = "";
+ try {
+ baseDomain = Services.eTLD.getBaseDomainFromHost(origin);
+ if (!domain.endsWith(baseDomain)) {
+ // getBaseDomainFromHost converts its resultant to ACE.
+ let IDNService = Cc["@mozilla.org/network/idn-service;1"].getService(
+ Ci.nsIIDNService
+ );
+ baseDomain = IDNService.convertACEtoUTF8(baseDomain);
+ }
+ } catch (e) {}
+ if (baseDomain != domain) {
+ subDomain = domain.slice(0, -baseDomain.length);
+ }
+
+ let selection = controller.getSelection(controller.SELECTION_URLSECONDARY);
+
+ let rangeLength = preDomain.length + subDomain.length - trimmedLength;
+ if (rangeLength) {
+ let range = this.document.createRange();
+ range.setStart(textNode, 0);
+ range.setEnd(textNode, rangeLength);
+ selection.addRange(range);
+ }
+
+ let startRest = preDomain.length + domain.length - trimmedLength;
+ if (startRest < url.length - trimmedLength) {
+ let range = this.document.createRange();
+ range.setStart(textNode, startRest);
+ range.setEnd(textNode, url.length - trimmedLength);
+ selection.addRange(range);
+ }
+
+ return true;
+ }
+
+ _formatScheme(selectionType, clear) {
+ let editor = this.scheme.editor;
+ let controller = editor.selectionController;
+ let textNode = editor.rootElement.firstChild;
+ let selection = controller.getSelection(selectionType);
+ if (clear) {
+ selection.removeAllRanges();
+ } else {
+ let r = this.document.createRange();
+ r.setStart(textNode, 0);
+ r.setEnd(textNode, textNode.textContent.length);
+ selection.addRange(r);
+ }
+ }
+
+ _removeSearchAliasFormat() {
+ if (!this._formattingApplied) {
+ return;
+ }
+ let selection = this.urlbarInput.editor.selectionController.getSelection(
+ Ci.nsISelectionController.SELECTION_FIND
+ );
+ selection.removeAllRanges();
+ }
+
+ /**
+ * If the input value starts with an @engine search alias, this highlights it.
+ *
+ * @returns {boolean}
+ * True if formatting was applied and false if not.
+ */
+ _formatSearchAlias() {
+ if (!UrlbarPrefs.get("formatting.enabled")) {
+ return false;
+ }
+
+ let editor = this.urlbarInput.editor;
+ let textNode = editor.rootElement.firstChild;
+ let value = textNode.textContent;
+ let trimmedValue = value.trim();
+
+ if (
+ !trimmedValue.startsWith("@") ||
+ (this.urlbarInput.popup || this.urlbarInput.view).oneOffSearchButtons
+ .selectedButton
+ ) {
+ return false;
+ }
+
+ let alias = this._getSearchAlias();
+ if (!alias) {
+ return false;
+ }
+
+ // Make sure the current input starts with the alias because it can change
+ // without the popup results changing. Most notably that happens when the
+ // user performs a search using an alias: The popup closes (preserving its
+ // results), the search results page loads, and the input value is set to
+ // the URL of the page.
+ if (trimmedValue != alias && !trimmedValue.startsWith(alias + " ")) {
+ return false;
+ }
+
+ let index = value.indexOf(alias);
+ if (index < 0) {
+ return false;
+ }
+
+ // We abuse the SELECTION_FIND selection type to do our highlighting.
+ // It's the only type that works with Selection.setColors().
+ let selection = editor.selectionController.getSelection(
+ Ci.nsISelectionController.SELECTION_FIND
+ );
+
+ let range = this.document.createRange();
+ range.setStart(textNode, index);
+ range.setEnd(textNode, index + alias.length);
+ selection.addRange(range);
+
+ let fg = "#2362d7";
+ let bg = "#d2e6fd";
+
+ // Selection.setColors() will swap the given foreground and background
+ // colors if it detects that the contrast between the background
+ // color and the frame color is too low. Normally we don't want that
+ // to happen; we want it to use our colors as given (even if setColors
+ // thinks the contrast is too low). But it's a nice feature for non-
+ // default themes, where the contrast between our background color and
+ // the input's frame color might actually be too low. We can
+ // (hackily) force setColors to use our colors as given by passing
+ // them as the alternate colors. Otherwise, allow setColors to swap
+ // them, which we can do by passing "currentColor". See
+ // nsTextPaintStyle::GetHighlightColors for details.
+ if (
+ this.document.documentElement.querySelector(":-moz-lwtheme") ||
+ (AppConstants.platform == "win" &&
+ this.window.matchMedia("(-moz-windows-default-theme: 0)").matches)
+ ) {
+ // non-default theme(s)
+ selection.setColors(fg, bg, "currentColor", "currentColor");
+ } else {
+ // default themes
+ selection.setColors(fg, bg, fg, bg);
+ }
+
+ return true;
+ }
+
+ _getSearchAlias() {
+ // To determine whether the input contains a valid alias, check if the
+ // selected result is a search result with an alias. If there is no selected
+ // result, we check the first result in the view, for cases when we do not
+ // highlight token alias results. The selected result is null when the popup
+ // is closed, but we want to continue highlighting the alias when the popup
+ // is closed, and that's why we keep around the previously selected result
+ // in _selectedResult.
+ this._selectedResult =
+ this.urlbarInput.view.selectedResult ||
+ this.urlbarInput.view.getResultAtIndex(0) ||
+ this._selectedResult;
+
+ if (
+ this._selectedResult &&
+ this._selectedResult.type == UrlbarUtils.RESULT_TYPE.SEARCH
+ ) {
+ return this._selectedResult.payload.keyword || null;
+ }
+ return null;
+ }
+
+ /**
+ * Passes DOM events to the _on_<event type> methods.
+ * @param {Event} event
+ * DOM event.
+ */
+ handleEvent(event) {
+ let methodName = "_on_" + event.type;
+ if (methodName in this) {
+ this[methodName](event);
+ } else {
+ throw new Error("Unrecognized UrlbarValueFormatter event: " + event.type);
+ }
+ }
+
+ _on_resize(event) {
+ if (event.target != this.window) {
+ return;
+ }
+ // Make sure the host remains visible in the input field when the window is
+ // resized. We don't want to hurt resize performance though, so do this
+ // only after resize events have stopped and a small timeout has elapsed.
+ if (this._resizeThrottleTimeout) {
+ this.window.clearTimeout(this._resizeThrottleTimeout);
+ }
+ this._resizeThrottleTimeout = this.window.setTimeout(() => {
+ this._resizeThrottleTimeout = null;
+ this._ensureFormattedHostVisible();
+ }, 100);
+ }
+}
diff --git a/browser/components/urlbar/UrlbarView.jsm b/browser/components/urlbar/UrlbarView.jsm
new file mode 100644
index 0000000000..c4c41e2c2d
--- /dev/null
+++ b/browser/components/urlbar/UrlbarView.jsm
@@ -0,0 +1,2232 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["UrlbarView"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+XPCOMUtils.defineLazyModuleGetters(this, {
+ BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
+ Services: "resource://gre/modules/Services.jsm",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
+ UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm",
+ UrlbarSearchOneOffs: "resource:///modules/UrlbarSearchOneOffs.jsm",
+ UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.jsm",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+});
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "styleSheetService",
+ "@mozilla.org/content/style-sheet-service;1",
+ "nsIStyleSheetService"
+);
+
+// Stale rows are removed on a timer with this timeout. Tests can override this
+// by setting UrlbarView.removeStaleRowsTimeout.
+const DEFAULT_REMOVE_STALE_ROWS_TIMEOUT = 400;
+
+// Query selector for selectable elements in tip and dynamic results.
+const SELECTABLE_ELEMENT_SELECTOR = "[role=button], [selectable=true]";
+
+const getBoundsWithoutFlushing = element =>
+ element.ownerGlobal.windowUtils.getBoundsWithoutFlushing(element);
+
+// Used to get a unique id to use for row elements, it wraps at 9999, that
+// should be plenty for our needs.
+let gUniqueIdSerial = 1;
+function getUniqueId(prefix) {
+ return prefix + (gUniqueIdSerial++ % 9999);
+}
+
+/**
+ * Receives and displays address bar autocomplete results.
+ */
+class UrlbarView {
+ /**
+ * @param {UrlbarInput} input
+ * The UrlbarInput instance belonging to this UrlbarView instance.
+ */
+ constructor(input) {
+ this.input = input;
+ this.panel = input.panel;
+ this.controller = input.controller;
+ this.document = this.panel.ownerDocument;
+ this.window = this.document.defaultView;
+
+ this._mainContainer = this.panel.querySelector(".urlbarView-body-inner");
+ this._rows = this.panel.querySelector(".urlbarView-results");
+
+ this._rows.addEventListener("mousedown", this);
+ this._rows.addEventListener("mouseup", this);
+
+ // For the horizontal fade-out effect, set the overflow attribute on result
+ // rows when they overflow.
+ this._rows.addEventListener("overflow", this);
+ this._rows.addEventListener("underflow", this);
+
+ // `noresults` is used to style the one-offs without their usual top border
+ // when no results are present.
+ this.panel.setAttribute("noresults", "true");
+
+ this.controller.setView(this);
+ this.controller.addQueryListener(this);
+ // This is used by autoOpen to avoid flickering results when reopening
+ // previously abandoned searches.
+ this._queryContextCache = new QueryContextCache(5);
+
+ for (let viewTemplate of UrlbarView.dynamicViewTemplatesByName.values()) {
+ if (viewTemplate.stylesheet) {
+ addDynamicStylesheet(this.window, viewTemplate.stylesheet);
+ }
+ }
+ }
+
+ get oneOffSearchButtons() {
+ if (!this._oneOffSearchButtons) {
+ this._oneOffSearchButtons = new UrlbarSearchOneOffs(this);
+ this._oneOffSearchButtons.addEventListener(
+ "SelectedOneOffButtonChanged",
+ this
+ );
+ }
+ return this._oneOffSearchButtons;
+ }
+
+ /**
+ * Whether the panel is open.
+ * @returns {boolean}
+ */
+ get isOpen() {
+ return this.input.hasAttribute("open");
+ }
+
+ get allowEmptySelection() {
+ return !(
+ this._queryContext &&
+ this._queryContext.results[0] &&
+ this._queryContext.results[0].heuristic
+ );
+ }
+
+ get selectedRowIndex() {
+ if (!this.isOpen) {
+ return -1;
+ }
+
+ let selectedRow = this._getSelectedRow();
+
+ if (!selectedRow) {
+ return -1;
+ }
+
+ return selectedRow.result.rowIndex;
+ }
+
+ set selectedRowIndex(val) {
+ if (!this.isOpen) {
+ throw new Error(
+ "UrlbarView: Cannot select an item if the view isn't open."
+ );
+ }
+
+ if (val < 0) {
+ this._selectElement(null);
+ return val;
+ }
+
+ let items = Array.from(this._rows.children).filter(r =>
+ this._isElementVisible(r)
+ );
+ if (val >= items.length) {
+ throw new Error(`UrlbarView: Index ${val} is out of bounds.`);
+ }
+ this._selectElement(items[val]);
+ return val;
+ }
+
+ get selectedElementIndex() {
+ if (!this.isOpen || !this._selectedElement) {
+ return -1;
+ }
+
+ return this._selectedElement.elementIndex;
+ }
+
+ set selectedElementIndex(val) {
+ if (!this.isOpen) {
+ throw new Error(
+ "UrlbarView: Cannot select an item if the view isn't open."
+ );
+ }
+
+ if (val < 0) {
+ this._selectElement(null);
+ return val;
+ }
+
+ let selectableElement = this._getFirstSelectableElement();
+ while (selectableElement && selectableElement.elementIndex != val) {
+ selectableElement = this._getNextSelectableElement(selectableElement);
+ }
+
+ if (!selectableElement) {
+ throw new Error(`UrlbarView: Index ${val} is out of bounds.`);
+ }
+
+ this._selectElement(selectableElement);
+ return val;
+ }
+
+ /**
+ * @returns {UrlbarResult}
+ * The currently selected result.
+ */
+ get selectedResult() {
+ if (!this.isOpen) {
+ return null;
+ }
+
+ let selectedRow = this._getSelectedRow();
+
+ if (!selectedRow) {
+ return null;
+ }
+
+ return selectedRow.result;
+ }
+
+ /**
+ * @returns {Element}
+ * The currently selected element.
+ */
+ get selectedElement() {
+ if (!this.isOpen) {
+ return null;
+ }
+
+ return this._selectedElement;
+ }
+
+ /**
+ * Clears selection, regardless of view status.
+ */
+ clearSelection() {
+ this._selectElement(null, { updateInput: false });
+ }
+
+ /**
+ * @returns {number}
+ * The number of visible results in the view. Note that this may be larger
+ * than the number of results in the current query context since the view
+ * may be showing stale results.
+ */
+ get visibleRowCount() {
+ let sum = 0;
+ for (let row of this._rows.children) {
+ sum += Number(this._isElementVisible(row));
+ }
+ return sum;
+ }
+
+ /**
+ * @returns {number}
+ * The number of selectable elements in the view.
+ */
+ get visibleElementCount() {
+ let sum = 0;
+ let element = this._getFirstSelectableElement();
+ while (element) {
+ if (this._isElementVisible(element)) {
+ sum++;
+ }
+ element = this._getNextSelectableElement(element);
+ }
+ return sum;
+ }
+
+ /**
+ * Returns the result of the row containing the given element, or the result
+ * of the element if it itself is a row.
+ *
+ * @param {Element} element
+ * An element in the view.
+ * @returns {UrlbarResult}
+ * The result of the element's row.
+ */
+ getResultFromElement(element) {
+ if (!this.isOpen) {
+ return null;
+ }
+
+ let row = this._getRowFromElement(element);
+
+ if (!row) {
+ return null;
+ }
+
+ return row.result;
+ }
+
+ /**
+ * @param {number} index
+ * The index from which to fetch the result.
+ * @returns {UrlbarResult}
+ * The result at `index`. Null if the view is closed or if there are no
+ * results.
+ */
+ getResultAtIndex(index) {
+ if (
+ !this.isOpen ||
+ !this._rows.children.length ||
+ index >= this._rows.children.length
+ ) {
+ return null;
+ }
+
+ return this._rows.children[index].result;
+ }
+
+ /**
+ * Returns the element closest to the given element that can be
+ * selected/picked. If the element itself can be selected, it's returned. If
+ * there is no such element, null is returned.
+ *
+ * @param {Element} element
+ * An element in the view.
+ * @returns {Element}
+ * The closest element that can be picked including the element itself, or
+ * null if there is no such element.
+ */
+ getClosestSelectableElement(element) {
+ let row = element.closest(".urlbarView-row");
+ if (!row) {
+ return null;
+ }
+ let closest = row;
+ if (
+ row.result.type == UrlbarUtils.RESULT_TYPE.TIP ||
+ row.result.type == UrlbarUtils.RESULT_TYPE.DYNAMIC
+ ) {
+ closest = element.closest(SELECTABLE_ELEMENT_SELECTOR);
+ }
+ return this._isElementVisible(closest) ? closest : null;
+ }
+
+ /**
+ * @param {UrlbarResult} result A result.
+ * @returns {boolean} True if the given result is selected.
+ */
+ resultIsSelected(result) {
+ if (this.selectedRowIndex < 0) {
+ return false;
+ }
+
+ return result.rowIndex == this.selectedRowIndex;
+ }
+
+ /**
+ * Moves the view selection forward or backward.
+ *
+ * @param {number} amount
+ * The number of steps to move.
+ * @param {boolean} options.reverse
+ * Set to true to select the previous item. By default the next item
+ * will be selected.
+ * @param {boolean} options.userPressedTab
+ * Set to true if the user pressed Tab to select a result. Default false.
+ */
+ selectBy(amount, { reverse = false, userPressedTab = false } = {}) {
+ if (!this.isOpen) {
+ throw new Error(
+ "UrlbarView: Cannot select an item if the view isn't open."
+ );
+ }
+
+ // Do not set aria-activedescendant if the user is moving to a
+ // tab-to-search result with the Tab key. If
+ // accessibility.tabToSearch.announceResults is set, the tab-to-search
+ // result was announced to the user as they typed. We don't set
+ // aria-activedescendant so the user doesn't think they have to press
+ // Enter to enter search mode. See bug 1647929.
+ const isSkippableTabToSearchAnnounce = selectedElt => {
+ let skipAnnouncement =
+ selectedElt?.result?.providerName == "TabToSearch" &&
+ !this._announceTabToSearchOnSelection &&
+ userPressedTab &&
+ UrlbarPrefs.get("accessibility.tabToSearch.announceResults");
+ if (skipAnnouncement) {
+ // Once we skip setting aria-activedescendant once, we should not skip
+ // it again if the user returns to that result.
+ this._announceTabToSearchOnSelection = true;
+ }
+ return skipAnnouncement;
+ };
+
+ // Freeze results as the user is interacting with them, unless we are
+ // deferring events while waiting for critical results.
+ if (!this.input.eventBufferer.isDeferringEvents) {
+ this.controller.cancelQuery();
+ }
+
+ let selectedElement = this._selectedElement;
+
+ // We cache the first and last rows since they will not change while
+ // selectBy is running.
+ let firstSelectableElement = this._getFirstSelectableElement();
+ // _getLastSelectableElement will not return an element that is over
+ // maxResults and thus may be hidden and not selectable.
+ let lastSelectableElement = this._getLastSelectableElement();
+
+ if (!selectedElement) {
+ selectedElement = reverse
+ ? lastSelectableElement
+ : firstSelectableElement;
+ this._selectElement(selectedElement, {
+ setAccessibleFocus: !isSkippableTabToSearchAnnounce(selectedElement),
+ });
+ return;
+ }
+ let endReached = reverse
+ ? selectedElement == firstSelectableElement
+ : selectedElement == lastSelectableElement;
+ if (endReached) {
+ if (this.allowEmptySelection) {
+ selectedElement = null;
+ } else {
+ selectedElement = reverse
+ ? lastSelectableElement
+ : firstSelectableElement;
+ }
+ this._selectElement(selectedElement, {
+ setAccessibleFocus: !isSkippableTabToSearchAnnounce(selectedElement),
+ });
+ return;
+ }
+
+ while (amount-- > 0) {
+ let next = reverse
+ ? this._getPreviousSelectableElement(selectedElement)
+ : this._getNextSelectableElement(selectedElement);
+ if (!next) {
+ break;
+ }
+ if (!this._isElementVisible(next)) {
+ continue;
+ }
+ selectedElement = next;
+ }
+ this._selectElement(selectedElement, {
+ setAccessibleFocus: !isSkippableTabToSearchAnnounce(selectedElement),
+ });
+ }
+
+ removeAccessibleFocus() {
+ this._setAccessibleFocus(null);
+ }
+
+ clear() {
+ this._rows.textContent = "";
+ this.panel.setAttribute("noresults", "true");
+ this.clearSelection();
+ }
+
+ /**
+ * Closes the view, cancelling the query if necessary.
+ * @param {boolean} [elementPicked]
+ * True if the view is being closed because a result was picked.
+ */
+ close(elementPicked = false) {
+ this.controller.cancelQuery();
+
+ if (!this.isOpen) {
+ return;
+ }
+
+ // We exit search mode preview on close since the result previewing it is
+ // implicitly unselected.
+ if (this.input.searchMode?.isPreview) {
+ this.input.searchMode = null;
+ }
+
+ this.removeAccessibleFocus();
+ this.input.inputField.setAttribute("aria-expanded", "false");
+ this._openPanelInstance = null;
+ this._previousTabToSearchEngine = null;
+
+ this.input.removeAttribute("open");
+ this.input.endLayoutExtend();
+
+ // Search Tips can open the view without the Urlbar being focused. If the
+ // tip is ignored (e.g. the page content is clicked or the window loses
+ // focus) we should discard the telemetry event created when the view was
+ // opened.
+ if (!this.input.focused && !elementPicked) {
+ this.controller.engagementEvent.discard();
+ this.controller.engagementEvent.record(null, {});
+ }
+
+ this.window.removeEventListener("resize", this);
+ this.window.removeEventListener("blur", this);
+
+ this.controller.notify(this.controller.NOTIFICATIONS.VIEW_CLOSE);
+ }
+
+ /**
+ * This can be used to open the view automatically as a consequence of
+ * specific user actions. For Top Sites searches (without a search string)
+ * the view is opened only for mouse or keyboard interactions.
+ * If the user abandoned a search (there is a search string) the view is
+ * reopened, and we try to use cached results to reduce flickering, then a new
+ * query is started to refresh results.
+ * @param {Event} queryOptions Options to use when starting a new query. The
+ * event property is mandatory for proper telemetry tracking.
+ * @returns {boolean} Whether the view was opened.
+ */
+ autoOpen(queryOptions = {}) {
+ if (this._pickSearchTipIfPresent(queryOptions.event)) {
+ return false;
+ }
+
+ if (!queryOptions.event) {
+ return false;
+ }
+
+ if (
+ !this.input.value ||
+ this.input.getAttribute("pageproxystate") == "valid"
+ ) {
+ if (
+ !this.isOpen &&
+ ["mousedown", "command"].includes(queryOptions.event.type)
+ ) {
+ this.input.startQuery(queryOptions);
+ return true;
+ }
+ return false;
+ }
+
+ // Reopen abandoned searches only if the input is focused.
+ if (!this.input.focused) {
+ return false;
+ }
+
+ // Tab switch is the only case where we requery if the view is open, because
+ // switching tabs doesn't necessarily close the view.
+ if (this.isOpen && queryOptions.event.type != "tabswitch") {
+ return false;
+ }
+
+ if (
+ this._rows.firstElementChild &&
+ this._queryContext.searchString == this.input.value
+ ) {
+ // We can reuse the current results.
+ queryOptions.allowAutofill = this._queryContext.allowAutofill;
+ } else {
+ // To reduce results flickering, try to reuse a cached UrlbarQueryContext.
+ let cachedQueryContext = this._queryContextCache.get(this.input.value);
+ if (cachedQueryContext) {
+ this.onQueryResults(cachedQueryContext);
+ }
+ }
+
+ this.controller.engagementEvent.discard();
+ queryOptions.searchString = this.input.value;
+ queryOptions.autofillIgnoresSelection = true;
+ queryOptions.event.interactionType = "returned";
+
+ if (
+ this._queryContext &&
+ this._queryContext.results &&
+ this._queryContext.results.length
+ ) {
+ this._openPanel();
+ }
+
+ // If we had cached results, this will just refresh them, avoiding results
+ // flicker, otherwise there may be some noise.
+ this.input.startQuery(queryOptions);
+ return true;
+ }
+
+ // UrlbarController listener methods.
+ onQueryStarted(queryContext) {
+ this._queryWasCancelled = false;
+ this._queryUpdatedResults = false;
+ this._openPanelInstance = null;
+ if (!queryContext.searchString) {
+ this._previousTabToSearchEngine = null;
+ }
+ this._startRemoveStaleRowsTimer();
+ }
+
+ onQueryCancelled(queryContext) {
+ this._queryWasCancelled = true;
+ this._cancelRemoveStaleRowsTimer();
+ }
+
+ onQueryFinished(queryContext) {
+ this._cancelRemoveStaleRowsTimer();
+ if (this._queryWasCancelled) {
+ return;
+ }
+
+ // If the query finished and it returned some results, remove stale rows.
+ if (this._queryUpdatedResults) {
+ this._removeStaleRows();
+ return;
+ }
+
+ // The query didn't return any results. Clear the view.
+ this.clear();
+
+ // If search mode isn't active, close the view.
+ if (!this.input.searchMode) {
+ this.close();
+ return;
+ }
+
+ // Search mode is active. If the one-offs should be shown, make sure they
+ // are enabled and show the view.
+ let openPanelInstance = (this._openPanelInstance = {});
+ this.oneOffSearchButtons.willHide().then(willHide => {
+ if (!willHide && openPanelInstance == this._openPanelInstance) {
+ this.oneOffSearchButtons.enable(true);
+ this._openPanel();
+ }
+ });
+ }
+
+ onQueryResults(queryContext) {
+ this._queryContextCache.put(queryContext);
+ this._queryContext = queryContext;
+
+ if (!this.isOpen) {
+ this.clear();
+ }
+ this._queryUpdatedResults = true;
+ this._updateResults(queryContext);
+
+ let firstResult = queryContext.results[0];
+
+ if (queryContext.lastResultCount == 0) {
+ // Clear the selection when we get a new set of results.
+ this._selectElement(null, {
+ updateInput: false,
+ });
+
+ // Show the one-off search buttons unless any of the following are true:
+ // * The first result is a search tip
+ // * The search string is empty
+ // * The search string starts with an `@` or a search restriction
+ // character
+ this.oneOffSearchButtons.enable(
+ (firstResult.providerName != "UrlbarProviderSearchTips" ||
+ queryContext.trimmedSearchString) &&
+ queryContext.trimmedSearchString[0] != "@" &&
+ (queryContext.trimmedSearchString[0] !=
+ UrlbarTokenizer.RESTRICT.SEARCH ||
+ queryContext.trimmedSearchString.length != 1)
+ );
+ }
+
+ if (!this.selectedElement && !this.oneOffSearchButtons.selectedButton) {
+ if (firstResult.heuristic) {
+ // Select the heuristic result. The heuristic may not be the first result
+ // added, which is why we do this check here when each result is added and
+ // not above.
+ this._selectElement(this._getFirstSelectableElement(), {
+ updateInput: false,
+ setAccessibleFocus: this.controller._userSelectionBehavior == "arrow",
+ });
+ } else if (
+ firstResult.payload.providesSearchMode &&
+ queryContext.trimmedSearchString != "@"
+ ) {
+ // Filtered keyword offer results can be in the first position but not
+ // be heuristic results. We do this so the user can press Tab to select
+ // them, resembling tab-to-search. In that case, the input value is
+ // still associated with the first result.
+ this.input.setResultForCurrentValue(firstResult);
+ }
+ }
+
+ // Announce tab-to-search results to screen readers as the user types.
+ // Check to make sure we don't announce the same engine multiple times in
+ // a row.
+ let secondResult = queryContext.results[1];
+ if (
+ secondResult?.providerName == "TabToSearch" &&
+ UrlbarPrefs.get("accessibility.tabToSearch.announceResults") &&
+ this._previousTabToSearchEngine != secondResult.payload.engine
+ ) {
+ let engine = secondResult.payload.engine;
+ this.window.A11yUtils.announce({
+ id: UrlbarUtils.WEB_ENGINE_NAMES.has(engine)
+ ? "urlbar-result-action-before-tabtosearch-web"
+ : "urlbar-result-action-before-tabtosearch-other",
+ args: { engine },
+ });
+ this._previousTabToSearchEngine = engine;
+ // Do not set aria-activedescendant when the user tabs to the result
+ // because we already announced it.
+ this._announceTabToSearchOnSelection = false;
+ }
+
+ // If we update the selected element, a new unique ID is generated for it.
+ // We need to ensure that aria-activedescendant reflects this new ID.
+ if (this.selectedElement && !this.oneOffSearchButtons.selectedButton) {
+ let aadID = this.input.inputField.getAttribute("aria-activedescendant");
+ if (aadID && !this.document.getElementById(aadID)) {
+ this._setAccessibleFocus(this.selectedElement);
+ }
+ }
+
+ this._openPanel();
+
+ if (firstResult.heuristic) {
+ // The heuristic result may be a search alias result, so apply formatting
+ // if necessary. Conversely, the heuristic result of the previous query
+ // may have been an alias, so remove formatting if necessary.
+ this.input.formatValue();
+ }
+
+ if (queryContext.deferUserSelectionProviders.size) {
+ // DeferUserSelectionProviders block user selection until the result is
+ // shown, so it's the view's duty to remove them.
+ // Doing it sooner, like when the results are added by the provider,
+ // would not suffice because there's still a delay before those results
+ // reach the view.
+ queryContext.results.forEach(r => {
+ queryContext.deferUserSelectionProviders.delete(r.providerName);
+ });
+ }
+ }
+
+ /**
+ * Handles removing a result from the view when it is removed from the query,
+ * and attempts to select the new result on the same row.
+ *
+ * This assumes that the result rows are in index order.
+ *
+ * @param {number} index The index of the result that has been removed.
+ */
+ onQueryResultRemoved(index) {
+ let rowToRemove = this._rows.children[index];
+ rowToRemove.remove();
+
+ this._updateIndices();
+
+ if (rowToRemove != this._getSelectedRow()) {
+ return;
+ }
+
+ // Select the row at the same index, if possible.
+ let newSelectionIndex = index;
+ if (index >= this._queryContext.results.length) {
+ newSelectionIndex = this._queryContext.results.length - 1;
+ }
+ if (newSelectionIndex >= 0) {
+ this.selectedRowIndex = newSelectionIndex;
+ }
+ }
+
+ /**
+ * Passes DOM events for the view to the _on_<event type> methods.
+ * @param {Event} event
+ * DOM event from the <view>.
+ */
+ handleEvent(event) {
+ let methodName = "_on_" + event.type;
+ if (methodName in this) {
+ this[methodName](event);
+ } else {
+ throw new Error("Unrecognized UrlbarView event: " + event.type);
+ }
+ }
+
+ static dynamicViewTemplatesByName = new Map();
+
+ /**
+ * Registers the view template for a dynamic result type. A view template is
+ * a plain object that describes the DOM subtree for a dynamic result type.
+ * When a dynamic result is shown in the urlbar view, its type's view template
+ * is used to construct the part of the view that represents the result.
+ *
+ * The specified view template will be available to the urlbars in all current
+ * and future browser windows until it is unregistered. A given dynamic
+ * result type has at most one view template. If this method is called for a
+ * dynamic result type more than once, the view template in the last call
+ * overrides those in previous calls.
+ *
+ * @param {string} name
+ * The view template will be registered for the dynamic result type with
+ * this name.
+ * @param {object} viewTemplate
+ * This object describes the DOM subtree for the given dynamic result type.
+ * It should be a tree-like nested structure with each object in the nesting
+ * representing a DOM element to be created. This tree-like structure is
+ * achieved using the `children` property described below. Each object in
+ * the structure may include the following properties:
+ *
+ * {string} name
+ * The name of the object. It is required for all objects in the
+ * structure except the root object and serves two important functions:
+ * (1) The element created for the object will automatically have a class
+ * named `urlbarView-dynamic-${dynamicType}-${name}`, where
+ * `dynamicType` is the name of the dynamic result type. The element
+ * will also automatically have an attribute "name" whose value is
+ * this name. The class and attribute allow the element to be styled
+ * in CSS.
+ * (2) The name is used when updating the view. See
+ * UrlbarProvider.getViewUpdate().
+ * Names must be unique within a view template, but they don't need to be
+ * globally unique. i.e., two different view templates can use the same
+ * names, and other DOM elements can use the same names in their IDs and
+ * classes. The name also suffixes the dynamic element's ID: an element
+ * with name `data` will get the ID `urlbarView-row-{unique number}-data`.
+ * If there is no name provided for the root element, the root element
+ * will not get an ID.
+ * {string} tag
+ * The tag name of the object. It is required for all objects in the
+ * structure except the root object and declares the kind of element that
+ * will be created for the object: span, div, img, etc.
+ * {object} [attributes]
+ * An optional mapping from attribute names to values. For each
+ * name-value pair, an attribute is added to the element created for the
+ * object. The `id` attribute is reserved and cannot be set by the
+ * provider. Element IDs are passed back to the provider in getViewUpdate
+ * if they are needed.
+ * {array} [children]
+ * An optional list of children. Each item in the array must be an object
+ * as described here. For each item, a child element as described by the
+ * item is created and added to the element created for the parent object.
+ * {array} [classList]
+ * An optional list of classes. Each class will be added to the element
+ * created for the object by calling element.classList.add().
+ * {string} [stylesheet]
+ * An optional stylesheet URL. This property is valid only on the root
+ * object in the structure. The stylesheet will be loaded in all browser
+ * windows so that the dynamic result type view may be styled.
+ */
+ static addDynamicViewTemplate(name, viewTemplate) {
+ this.dynamicViewTemplatesByName.set(name, viewTemplate);
+ if (viewTemplate.stylesheet) {
+ for (let window of BrowserWindowTracker.orderedWindows) {
+ addDynamicStylesheet(window, viewTemplate.stylesheet);
+ }
+ }
+ }
+
+ /**
+ * Unregisters the view template for a dynamic result type.
+ *
+ * @param {string} name
+ * The view template will be unregistered for the dynamic result type with
+ * this name.
+ */
+ static removeDynamicViewTemplate(name) {
+ let viewTemplate = this.dynamicViewTemplatesByName.get(name);
+ if (!viewTemplate) {
+ return;
+ }
+ this.dynamicViewTemplatesByName.delete(name);
+ if (viewTemplate.stylesheet) {
+ for (let window of BrowserWindowTracker.orderedWindows) {
+ removeDynamicStylesheet(window, viewTemplate.stylesheet);
+ }
+ }
+ }
+
+ // Private methods below.
+
+ _createElement(name) {
+ return this.document.createElementNS("http://www.w3.org/1999/xhtml", name);
+ }
+
+ _openPanel() {
+ if (this.isOpen) {
+ return;
+ }
+ this.controller.userSelectionBehavior = "none";
+
+ this.panel.removeAttribute("actionoverride");
+
+ this._enableOrDisableRowWrap();
+
+ this.input.inputField.setAttribute("aria-expanded", "true");
+
+ this.input.setAttribute("open", "true");
+ this.input.startLayoutExtend();
+
+ this.window.addEventListener("resize", this);
+ this.window.addEventListener("blur", this);
+
+ this.controller.notify(this.controller.NOTIFICATIONS.VIEW_OPEN);
+ }
+
+ /**
+ * Whether a result is a search suggestion.
+ * @param {UrlbarResult} result The result to examine.
+ * @returns {boolean} Whether the result is a search suggestion.
+ */
+ _resultIsSearchSuggestion(result) {
+ return Boolean(
+ result &&
+ result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
+ result.payload.suggestion
+ );
+ }
+
+ /**
+ * Checks whether the given row index can be update to the result we want
+ * to apply. This is used in _updateResults to avoid flickering of results, by
+ * reusing existing rows.
+ * @param {number} rowIndex Index of the row to examine.
+ * @param {UrlbarResult} result The result we'd like to apply.
+ * @param {number} firstSearchSuggestionIndex Index of the first search suggestion.
+ * @param {number} lastSearchSuggestionIndex Index of the last search suggestion.
+ * @returns {boolean} Whether the row can be updated to this result.
+ */
+ _rowCanUpdateToResult(
+ rowIndex,
+ result,
+ firstSearchSuggestionIndex,
+ lastSearchSuggestionIndex
+ ) {
+ // The heuristic result must always be current, thus it's always compatible.
+ if (result.heuristic) {
+ return true;
+ }
+ let row = this._rows.children[rowIndex];
+ let resultIsSearchSuggestion = this._resultIsSearchSuggestion(result);
+ // If the row is same type, just update it.
+ if (
+ resultIsSearchSuggestion == this._resultIsSearchSuggestion(row.result)
+ ) {
+ return true;
+ }
+ // If the row has a different type, update it if we are in a compatible
+ // index range.
+ // In practice we don't want to overwrite a search suggestion with a non
+ // search suggestion, but we allow the opposite.
+ return resultIsSearchSuggestion && rowIndex >= firstSearchSuggestionIndex;
+ }
+
+ _updateResults(queryContext) {
+ // TODO: For now this just compares search suggestions to the rest, in the
+ // future we should make it support any type of result. Or, even better,
+ // results should be grouped, thus we can directly update groups.
+
+ // Find where are existing search suggestions.
+ let firstSearchSuggestionIndex = -1;
+ let lastSearchSuggestionIndex = -1;
+ for (let i = 0; i < this._rows.children.length; ++i) {
+ let row = this._rows.children[i];
+ // Mark every row as stale, _updateRow will unmark them later.
+ row.setAttribute("stale", "true");
+ // Skip any row that isn't a search suggestion, or is non-visible because
+ // over maxResults.
+ if (
+ row.result.heuristic ||
+ i >= queryContext.maxResults ||
+ !this._resultIsSearchSuggestion(row.result)
+ ) {
+ continue;
+ }
+ if (firstSearchSuggestionIndex == -1) {
+ firstSearchSuggestionIndex = i;
+ }
+ lastSearchSuggestionIndex = i;
+ }
+
+ // Walk rows and find an insertion index for results. To avoid flicker, we
+ // skip rows until we find one compatible with the result we want to apply.
+ // If we couldn't find a compatible range, we'll just update.
+ let results = queryContext.results;
+ let resultIndex = 0;
+ // We can have more rows than the visible ones.
+ for (
+ let rowIndex = 0;
+ rowIndex < this._rows.children.length && resultIndex < results.length;
+ ++rowIndex
+ ) {
+ let row = this._rows.children[rowIndex];
+ let result = results[resultIndex];
+ if (
+ this._rowCanUpdateToResult(
+ rowIndex,
+ result,
+ firstSearchSuggestionIndex,
+ lastSearchSuggestionIndex
+ )
+ ) {
+ this._updateRow(row, result);
+ resultIndex++;
+ }
+ }
+ // Add remaining results, if we have fewer rows than results.
+ for (; resultIndex < results.length; ++resultIndex) {
+ let row = this._createRow();
+ this._updateRow(row, results[resultIndex]);
+ // Due to stale rows, we may have more rows than maxResults, thus we must
+ // hide them, and we'll revert this when stale rows are removed.
+ if (this._rows.children.length >= queryContext.maxResults) {
+ this._setRowVisibility(row, false);
+ }
+ this._rows.appendChild(row);
+ }
+
+ this._updateIndices();
+ }
+
+ _createRow() {
+ let item = this._createElement("div");
+ item.className = "urlbarView-row";
+ item.setAttribute("role", "option");
+ item._elements = new Map();
+ return item;
+ }
+
+ _createRowContent(item) {
+ // The url is the only element that can wrap, thus all the other elements
+ // are child of noWrap.
+ let noWrap = this._createElement("span");
+ noWrap.className = "urlbarView-no-wrap";
+ item._content.appendChild(noWrap);
+
+ let favicon = this._createElement("img");
+ favicon.className = "urlbarView-favicon";
+ noWrap.appendChild(favicon);
+ item._elements.set("favicon", favicon);
+
+ let typeIcon = this._createElement("span");
+ typeIcon.className = "urlbarView-type-icon";
+ noWrap.appendChild(typeIcon);
+
+ let tailPrefix = this._createElement("span");
+ tailPrefix.className = "urlbarView-tail-prefix";
+ noWrap.appendChild(tailPrefix);
+ item._elements.set("tailPrefix", tailPrefix);
+ // tailPrefix holds text only for alignment purposes so it should never be
+ // read to screen readers.
+ tailPrefix.toggleAttribute("aria-hidden", true);
+
+ let tailPrefixStr = this._createElement("span");
+ tailPrefixStr.className = "urlbarView-tail-prefix-string";
+ tailPrefix.appendChild(tailPrefixStr);
+ item._elements.set("tailPrefixStr", tailPrefixStr);
+
+ let tailPrefixChar = this._createElement("span");
+ tailPrefixChar.className = "urlbarView-tail-prefix-char";
+ tailPrefix.appendChild(tailPrefixChar);
+ item._elements.set("tailPrefixChar", tailPrefixChar);
+
+ let title = this._createElement("span");
+ title.className = "urlbarView-title";
+ noWrap.appendChild(title);
+ item._elements.set("title", title);
+
+ let tagsContainer = this._createElement("span");
+ tagsContainer.className = "urlbarView-tags";
+ noWrap.appendChild(tagsContainer);
+ item._elements.set("tagsContainer", tagsContainer);
+
+ let titleSeparator = this._createElement("span");
+ titleSeparator.className = "urlbarView-title-separator";
+ noWrap.appendChild(titleSeparator);
+ item._elements.set("titleSeparator", titleSeparator);
+
+ let action = this._createElement("span");
+ action.className = "urlbarView-action";
+ noWrap.appendChild(action);
+ item._elements.set("action", action);
+
+ let url = this._createElement("span");
+ url.className = "urlbarView-url";
+ item._content.appendChild(url);
+ item._elements.set("url", url);
+ }
+
+ _createRowContentForTip(item) {
+ // We use role="group" so screen readers will read the group's label when a
+ // button inside it gets focus. (Screen readers don't do this for
+ // role="option".) We set aria-labelledby for the group in _updateIndices.
+ item._content.setAttribute("role", "group");
+
+ let favicon = this._createElement("img");
+ favicon.className = "urlbarView-favicon";
+ favicon.setAttribute("data-l10n-id", "urlbar-tip-icon-description");
+ item._content.appendChild(favicon);
+ item._elements.set("favicon", favicon);
+
+ let title = this._createElement("span");
+ title.className = "urlbarView-title";
+ item._content.appendChild(title);
+ item._elements.set("title", title);
+
+ let buttonSpacer = this._createElement("span");
+ buttonSpacer.className = "urlbarView-tip-button-spacer";
+ item._content.appendChild(buttonSpacer);
+
+ let tipButton = this._createElement("span");
+ tipButton.className = "urlbarView-tip-button";
+ tipButton.setAttribute("role", "button");
+ item._content.appendChild(tipButton);
+ item._elements.set("tipButton", tipButton);
+
+ let helpIcon = this._createElement("span");
+ helpIcon.className = "urlbarView-tip-help";
+ helpIcon.setAttribute("role", "button");
+ helpIcon.setAttribute("data-l10n-id", "urlbar-tip-help-icon");
+ item._elements.set("helpButton", helpIcon);
+ item._content.appendChild(helpIcon);
+
+ // Due to role=button, the button and help icon can sometimes become
+ // focused. We want to prevent that because the input should always be
+ // focused instead. (This happens when input.search("", { focus: false })
+ // is called, a tip is the first result but not heuristic, and the user tabs
+ // the into the button from the navbar buttons. The input is skipped and
+ // the focus goes straight to the tip button.)
+ item.addEventListener("focus", () => this.input.focus(), true);
+ }
+
+ _createRowContentForDynamicType(item, result) {
+ let { dynamicType } = result.payload;
+ let viewTemplate = UrlbarView.dynamicViewTemplatesByName.get(dynamicType);
+ this._buildViewForDynamicType(
+ dynamicType,
+ item._content,
+ item._elements,
+ viewTemplate
+ );
+ }
+
+ _buildViewForDynamicType(type, parentNode, elementsByName, template) {
+ // Add classes to parentNode's classList.
+ for (let className of template.classList || []) {
+ parentNode.classList.add(className);
+ }
+ // Set attributes on parentNode.
+ for (let [name, value] of Object.entries(template.attributes || {})) {
+ if (name == "id") {
+ // We do not allow dynamic results to set IDs for their Nodes. IDs are
+ // managed by the view to ensure they are unique.
+ Cu.reportError(
+ "Dynamic results are prohibited from setting their own IDs."
+ );
+ continue;
+ }
+ parentNode.setAttribute(name, value);
+ }
+ if (template.name) {
+ parentNode.setAttribute("name", template.name);
+ elementsByName.set(template.name, parentNode);
+ }
+ // Recurse into children.
+ for (let childTemplate of template.children || []) {
+ let child = this._createElement(childTemplate.tag);
+ child.classList.add(`urlbarView-dynamic-${type}-${childTemplate.name}`);
+ parentNode.appendChild(child);
+ this._buildViewForDynamicType(type, child, elementsByName, childTemplate);
+ }
+ }
+
+ _updateRow(item, result) {
+ let oldResult = item.result;
+ let oldResultType = item.result && item.result.type;
+ item.result = result;
+ item.removeAttribute("stale");
+ item.id = getUniqueId("urlbarView-row-");
+
+ let needsNewContent =
+ oldResultType === undefined ||
+ (oldResultType == UrlbarUtils.RESULT_TYPE.TIP) !=
+ (result.type == UrlbarUtils.RESULT_TYPE.TIP) ||
+ (oldResultType == UrlbarUtils.RESULT_TYPE.DYNAMIC) !=
+ (result.type == UrlbarUtils.RESULT_TYPE.DYNAMIC) ||
+ (oldResultType == UrlbarUtils.RESULT_TYPE.DYNAMIC &&
+ result.type == UrlbarUtils.RESULT_TYPE.DYNAMIC &&
+ oldResult.dynamicType != result.dynamicType);
+
+ if (needsNewContent) {
+ if (item._content) {
+ item._content.remove();
+ item._elements.clear();
+ }
+ item._content = this._createElement("span");
+ item._content.className = "urlbarView-row-inner";
+ item.appendChild(item._content);
+ item.removeAttribute("dynamicType");
+ if (item.result.type == UrlbarUtils.RESULT_TYPE.TIP) {
+ this._createRowContentForTip(item);
+ } else if (item.result.type == UrlbarUtils.RESULT_TYPE.DYNAMIC) {
+ this._createRowContentForDynamicType(item, result);
+ } else {
+ this._createRowContent(item);
+ }
+ }
+
+ if (
+ result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
+ !result.payload.providesSearchMode &&
+ !result.payload.inPrivateWindow
+ ) {
+ item.setAttribute("type", "search");
+ } else if (result.type == UrlbarUtils.RESULT_TYPE.REMOTE_TAB) {
+ item.setAttribute("type", "remotetab");
+ } else if (result.type == UrlbarUtils.RESULT_TYPE.TAB_SWITCH) {
+ item.setAttribute("type", "switchtab");
+ } else if (result.type == UrlbarUtils.RESULT_TYPE.TIP) {
+ item.setAttribute("type", "tip");
+ this._updateRowForTip(item, result);
+ return;
+ } else if (result.source == UrlbarUtils.RESULT_SOURCE.BOOKMARKS) {
+ item.setAttribute("type", "bookmark");
+ } else if (result.type == UrlbarUtils.RESULT_TYPE.DYNAMIC) {
+ item.setAttribute("type", "dynamic");
+ this._updateRowForDynamicType(item, result);
+ return;
+ } else if (result.providerName == "TabToSearch") {
+ item.setAttribute("type", "tabtosearch");
+ } else {
+ item.removeAttribute("type");
+ }
+
+ let favicon = item._elements.get("favicon");
+ if (
+ result.type == UrlbarUtils.RESULT_TYPE.SEARCH ||
+ result.type == UrlbarUtils.RESULT_TYPE.KEYWORD
+ ) {
+ favicon.src = this._iconForResult(result);
+ } else {
+ favicon.src = result.payload.icon || UrlbarUtils.ICON.DEFAULT;
+ }
+
+ let title = item._elements.get("title");
+ this._setResultTitle(result, title);
+
+ if (result.payload.tail && result.payload.tailOffsetIndex > 0) {
+ this._fillTailSuggestionPrefix(item, result);
+ title.setAttribute("aria-label", result.payload.suggestion);
+ item.toggleAttribute("tail-suggestion", true);
+ } else {
+ item.removeAttribute("tail-suggestion");
+ title.removeAttribute("aria-label");
+ }
+
+ title._tooltip = result.title;
+ if (title.hasAttribute("overflow")) {
+ title.setAttribute("title", title._tooltip);
+ }
+
+ let tagsContainer = item._elements.get("tagsContainer");
+ tagsContainer.textContent = "";
+ if (result.payload.tags && result.payload.tags.length) {
+ tagsContainer.append(
+ ...result.payload.tags.map((tag, i) => {
+ const element = this._createElement("span");
+ element.className = "urlbarView-tag";
+ this._addTextContentWithHighlights(
+ element,
+ tag,
+ result.payloadHighlights.tags[i]
+ );
+ return element;
+ })
+ );
+ }
+
+ let action = item._elements.get("action");
+ let actionSetter = null;
+ let isVisitAction = false;
+ let setURL = false;
+ switch (result.type) {
+ case UrlbarUtils.RESULT_TYPE.TAB_SWITCH:
+ actionSetter = () => {
+ this.document.l10n.setAttributes(
+ action,
+ "urlbar-result-action-switch-tab"
+ );
+ };
+ setURL = true;
+ break;
+ case UrlbarUtils.RESULT_TYPE.REMOTE_TAB:
+ actionSetter = () => {
+ action.removeAttribute("data-l10n-id");
+ action.textContent = result.payload.device;
+ };
+ setURL = true;
+ break;
+ case UrlbarUtils.RESULT_TYPE.SEARCH:
+ if (result.payload.inPrivateWindow) {
+ if (result.payload.isPrivateEngine) {
+ actionSetter = () => {
+ this.document.l10n.setAttributes(
+ action,
+ "urlbar-result-action-search-in-private-w-engine",
+ { engine: result.payload.engine }
+ );
+ };
+ } else {
+ actionSetter = () => {
+ this.document.l10n.setAttributes(
+ action,
+ "urlbar-result-action-search-in-private"
+ );
+ };
+ }
+ } else if (result.providerName == "TabToSearch") {
+ actionSetter = () => {
+ this.document.l10n.setAttributes(
+ action,
+ UrlbarUtils.WEB_ENGINE_NAMES.has(result.payload.engine)
+ ? "urlbar-result-action-tabtosearch-web"
+ : "urlbar-result-action-tabtosearch-other-engine",
+ { engine: result.payload.engine }
+ );
+ };
+ } else if (!result.payload.providesSearchMode) {
+ actionSetter = () => {
+ this.document.l10n.setAttributes(
+ action,
+ "urlbar-result-action-search-w-engine",
+ { engine: result.payload.engine }
+ );
+ };
+ }
+ break;
+ case UrlbarUtils.RESULT_TYPE.KEYWORD:
+ isVisitAction = result.payload.input.trim() == result.payload.keyword;
+ break;
+ case UrlbarUtils.RESULT_TYPE.OMNIBOX:
+ actionSetter = () => {
+ action.removeAttribute("data-l10n-id");
+ action.textContent = result.payload.content;
+ };
+ break;
+ default:
+ if (result.heuristic) {
+ isVisitAction = true;
+ } else {
+ setURL = true;
+ }
+ break;
+ }
+
+ if (result.providerName == "TabToSearch") {
+ action.toggleAttribute("slide-in", true);
+ } else {
+ action.removeAttribute("slide-in");
+ }
+
+ if (result.payload.isPinned) {
+ item.toggleAttribute("pinned", true);
+ } else {
+ item.removeAttribute("pinned");
+ }
+
+ if (
+ result.payload.isSponsored &&
+ result.type != UrlbarUtils.RESULT_TYPE.TAB_SWITCH
+ ) {
+ item.toggleAttribute("sponsored", true);
+ actionSetter = () => {
+ this.document.l10n.setAttributes(
+ action,
+ "urlbar-result-action-sponsored"
+ );
+ };
+ } else {
+ item.removeAttribute("sponsored");
+ }
+
+ let url = item._elements.get("url");
+ if (setURL) {
+ item.setAttribute("has-url", "true");
+ this._addTextContentWithHighlights(
+ url,
+ result.payload.displayUrl,
+ result.payloadHighlights.displayUrl || []
+ );
+ url._tooltip = result.payload.displayUrl;
+ } else {
+ item.removeAttribute("has-url");
+ url.textContent = "";
+ url._tooltip = "";
+ }
+ if (url.hasAttribute("overflow")) {
+ url.setAttribute("title", url._tooltip);
+ }
+
+ if (isVisitAction) {
+ actionSetter = () => {
+ this.document.l10n.setAttributes(action, "urlbar-result-action-visit");
+ };
+ title.setAttribute("isurl", "true");
+ } else {
+ title.removeAttribute("isurl");
+ }
+
+ if (actionSetter) {
+ actionSetter();
+ item._originalActionSetter = actionSetter;
+ item.setAttribute("has-action", "true");
+ } else {
+ item._originalActionSetter = () => {
+ action.removeAttribute("data-l10n-id");
+ action.textContent = "";
+ };
+ item._originalActionSetter();
+ item.removeAttribute("has-action");
+ }
+
+ if (!title.hasAttribute("isurl")) {
+ title.setAttribute("dir", "auto");
+ } else {
+ title.removeAttribute("dir");
+ }
+ }
+
+ _iconForResult(result, iconUrlOverride = null) {
+ return (
+ (result.source == UrlbarUtils.RESULT_SOURCE.HISTORY &&
+ (result.type == UrlbarUtils.RESULT_TYPE.SEARCH ||
+ result.type == UrlbarUtils.RESULT_TYPE.KEYWORD) &&
+ UrlbarUtils.ICON.HISTORY) ||
+ iconUrlOverride ||
+ result.payload.icon ||
+ ((result.type == UrlbarUtils.RESULT_TYPE.SEARCH ||
+ result.type == UrlbarUtils.RESULT_TYPE.KEYWORD) &&
+ UrlbarUtils.ICON.SEARCH_GLASS) ||
+ UrlbarUtils.ICON.DEFAULT
+ );
+ }
+
+ _updateRowForTip(item, result) {
+ let favicon = item._elements.get("favicon");
+ favicon.src = result.payload.icon || UrlbarUtils.ICON.TIP;
+ favicon.id = item.id + "-icon";
+
+ let title = item._elements.get("title");
+ title.id = item.id + "-title";
+ // Add-ons will provide text, rather than l10n ids.
+ if (result.payload.textData) {
+ this.document.l10n.setAttributes(
+ title,
+ result.payload.textData.id,
+ result.payload.textData.args
+ );
+ } else {
+ title.textContent = result.payload.text;
+ }
+
+ item._content.setAttribute("aria-labelledby", `${favicon.id} ${title.id}`);
+
+ let tipButton = item._elements.get("tipButton");
+ tipButton.id = item.id + "-tip-button";
+ // Add-ons will provide buttonText, rather than l10n ids.
+ if (result.payload.buttonTextData) {
+ this.document.l10n.setAttributes(
+ tipButton,
+ result.payload.buttonTextData.id,
+ result.payload.buttonTextData.args
+ );
+ } else {
+ tipButton.textContent = result.payload.buttonText;
+ }
+
+ let helpIcon = item._elements.get("helpButton");
+ helpIcon.id = item.id + "-tip-help";
+ helpIcon.style.display = result.payload.helpUrl ? "" : "none";
+
+ if (result.providerName == "UrlbarProviderSearchTips") {
+ // For a11y, we treat search tips as alerts. We use A11yUtils.announce
+ // instead of role="alert" because role="alert" will only fire an alert
+ // event when the alert (or something inside it) is the root of an
+ // insertion. In this case, the entire tip result gets inserted into the
+ // a11y tree as a single insertion, so no alert event would be fired.
+ this.window.A11yUtils.announce(result.payload.textData);
+ }
+ }
+
+ async _updateRowForDynamicType(item, result) {
+ item.setAttribute("dynamicType", result.payload.dynamicType);
+
+ let idsByName = new Map();
+ for (let [name, node] of item._elements) {
+ node.id = `${item.id}-${name}`;
+ idsByName.set(name, node.id);
+ }
+
+ // First, apply highlighting. We do this before updating via getViewUpdate
+ // so the dynamic provider can override the highlighting by setting the
+ // textContent of the highlighted node, if it wishes.
+ for (let [payloadName, highlights] of Object.entries(
+ result.payloadHighlights
+ )) {
+ if (!highlights.length) {
+ continue;
+ }
+ // Highlighting only works if the dynamic element name is the same as the
+ // highlighted payload property name.
+ let nodeToHighlight = item.querySelector(`#${item.id}-${payloadName}`);
+ this._addTextContentWithHighlights(
+ nodeToHighlight,
+ result.payload[payloadName],
+ highlights
+ );
+ }
+
+ // Get the view update from the result's provider.
+ let provider = UrlbarProvidersManager.getProvider(result.providerName);
+ let viewUpdate = await provider.getViewUpdate(result, idsByName);
+
+ // Update each node in the view by name.
+ for (let [nodeName, update] of Object.entries(viewUpdate)) {
+ let node = item.querySelector(`#${item.id}-${nodeName}`);
+ for (let [attrName, value] of Object.entries(update.attributes || {})) {
+ if (attrName == "id") {
+ // We do not allow dynamic results to set IDs for their Nodes. IDs are
+ // managed by the view to ensure they are unique.
+ Cu.reportError(
+ "Dynamic results are prohibited from setting their own IDs."
+ );
+ continue;
+ }
+ node.setAttribute(attrName, value);
+ }
+ for (let [styleName, value] of Object.entries(update.style || {})) {
+ node.style[styleName] = value;
+ }
+ if (update.l10n) {
+ this.document.l10n.setAttributes(
+ node,
+ update.l10n.id,
+ update.l10n.args || undefined
+ );
+ } else if (update.textContent) {
+ node.textContent = update.textContent;
+ }
+ }
+ }
+
+ _updateIndices() {
+ let visibleRowsExist = false;
+ for (let i = 0; i < this._rows.children.length; i++) {
+ let item = this._rows.children[i];
+ item.result.rowIndex = i;
+ visibleRowsExist = visibleRowsExist || this._isElementVisible(item);
+ }
+ let selectableElement = this._getFirstSelectableElement();
+ let uiIndex = 0;
+ while (selectableElement) {
+ selectableElement.elementIndex = uiIndex++;
+ selectableElement = this._getNextSelectableElement(selectableElement);
+ }
+ if (visibleRowsExist) {
+ this.panel.removeAttribute("noresults");
+ } else {
+ this.panel.setAttribute("noresults", "true");
+ }
+ }
+
+ _setRowVisibility(row, visible) {
+ row.style.display = visible ? "" : "none";
+ if (
+ !visible &&
+ row.result.type != UrlbarUtils.RESULT_TYPE.TIP &&
+ row.result.type != UrlbarUtils.RESULT_TYPE.DYNAMIC
+ ) {
+ // Reset the overflow state of elements that can overflow in case their
+ // content changes while they're hidden. When making the row visible
+ // again, we'll get new overflow events if needed.
+ this._setElementOverflowing(row._elements.get("title"), false);
+ this._setElementOverflowing(row._elements.get("url"), false);
+ }
+ }
+
+ /**
+ * Returns true if the given element and its row are both visible.
+ *
+ * @param {Element} element
+ * An element in the view.
+ * @returns {boolean}
+ * True if the given element and its row are both visible.
+ */
+ _isElementVisible(element) {
+ if (!element || element.style.display == "none") {
+ return false;
+ }
+ let row = element.closest(".urlbarView-row");
+ return row && row.style.display != "none";
+ }
+
+ _removeStaleRows() {
+ let row = this._rows.lastElementChild;
+ while (row) {
+ let next = row.previousElementSibling;
+ if (row.hasAttribute("stale")) {
+ row.remove();
+ } else {
+ this._setRowVisibility(row, true);
+ }
+ row = next;
+ }
+ this._updateIndices();
+ }
+
+ _startRemoveStaleRowsTimer() {
+ this._removeStaleRowsTimer = this.window.setTimeout(() => {
+ this._removeStaleRowsTimer = null;
+ this._removeStaleRows();
+ }, UrlbarView.removeStaleRowsTimeout);
+ }
+
+ _cancelRemoveStaleRowsTimer() {
+ if (this._removeStaleRowsTimer) {
+ this.window.clearTimeout(this._removeStaleRowsTimer);
+ this._removeStaleRowsTimer = null;
+ }
+ }
+
+ _selectElement(item, { updateInput = true, setAccessibleFocus = true } = {}) {
+ if (this._selectedElement) {
+ this._selectedElement.toggleAttribute("selected", false);
+ this._selectedElement.removeAttribute("aria-selected");
+ }
+ if (item) {
+ item.toggleAttribute("selected", true);
+ item.setAttribute("aria-selected", "true");
+ }
+ this._setAccessibleFocus(setAccessibleFocus && item);
+ this._selectedElement = item;
+
+ let result = item?.closest(".urlbarView-row")?.result;
+ if (updateInput) {
+ this.input.setValueFromResult(result);
+ } else {
+ this.input.setResultForCurrentValue(result);
+ }
+
+ let provider = UrlbarProvidersManager.getProvider(result?.providerName);
+ if (provider) {
+ provider.tryMethod("onSelection", result, item);
+ }
+ }
+
+ /**
+ * Returns true if the given element is selectable.
+ *
+ * @param {Element} element
+ * The element to test.
+ * @returns {boolean}
+ * True if the element is selectable and false if not.
+ */
+ _isSelectableElement(element) {
+ return this.getClosestSelectableElement(element) == element;
+ }
+
+ /**
+ * Returns the first selectable element in the view.
+ *
+ * @returns {Element}
+ * The first selectable element in the view.
+ */
+ _getFirstSelectableElement() {
+ let element = this._rows.firstElementChild;
+ if (element && !this._isSelectableElement(element)) {
+ element = this._getNextSelectableElement(element);
+ }
+ return element;
+ }
+
+ /**
+ * Returns the last selectable element in the view.
+ *
+ * @returns {Element}
+ * The last selectable element in the view.
+ */
+ _getLastSelectableElement() {
+ let element = this._rows.lastElementChild;
+ if (element && !this._isSelectableElement(element)) {
+ element = this._getPreviousSelectableElement(element);
+ }
+ return element;
+ }
+
+ /**
+ * Returns the next selectable element after the given element. If the
+ * element is the last selectable element, returns null.
+ *
+ * @param {Element} element
+ * An element in the view.
+ * @returns {Element}
+ * The next selectable element after `element` or null if `element` is the
+ * last selectable element.
+ */
+ _getNextSelectableElement(element) {
+ let row = element.closest(".urlbarView-row");
+ if (!row) {
+ return null;
+ }
+
+ let next;
+ if (
+ row.result.type == UrlbarUtils.RESULT_TYPE.TIP ||
+ row.result.type == UrlbarUtils.RESULT_TYPE.DYNAMIC
+ ) {
+ let selectables = [...row.querySelectorAll(SELECTABLE_ELEMENT_SELECTOR)];
+ let index = selectables.indexOf(element);
+ if (index == selectables.length - 1) {
+ next = row.nextElementSibling;
+ } else {
+ next = selectables[index + 1];
+ }
+ } else {
+ next = row.nextElementSibling;
+ }
+
+ if (next && !this._isSelectableElement(next)) {
+ next = this._getNextSelectableElement(next);
+ }
+
+ return next;
+ }
+
+ /**
+ * Returns the previous selectable element before the given element. If the
+ * element is the first selectable element, returns null.
+ *
+ * @param {Element} element
+ * An element in the view.
+ * @returns {Element}
+ * The previous selectable element before `element` or null if `element` is
+ * the first selectable element.
+ */
+ _getPreviousSelectableElement(element) {
+ let row = element.closest(".urlbarView-row");
+ if (!row) {
+ return null;
+ }
+
+ let previous;
+ if (
+ row.result.type == UrlbarUtils.RESULT_TYPE.TIP ||
+ row.result.type == UrlbarUtils.RESULT_TYPE.DYNAMIC
+ ) {
+ let selectables = [...row.querySelectorAll(SELECTABLE_ELEMENT_SELECTOR)];
+ let index = selectables.indexOf(element);
+ if (index == 0 || !selectables.length) {
+ previous = row.previousElementSibling;
+ } else if (index < 0) {
+ previous = selectables[selectables.length - 1];
+ } else {
+ previous = selectables[index - 1];
+ }
+ } else {
+ previous = row.previousElementSibling;
+ }
+
+ if (previous && !this._isSelectableElement(previous)) {
+ previous = this._getPreviousSelectableElement(previous);
+ }
+
+ return previous;
+ }
+
+ /**
+ * Returns the currently selected row. Useful when this._selectedElement may
+ * be a non-row element, such as a descendant element of RESULT_TYPE.TIP.
+ *
+ * @returns {Element}
+ * The currently selected row, or ancestor row of the currently selected
+ * item.
+ */
+ _getSelectedRow() {
+ if (!this.isOpen || !this._selectedElement) {
+ return null;
+ }
+ let selected = this._selectedElement;
+
+ if (!selected.classList.contains("urlbarView-row")) {
+ // selected may be an element in a result group, like RESULT_TYPE.TIP.
+ selected = selected.closest(".urlbarView-row");
+ }
+
+ return selected;
+ }
+
+ /**
+ * @param {Element} element
+ * An element that is potentially a row or descendant of a row.
+ * @returns {Element}
+ * The row containing `element`, or `element` itself if it is a row.
+ */
+ _getRowFromElement(element) {
+ if (!this.isOpen || !element) {
+ return null;
+ }
+
+ if (!element.classList.contains("urlbarView-row")) {
+ element = element.closest(".urlbarView-row");
+ }
+
+ return element;
+ }
+
+ _setAccessibleFocus(item) {
+ if (item) {
+ this.input.inputField.setAttribute("aria-activedescendant", item.id);
+ } else {
+ this.input.inputField.removeAttribute("aria-activedescendant");
+ }
+ }
+
+ /**
+ * Sets `result`'s title in `titleNode`'s DOM.
+ * @param {UrlbarResult} result
+ * The result for which the title is being set.
+ * @param {Node} titleNode
+ * The DOM node for the result's tile.
+ */
+ _setResultTitle(result, titleNode) {
+ if (result.payload.providesSearchMode) {
+ // Keyword offers are the only result that require a localized title.
+ // We localize the title instead of using the action text as a title
+ // because some keyword offer results use both a title and action text
+ // (e.g. tab-to-search).
+ this.document.l10n.setAttributes(
+ titleNode,
+ "urlbar-result-action-search-w-engine",
+ { engine: result.payload.engine }
+ );
+ return;
+ }
+
+ titleNode.removeAttribute("data-l10n-id");
+ this._addTextContentWithHighlights(
+ titleNode,
+ result.title,
+ result.titleHighlights
+ );
+ }
+
+ /**
+ * Adds text content to a node, placing substrings that should be highlighted
+ * inside <em> nodes.
+ *
+ * @param {Node} parentNode
+ * The text content will be added to this node.
+ * @param {string} textContent
+ * The text content to give the node.
+ * @param {array} highlights
+ * The matches to highlight in the text.
+ */
+ _addTextContentWithHighlights(parentNode, textContent, highlights) {
+ parentNode.textContent = "";
+ if (!textContent) {
+ return;
+ }
+ highlights = (highlights || []).concat([[textContent.length, 0]]);
+ let index = 0;
+ for (let [highlightIndex, highlightLength] of highlights) {
+ if (highlightIndex - index > 0) {
+ parentNode.appendChild(
+ this.document.createTextNode(
+ textContent.substring(index, highlightIndex)
+ )
+ );
+ }
+ if (highlightLength > 0) {
+ let strong = this._createElement("strong");
+ strong.textContent = textContent.substring(
+ highlightIndex,
+ highlightIndex + highlightLength
+ );
+ parentNode.appendChild(strong);
+ }
+ index = highlightIndex + highlightLength;
+ }
+ }
+
+ /**
+ * Adds markup for a tail suggestion prefix to a row.
+ * @param {Node} item
+ * The node for the result row.
+ * @param {UrlbarResult} result
+ * A UrlbarResult representing a tail suggestion.
+ */
+ _fillTailSuggestionPrefix(item, result) {
+ let tailPrefixStrNode = item._elements.get("tailPrefixStr");
+ let tailPrefixStr = result.payload.suggestion.substring(
+ 0,
+ result.payload.tailOffsetIndex
+ );
+ tailPrefixStrNode.textContent = tailPrefixStr;
+
+ let tailPrefixCharNode = item._elements.get("tailPrefixChar");
+ tailPrefixCharNode.textContent = result.payload.tailPrefix;
+ }
+
+ _enableOrDisableRowWrap() {
+ if (getBoundsWithoutFlushing(this.input.textbox).width < 650) {
+ this._rows.setAttribute("wrap", "true");
+ } else {
+ this._rows.removeAttribute("wrap");
+ }
+ }
+
+ _setElementOverflowing(element, overflowing) {
+ element.toggleAttribute("overflow", overflowing);
+ if (overflowing) {
+ element.setAttribute("title", element._tooltip);
+ } else {
+ element.removeAttribute("title");
+ }
+ }
+
+ /**
+ * If the view is open and showing a single search tip, this method picks it
+ * and closes the view. This counts as an engagement, so this method should
+ * only be called due to user interaction.
+ *
+ * @param {event} event
+ * The user-initiated event for the interaction. Should not be null.
+ * @returns {boolean}
+ * True if this method picked a tip, false otherwise.
+ */
+ _pickSearchTipIfPresent(event) {
+ if (
+ !this.isOpen ||
+ !this._queryContext ||
+ this._queryContext.results.length != 1
+ ) {
+ return false;
+ }
+ let result = this._queryContext.results[0];
+ if (result.type != UrlbarUtils.RESULT_TYPE.TIP) {
+ return false;
+ }
+ let tipButton = this._rows.firstElementChild.querySelector(
+ ".urlbarView-tip-button"
+ );
+ if (!tipButton) {
+ throw new Error("Expected a tip button");
+ }
+ this.input.pickElement(tipButton, event);
+ return true;
+ }
+
+ // Event handlers below.
+
+ _on_SelectedOneOffButtonChanged() {
+ if (!this.isOpen || !this._queryContext) {
+ return;
+ }
+
+ let engine = this.oneOffSearchButtons.selectedButton?.engine;
+ let source = this.oneOffSearchButtons.selectedButton?.source;
+
+ let localSearchMode;
+ if (source) {
+ localSearchMode = UrlbarUtils.LOCAL_SEARCH_MODES.find(
+ m => m.source == source
+ );
+ }
+
+ for (let item of this._rows.children) {
+ let result = item.result;
+
+ let isPrivateSearchWithoutPrivateEngine =
+ result.payload.inPrivateWindow && !result.payload.isPrivateEngine;
+ let isSearchHistory =
+ result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
+ result.source == UrlbarUtils.RESULT_SOURCE.HISTORY;
+ let isSearchSuggestion = result.payload.suggestion && !isSearchHistory;
+
+ // For one-off buttons having a source, we update the action for the
+ // heuristic result, or for any non-heuristic that is a remote search
+ // suggestion or a private search with no private engine.
+ if (
+ !result.heuristic &&
+ !isSearchSuggestion &&
+ !isPrivateSearchWithoutPrivateEngine
+ ) {
+ continue;
+ }
+
+ // If there is no selected button and we are in full search mode, it is
+ // because the user just confirmed a one-off button, thus starting a new
+ // query. Don't change the heuristic result because it would be
+ // immediately replaced with the search mode heuristic, causing flicker.
+ if (
+ result.heuristic &&
+ !engine &&
+ !localSearchMode &&
+ this.input.searchMode &&
+ !this.input.searchMode.isPreview
+ ) {
+ continue;
+ }
+
+ let action = item.querySelector(".urlbarView-action");
+ let favicon = item.querySelector(".urlbarView-favicon");
+ let title = item.querySelector(".urlbarView-title");
+
+ // If a one-off button is the only selection, force the heuristic result
+ // to show its action text, so the engine name is visible.
+ if (
+ result.heuristic &&
+ !this.selectedElement &&
+ (localSearchMode || engine)
+ ) {
+ item.setAttribute("show-action-text", "true");
+ } else {
+ item.removeAttribute("show-action-text");
+ }
+
+ // If an engine is selected, update search results to use that engine.
+ // Otherwise, restore their original engines.
+ if (result.type == UrlbarUtils.RESULT_TYPE.SEARCH) {
+ if (engine) {
+ if (!result.payload.originalEngine) {
+ result.payload.originalEngine = result.payload.engine;
+ }
+ result.payload.engine = engine.name;
+ } else if (result.payload.originalEngine) {
+ result.payload.engine = result.payload.originalEngine;
+ delete result.payload.originalEngine;
+ }
+ }
+
+ // If the result is the heuristic and a one-off is selected (i.e.,
+ // localSearchMode || engine), then restyle it to look like a search
+ // result; otherwise, remove such styling. For restyled results, we
+ // override the usual result-picking behaviour in UrlbarInput.pickResult.
+ if (result.heuristic) {
+ title.textContent =
+ localSearchMode || engine
+ ? this._queryContext.searchString
+ : result.title;
+
+ // Set the restyled-search attribute so the action text and title
+ // separator are shown or hidden via CSS as appropriate.
+ if (localSearchMode || engine) {
+ item.setAttribute("restyled-search", "true");
+ } else {
+ item.removeAttribute("restyled-search");
+ }
+ }
+
+ // Update result action text.
+ if (localSearchMode) {
+ // Update the result action text for a local one-off.
+ let name = UrlbarUtils.getResultSourceName(localSearchMode.source);
+ this.document.l10n.setAttributes(
+ action,
+ `urlbar-result-action-search-${name}`
+ );
+ if (result.heuristic) {
+ item.setAttribute("source", name);
+ }
+ } else if (engine && !result.payload.inPrivateWindow) {
+ // Update the result action text for an engine one-off.
+ this.document.l10n.setAttributes(
+ action,
+ "urlbar-result-action-search-w-engine",
+ { engine: engine.name }
+ );
+ } else {
+ // No one-off is selected. If we replaced the action while a one-off
+ // button was selected, it should be restored.
+ if (item._originalActionSetter) {
+ item._originalActionSetter();
+ if (result.heuristic) {
+ favicon.src = result.payload.icon || UrlbarUtils.ICON.DEFAULT;
+ }
+ } else {
+ Cu.reportError("An item is missing the action setter");
+ }
+ item.removeAttribute("source");
+ }
+
+ // Update result favicons.
+ let iconOverride = localSearchMode?.icon || engine?.iconURI?.spec;
+ if (!iconOverride && (localSearchMode || engine)) {
+ // For one-offs without an icon, do not allow restyled URL results to
+ // use their own icons.
+ iconOverride = UrlbarUtils.ICON.SEARCH_GLASS;
+ }
+ if (
+ result.heuristic ||
+ (result.payload.inPrivateWindow && !result.payload.isPrivateEngine)
+ ) {
+ // If we just changed the engine from the original engine and it had an
+ // icon, then make sure the result now uses the new engine's icon or
+ // failing that the default icon. If we changed it back to the original
+ // engine, go back to the original or default icon.
+ favicon.src = this._iconForResult(result, iconOverride);
+ }
+ }
+ }
+
+ _on_blur(event) {
+ // If the view is open without the input being focused, it will not close
+ // automatically when the window loses focus. We might be in this state
+ // after a Search Tip is shown on an engine homepage.
+ if (!UrlbarPrefs.get("ui.popup.disable_autohide")) {
+ this.close();
+ }
+ }
+
+ _on_mousedown(event) {
+ if (event.button == 2) {
+ // Ignore right clicks.
+ return;
+ }
+ let element = this.getClosestSelectableElement(event.target);
+ if (!element) {
+ // Ignore clicks on elements that can't be selected/picked.
+ return;
+ }
+ this._selectElement(element, { updateInput: false });
+ this.controller.speculativeConnect(
+ this.selectedResult,
+ this._queryContext,
+ "mousedown"
+ );
+ }
+
+ _on_mouseup(event) {
+ if (event.button == 2) {
+ // Ignore right clicks.
+ return;
+ }
+ let element = this.getClosestSelectableElement(event.target);
+ if (!element) {
+ // Ignore clicks on elements that can't be selected/picked.
+ return;
+ }
+ this.input.pickElement(element, event);
+ }
+
+ _on_overflow(event) {
+ if (
+ event.detail == 1 &&
+ (event.target.classList.contains("urlbarView-url") ||
+ event.target.classList.contains("urlbarView-title"))
+ ) {
+ this._setElementOverflowing(event.target, true);
+ }
+ }
+
+ _on_underflow(event) {
+ if (
+ event.detail == 1 &&
+ (event.target.classList.contains("urlbarView-url") ||
+ event.target.classList.contains("urlbarView-title"))
+ ) {
+ this._setElementOverflowing(event.target, false);
+ }
+ }
+
+ _on_resize() {
+ this._enableOrDisableRowWrap();
+ }
+}
+
+UrlbarView.removeStaleRowsTimeout = DEFAULT_REMOVE_STALE_ROWS_TIMEOUT;
+
+/**
+ * Implements a QueryContext cache, working as a circular buffer, when a new
+ * entry is added at the top, the last item is remove from the bottom.
+ */
+class QueryContextCache {
+ /**
+ * Constructor.
+ * @param {number} size The number of entries to keep in the cache.
+ */
+ constructor(size) {
+ this.size = size;
+ this._cache = [];
+ }
+
+ /**
+ * Adds a new entry to the cache.
+ * @param {UrlbarQueryContext} queryContext The UrlbarQueryContext to add.
+ * @note QueryContexts without a searchString or without results are ignored
+ * and not added.
+ */
+ put(queryContext) {
+ let searchString = queryContext.searchString;
+ if (!searchString || !queryContext.results.length) {
+ return;
+ }
+
+ let index = this._cache.findIndex(e => e.searchString == searchString);
+ if (index != -1) {
+ if (this._cache[index] == queryContext) {
+ return;
+ }
+ this._cache.splice(index, 1);
+ }
+ if (this._cache.unshift(queryContext) > this.size) {
+ this._cache.length = this.size;
+ }
+ }
+
+ get(searchString) {
+ return this._cache.find(e => e.searchString == searchString);
+ }
+}
+
+/**
+ * Adds a dynamic result type stylesheet to a specified window.
+ *
+ * @param {Window} window
+ * The window to which to add the stylesheet.
+ * @param {string} stylesheetURL
+ * The stylesheet's URL.
+ */
+async function addDynamicStylesheet(window, stylesheetURL) {
+ // Try-catch all of these so that failing to load a stylesheet doesn't break
+ // callers and possibly the urlbar. If a stylesheet does fail to load, the
+ // dynamic results that depend on it will appear broken, but at least we
+ // won't break the whole urlbar.
+ try {
+ let uri = Services.io.newURI(stylesheetURL);
+ let sheet = await styleSheetService.preloadSheetAsync(
+ uri,
+ Ci.nsIStyleSheetService.AGENT_SHEET
+ );
+ window.windowUtils.addSheet(sheet, Ci.nsIDOMWindowUtils.AGENT_SHEET);
+ } catch (ex) {
+ Cu.reportError(`Error adding dynamic stylesheet: ${ex}`);
+ }
+}
+
+/**
+ * Removes a dynamic result type stylesheet from the view's window.
+ *
+ * @param {Window} window
+ * The window from which to remove the stylesheet.
+ * @param {string} stylesheetURL
+ * The stylesheet's URL.
+ */
+function removeDynamicStylesheet(window, stylesheetURL) {
+ // Try-catch for the same reason as desribed in addDynamicStylesheet.
+ try {
+ window.windowUtils.removeSheetUsingURIString(
+ stylesheetURL,
+ Ci.nsIDOMWindowUtils.AGENT_SHEET
+ );
+ } catch (ex) {
+ Cu.reportError(`Error removing dynamic stylesheet: ${ex}`);
+ }
+}
diff --git a/browser/components/urlbar/content/interventions.ftl b/browser/components/urlbar/content/interventions.ftl
new file mode 100644
index 0000000000..01f97e61d8
--- /dev/null
+++ b/browser/components/urlbar/content/interventions.ftl
@@ -0,0 +1,40 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+### These strings appear in Urlbar Interventions. Interventions appear in the
+### Urlbar in response to the user's query. For example, if we detect that the
+### user is searching how to clear their history, we show the Intervention
+### described by clear-data.
+
+intervention-clear-data = Clear your cache, cookies, history and more.
+intervention-clear-data-confirm = Choose What to Clear…
+intervention-refresh-profile = Restore default settings and remove old add-ons for optimal performance.
+intervention-refresh-profile-confirm = Refresh { -brand-short-name }…
+
+## These strings describe Interventions helping the user with the Firefox update
+## process.
+
+## Shown when an update is available to download.
+
+intervention-update-ask = A new version of { -brand-short-name } is available.
+intervention-update-ask-confirm = Install and Restart to Update
+
+## Shown when Firefox does not need to update so instead we offer to refresh
+## the user's profile.
+
+intervention-update-refresh = { -brand-short-name } is up to date. Trying to fix a problem? Restore default settings and remove old add-ons for optimal performance.
+intervention-update-refresh-confirm = Refresh { -brand-short-name }…
+
+## Shown when an update is downloaded and Firefox is ready to install it.
+
+intervention-update-restart = The latest { -brand-short-name } is downloaded and ready to install.
+intervention-update-restart-confirm = Restart to Update
+
+## Shown when Firefox cannot update itself. The button will open the download
+## page on the Firefox website.
+
+intervention-update-web = Get the latest { -brand-short-name } browser.
+intervention-update-web-confirm = Download Now
+
+##
diff --git a/browser/components/urlbar/docs/contact.rst b/browser/components/urlbar/docs/contact.rst
new file mode 100644
index 0000000000..abd9947528
--- /dev/null
+++ b/browser/components/urlbar/docs/contact.rst
@@ -0,0 +1,9 @@
+Getting in Touch
+================
+
+For any questions regarding the Address Bar, the team is available through
+the #search channel on Slack and the fx-search@mozilla.com mailing
+list.
+
+Issues can be `filed in Bugzilla <https://bugzilla.mozilla.org/enter_bug.cgi?product=Firefox&component=Address%20Bar>`_
+under the Firefox / Address Bar component.
diff --git a/browser/components/urlbar/docs/debugging.rst b/browser/components/urlbar/docs/debugging.rst
new file mode 100644
index 0000000000..689657c068
--- /dev/null
+++ b/browser/components/urlbar/docs/debugging.rst
@@ -0,0 +1,4 @@
+Debugging & Logging
+===================
+
+*Content to be written*
diff --git a/browser/components/urlbar/docs/experiments.rst b/browser/components/urlbar/docs/experiments.rst
new file mode 100644
index 0000000000..f1217df391
--- /dev/null
+++ b/browser/components/urlbar/docs/experiments.rst
@@ -0,0 +1,727 @@
+Extensions & Experiments
+========================
+
+This document describes address bar extensions and experiments: what they are,
+how to run them, how to write them, and the processes involved in each.
+
+The primary purpose right now for writing address bar extensions is to run
+address bar experiments. But extensions are useful outside of experiments, and
+not all experiments use extensions.
+
+Like all Firefox extensions, address bar extensions use the WebExtensions_
+framework.
+
+.. _WebExtensions: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions
+
+.. toctree::
+ :caption: Table of Contents
+
+ experiments
+
+WebExtensions
+-------------
+
+**WebExtensions** is the name of Firefox's extension architecture. The "web"
+part of the name hints at the fact that Firefox extensions are built using Web
+technologies: JavaScript, HTML, CSS, and to a certain extent the DOM.
+
+Individual extensions themselves often are referred to as *WebExtensions*. For
+clarity and conciseness, this document will refer to WebExtensions as
+*extensions*.
+
+Why are we interested in extensions? Mainly because they're a powerful way to
+run experiments in Firefox. See Experiments_ for more on that. In addition, we'd
+also like to build up a robust set of APIs useful to extension authors, although
+right now the API can only be used by Mozilla extensions.
+
+WebExtensions are introduced and discussed in detail on `MDN
+<WebExtensions_>`__. You'll need a lot of that knowledge in order to build
+address bar extensions.
+
+Developing Address Bar Extensions
+---------------------------------
+
+Overview
+~~~~~~~~
+
+The address bar WebExtensions API currently lives in two API namespaces,
+``browser.urlbar`` and ``browser.experiments.urlbar``. The reason for this is
+historical and is discussed in the `Developing Address Bar Extension APIs`_
+section. As a consumer of the API, there are only two important things you need
+to know:
+
+* There's no meaningful difference between the APIs of the two namespaces.
+ Their kinds of functions, events, and properties are similar. You should
+ think of the address bar API as one single API that happens to be split into
+ two namespaces.
+
+* However, there is a big difference between the two when it comes to setting up
+ your extension to use them. This is discussed next.
+
+The ``browser.urlbar`` API namespace is built into Firefox. It's a
+**privileged API**, which means that only Mozilla-signed and temporarily
+installed extensions can use it. The only thing your Mozilla extension needs to
+do in order to use it is to request the ``urlbar`` permission in its
+manifest.json, as illustrated `here <urlbarPermissionExample_>`__.
+
+In contrast, the ``browser.experiments.urlbar`` API namespace is bundled inside
+your extension. APIs that are bundled inside extensions are called
+**experimental APIs**, and the extensions in which they're bundled are called
+**WebExtension experiments**. As with privileged APIs, experimental APIs are
+available only to Mozilla-signed and temporarily installed extensions.
+("WebExtension experiments" is a term of art and shouldn't be confused with the
+general notion of experiments that happen to use extensions.) For more on
+experimental APIs and WebExtension experiments, see the `WebExtensions API
+implementation documentation <webextAPIImplBasicsDoc_>`__.
+
+Since ``browser.experiments.urlbar`` is bundled inside your extension, you'll
+need to include it in your extension's repo by doing the following:
+
+1. The implementation consists of two files, api.js_ and schema.json_. In your
+ extension repo, create a *experiments/urlbar* subdirectory and copy the
+ files there. See `this repo`__ for an example.
+
+2. Add the following ``experiment_apis`` key to your manifest.json (see here__
+ for an example in context)::
+
+ "experiment_apis": {
+ "experiments_urlbar": {
+ "schema": "experiments/urlbar/schema.json",
+ "parent": {
+ "scopes": ["addon_parent"],
+ "paths": [["experiments", "urlbar"]],
+ "script": "experiments/urlbar/api.js"
+ }
+ }
+ }
+
+As mentioned, only Mozilla-signed and temporarily installed extensions can use
+these two API namespaces. For information on running the extensions you develop
+that use these namespaces, see `Running Address Bar Extensions`_.
+
+.. _urlbarPermissionExample: https://github.com/0c0w3/urlbar-top-sites-experiment/blob/ac1517118bb7ee165fb9989834514b1082575c10/src/manifest.json#L24
+.. _webextAPIImplBasicsDoc: https://firefox-source-docs.mozilla.org/toolkit/components/extensions/webextensions/basics.html
+.. _api.js: https://searchfox.org/mozilla-central/source/browser/components/urlbar/tests/ext/api.js
+.. _schema.json: https://searchfox.org/mozilla-central/source/browser/components/urlbar/tests/ext/schema.json
+__ https://github.com/0c0w3/dynamic-result-type-extension/tree/master/src/experiments/urlbar
+__ https://github.com/0c0w3/dynamic-result-type-extension/blob/0987da4b259b9fcb139b31d771883a2f822712b5/src/manifest.json#L28
+
+browser.urlbar
+~~~~~~~~~~~~~~
+
+Currently the only documentation for ``browser.urlbar`` is its `schema
+<urlbar.json_>`__. Fortunately WebExtension schemas are JSON and aren't too hard
+to read. If you need help understanding it, see the `WebExtensions API
+implementation documentation <webextAPIImplDoc_>`__.
+
+For examples on using the API, see the Cookbook_ section.
+
+.. _urlbar.json: https://searchfox.org/mozilla-central/source/browser/components/extensions/schemas/urlbar.json
+
+browser.experiments.urlbar
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+As with ``browser.urlbar``, currently the only documentation for
+``browser.experiments.urlbar`` is its schema__. For examples on using the API,
+see the Cookbook_ section.
+
+__ https://searchfox.org/mozilla-central/source/browser/components/urlbar/tests/ext/schema.json
+
+Workflow
+~~~~~~~~
+
+The web-ext_ command-line tool makes the extension-development workflow very
+simple. Simply start it with the *run* command, passing it the location of the
+Firefox binary you want to use. web-ext will launch your Firefox and remain
+running until you stop it, watching for changes you make to your extension's
+files. When it sees a change, it automatically reloads your extension — in
+Firefox, in the background — without your having to do anything. It's really
+nice.
+
+The `web-ext documentation <web-ext commands_>`__ lists all its options, but
+here are some worth calling out for the *run* command:
+
+``--browser-console``
+ Automatically open the browser console when Firefox starts. Very useful for
+ watching your extension's console logging. (Make sure "Show Content Messages"
+ is checked in the console.)
+
+``-p``
+ This option lets you specify a path to a profile directory.
+
+``--keep-profile-changes``
+ Normally web-ext doesn't save any changes you make to the profile. Use this
+ option along with ``-p`` to reuse the same profile again and again.
+
+``--verbose``
+ web-ext suppresses Firefox messages in the terminal unless you pass this
+ option. If you've added some ``dump`` calls in Firefox because you're working
+ on a new ``browser.urlbar`` API, for example, you won't see them without this.
+
+web-ext also has a *build* command that packages your extension's files into a
+zip file. The following *build* options are useful:
+
+``--overwrite-dest``
+ Without this option, web-ext won't overwrite a zip file it previously created.
+
+web-ext can load its configuration from your extension's package.json. That's
+the recommended way to configure it. Here's an example__.
+
+Finally, web-ext can also sign extensions, but if you're developing your
+extension for an experiment, you'll use a different process for signing. See
+`The Experiment Development Process`_.
+
+.. _web-ext: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Getting_started_with_web-ext
+.. _web-ext commands: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/web-ext_command_reference
+__ https://github.com/0c0w3/urlbar-top-sites-experiment/blob/6681a7126986bc2565d036b888cb5b8807397ce5/package.json#L7
+
+Automated Tests
+~~~~~~~~~~~~~~~
+
+It's possible to write `browser chrome mochitests`_ for your extension the same
+way we write tests for Firefox. One of the example extensions linked throughout
+this document includes a test_, for instance.
+
+See the readme in the example-addon-experiment_ repo for a workflow.
+
+.. _browser chrome mochitests: https://developer.mozilla.org/en-US/docs/Mozilla/Browser_chrome_tests
+.. _test: https://github.com/0c0w3/urlbar-top-sites-experiment/blob/master/tests/tests/browser/browser_urlbarTopSitesExtension.js
+
+Cookbook
+~~~~~~~~
+
+*To be written.* For now, you can find example uses of ``browser.experiments.urlbar`` and ``browser.urlbar`` in the following repos:
+
+* https://github.com/mozilla-extensions/firefox-quick-suggest-weather
+* https://github.com/0c0w3/urlbar-tips-experiment
+* https://github.com/0c0w3/urlbar-top-sites-experiment
+* https://github.com/0c0w3/urlbar-search-interventions-experiment
+
+Further Reading
+~~~~~~~~~~~~~~~
+
+`WebExtensions on MDN <WebExtensions_>`__
+ The place to learn about developing WebExtensions in general.
+
+`Getting started with web-ext <web-ext_>`__
+ MDN's tutorial on using web-ext.
+
+`web-ext command reference <web-ext commands_>`__
+ MDN's documentation on web-ext's commands and their options.
+
+Developing Address Bar Extension APIs
+-------------------------------------
+
+Built-In APIs vs. Experimental APIs
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Originally we developed the address bar extension API in the ``browser.urlbar``
+namespace, which is built into Firefox as discussed above. By "built into
+Firefox," we mean that the API is developed in `mozilla-central
+<urlbar.json_>`__ and shipped inside Firefox just like any other Firefox
+feature. At the time, that seemed like the right thing to do because we wanted
+to build an API that ultimately could be used by all extension authors, not only
+Mozilla.
+
+However, there were a number of disadvantages to this development model. The
+biggest was that it tightly coupled our experiments to specific versions of
+Firefox. For example, if we were working on an experiment that targeted Firefox
+72, then any APIs used by that experiment needed to land and ship in 72. If we
+weren't able to finish an API by the time 72 shipped, then the experiment would
+have to be postponed until 73. Our experiment development timeframes were always
+very short because we always wanted to ship our experiments ASAP. Often we
+targeted the Firefox version that was then in Nightly; sometimes we even
+targeted the version in Beta. Either way, it meant that we were always uplifting
+patch after patch to Beta. This tight coupling between Firefox versions and
+experiments erased what should have been a big advantage of implementing
+experiments as extensions in the first place: the ability to ship experiments
+outside the usual cyclical release process.
+
+Another notable disadvantage of this model was just the cognitive weight of the
+idea that we were developing APIs not only for ourselves and our experiments but
+potentially for all extensions. This meant that not only did we have to design
+APIs to meet our immediate needs, we also had to imagine use cases that could
+potentially arise and then design for them as well.
+
+For these reasons, we stopped developing ``browser.urlbar`` and created the
+``browser.experiments.urlbar`` experimental API. As discussed earlier,
+experimental APIs are APIs that are bundled inside extensions. Experimental APIs
+can do anything that built-in APIs can do with the added flexibility of not
+being tied to specific versions of Firefox.
+
+Adding New APIs
+~~~~~~~~~~~~~~~
+
+All new address bar APIs should be added to ``browser.experiments.urlbar``.
+Although this API does not ship in Firefox, it's currently developed in
+mozilla-central, in `browser/components/urlbar/tests/ext/ <extDirectory_>`__ --
+note the "tests" subdirectory. Developing it in mozilla-central lets us take
+advantage of our usual build and testing infrastructure. This way we have API
+tests running against each mozilla-central checkin, against all versions of
+Firefox that are tested on Mozilla's infrastructure, and we're alerted to any
+breaking changes we accidentally make. When we start a new extension repo, we
+copy schema.json and api.js to it as described earlier (or clone an example repo
+with up-to-date copies of these files).
+
+Generally changes to the API should be reviewed by someone on the address bar
+team and someone on the WebExtensions team. Shane (mixedpuppy) is a good
+contact.
+
+.. _extDirectory: https://searchfox.org/mozilla-central/source/browser/components/urlbar/tests/ext/
+
+Anatomy of an API
+~~~~~~~~~~~~~~~~~
+
+Roughly speaking, a WebExtensions API implementation comprises three different
+pieces:
+
+Schema
+ The schema declares the functions, properties, events, and types that the API
+ makes available to extensions. Schemas are written in JSON.
+
+ The ``browser.experiments.urlbar`` schema is schema.json_, and the
+ ``browser.urlbar`` schema is urlbar.json_.
+
+ For reference, the schemas of built-in APIs are in
+ `browser/components/extensions/schemas`_ and
+ `toolkit/components/extensions/schemas`_.
+
+ .. _browser/components/extensions/schemas: https://searchfox.org/mozilla-central/source/browser/components/extensions/schemas/
+ .. _toolkit/components/extensions/schemas: https://searchfox.org/mozilla-central/source/toolkit/components/extensions/schemas/
+
+Internals
+ Every API hooks into some internal part of Firefox. For the address bar API,
+ that's the Urlbar implementation in `browser/components/urlbar`_.
+
+ .. _browser/components/urlbar: https://searchfox.org/mozilla-central/source/browser/components/urlbar/
+
+Glue
+ Finally, there's some glue code that implements everything declared in the
+ schema. Essentially, this code mediates between the previous two pieces. It
+ translates the function calls, property accesses, and event listener
+ registrations made by extensions using the public-facing API into terms that
+ the Firefox internals understand, and vice versa.
+
+ For ``browser.experiments.urlbar``, this is api.js_, and for
+ ``browser.urlbar``, it's ext-urlbar.js_.
+
+ For reference, the implementations of built-in APIs are in
+ `browser/components/extensions`_ and `toolkit/components/extensions`_, in the
+ *parent* and *child* subdirecties. As you might guess, code in *parent* runs
+ in the main process, and code in *child* runs in the extensions process.
+ Address bar APIs deal with browser chrome and their implementations therefore
+ run in the parent process.
+
+ .. _ext-urlbar.js: https://searchfox.org/mozilla-central/source/browser/components/extensions/parent/ext-urlbar.js
+ .. _browser/components/extensions: https://searchfox.org/mozilla-central/source/browser/components/extensions/
+ .. _toolkit/components/extensions: https://searchfox.org/mozilla-central/source/toolkit/components/extensions/
+
+Keep in mind that extensions run in a different process from the main process.
+That has implications for your APIs. They'll generally need to be async, for
+example.
+
+Further Reading
+~~~~~~~~~~~~~~~
+
+`WebExtensions API implementation documentation <webextAPIImplDoc_>`__
+ Detailed info on implementing a WebExtensions API.
+
+.. _webextAPIImplDoc: https://firefox-source-docs.mozilla.org/toolkit/components/extensions/webextensions/
+
+Running Address Bar Extensions
+------------------------------
+
+As discussed above, ``browser.experiments.urlbar`` and ``browser.urlbar`` are
+privileged APIs. There are two different points to consider when it comes to
+running an extension that uses privileged APIs: loading the extension in the
+first place, and granting it access to privileged APIs. There's a certain bar
+for loading any extension regardless of its API usage that depends on its signed
+state and the Firefox build you want to run it in. There's yet a higher bar for
+granting it access to privileged APIs. This section discusses how to load
+extensions so that they can access privileged APIs.
+
+Since we're interested in extensions primarily for running experiments, there
+are three particular signed states relevant to us:
+
+Unsigned
+ There are two ways to run unsigned extensions that use privileged APIs.
+
+ They can be loaded temporarily using a Firefox Nightly build or
+ Developer Edition but not Beta or Release [source__], and the
+ ``extensions.experiments.enabled`` preference must be set to true [source__].
+ You can load extensions temporarily by visiting
+ about:debugging#/runtime/this-firefox and clicking "Load Temporary Add-on."
+ `web-ext <Workflow_>`__ also loads extensions temporarily.
+
+ __ https://searchfox.org/mozilla-central/rev/053826b10f838f77c27507e5efecc96e34718541/toolkit/components/extensions/Extension.jsm#1884
+ __ https://searchfox.org/mozilla-central/rev/014fe72eaba26dcf6082fb9bbaf208f97a38594e/toolkit/mozapps/extensions/internal/AddonSettings.jsm#93
+
+ They can be also be loaded normally (not temporarily) in a custom build where
+ the build-time setting ``AppConstants.MOZ_REQUIRE_SIGNING`` [source__, source__]
+ and ``xpinstall.signatures.required`` pref are both false. As in the previous
+ paragraph, such builds include Nightly and Developer Edition but not Beta or
+ Release [source__]. In addition, your custom build must modify the
+ ``Extension.isPrivileged`` getter__ to return true. This getter determines
+ whether an extension can access privileged APIs.
+
+ __ https://searchfox.org/mozilla-central/rev/053826b10f838f77c27507e5efecc96e34718541/toolkit/mozapps/extensions/internal/XPIProvider.jsm#2382
+ __ https://searchfox.org/mozilla-central/rev/053826b10f838f77c27507e5efecc96e34718541/toolkit/mozapps/extensions/internal/AddonSettings.jsm#36
+ __ https://searchfox.org/mozilla-central/search?q=MOZ_REQUIRE_SIGNING&case=false&regexp=false&path=
+ __ https://searchfox.org/mozilla-central/rev/053826b10f838f77c27507e5efecc96e34718541/toolkit/components/extensions/Extension.jsm#1874
+
+ Extensions remain unsigned as you develop them. See the Workflow_ section for
+ more.
+
+Signed for testing (Signed for QA)
+ Signed-for-testing extensions that use privileged APIs can be run using the
+ same techniques for running unsigned extensions.
+
+ They can also be loaded normally (not temporarily) if you use a Firefox build
+ where the build-time setting ``AppConstants.MOZ_REQUIRE_SIGNING`` is false and
+ you set the ``xpinstall.signatures.dev-root`` pref to true
+ [source__]. ``xpinstall.signatures.dev-root`` does not exist by default and
+ must be created.
+
+ __ https://searchfox.org/mozilla-central/rev/053826b10f838f77c27507e5efecc96e34718541/toolkit/mozapps/extensions/internal/XPIInstall.jsm#262
+
+ You encounter extensions that are signed for testing when you are writing
+ extensions for experiments. See the Experiments_ section for details.
+
+ "Signed for QA" is another way of referring to this signed state.
+
+Signed for release
+ Signed-for-release extensions that use privileged APIs can be run in any
+ Firefox build with no special requirements.
+
+ You encounter extensions that are signed for release when you are writing
+ extensions for experiments. See the Experiments_ section for details.
+
+.. important::
+ To see console logs from extensions in the browser console, select the "Show
+ Content Messages" option in the console's settings. This is necessary because
+ extensions run outside the main process.
+
+Experiments
+-----------
+
+**Experiments** let us try out ideas in Firefox outside the usual release cycle
+and on particular populations of users.
+
+For example, say we have a hunch that the top sites shown on the new-tab page
+aren't very discoverable, so we want to make them more visible. We have one idea
+that might work — show them every time the user begins an interaction with the
+address bar — but we aren't sure how good an idea it is. So we test it. We write
+an extension that does just that, make sure it collects telemetry that will help
+us answer our question, ship it outside the usual release cycle to a small
+percentage of Beta users, collect and analyze the telemetry, and determine
+whether the experiment was successful. If it was, then we might want to ship the
+feature to all Firefox users.
+
+Experiments sometimes are also called **studies** (not to be confused with *user
+studies*, which are face-to-face interviews with users conducted by user
+researchers).
+
+There are two types of experiments:
+
+Pref-flip experiments
+ Pref-flip experiments are simple. If we have a fully baked feature in the
+ browser that's preffed off, a pref-flip experiment just flips the pref on,
+ enabling the feature for users running the experiment. No code is required.
+ We tell the experiments team the name of the pref we want to flip, and they
+ handle it.
+
+ One important caveat to pref-flip studies is that they're currently capable of
+ flipping only a single pref. There's an extension called Multipreffer_ that
+ can flip multiple prefs, though.
+
+ .. _Multipreffer: https://github.com/mozilla/multipreffer
+
+Add-on experiments
+ Add-on experiments are much more complex but much more powerful. (Here
+ *add-on* is a synonym for extension.) They're the type of experiments that
+ this document has been discussing all along.
+
+ An add-on experiment is shipped as an extension that we write and that
+ implements the experimental feature we want to test. To reiterate, the
+ extension is a WebExtension and uses WebExtensions APIs. If the current
+ WebExtensions APIs do not meet the needs of your experiment, then you must
+ create either experimental or built-in APIs so that your extension can use
+ them. If necessary, you can make any new built-in APIs privileged so that they
+ are available only to Mozilla extensions.
+
+ An add-on experiment can collect additional telemetry that's not collected in
+ the product by using the privileged ``browser.telemetry`` WebExtensions API,
+ and of course the product will continue to collect all the telemetry it
+ usually does. The telemetry pings from users running the experiment will be
+ correlated with the experiment with no extra work on our part.
+
+A single experiment can deliver different UXes to different groups of users
+running the experiment. Each group or UX within an experiment is called a
+**branch**. Experiments often have two branches, control and treatment. The
+**control branch** actually makes no UX changes. It may capture additional
+telemetry, though. Think of it as the control in a science experiment. It's
+there so we can compare it to data from the **treatment branch**, which does
+make UX changes. Some experiments may require multiple treatment branches, in
+which case the different branches will have different names. Add-on experiments
+can implement all branches in the same extension or each branch in its own
+extension.
+
+Experiments are delivered to users by a system called **Normandy**. Normandy
+comprises a client side that lives in Firefox and a server side. In Normandy,
+experiments are defined server-side in files called **recipes**. Recipes include
+information about the experiment like the Firefox release channel and version
+that the experiment targets, the number of users to be included in the
+experiment, the branches in the experiment, the percentage of users on each
+branch, and so on.
+
+Experiments are tracked by Mozilla project management using a system called
+Experimenter_.
+
+Finally, there was an older version of the experiments program called
+**Shield**. Experiments under this system were called **Shield studies** and
+could be be shipped as extensions too.
+
+.. _Experimenter: https://experimenter.services.mozilla.com/
+
+Further Reading
+~~~~~~~~~~~~~~~
+
+`Pref-Flip and Add-On Experiments <https://mana.mozilla.org/wiki/pages/viewpage.action?spaceKey=FIREFOX&title=Pref-Flip+and+Add-On+Experiments>`__
+ A comprehensive document on experiments from the Experimenter team. See the
+ child pages in the sidebar, too.
+
+`Client Implementation Guidelines for Experiments <https://docs.telemetry.mozilla.org/cookbooks/client_guidelines.html>`_
+ Relevant documentation from the telemetry team.
+
+#ask-experimenter Slack channel
+ A friendly place to get answers to your experiment questions.
+
+The Experiment Development Process
+----------------------------------
+
+This section describes an experiment's life cycle.
+
+1. Experiments usually originate with product management and UX. They're
+ responsible for identifying a problem, deciding how an experiment should
+ approach it, the questions we want to answer, the data we need to answer
+ those questions, the user population that should be enrolled in the
+ experiment, the definition of success, and so on.
+
+2. UX makes a spec that describes what the extension looks like and how it
+ behaves.
+
+3. There's a kickoff meeting among the team to introduce the experiment and UX
+ spec. It's an opportunity for engineering to ask questions of management, UX,
+ and data science. It's really important for engineering to get a precise and
+ accurate understanding of how the extension is supposed to behave — right
+ down to the UI changes — so that no one makes erroneous assumptions during
+ development.
+
+4. At some point around this time, the team (usually management) creates a few
+ artifacts to track the work and facilitate communication with outside teams
+ involved in shipping experiments. They include:
+
+ * A page on `Experimenter <Experiments_>`__
+ * A QA PI (product integrity) request so that QA resources are allocated
+ * A bug in `Data Science :: Experiment Collaboration`__ so that data science
+ can track the work and discuss telemetry (engineering might file this one)
+
+ __ https://bugzilla.mozilla.org/enter_bug.cgi?assigned_to=nobody%40mozilla.org&bug_ignored=0&bug_severity=normal&bug_status=NEW&bug_type=task&cf_firefox_messaging_system=---&cf_fx_iteration=---&cf_fx_points=---&comment=%23%23%20Brief%20Description%20of%20the%20request%20%28required%29%3A%0D%0A%0D%0A%23%23%20Business%20purpose%20for%20this%20request%20%28required%29%3A%0D%0A%0D%0A%23%23%20Requested%20timelines%20for%20the%20request%20or%20how%20this%20fits%20into%20roadmaps%20or%20critical%20decisions%20%28required%29%3A%0D%0A%0D%0A%23%23%20Links%20to%20any%20assets%20%28e.g%20Start%20of%20a%20PHD%2C%20BRD%3B%20any%20document%20that%20helps%20describe%20the%20project%29%3A%0D%0A%0D%0A%23%23%20Name%20of%20Data%20Scientist%20%28If%20Applicable%29%3A%0D%0A%0D%0A%2APlease%20note%20if%20it%20is%20found%20that%20not%20enough%20information%20has%20been%20given%20this%20will%20delay%20the%20triage%20of%20this%20request.%2A&component=Experiment%20Collaboration&contenttypemethod=list&contenttypeselection=text%2Fplain&filed_via=standard_form&flag_type-4=X&flag_type-607=X&flag_type-800=X&flag_type-803=X&flag_type-936=X&form_name=enter_bug&maketemplate=Remember%20values%20as%20bookmarkable%20template&op_sys=Unspecified&priority=--&product=Data%20Science&rep_platform=Unspecified&target_milestone=---&version=unspecified
+
+5. Engineering breaks down the work and files bugs. There's another engineering
+ meeting to discuss the breakdown, or it's discussed asynchronously.
+
+6. Engineering sets up a GitHub repo for the extension. See `Implementing
+ Experiments`_ for an example repo you can clone to get started. Disable
+ GitHub Issues on the repo so that QA will file bugs in Bugzilla instead of
+ GitHub. There's nothing wrong with GitHub Issues, but our team's project
+ management tracks all work through Bugzilla. If it's not there, it's not
+ captured.
+
+7. Engineering or management fills out the Add-on section of the Experimenter
+ page as much as possible at this point. "Active Experiment Name" isn't
+ necessary, and "Signed Release URL" won't be available until the end of the
+ process.
+
+8. Engineering implements the extension and any new WebExtensions APIs it
+ requires.
+
+9. When the extension is done, engineering or management clicks the "Ready for
+ Sign-Off" button on the Experimenter page. That changes the page's status
+ from "Draft" to "Ready for Sign-Off," which allows QA and other teams to sign
+ off on their portions of the experiment.
+
+10. Engineering requests the extension be signed "for testing" (or "for
+ QA"). Michael (mythmon) from the Experiments team and Rehan (rdalal) from
+ Services Engineering are good contacts. Build the extension zip file using
+ web-ext as discussed in Workflow_. Attach it to a bug (a metabug for
+ implementing the extension, for example), needinfo Michael or Rehan, and ask
+ him to sign it. He'll attach the signed version to the bug. If neither
+ Michael nor Rehan is available, try asking in the #ask-experimenter Slack
+ channel.
+
+11. Engineering sends QA the link to the signed extension and works with them to
+ resolve bugs they find.
+
+12. When QA signs off, engineering asks Michael to sign the extension "for
+ release" using the same needinfo process described earlier.
+
+13. Paste the URL of the signed extension in the "Signed Release URL" textbox of
+ the Add-on section of the Experimenter page.
+
+14. Other teams sign off as they're ready.
+
+15. The experiment ships! 🎉
+
+
+Implementing Experiments
+------------------------
+
+This section discusses how to implement add-on experiments. Pref-flip
+experiments are much simpler and don't need a lot of explanation. You should be
+familiar with the concepts discussed in the `Developing Address Bar Extensions`_
+and `Running Address Bar Extensions`_ sections before reading this one.
+
+The most salient thing about add-on experiments is that they're implemented
+simply as privileged extensions. Other than being privileged and possibly
+containing bundled experimental APIs, they're similar to all other extensions.
+
+The `top-sites experiment extension <topSites_>`__ is an example of a real,
+shipped experiment.
+
+.. _topSites: https://github.com/0c0w3/urlbar-top-sites-experiment
+
+Setup
+~~~~~
+
+example-addon-experiment_ is a repo you can clone to get started. It's geared
+toward urlbar extensions and includes the stub of a browser chrome mochitest.
+
+.. _example-addon-experiment: https://github.com/0c0w3/example-addon-experiment
+
+browser.normandyAddonStudy
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+As discussed in Experiments_, an experiment typically has more than one branch
+so that it can test different UXes. The experiment's extension(s) needs to know
+the branch the user is enrolled in so that it can behave appropriately for the
+branch: show the user the proper UX, collect the proper telemetry, and so on.
+
+This is the purpose of the ``browser.normandyAddonStudy`` WebExtensions API.
+Like ``browser.urlbar``, it's a privileged API available only to Mozilla
+extensions.
+
+Its schema is normandyAddonStudy.json_.
+
+It's a very simple API. The primary function is ``getStudy``, which returns the
+study the user is currently enrolled in or null if there isn't one. (Recall that
+*study* is a synonym for *experiment*.) One of the first things an experiment
+extension typically does is to call this function.
+
+The Normandy client in Firefox will keep an experiment extension installed only
+while the experiment is active. Therefore, ``getStudy`` should always return a
+non-null study object. Nevertheless, the study object has an ``active`` boolean
+property that's trivial to sanity check. (The example extension does.)
+
+The more important property is ``branch``, the name of the branch that the user
+is enrolled in. Your extension should use it to determine the appropriate UX.
+
+Finally, there's an ``onUnenroll`` event that's fired when the user is
+unenrolled in the study. It's not quite clear in what cases an extension would
+need to listen for this event given that Normandy automatically uninstalls
+extensions on unenrollment. Maybe if they create some persistent state that's
+not automatically undone on uninstall by the WebExtensions framework?
+
+If your extension itself needs to unenroll the user for some reason, call
+``endStudy``.
+
+.. _normandyAddonStudy.json: https://searchfox.org/mozilla-central/source/browser/components/extensions/schemas/normandyAddonStudy.json
+
+Telemetry
+~~~~~~~~~
+
+Experiments can capture telemetry in two places: in the product itself and
+through the privileged ``browser.telemetry`` WebExtensions API. The API schema
+is telemetry.json_.
+
+The telemetry pings from users running experiments are automatically correlated
+with those experiments, no extra work required. That's true regardless of
+whether the telemetry is captured in the product or though
+``browser.telemetry``.
+
+The address bar has some in-product, preffed off telemetry that we want to
+enable for all our experiments — at least that's the thinking as of August 2019.
+It's called `engagement event telemetry`_, and it records user *engagements*
+with and *abandonments* of the address bar [source__]. We added a
+BrowserSetting_ on ``browser.urlbar`` just to let us flip the pref and enable
+this telemetry in our experiment extensions. Call it like this::
+
+ await browser.urlbar.engagementTelemetry.set({ value: true });
+
+.. _telemetry.json: https://searchfox.org/mozilla-central/source/toolkit/components/extensions/schemas/telemetry.json
+.. _engagement event telemetry: https://bugzilla.mozilla.org/show_bug.cgi?id=1559136
+__ https://searchfox.org/mozilla-central/rev/7088fc958db5935eba24b413b1f16d6ab7bd13ea/browser/components/urlbar/UrlbarController.jsm#598
+.. _BrowserSetting: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/types/BrowserSetting
+
+Engineering Best Practices
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Clear up questions with your UX person early and often. There's often a gap
+between what they have in their mind and what you have in yours. Nothing wrong
+with that, it's just the nature of development. But misunderstandings can cause
+big problems when they're discovered late. This is especially true of UX
+behaviors, as opposed to visuals or styling. It's no fun to realize at the end
+of a release cycle that you've designed the wrong WebExtensions API because some
+UX detail was overlooked.
+
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Related to the previous point, make builds of your extension for your UX person
+so they can test it.
+
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Taking the previous point even further, if your experiment will require a
+substantial new API(s), you might think about prototyping the experiment
+entirely in a custom Firefox build before designing the API at all. Give it to
+your UX person. Let them disect it and tell you all the problems with it. Fill
+in all the gaps in your understanding, and then design the API. We've never
+actually done this, though.
+
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+It's a good idea to work on the extension as you're designing and developing the
+APIs it'll use. You might even go as far as writing the first draft of the
+extension before even starting to implement the APIs. That lets you spot
+problems that may not be obvious were you to design the API in isolation.
+
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Your extension's ID should end in ``@shield.mozilla.org``. QA will flag it if it
+doesn't.
+
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Set ``"hidden": true`` in your extension's manifest.json. That hides it on
+about:addons. (It can still be seen on about:studies.) QA will spot this if you
+don't.
+
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+There are drawbacks of hiding features behind prefs and enabling them in
+experiment extensions. Consider not doing that if feasible, or at least weigh
+these drawbacks against your expected benefits.
+
+* Prefs stay flipped on in private windows, but experiments often have special
+ requirements around private-browsing mode (PBM). Usually, they shouldn't be
+ active in PBM at all, unless of course the point of the experiment is to test
+ PBM. Extensions also must request PBM access ("incognito" in WebExtensions
+ terms), and the user can disable access at any time. The result is that part
+ of your experiment could remain enabled — the part behind the pref — while
+ other parts are disabled.
+
+* Prefs stay flipped on in safe mode, even though your extension (like all
+ extensions) will be disabled. This might be a bug__ in the WebExtensions
+ framework, though.
+
+ __ https://bugzilla.mozilla.org/show_bug.cgi?id=1576997
diff --git a/browser/components/urlbar/docs/index.rst b/browser/components/urlbar/docs/index.rst
new file mode 100644
index 0000000000..633bca91b6
--- /dev/null
+++ b/browser/components/urlbar/docs/index.rst
@@ -0,0 +1,26 @@
+Address Bar
+===========
+
+This document describes the implementation of Firefox's address bar, also known
+as the quantumbar or urlbar. The address bar was also called the awesomebar
+until Firefox 68, when it was substantially rewritten.
+
+The address bar is a specialized search access point that aggregates data from
+several different sources, including:
+
+ * Places (Firefox's history and bookmarks system)
+ * Search engines (including search suggestions)
+ * WebExtensions
+ * Open tabs
+
+Most of the address bar code lives in `browser/components/urlbar <https://searchfox.org/mozilla-central/source/browser/components/urlbar/>`_.
+A separate and important back-end piece currently is `toolkit/components/places/UnifiedComplete.jsm <https://searchfox.org/mozilla-central/source/toolkit/components/places/UnifiedComplete.jsm>`_, which was carried over from awesomebar and has not yet been rewritten for quantumbar.
+
+.. toctree::
+
+ overview
+ utilities
+ telemetry
+ debugging
+ experiments
+ contact
diff --git a/browser/components/urlbar/docs/overview.rst b/browser/components/urlbar/docs/overview.rst
new file mode 100644
index 0000000000..e7306bceea
--- /dev/null
+++ b/browser/components/urlbar/docs/overview.rst
@@ -0,0 +1,413 @@
+Architecture Overview
+=====================
+
+The address bar is implemented as a *model-view-controller* (MVC) system. One of
+the scopes of this architecture is to allow easy replacement of its components,
+for easier experimentation.
+
+Each search is represented by a unique object, the *UrlbarQueryContext*. This
+object, created by the *View*, describes the search and is passed through all of
+the components, along the way it gets augmented with additional information.
+The *UrlbarQueryContext* is passed to the *Controller*, and finally to the
+*Model*. The model appends results to a property of *UrlbarQueryContext* in
+chunks, it sorts them through a *Muxer* and then notifies the *Controller*.
+
+See the specific components below, for additional details about each one's tasks
+and responsibilities.
+
+
+The UrlbarQueryContext
+----------------------
+
+The *UrlbarQueryContext* object describes a single instance of a search.
+It is augmented as it progresses through the system, with various information:
+
+.. highlight:: JavaScript
+.. code::
+
+ UrlbarQueryContext {
+ allowAutofill; // {boolean} If true, providers are allowed to return
+ // autofill results. Even if true, it's up to providers
+ // whether to include autofill results, but when false, no
+ // provider should include them.
+ isPrivate; // {boolean} Whether the search started in a private context.
+ maxResults; // {integer} The maximum number of results requested. It is
+ // possible to request more results than the shown ones, and
+ // do additional filtering at the View level.
+ searchString; // {string} The user typed string.
+ userContextId; // {integer} The user context ID (containers feature).
+
+ // Optional properties.
+ muxer; // {string} Name of a registered muxer. Muxers can be registered
+ // through the UrlbarProvidersManager.
+ providers; // {array} List of registered provider names. Providers can be
+ // registered through the UrlbarProvidersManager.
+ sources: {array} list of accepted UrlbarUtils.RESULT_SOURCE for the context.
+ // This allows to switch between different search modes. If not
+ // provided, a default will be generated by the Model, depending on
+ // the search string.
+ engineName: // {string} if sources is restricting to just SEARCH, this
+ // property can be used to pick a specific search engine, by
+ // setting it to the name under which the engine is registered
+ // with the search service.
+ currentPage: // {string} url of the page that was loaded when the search
+ // began.
+ allowSearchSuggestions: // {boolean} Whether to allow search suggestions.
+ // This is a veto, meaning that when false,
+ // suggestions will not be fetched, but when true,
+ // some other condition may still prohibit
+ // suggestions, like private browsing mode. Defaults
+ // to true.
+
+ // Properties added by the Model.
+ results; // {array} list of UrlbarResult objects.
+ tokens; // {array} tokens extracted from the searchString, each token is an
+ // object in the form {type, value, lowerCaseValue}.
+ }
+
+
+The Model
+---------
+
+The *Model* is the component responsible for retrieving search results based on
+the user's input, and sorting them accordingly to their importance.
+At the core is the `UrlbarProvidersManager <https://searchfox.org/mozilla-central/source/browser/components/urlbar/UrlbarProvidersManager.jsm>`_,
+a component tracking all the available search providers, and managing searches
+across them.
+
+The *UrlbarProvidersManager* is a singleton, it registers internal providers on
+startup and can register/unregister providers on the fly.
+It can manage multiple concurrent queries, and tracks them internally as
+separate *Query* objects.
+
+The *Controller* starts and stops queries through the *UrlbarProvidersManager*.
+It's possible to wait for the promise returned by *startQuery* to know when no
+more results will be returned, it is not mandatory though.
+Queries can be canceled.
+
+.. note::
+
+ Canceling a query will issue an interrupt() on the database connection,
+ terminating any running and future SQL query, unless a query is running inside
+ a *runInCriticalSection* task.
+
+The *searchString* gets tokenized by the `UrlbarTokenizer <https://searchfox.org/mozilla-central/source/browser/components/urlbar/UrlbarTokenizer.jsm>`_
+component into tokens, some of these tokens have a special meaning and can be
+used by the user to restrict the search to specific result type (See the
+*UrlbarTokenizer::TYPE* enum).
+
+.. caution::
+
+ The tokenizer uses heuristics to determine each token's type, as such the
+ consumer may want to check the value before applying filters.
+
+.. highlight:: JavaScript
+.. code::
+
+ UrlbarProvidersManager {
+ registerProvider(providerObj);
+ unregisterProvider(providerObj);
+ registerMuxer(muxerObj);
+ unregisterMuxer(muxerObjOrName);
+ async startQuery(queryContext);
+ cancelQuery(queryContext);
+ // Can be used by providers to run uninterruptible queries.
+ runInCriticalSection(taskFn);
+ }
+
+UrlbarProvider
+~~~~~~~~~~~~~~
+
+A provider is specialized into searching and returning results from different
+information sources. Internal providers are usually implemented in separate
+*jsm* modules with a *UrlbarProvider* name prefix. External providers can be
+registered as *Objects* through the *UrlbarProvidersManager*.
+Each provider is independent and must satisfy a base API, while internal
+implementation details may vary deeply among different providers.
+
+.. important::
+
+ Providers are singleton, and must track concurrent searches internally, for
+ example mapping them by UrlbarQueryContext.
+
+.. note::
+
+ Internal providers can access the Places database through the
+ *PlacesUtils.promiseLargeCacheDBConnection* utility.
+
+.. highlight:: JavaScript
+.. code::
+
+ class UrlbarProvider {
+ /**
+ * Unique name for the provider, used by the context to filter on providers.
+ * Not using a unique name will cause the newest registration to win.
+ * @abstract
+ */
+ get name() {
+ return "UrlbarProviderBase";
+ }
+ /**
+ * The type of the provider, must be one of UrlbarUtils.PROVIDER_TYPE.
+ * @abstract
+ */
+ get type() {
+ throw new Error("Trying to access the base class, must be overridden");
+ }
+ /**
+ * Whether this provider should be invoked for the given context.
+ * If this method returns false, the providers manager won't start a query
+ * with this provider, to save on resources.
+ * @param {UrlbarQueryContext} queryContext The query context object
+ * @returns {boolean} Whether this provider should be invoked for the search.
+ * @abstract
+ */
+ isActive(queryContext) {
+ throw new Error("Trying to access the base class, must be overridden");
+ }
+ /**
+ * Gets the provider's priority. Priorities are numeric values starting at
+ * zero and increasing in value. Smaller values are lower priorities, and
+ * larger values are higher priorities. For a given query, `startQuery` is
+ * called on only the active and highest-priority providers.
+ * @param {UrlbarQueryContext} queryContext The query context object
+ * @returns {number} The provider's priority for the given query.
+ * @abstract
+ */
+ getPriority(queryContext) {
+ // By default, all providers share the lowest priority.
+ return 0;
+ }
+ /**
+ * Starts querying.
+ * @param {UrlbarQueryContext} queryContext The query context object
+ * @param {function} addCallback Callback invoked by the provider to add a new
+ * result. A UrlbarResult should be passed to it.
+ * @note Extended classes should return a Promise resolved when the provider
+ * is done searching AND returning results.
+ * @abstract
+ */
+ startQuery(queryContext, addCallback) {
+ throw new Error("Trying to access the base class, must be overridden");
+ }
+ /**
+ * Cancels a running query,
+ * @param {UrlbarQueryContext} queryContext The query context object to cancel
+ * query for.
+ * @abstract
+ */
+ cancelQuery(queryContext) {
+ throw new Error("Trying to access the base class, must be overridden");
+ }
+ }
+
+UrlbarMuxer
+~~~~~~~~~~~
+
+The *Muxer* is responsible for sorting results based on their importance and
+additional rules that depend on the UrlbarQueryContext. The muxer to use is
+indicated by the UrlbarQueryContext.muxer property.
+
+.. caution::
+
+ The Muxer is a replaceable component, as such what is described here is a
+ reference for the default View, but may not be valid for other implementations.
+
+.. highlight:: JavaScript
+.. code::
+
+ class UrlbarMuxer {
+ /**
+ * Unique name for the muxer, used by the context to sort results.
+ * Not using a unique name will cause the newest registration to win.
+ * @abstract
+ */
+ get name() {
+ return "UrlbarMuxerBase";
+ }
+ /**
+ * Sorts UrlbarQueryContext results in-place.
+ * @param {UrlbarQueryContext} queryContext the context to sort results for.
+ * @abstract
+ */
+ sort(queryContext) {
+ throw new Error("Trying to access the base class, must be overridden");
+ }
+ }
+
+
+The Controller
+--------------
+
+`UrlbarController <https://searchfox.org/mozilla-central/source/browser/components/urlbar/UrlbarController.jsm>`_
+is the component responsible for reacting to user's input, by communicating
+proper course of action to the Model (e.g. starting/stopping a query) and the
+View (e.g. showing/hiding a panel). It is also responsible for reporting Telemetry.
+
+.. note::
+
+ Each *View* has a different *Controller* instance.
+
+.. highlight:: JavaScript
+.. code::
+
+ UrlbarController {
+ async startQuery(queryContext);
+ cancelQuery(queryContext);
+ // Invoked by the ProvidersManager when results are available.
+ receiveResults(queryContext);
+ // Used by the View to listen for results.
+ addQueryListener(listener);
+ removeQueryListener(listener);
+ }
+
+
+The View
+--------
+
+The View is the component responsible for presenting search results to the
+user and handling their input.
+
+.. caution
+
+ The View is a replaceable component, as such what is described here is a
+ reference for the default View, but may not be valid for other implementations.
+
+`UrlbarInput.jsm <https://searchfox.org/mozilla-central/source/browser/components/urlbar/UrlbarInput.jsm>`_
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Implements an input box *View*, owns an *UrlbarView*.
+
+.. highlight:: JavaScript
+.. code::
+
+ UrlbarInput {
+ constructor(options = { textbox, panel });
+ // Uses UrlbarValueFormatter to highlight the base host, search aliases
+ // and to keep the host visible on overflow.
+ formatValue(val);
+ openResults();
+ // Converts an internal URI (e.g. a URI with a username or password) into
+ // one which we can expose to the user.
+ makeURIReadable(uri);
+ // Handles an event which would cause a url or text to be opened.
+ handleCommand();
+ // Called by the view when a result is selected.
+ resultsSelected();
+ // The underlying textbox
+ textbox;
+ // The results panel.
+ panel;
+ // The containing window.
+ window;
+ // The containing document.
+ document;
+ // An UrlbarController instance.
+ controller;
+ // An UrlbarView instance.
+ view;
+ // Whether the current value was typed by the user.
+ valueIsTyped;
+ // Whether the context is in Private Browsing mode.
+ isPrivate;
+ // Whether the input box is focused.
+ focused;
+ // The go button element.
+ goButton;
+ // The current value, can also be set.
+ value;
+ }
+
+`UrlbarView.jsm <https://searchfox.org/mozilla-central/source/browser/components/urlbar/UrlbarView.jsm>`_
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Represents the base *View* implementation, communicates with the *Controller*.
+
+.. highlight:: JavaScript
+.. code::
+
+ UrlbarView {
+ // Manage View visibility.
+ open();
+ close();
+ // Invoked when the query starts.
+ onQueryStarted(queryContext);
+ // Invoked when new results are available.
+ onQueryResults(queryContext);
+ // Invoked when the query has been canceled.
+ onQueryCancelled(queryContext);
+ // Invoked when the query is done. This is invoked in any case, even if the
+ // query was canceled earlier.
+ onQueryFinished(queryContext);
+ // Invoked when the view opens.
+ onViewOpen();
+ // Invoked when the view closes.
+ onViewClose();
+ }
+
+
+UrlbarResult
+------------
+
+An `UrlbarResult <https://searchfox.org/mozilla-central/source/browser/components/urlbar/UrlbarResult.jsm>`_
+instance represents a single search result with a result type, that
+identifies specific kind of results.
+Each kind has its own properties, that the *View* may support, and a few common
+properties, supported by all of the results.
+
+.. note::
+
+ Result types are also enumerated by *UrlbarUtils.RESULT_TYPE*.
+
+.. highlight:: JavaScript
+.. code::
+
+ UrlbarResult {
+ constructor(resultType, payload);
+
+ type: {integer} One of UrlbarUtils.RESULT_TYPE.
+ source: {integer} One of UrlbarUtils.RESULT_SOURCE.
+ title: {string} A title that may be used as a label for this result.
+ icon: {string} Url of an icon for this result.
+ payload: {object} Object containing properties for the specific RESULT_TYPE.
+ autofill: {object} An object describing the text that should be
+ autofilled in the input when the result is selected, if any.
+ autofill.value: {string} The autofill value.
+ autofill.selectionStart: {integer} The first index in the autofill
+ selection.
+ autofill.selectionEnd: {integer} The last index in the autofill selection.
+ suggestedIndex: {integer} Suggest a preferred position for this result
+ within the result set.
+ }
+
+The following RESULT_TYPEs are supported:
+
+.. highlight:: JavaScript
+.. code::
+
+ // An open tab.
+ // Payload: { icon, url, userContextId }
+ TAB_SWITCH: 1,
+ // A search suggestion or engine.
+ // Payload: { icon, suggestion, keyword, query, providesSearchMode, inPrivateWindow, isPrivateEngine }
+ SEARCH: 2,
+ // A common url/title tuple, may be a bookmark with tags.
+ // Payload: { icon, url, title, tags }
+ URL: 3,
+ // A bookmark keyword.
+ // Payload: { icon, url, keyword, postData }
+ KEYWORD: 4,
+ // A WebExtension Omnibox result.
+ // Payload: { icon, keyword, title, content }
+ OMNIBOX: 5,
+ // A tab from another synced device.
+ // Payload: { icon, url, device, title }
+ REMOTE_TAB: 6,
+ // An actionable message to help the user with their query.
+ // textData and buttonTextData are objects containing an l10n id and args.
+ // If a tip is untranslated it's possible to provide text and buttonText.
+ // Payload: { icon, textData, buttonTextData, [buttonUrl], [helpUrl] }
+ TIP: 7,
+ // A type of result created at runtime, for example by an extension.
+ // Payload: { dynamicType }
+ DYNAMIC: 8,
diff --git a/browser/components/urlbar/docs/telemetry.rst b/browser/components/urlbar/docs/telemetry.rst
new file mode 100644
index 0000000000..6ebb5f0d07
--- /dev/null
+++ b/browser/components/urlbar/docs/telemetry.rst
@@ -0,0 +1,445 @@
+Telemetry
+=========
+
+This section describes existing telemetry probes measuring interaction with the
+Address Bar.
+
+.. toctree::
+ :caption: Table of Contents
+
+ telemetry
+
+Histograms
+----------
+
+PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS
+ This probe tracks the amount of time it takes to get the first result.
+ It is an exponential histogram with values between 5 and 100.
+
+PLACES_AUTOCOMPLETE_6_FIRST_RESULTS_TIME_MS
+ This probe tracks the amount of time it takes to get the first six results.
+ It is an exponential histogram with values between 50 and 1000.
+
+FX_URLBAR_SELECTED_RESULT_METHOD
+ This probe tracks how a result was picked by the user from the list.
+ It is a categorical histogram with these values:
+
+ - ``enter``
+ The user pressed Enter without selecting a result first.
+ This most likely happens when the user confirms the default preselected
+ result (aka *heuristic result*), or when they select with the keyboard a
+ one-off search button and confirm with Enter.
+ - ``enterSelection``
+ The user selected a result, but not using Tab or the arrow keys, and then
+ pressed Enter. This is a rare and generally unexpected event, there may be
+ exotic ways to select a result we didn't consider, that are tracked here.
+ Look at arrowEnterSelection and tabEnterSelection for more common actions.
+ - ``click``
+ The user clicked on a result.
+ - ``arrowEnterSelection``
+ The user selected a result using the arrow keys, and then pressed Enter.
+ - ``tabEnterSelection``
+ The first key the user pressed to select a result was the Tab key, and then
+ they pressed Enter. Note that this means the user could have used the arrow
+ keys after first pressing the Tab key.
+ - ``rightClickEnter``
+ Before QuantumBar, it was possible to right-click a result to highlight but
+ not pick it. Then the user could press Enter. This is no more possible.
+
+Scalars
+-------
+
+urlbar.tips
+ This is a keyed scalar whose values are uints and are incremented each time a
+ tip result is shown, a tip is picked, and a tip's help button is picked. The
+ keys are:
+
+ - ``intervention_clear-help``
+ Incremented when the user picks the help button in the clear-history search
+ intervention.
+ - ``intervention_clear-picked``
+ Incremented when the user picks the clear-history search intervention.
+ - ``intervention_clear-shown``
+ Incremented when the clear-history search intervention is shown.
+ - ``intervention_refresh-help``
+ Incremented when the user picks the help button in the refresh-Firefox
+ search intervention.
+ - ``intervention_refresh-picked``
+ Incremented when the user picks the refresh-Firefox search intervention.
+ - ``intervention_refresh-shown``
+ Incremented when the refresh-Firefox search intervention is shown.
+ - ``intervention_update_ask-help``
+ Incremented when the user picks the help button in the update_ask search
+ intervention, which is shown when there's a Firefox update available but the
+ user's preference says we should ask them to download and apply it.
+ - ``intervention_update_ask-picked``
+ Incremented when the user picks the update_ask search intervention.
+ - ``intervention_update_ask-shown``
+ Incremented when the update_ask search intervention is shown.
+ - ``intervention_update_refresh-help``
+ Incremented when the user picks the help button in the update_refresh search
+ intervention, which is shown when the user's browser is up to date but they
+ triggered the update intervention. We show this special refresh intervention
+ instead.
+ - ``intervention_update_refresh-picked``
+ Incremented when the user picks the update_refresh search intervention.
+ - ``intervention_update_refresh-shown``
+ Incremented when the update_refresh search intervention is shown.
+ - ``intervention_update_restart-help``
+ Incremented when the user picks the help button in the update_restart search
+ intervention, which is shown when there's an update and it's been downloaded
+ and applied. The user needs to restart to finish.
+ - ``intervention_update_restart-picked``
+ Incremented when the user picks the update_restart search intervention.
+ - ``intervention_update_restart-shown``
+ Incremented when the update_restart search intervention is shown.
+ - ``intervention_update_web-help``
+ Incremented when the user picks the help button in the update_web search
+ intervention, which is shown when we can't update the browser or possibly
+ even check for updates for some reason, so the user should download the
+ latest version from the web.
+ - ``intervention_update_web-picked``
+ Incremented when the user picks the update_web search intervention.
+ - ``intervention_update_web-shown``
+ Incremented when the update_web search intervention is shown.
+ - ``tabtosearch_onboard-shown``
+ Incremented when a tab-to-search onboarding result is shown, once per engine
+ per engagement. Please note that the number of times tab-to-search
+ onboarding results are picked is the sum of all keys in
+ ``urlbar.searchmode.tabtosearch_onboard``.
+ - ``searchTip_onboard-picked``
+ Incremented when the user picks the onboarding search tip.
+ - ``searchTip_onboard-shown``
+ Incremented when the onboarding search tip is shown.
+ - ``searchTip_redirect-picked``
+ Incremented when the user picks the redirect search tip.
+ - ``searchTip_redirect-shown``
+ Incremented when the redirect search tip is shown.
+
+urlbar.searchmode.*
+ This is a set of keyed scalars whose values are uints incremented each
+ time search mode is entered in the Urlbar. The suffix on the scalar name
+ describes how search mode was entered. Possibilities include:
+
+ - ``bookmarkmenu``
+ Used when the user selects the Search Bookmarks menu item in the Library
+ menu.
+ - ``handoff``
+ Used when the user uses the search box on the new tab page and is handed off
+ to the address bar.
+ - ``keywordoffer``
+ Used when the user selects a keyword offer result.
+ - ``oneoff``
+ Used when the user selects a one-off engine in the Urlbar.
+ - ``shortcut``
+ Used when the user enters search mode with a keyboard shortcut or menu bar
+ item (e.g. ``Accel+K``).
+ - ``tabmenu``
+ Used when the user selects the Search Tabs menu item in the tab overflow
+ menu.
+ - ``tabtosearch``
+ Used when the user selects a tab-to-search result. These results suggest a
+ search engine when the search engine's domain is autofilled.
+ - ``tabtosearch_onboard``
+ Used when the user selects a tab-to-search onboarding result. These are
+ shown the first few times the user encounters a tab-to-search result.
+ - ``topsites_newtab``
+ Used when the user selects a search shortcut Top Site from the New Tab Page.
+ - ``topsites_urlbar``
+ Used when the user selects a search shortcut Top Site from the Urlbar.
+ - ``touchbar``
+ Used when the user taps a search shortct on the Touch Bar, available on some
+ Macs.
+ - ``typed``
+ Used when the user types an engine alias in the Urlbar.
+ - ``other``
+ Used as a catchall for other behaviour. We don't expect this scalar to hold
+ any values. If it does, we need to correct an issue with search mode entry
+ points.
+
+ The keys for the scalars above are engine and source names. If the user enters
+ a remote search mode with a built-in engine, we record the engine name. If the
+ user enters a remote search mode with an engine they installed (e.g. via
+ OpenSearch or a WebExtension), we record ``other`` (not to be confused with
+ the ``urlbar.searchmode.other`` scalar above). If they enter a local search
+ mode, we record the English name of the result source (e.g. "bookmarks",
+ "history", "tabs"). Note that we slightly modify the engine name for some
+ built-in engines: we flatten all localized Amazon sites (Amazon.com,
+ Amazon.ca, Amazon.de, etc.) to "Amazon" and we flatten all localized
+ Wikipedia sites (Wikipedia (en), Wikipedia (fr), etc.) to "Wikipedia". This
+ is done to reduce the number of keys used by these scalars.
+
+urlbar.picked.*
+ This is a set of keyed scalars whose values are uints incremented each
+ time a result is picked from the Urlbar. The suffix on the scalar name
+ is the result type. The keys for the scalars above are the 0-based index of
+ the result in the urlbar panel when it was picked.
+
+ .. note::
+ Available from Firefox 84 on. Use the *FX_URLBAR_SELECTED_** histograms in
+ earlier versions. See the `Obsolete probes`_ section below.
+
+ Valid result types are:
+
+ - ``autofill``
+ An origin or a URL completed the user typed text inline.
+ - ``bookmark``
+ A bookmarked URL.
+ - ``dynamic``
+ A specially crafted result, often used in experiments when basic types are
+ not flexible enough for a rich layout.
+ - ``extension``
+ Added by an add-on through the omnibox WebExtension API.
+ - ``formhistory``
+ A search suggestion from previous search history.
+ - ``history``
+ A URL from history.
+ - ``keyword``
+ A bookmark keyword.
+ - ``remotetab``
+ A tab synced from another device.
+ - ``searchengine``
+ A search result, but not a suggestion. May be the default search action
+ or a search alias.
+ - ``searchsuggestion``
+ A remote search suggestion.
+ - ``switchtab``
+ An open tab.
+ - ``tabtosearch``
+ A tab to search result.
+ - ``tip``
+ A tip result.
+ - ``topsite``
+ An entry from top sites.
+ - ``unknown``
+ An unknown result type, a bug should be filed to figure out what it is.
+ - ``visiturl``
+ The user typed string can be directly visited.
+
+urlbar.picked.searchmode.*
+ This is a set of keyed scalars whose values are uints incremented each time a
+ result is picked from the Urlbar while the Urlbar is in search mode. The
+ suffix on the scalar name is the search mode entry point. The keys for the
+ scalars are the 0-based index of the result in the urlbar panel when it was
+ picked.
+
+ .. note::
+ These scalars share elements of both ``urlbar.picked.*`` and
+ ``urlbar.searchmode.*``. Scalar name suffixes are search mode entry points,
+ like ``urlbar.searchmode.*``. The keys for these scalars are result indices,
+ like ``urlbar.picked.*``.
+
+ .. note::
+ These data are a subset of the data recorded by ``urlbar.picked.*``. For
+ example, if the user enters search mode by clicking a one-off then selects
+ a Google search suggestion at index 2, we would record in **both**
+ ``urlbar.picked.searchsuggestion`` and ``urlbar.picked.searchmode.oneoff``.
+
+
+Event Telemetry
+---------------
+
+The event telemetry is grouped under the ``urlbar`` category.
+
+Event Method
+ There are two methods to describe the interaction with the urlbar:
+
+ - ``engagement``
+ It is defined as a completed action in urlbar, where a user inserts text
+ and executes one of the actions described in the Event Object.
+ - ``abandonment``
+ It is defined as an action where the user inserts text but does not
+ complete an engagement action, usually unfocusing the urlbar. This also
+ happens when the user switches to another window, regardless of urlbar
+ focus.
+
+Event Value
+ This is how the user interaction started
+
+ - ``typed``: The text was typed into the urlbar.
+ - ``dropped``: The text was drag and dropped into the urlbar.
+ - ``pasted``: The text was pasted into the urlbar.
+ - ``topsites``: The user opened the urlbar view without typing, dropping,
+ or pasting.
+ In these cases, if the urlbar input is showing the URL of the loaded page
+ and the user has not modified the input’s content, the urlbar views shows
+ the user’s top sites. Otherwise, if the user had modified the input’s
+ content, the urlbar view shows results based on what the user has typed.
+ To tell whether top sites were shown, it's enough to check whether value is
+ ``topsites``. To know whether the user actually picked a top site, check
+ check that ``numChars`` == 0. If ``numChars`` > 0, the user initially opened
+ top sites, but then they started typing and confirmed a different result.
+ - ``returned``: The user abandoned a search, for example by switching to
+ another tab/window, or focusing something else, then came back to it
+ and continued. We consider a search continued if the user kept at least the
+ first char of the original search string.
+ - ``restarted``: The user abandoned a search, for example by switching to
+ another tab/window, or focusing something else, then came back to it,
+ cleared it and then typed a new string.
+
+Event Object
+ These describe actions in the urlbar:
+
+ - ``click``
+ The user clicked on a result.
+ - ``enter``
+ The user confirmed a result with Enter.
+ - ``drop_go``
+ The user dropped text on the input field.
+ - ``paste_go``
+ The user used Paste & Go feature. It is not the same as paste and Enter.
+ - ``blur``
+ The user unfocused the urlbar. This is only valid for ``abandonment``.
+
+Event Extra
+ This object contains additional information about the interaction.
+ Extra is a key-value store, where all the keys and values are strings.
+
+ - ``elapsed``
+ Time in milliseconds from the initial interaction to an action.
+ - ``numChars``
+ Number of input characters the user typed or pasted at the time of
+ submission.
+ - ``numWords``
+ Number of words in the input. The measurement is taken from a trimmed input
+ split up by its spaces. This is not a perfect measurement, since it will
+ return an incorrect value for languages that do not use spaces or URLs
+ containing spaces in its query parameters, for example.
+ - ``selType``
+ The type of the selected result at the time of submission.
+ This is only present for ``engagement`` events.
+ It can be one of: ``none``, ``autofill``, ``visiturl``, ``bookmark``,
+ ``history``, ``keyword``, ``searchengine``, ``searchsuggestion``,
+ ``switchtab``, ``remotetab``, ``extension``, ``oneoff``, ``keywordoffer``,
+ ``canonized``, ``tip``, ``tiphelp``, ``formhistory``, ``tabtosearch``,
+ ``unknown``
+ In practice, ``tabtosearch`` should not appear in real event telemetry.
+ Opening a tab-to-search result enters search mode and entering search mode
+ does not currently mark the end of an engagement. It is noted here for
+ completeness.
+ - ``selIndex``
+ Index of the selected result in the urlbar panel, or -1 for no selection.
+ There won't be a selection when a one-off button is the only selection, and
+ for the ``paste_go`` or ``drop_go`` objects. There may also not be a
+ selection if the system was busy and results arrived too late, then we
+ directly decide whether to search or visit the given string without having
+ a fully built result.
+ This is only present for ``engagement`` events.
+ - ``provider``
+ The name of the result provider for the selected result. Existing values
+ are: ``HeuristicFallback``, ``Autofill``, ``UnifiedComplete``,
+ ``TokenAliasEngines``, ``SearchSuggestions``, ``UrlbarProviderTopSites``.
+ Values can also be defined by `URLBar provider experiments`_.
+
+ .. _URLBar provider experiments: experiments.html#developing-address-bar-extensions
+
+Search probes relevant to the Address Bar
+-----------------------------------------
+
+SEARCH_COUNTS
+ This histogram tracks search engines and Search Access Points. It is augmented
+ by multiple SAPs, including the urlbar.
+ It's a keyed histogram, the keys are strings made up of search engine names
+ and SAP names, for example ``google.urlbar``.
+ For each key, this records the count of searches made using that engine and SAP.
+ SAP names can be:
+
+ - ``alias`` This is when using an alias (like ``@google``) in the urlbar.
+ Note there is often confusion between the terms alias and keyword, and
+ they may be used inappropriately: aliases refer to search engines, while
+ keywords refer to bookmarks. We expect no results for this SAP in Firefox
+ 83+, since urlbar-searchmode replaces it.
+ - ``abouthome``
+ - ``contextmenu``
+ - ``newtab``
+ - ``searchbar``
+ - ``system``
+ - ``urlbar`` Except aliases and search mode.
+ - ``urlbar-searchmode`` Used when the Urlbar is in search mode.
+ - ``webextension``
+ - ``oneoff-urlbar``
+ - ``oneoff-searchbar``
+ - ``unknown`` This is actually the searchbar, when using the current engine
+ one-off button.
+
+browser.engagement.navigation.*
+ These keyed scalars track search through different SAPs, for example the
+ urlbar is tracked by ``browser.engagement.navigation.urlbar``.
+ It counts loads triggered in a subsession from the specified SAP, broken down
+ by the originating action.
+ Possible SAPs are:
+
+ - ``urlbar`` Except search mode.
+ - ``urlbar_searchmode`` Used when the Urlbar is in search mode.
+ - ``searchbar``
+ - ``about_home``
+ - ``about_newtab``
+ - ``contextmenu``
+ - ``webextension``
+ - ``system`` Indicates a search from the command line.
+
+ Recorded actions may be:
+
+ - ``search``
+ Used for any search from ``contextmenu``, ``system`` and ``webextension``.
+ - ``search_alias``
+ For ``urlbar``, indicates the user confirmed a search through an alias.
+ - ``search_enter``
+ For ``about_home`` and ``about:newtab`` this counts any search.
+ For the other SAPs it tracks typing and then pressing Enter.
+ - ``search_formhistory``
+ For ``urlbar``, indicates the user picked a form history result.
+ - ``search_oneoff``
+ For ``urlbar`` or ``searchbar``, indicates the user confirmed a search
+ using a one-off button.
+ - ``search_suggestion``
+ For ``urlbar`` or ``searchbar``, indicates the user confirmed a search
+ suggestion.
+
+Obsolete probes
+---------------
+
+Obsolete histograms
+~~~~~~~~~~~~~~~~~~~
+
+FX_URLBAR_SELECTED_RESULT_INDEX (OBSOLETE)
+ This probe tracked the indexes of picked results in the results list.
+ It was an enumerated histogram with 17 buckets.
+
+FX_URLBAR_SELECTED_RESULT_TYPE and FX_URLBAR_SELECTED_RESULT_TYPE_2 (from Firefox 78 on) (OBSOLETE)
+ This probe tracked the types of picked results.
+ It was an enumerated histogram with 17 buckets:
+
+ 0. autofill
+ 1. bookmark
+ 2. history
+ 3. keyword
+ 4. searchengine
+ 5. searchsuggestion
+ 6. switchtab
+ 7. tag
+ 8. visiturl
+ 9. remotetab
+ 10. extension
+ 11. preloaded-top-site
+ 12. tip
+ 13. topsite
+ 14. formhistory
+ 15. dynamic
+ 16. tabtosearch
+
+FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE and FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2 (from Firefox 78 on) (OBSOLETE)
+ This probe tracked picked result type, for each one it tracked the index where
+ it appeared.
+ It was a keyed histogram where the keys were result types (see
+ FX_URLBAR_SELECTED_RESULT_TYPE above). For each key, this recorded the indexes
+ of picked results for that result type.
+
+Obsolete search probes
+----------------------
+
+navigation.search (OBSOLETE)
+ This is a legacy and disabled event telemetry that is currently under
+ discussion for removal or modernization. It can't be enabled through a pref.
+ it's more or less equivalent to browser.engagement.navigation, but can also
+ report the picked search engine.
diff --git a/browser/components/urlbar/docs/utilities.rst b/browser/components/urlbar/docs/utilities.rst
new file mode 100644
index 0000000000..8fa3b8509e
--- /dev/null
+++ b/browser/components/urlbar/docs/utilities.rst
@@ -0,0 +1,26 @@
+Utilities
+=========
+
+Various modules provide shared utilities to the other components:
+
+`UrlbarPrefs.jsm <https://searchfox.org/mozilla-central/source/browser/components/urlbar/UrlbarPrefs.jsm>`_
+-------------------------------------------------------------------------------------------------------------
+
+Implements a Map-like storage or urlbar related preferences. The values are kept
+up-to-date.
+
+.. highlight:: JavaScript
+.. code::
+
+ // Always use browser.urlbar. relative branch, except for the preferences in
+ // PREF_OTHER_DEFAULTS.
+ UrlbarPrefs.get("delay"); // Gets value of browser.urlbar.delay.
+
+.. note::
+
+ Newly added preferences should always be properly documented in UrlbarPrefs.
+
+`UrlbarUtils.jsm <https://searchfox.org/mozilla-central/source/browser/components/urlbar/UrlbarUtils.jsm>`_
+-------------------------------------------------------------------------------------------------------------
+
+Includes shared utils and constants shared across all the components.
diff --git a/browser/components/urlbar/moz.build b/browser/components/urlbar/moz.build
new file mode 100644
index 0000000000..cdade1db67
--- /dev/null
+++ b/browser/components/urlbar/moz.build
@@ -0,0 +1,47 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Firefox", "Address Bar")
+
+EXTRA_JS_MODULES += [
+ "UrlbarController.jsm",
+ "UrlbarEventBufferer.jsm",
+ "UrlbarInput.jsm",
+ "UrlbarMuxerUnifiedComplete.jsm",
+ "UrlbarPrefs.jsm",
+ "UrlbarProviderAutofill.jsm",
+ "UrlbarProviderExtension.jsm",
+ "UrlbarProviderHeuristicFallback.jsm",
+ "UrlbarProviderInterventions.jsm",
+ "UrlbarProviderOmnibox.jsm",
+ "UrlbarProviderOpenTabs.jsm",
+ "UrlbarProviderPrivateSearch.jsm",
+ "UrlbarProviderSearchSuggestions.jsm",
+ "UrlbarProviderSearchTips.jsm",
+ "UrlbarProvidersManager.jsm",
+ "UrlbarProviderTabToSearch.jsm",
+ "UrlbarProviderTokenAliasEngines.jsm",
+ "UrlbarProviderTopSites.jsm",
+ "UrlbarProviderUnifiedComplete.jsm",
+ "UrlbarResult.jsm",
+ "UrlbarSearchOneOffs.jsm",
+ "UrlbarSearchUtils.jsm",
+ "UrlbarTokenizer.jsm",
+ "UrlbarUtils.jsm",
+ "UrlbarValueFormatter.jsm",
+ "UrlbarView.jsm",
+]
+
+TESTING_JS_MODULES += [
+ "tests/UrlbarTestUtils.jsm",
+]
+BROWSER_CHROME_MANIFESTS += [
+ "tests/browser-tips/browser.ini",
+ "tests/browser/browser.ini",
+ "tests/ext/browser/browser.ini",
+]
+XPCSHELL_TESTS_MANIFESTS += ["tests/unit/xpcshell.ini"]
+
+SPHINX_TREES["/browser/urlbar"] = "docs"
diff --git a/browser/components/urlbar/tests/UrlbarTestUtils.jsm b/browser/components/urlbar/tests/UrlbarTestUtils.jsm
new file mode 100644
index 0000000000..2150b5364a
--- /dev/null
+++ b/browser/components/urlbar/tests/UrlbarTestUtils.jsm
@@ -0,0 +1,915 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const EXPORTED_SYMBOLS = ["UrlbarTestUtils"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AddonTestUtils: "resource://testing-common/AddonTestUtils.jsm",
+ AppConstants: "resource://gre/modules/AppConstants.jsm",
+ BrowserTestUtils: "resource://testing-common/BrowserTestUtils.jsm",
+ BrowserUtils: "resource://gre/modules/BrowserUtils.jsm",
+ BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
+ FormHistoryTestUtils: "resource://testing-common/FormHistoryTestUtils.jsm",
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
+ Services: "resource://gre/modules/Services.jsm",
+ setTimeout: "resource://gre/modules/Timer.jsm",
+ TestUtils: "resource://testing-common/TestUtils.jsm",
+ UrlbarController: "resource:///modules/UrlbarController.jsm",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
+ UrlbarProvider: "resource:///modules/UrlbarUtils.jsm",
+ UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.jsm",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+});
+
+var UrlbarTestUtils = {
+ /**
+ * This maps the categories used by the FX_URLBAR_SELECTED_RESULT_METHOD and
+ * FX_SEARCHBAR_SELECTED_RESULT_METHOD histograms to their indexes in the
+ * `labels` array. This only needs to be used by tests that need to map from
+ * category names to indexes in histogram snapshots. Actual app code can use
+ * these category names directly when they add to a histogram.
+ */
+ SELECTED_RESULT_METHODS: {
+ enter: 0,
+ enterSelection: 1,
+ click: 2,
+ arrowEnterSelection: 3,
+ tabEnterSelection: 4,
+ rightClickEnter: 5,
+ },
+
+ /**
+ * Running this init allows helpers to access test scope helpers, like Assert
+ * and SimpleTest. Note this initialization is not enforced, thus helpers
+ * should always check _testScope and provide a fallback path.
+ * @param {object} scope The global scope where tests are being run.
+ */
+ init(scope) {
+ this._testScope = scope;
+ if (scope) {
+ this.Assert = scope.Assert;
+ this.EventUtils = scope.EventUtils;
+ }
+ },
+
+ /**
+ * If tests initialize UrlbarTestUtils, they may need to call this function in
+ * their cleanup callback, or else their scope will affect subsequent tests.
+ * This is usually only required for tests outside browser/components/urlbar.
+ */
+ uninit() {
+ this._testScope = null;
+ },
+
+ /**
+ * Waits to a search to be complete.
+ * @param {object} win The window containing the urlbar
+ * @returns {Promise} Resolved when done.
+ */
+ async promiseSearchComplete(win) {
+ let waitForQuery = () => {
+ return this.promisePopupOpen(win, () => {}).then(
+ () => win.gURLBar.lastQueryContextPromise
+ );
+ };
+ let context = await waitForQuery();
+ if (win.gURLBar.searchMode) {
+ // Search mode may start a second query.
+ context = await waitForQuery();
+ }
+ return context;
+ },
+
+ /**
+ * Starts a search for a given string and waits for the search to be complete.
+ * @param {object} options.window The window containing the urlbar
+ * @param {string} options.value the search string
+ * @param {function} options.waitForFocus The SimpleTest function
+ * @param {boolean} [options.fireInputEvent] whether an input event should be
+ * used when starting the query (simulates the user's typing, sets
+ * userTypedValued, triggers engagement event telemetry, etc.)
+ * @param {number} [options.selectionStart] The input's selectionStart
+ * @param {number} [options.selectionEnd] The input's selectionEnd
+ */
+ async promiseAutocompleteResultPopup({
+ window,
+ value,
+ waitForFocus,
+ fireInputEvent = false,
+ selectionStart = -1,
+ selectionEnd = -1,
+ } = {}) {
+ if (this._testScope) {
+ await this._testScope.SimpleTest.promiseFocus(window);
+ } else {
+ await new Promise(resolve => waitForFocus(resolve, window));
+ }
+ window.gURLBar.inputField.focus();
+ // Using the value setter in some cases may trim and fetch unexpected
+ // results, then pick an alternate path.
+ if (UrlbarPrefs.get("trimURLs") && value != BrowserUtils.trimURL(value)) {
+ window.gURLBar.inputField.value = value;
+ fireInputEvent = true;
+ } else {
+ window.gURLBar.value = value;
+ }
+ if (selectionStart >= 0 && selectionEnd >= 0) {
+ window.gURLBar.selectionEnd = selectionEnd;
+ window.gURLBar.selectionStart = selectionStart;
+ }
+
+ // An input event will start a new search, so be careful not to start a
+ // search if we fired an input event since that would start two searches.
+ if (fireInputEvent) {
+ // This is necessary to get the urlbar to set gBrowser.userTypedValue.
+ this.fireInputEvent(window);
+ } else {
+ window.gURLBar.setPageProxyState("invalid");
+ window.gURLBar.startQuery();
+ }
+
+ return this.promiseSearchComplete(window);
+ },
+
+ /**
+ * Waits for a result to be added at a certain index. Since we implement lazy
+ * results replacement, even if we have a result at an index, it may be
+ * related to the previous query, this methods ensures the result is current.
+ * @param {object} win The window containing the urlbar
+ * @param {number} index The index to look for
+ * @returns {HtmlElement|XulElement} the result's element.
+ */
+ async waitForAutocompleteResultAt(win, index) {
+ // TODO Bug 1530338: Quantum Bar doesn't yet implement lazy results replacement.
+ await this.promiseSearchComplete(win);
+ if (index >= win.gURLBar.view._rows.children.length) {
+ throw new Error("Not enough results");
+ }
+ return win.gURLBar.view._rows.children[index];
+ },
+
+ /**
+ * Returns the oneOffSearchButtons object for the urlbar.
+ * @param {object} win The window containing the urlbar
+ * @returns {object} The oneOffSearchButtons
+ */
+ getOneOffSearchButtons(win) {
+ return win.gURLBar.view.oneOffSearchButtons;
+ },
+
+ /**
+ * Returns true if the oneOffSearchButtons are visible.
+ * @param {object} win The window containing the urlbar
+ * @returns {boolean} True if the buttons are visible.
+ */
+ getOneOffSearchButtonsVisible(win) {
+ let buttons = this.getOneOffSearchButtons(win);
+ return buttons.style.display != "none" && !buttons.container.hidden;
+ },
+
+ /**
+ * Gets an abstracted representation of the result at an index.
+ * @param {object} win The window containing the urlbar
+ * @param {number} index The index to look for
+ * @returns {object} An object with numerous properties describing the result.
+ */
+ async getDetailsOfResultAt(win, index) {
+ let element = await this.waitForAutocompleteResultAt(win, index);
+ let details = {};
+ let result = element.result;
+ let { url, postData } = UrlbarUtils.getUrlFromResult(result);
+ details.url = url;
+ details.postData = postData;
+ details.type = result.type;
+ details.source = result.source;
+ details.heuristic = result.heuristic;
+ details.autofill = !!result.autofill;
+ details.image = element.getElementsByClassName("urlbarView-favicon")[0].src;
+ details.title = result.title;
+ details.tags = "tags" in result.payload ? result.payload.tags : [];
+ let actions = element.getElementsByClassName("urlbarView-action");
+ let urls = element.getElementsByClassName("urlbarView-url");
+ let typeIcon = element.querySelector(".urlbarView-type-icon");
+ await win.document.l10n.translateFragment(element);
+ details.displayed = {
+ title: element.getElementsByClassName("urlbarView-title")[0].textContent,
+ action: actions.length ? actions[0].textContent : null,
+ url: urls.length ? urls[0].textContent : null,
+ typeIcon: typeIcon
+ ? win.getComputedStyle(typeIcon)["background-image"]
+ : null,
+ };
+ details.element = {
+ action: element.getElementsByClassName("urlbarView-action")[0],
+ row: element,
+ separator: element.getElementsByClassName(
+ "urlbarView-title-separator"
+ )[0],
+ title: element.getElementsByClassName("urlbarView-title")[0],
+ url: element.getElementsByClassName("urlbarView-url")[0],
+ };
+ if (details.type == UrlbarUtils.RESULT_TYPE.SEARCH) {
+ details.searchParams = {
+ engine: result.payload.engine,
+ keyword: result.payload.keyword,
+ query: result.payload.query,
+ suggestion: result.payload.suggestion,
+ inPrivateWindow: result.payload.inPrivateWindow,
+ isPrivateEngine: result.payload.isPrivateEngine,
+ };
+ } else if (details.type == UrlbarUtils.RESULT_TYPE.KEYWORD) {
+ details.keyword = result.payload.keyword;
+ }
+ return details;
+ },
+
+ /**
+ * Gets the currently selected element.
+ * @param {object} win The window containing the urlbar.
+ * @returns {HtmlElement|XulElement} The selected element.
+ */
+ getSelectedElement(win) {
+ return win.gURLBar.view.selectedElement || null;
+ },
+
+ /**
+ * Gets the index of the currently selected element.
+ * @param {object} win The window containing the urlbar.
+ * @returns {number} The selected index.
+ */
+ getSelectedElementIndex(win) {
+ return win.gURLBar.view.selectedElementIndex;
+ },
+
+ /**
+ * Gets the currently selected row. If the selected element is a descendant of
+ * a row, this will return the ancestor row.
+ * @param {object} win The window containing the urlbar.
+ * @returns {HTMLElement|XulElement} The selected row.
+ */
+ getSelectedRow(win) {
+ return win.gURLBar.view._getSelectedRow() || null;
+ },
+
+ /**
+ * Gets the index of the currently selected element.
+ * @param {object} win The window containing the urlbar.
+ * @returns {number} The selected row index.
+ */
+ getSelectedRowIndex(win) {
+ return win.gURLBar.view.selectedRowIndex;
+ },
+
+ /**
+ * Selects the element at the index specified.
+ * @param {object} win The window containing the urlbar.
+ * @param {index} index The index to select.
+ */
+ setSelectedRowIndex(win, index) {
+ win.gURLBar.view.selectedRowIndex = index;
+ },
+
+ /**
+ * Gets the number of results.
+ * You must wait for the query to be complete before using this.
+ * @param {object} win The window containing the urlbar
+ * @returns {number} the number of results.
+ */
+ getResultCount(win) {
+ return win.gURLBar.view._rows.children.length;
+ },
+
+ /**
+ * Ensures at least one search suggestion is present.
+ * @param {object} win The window containing the urlbar
+ * @returns {boolean} whether at least one search suggestion is present.
+ */
+ promiseSuggestionsPresent(win) {
+ // TODO Bug 1530338: Quantum Bar doesn't yet implement lazy results replacement. When
+ // we do that, we'll have to be sure the suggestions we find are relevant
+ // for the current query. For now let's just wait for the search to be
+ // complete.
+ return this.promiseSearchComplete(win).then(context => {
+ // Look for search suggestions.
+ let firstSearchSuggestionIndex = context.results.findIndex(
+ r => r.type == UrlbarUtils.RESULT_TYPE.SEARCH && r.payload.suggestion
+ );
+ if (firstSearchSuggestionIndex == -1) {
+ throw new Error("Cannot find a search suggestion");
+ }
+ return firstSearchSuggestionIndex;
+ });
+ },
+
+ /**
+ * Waits for the given number of connections to an http server.
+ * @param {object} httpserver an HTTP Server instance
+ * @param {number} count Number of connections to wait for
+ * @returns {Promise} resolved when all the expected connections were started.
+ */
+ promiseSpeculativeConnections(httpserver, count) {
+ if (!httpserver) {
+ throw new Error("Must provide an http server");
+ }
+ return BrowserTestUtils.waitForCondition(
+ () => httpserver.connectionNumber == count,
+ "Waiting for speculative connection setup"
+ );
+ },
+
+ /**
+ * Waits for the popup to be shown.
+ * @param {object} win The window containing the urlbar
+ * @param {function} openFn Function to be used to open the popup.
+ * @returns {Promise} resolved once the popup is closed
+ */
+ async promisePopupOpen(win, openFn) {
+ if (!openFn) {
+ throw new Error("openFn should be supplied to promisePopupOpen");
+ }
+ await openFn();
+ if (win.gURLBar.view.isOpen) {
+ return;
+ }
+ if (this._testScope) {
+ this._testScope.info("Awaiting for the urlbar panel to open");
+ }
+ await new Promise(resolve => {
+ win.gURLBar.controller.addQueryListener({
+ onViewOpen() {
+ win.gURLBar.controller.removeQueryListener(this);
+ resolve();
+ },
+ });
+ });
+ },
+
+ /**
+ * Waits for the popup to be hidden.
+ * @param {object} win The window containing the urlbar
+ * @param {function} [closeFn] Function to be used to close the popup, if not
+ * supplied it will default to a closing the popup directly.
+ * @returns {Promise} resolved once the popup is closed
+ */
+ async promisePopupClose(win, closeFn = null) {
+ if (closeFn) {
+ await closeFn();
+ } else {
+ win.gURLBar.view.close();
+ }
+ if (!win.gURLBar.view.isOpen) {
+ return;
+ }
+ if (this._testScope) {
+ this._testScope.info("Awaiting for the urlbar panel to close");
+ }
+ await new Promise(resolve => {
+ win.gURLBar.controller.addQueryListener({
+ onViewClose() {
+ win.gURLBar.controller.removeQueryListener(this);
+ resolve();
+ },
+ });
+ });
+ },
+
+ /**
+ * @param {object} win The browser window
+ * @returns {boolean} Whether the popup is open
+ */
+ isPopupOpen(win) {
+ return win.gURLBar.view.isOpen;
+ },
+
+ /**
+ * Asserts that the input is in a given search mode, or no search mode.
+ *
+ * @param {Window} window
+ * The browser window.
+ * @param {object} expectedSearchMode
+ * The expected search mode object.
+ * @note Can only be used if UrlbarTestUtils has been initialized with init().
+ */
+ async assertSearchMode(window, expectedSearchMode) {
+ this.Assert.equal(
+ !!window.gURLBar.searchMode,
+ window.gURLBar.hasAttribute("searchmode"),
+ "Urlbar should never be in search mode without the corresponding attribute."
+ );
+
+ this.Assert.equal(
+ !!window.gURLBar.searchMode,
+ !!expectedSearchMode,
+ "gURLBar.searchMode should exist as expected"
+ );
+
+ if (!expectedSearchMode) {
+ // Check the input's placeholder.
+ const prefName =
+ "browser.urlbar.placeholderName" +
+ (PrivateBrowsingUtils.isWindowPrivate(window) ? ".private" : "");
+ let engineName = Services.prefs.getStringPref(prefName, "");
+ this.Assert.deepEqual(
+ window.document.l10n.getAttributes(window.gURLBar.inputField),
+ engineName
+ ? { id: "urlbar-placeholder-with-name", args: { name: engineName } }
+ : { id: "urlbar-placeholder", args: null },
+ "Expected placeholder l10n when search mode is inactive"
+ );
+ return;
+ }
+
+ // Default to full search mode for less verbose tests.
+ expectedSearchMode = { ...expectedSearchMode };
+ if (!expectedSearchMode.hasOwnProperty("isPreview")) {
+ expectedSearchMode.isPreview = false;
+ }
+
+ // expectedSearchMode may come from UrlbarUtils.LOCAL_SEARCH_MODES. The
+ // objects in that array include useful metadata like icon URIs and pref
+ // names that are not usually included in actual search mode objects. For
+ // convenience, ignore those properties if they aren't also present in the
+ // urlbar's actual search mode object.
+ let ignoreProperties = ["icon", "pref", "restrict"];
+ for (let prop of ignoreProperties) {
+ if (prop in expectedSearchMode && !(prop in window.gURLBar.searchMode)) {
+ if (this._testScope) {
+ this._testScope.info(
+ `Ignoring unimportant property '${prop}' in expected search mode`
+ );
+ }
+ delete expectedSearchMode[prop];
+ }
+ }
+
+ this.Assert.deepEqual(
+ window.gURLBar.searchMode,
+ expectedSearchMode,
+ "Expected searchMode"
+ );
+
+ // Check the textContent and l10n attributes of the indicator and label.
+ let expectedTextContent = "";
+ let expectedL10n = { id: null, args: null };
+ if (expectedSearchMode.engineName) {
+ expectedTextContent = expectedSearchMode.engineName;
+ } else if (expectedSearchMode.source) {
+ let name = UrlbarUtils.getResultSourceName(expectedSearchMode.source);
+ this.Assert.ok(name, "Expected result source should have a name");
+ expectedL10n = { id: `urlbar-search-mode-${name}`, args: null };
+ } else {
+ this.Assert.ok(false, "Unexpected searchMode");
+ }
+
+ for (let element of [
+ window.gURLBar._searchModeIndicatorTitle,
+ window.gURLBar._searchModeLabel,
+ ]) {
+ if (expectedTextContent) {
+ this.Assert.equal(
+ element.textContent,
+ expectedTextContent,
+ "Expected textContent"
+ );
+ }
+ this.Assert.deepEqual(
+ window.document.l10n.getAttributes(element),
+ expectedL10n,
+ "Expected l10n"
+ );
+ }
+
+ // Check the input's placeholder.
+ let expectedPlaceholderL10n;
+ if (expectedSearchMode.engineName) {
+ expectedPlaceholderL10n = {
+ id: UrlbarUtils.WEB_ENGINE_NAMES.has(expectedSearchMode.engineName)
+ ? "urlbar-placeholder-search-mode-web-2"
+ : "urlbar-placeholder-search-mode-other-engine",
+ args: { name: expectedSearchMode.engineName },
+ };
+ } else if (expectedSearchMode.source) {
+ let name = UrlbarUtils.getResultSourceName(expectedSearchMode.source);
+ expectedPlaceholderL10n = {
+ id: `urlbar-placeholder-search-mode-other-${name}`,
+ args: null,
+ };
+ }
+ this.Assert.deepEqual(
+ window.document.l10n.getAttributes(window.gURLBar.inputField),
+ expectedPlaceholderL10n,
+ "Expected placeholder l10n when search mode is active"
+ );
+
+ // If this is an engine search mode, check that all results are either
+ // search results with the same engine or have the same host as the engine.
+ // Search mode preview can show other results since it is not supposed to
+ // start a query.
+ if (
+ expectedSearchMode.engineName &&
+ !expectedSearchMode.isPreview &&
+ this.isPopupOpen(window)
+ ) {
+ let resultCount = this.getResultCount(window);
+ for (let i = 0; i < resultCount; i++) {
+ let result = await this.getDetailsOfResultAt(window, i);
+ if (result.source == UrlbarUtils.RESULT_SOURCE.SEARCH) {
+ this.Assert.equal(
+ expectedSearchMode.engineName,
+ result.searchParams.engine,
+ "Search mode result matches engine name."
+ );
+ } else {
+ let engine = Services.search.getEngineByName(
+ expectedSearchMode.engineName
+ );
+ let engineRootDomain = UrlbarSearchUtils.getRootDomainFromEngine(
+ engine
+ );
+ let resultUrl = new URL(result.url);
+ this.Assert.ok(
+ resultUrl.hostname.includes(engineRootDomain),
+ "Search mode result matches engine host."
+ );
+ }
+ }
+ }
+ },
+
+ /**
+ * Enters search mode by clicking a one-off. The view must already be open
+ * before you call this.
+ * @param {object} window
+ * @param {object} searchMode
+ * If given, the one-off matching this search mode will be clicked; it
+ * should be a full search mode object as described in
+ * UrlbarInput.setSearchMode. If not given, the first one-off is clicked.
+ * @note Can only be used if UrlbarTestUtils has been initialized with init().
+ */
+ async enterSearchMode(window, searchMode = null) {
+ // Ensure any pending query is complete.
+ await this.promiseSearchComplete(window);
+
+ // Ensure the the one-offs are finished rebuilding and visible.
+ let oneOffs = this.getOneOffSearchButtons(window);
+ await TestUtils.waitForCondition(
+ () => !oneOffs._rebuilding,
+ "Waiting for one-offs to finish rebuilding"
+ );
+ this.Assert.equal(
+ UrlbarTestUtils.getOneOffSearchButtonsVisible(window),
+ true,
+ "One-offs are visible"
+ );
+
+ let buttons = oneOffs.getSelectableButtons(true);
+ if (!searchMode) {
+ searchMode = { engineName: buttons[0].engine.name };
+ if (UrlbarUtils.WEB_ENGINE_NAMES.has(searchMode.engineName)) {
+ searchMode.source = UrlbarUtils.RESULT_SOURCE.SEARCH;
+ }
+ }
+
+ if (!searchMode.entry) {
+ searchMode.entry = "oneoff";
+ }
+
+ let oneOff = buttons.find(o =>
+ searchMode.engineName
+ ? o.engine.name == searchMode.engineName
+ : o.source == searchMode.source
+ );
+ this.Assert.ok(oneOff, "Found one-off button for search mode");
+ this.EventUtils.synthesizeMouseAtCenter(oneOff, {}, window);
+ await this.promiseSearchComplete(window);
+ this.Assert.ok(this.isPopupOpen(window), "Urlbar view is still open.");
+ await this.assertSearchMode(window, searchMode);
+ },
+
+ /**
+ * Exits search mode.
+ * @param {object} window
+ * @param {boolean} options.backspace
+ * Exits search mode by backspacing at the beginning of the search string.
+ * @param {boolean} options.clickClose
+ * Exits search mode by clicking the close button on the search mode
+ * indicator.
+ * @param {boolean} [waitForSearch]
+ * Whether the test should wait for a search after exiting search mode.
+ * Defaults to true.
+ * @note If neither `backspace` nor `clickClose` is given, we'll default to
+ * backspacing.
+ * @note Can only be used if UrlbarTestUtils has been initialized with init().
+ */
+ async exitSearchMode(
+ window,
+ { backspace, clickClose, waitForSearch = true } = {}
+ ) {
+ let urlbar = window.gURLBar;
+ // If the Urlbar is not extended, ignore the clickClose parameter. The close
+ // button is not clickable in this state. This state might be encountered on
+ // Linux, where prefers-reduced-motion is enabled in automation.
+ if (!urlbar.hasAttribute("breakout-extend") && clickClose) {
+ if (waitForSearch) {
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ urlbar.searchMode = null;
+ await searchPromise;
+ } else {
+ urlbar.searchMode = null;
+ }
+ return;
+ }
+
+ if (!backspace && !clickClose) {
+ backspace = true;
+ }
+
+ if (backspace) {
+ let urlbarValue = urlbar.value;
+ urlbar.selectionStart = urlbar.selectionEnd = 0;
+ if (waitForSearch) {
+ let searchPromise = this.promiseSearchComplete(window);
+ this.EventUtils.synthesizeKey("KEY_Backspace", {}, window);
+ await searchPromise;
+ } else {
+ this.EventUtils.synthesizeKey("KEY_Backspace", {}, window);
+ }
+ this.Assert.equal(
+ urlbar.value,
+ urlbarValue,
+ "Urlbar value hasn't changed."
+ );
+ this.assertSearchMode(window, null);
+ } else if (clickClose) {
+ // We need to hover the indicator to make the close button clickable in the
+ // test.
+ let indicator = urlbar.querySelector("#urlbar-search-mode-indicator");
+ this.EventUtils.synthesizeMouseAtCenter(
+ indicator,
+ { type: "mouseover" },
+ window
+ );
+ let closeButton = urlbar.querySelector(
+ "#urlbar-search-mode-indicator-close"
+ );
+ if (waitForSearch) {
+ let searchPromise = this.promiseSearchComplete(window);
+ this.EventUtils.synthesizeMouseAtCenter(closeButton, {}, window);
+ await searchPromise;
+ } else {
+ this.EventUtils.synthesizeMouseAtCenter(closeButton, {}, window);
+ }
+ await this.assertSearchMode(window, null);
+ }
+ },
+
+ /**
+ * Returns the userContextId (container id) for the last search.
+ * @param {object} win The browser window
+ * @returns {Promise} resolved when fetching is complete
+ * @resolves {number} a userContextId
+ */
+ async promiseUserContextId(win) {
+ const defaultId = Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID;
+ let context = await win.gURLBar.lastQueryContextPromise;
+ return context.userContextId || defaultId;
+ },
+
+ /**
+ * Dispatches an input event to the input field.
+ * @param {object} win The browser window
+ */
+ fireInputEvent(win) {
+ // Set event.data to the last character in the input, for a couple of
+ // reasons: It simulates the user typing, and it's necessary for autofill.
+ let event = new InputEvent("input", {
+ data: win.gURLBar.value[win.gURLBar.value.length - 1] || null,
+ });
+ win.gURLBar.inputField.dispatchEvent(event);
+ },
+
+ /**
+ * Returns a new mock controller. This is useful for xpcshell tests.
+ * @param {object} options Additional options to pass to the UrlbarController
+ * constructor.
+ * @returns {UrlbarController} A new controller.
+ */
+ newMockController(options = {}) {
+ return new UrlbarController(
+ Object.assign(
+ {
+ input: {
+ isPrivate: false,
+ onFirstResult() {
+ return false;
+ },
+ window: {
+ location: {
+ href: AppConstants.BROWSER_CHROME_URL,
+ },
+ },
+ },
+ },
+ options
+ )
+ );
+ },
+
+ /**
+ * Initializes some external components used by the urlbar. This is necessary
+ * in xpcshell tests but not in browser tests.
+ */
+ async initXPCShellDependencies() {
+ // The FormHistoryStartup component must be initialized since urlbar uses
+ // form history.
+ Cc["@mozilla.org/satchel/form-history-startup;1"]
+ .getService(Ci.nsIObserver)
+ .observe(null, "profile-after-change", null);
+
+ // This is necessary because UrlbarMuxerUnifiedComplete.sort calls
+ // Services.search.parseSubmissionURL, so we need engines.
+ try {
+ await AddonTestUtils.promiseStartupManager();
+ } catch (error) {
+ if (!error.message.includes("already started")) {
+ throw error;
+ }
+ }
+ },
+};
+
+UrlbarTestUtils.formHistory = {
+ /**
+ * Adds values to the urlbar's form history.
+ *
+ * @param {array} values
+ * The form history entries to remove.
+ * @param {object} window
+ * The window containing the urlbar.
+ * @returns {Promise} resolved once the operation is complete.
+ */
+ add(values = [], window = BrowserWindowTracker.getTopWindow()) {
+ let fieldname = this.getFormHistoryName(window);
+ return FormHistoryTestUtils.add(fieldname, values);
+ },
+
+ /**
+ * Removes values from the urlbar's form history. If you want to remove all
+ * history, use clearFormHistory.
+ *
+ * @param {array} values
+ * The form history entries to remove.
+ * @param {object} window
+ * The window containing the urlbar.
+ * @returns {Promise} resolved once the operation is complete.
+ */
+ remove(values = [], window = BrowserWindowTracker.getTopWindow()) {
+ let fieldname = this.getFormHistoryName(window);
+ return FormHistoryTestUtils.remove(fieldname, values);
+ },
+
+ /**
+ * Removes all values from the urlbar's form history. If you want to remove
+ * individual values, use removeFormHistory.
+ *
+ * @param {object} window
+ * The window containing the urlbar.
+ * @returns {Promise} resolved once the operation is complete.
+ */
+ clear(window = BrowserWindowTracker.getTopWindow()) {
+ let fieldname = this.getFormHistoryName(window);
+ return FormHistoryTestUtils.clear(fieldname);
+ },
+
+ /**
+ * Searches the urlbar's form history.
+ *
+ * @param {object} criteria
+ * Criteria to narrow the search. See FormHistory.search.
+ * @param {object} window
+ * The window containing the urlbar.
+ * @returns {Promise}
+ * A promise resolved with an array of found form history entries.
+ */
+ search(criteria = {}, window = BrowserWindowTracker.getTopWindow()) {
+ let fieldname = this.getFormHistoryName(window);
+ return FormHistoryTestUtils.search(fieldname, criteria);
+ },
+
+ /**
+ * Returns a promise that's resolved on the next form history change.
+ *
+ * @param {string} change
+ * Null to listen for any change, or one of: add, remove, update
+ * @returns {Promise}
+ * Resolved on the next specified form history change.
+ */
+ promiseChanged(change = null) {
+ return TestUtils.topicObserved(
+ "satchel-storage-changed",
+ (subject, data) => !change || data == "formhistory-" + change
+ );
+ },
+
+ /**
+ * Returns the form history name for the urlbar in a window.
+ *
+ * @param {object} window
+ * The window.
+ * @returns {string}
+ * The form history name of the urlbar in the window.
+ */
+ getFormHistoryName(window = BrowserWindowTracker.getTopWindow()) {
+ return window ? window.gURLBar.formHistoryName : "searchbar-history";
+ },
+};
+
+/**
+ * A test provider. If you need a test provider whose behavior is different
+ * from this, then consider modifying the implementation below if you think the
+ * new behavior would be useful for other tests. Otherwise, you can create a
+ * new TestProvider instance and then override its methods.
+ */
+class TestProvider extends UrlbarProvider {
+ /**
+ * Constructor.
+ *
+ * @param {array} results
+ * An array of UrlbarResult objects that will be the provider's results.
+ * @param {string} [name]
+ * The provider's name. Provider names should be unique.
+ * @param {UrlbarUtils.PROVIDER_TYPE} [type]
+ * The provider's type.
+ * @param {number} [priority]
+ * The provider's priority. Built-in providers have a priority of zero.
+ * @param {number} [addTimeout]
+ * If non-zero, each result will be added on this timeout. If zero, all
+ * results will be added immediately and synchronously.
+ * @param {function} [onCancel]
+ * If given, a function that will be called when the provider's cancelQuery
+ * method is called.
+ */
+ constructor({
+ results,
+ name = Math.floor(Math.random() * 100000),
+ type = UrlbarUtils.PROVIDER_TYPE.PROFILE,
+ priority = 0,
+ addTimeout = 0,
+ onCancel = null,
+ onSelection = null,
+ } = {}) {
+ super();
+ this._results = results;
+ this._name = name;
+ this._type = type;
+ this._priority = priority;
+ this._addTimeout = addTimeout;
+ this._onCancel = onCancel;
+ this._onSelection = onSelection;
+ }
+ get name() {
+ return "TestProvider" + this._name;
+ }
+ get type() {
+ return this._type;
+ }
+ getPriority(context) {
+ return this._priority;
+ }
+ isActive(context) {
+ return true;
+ }
+ async startQuery(context, addCallback) {
+ for (let result of this._results) {
+ if (!this._addTimeout) {
+ addCallback(this, result);
+ } else {
+ await new Promise(resolve => {
+ setTimeout(() => {
+ addCallback(this, result);
+ resolve();
+ }, this._addTimeout);
+ });
+ }
+ }
+ }
+ cancelQuery(context) {
+ if (this._onCancel) {
+ this._onCancel();
+ }
+ }
+
+ onSelection(result, element) {
+ if (this._onSelection) {
+ this._onSelection(result, element);
+ }
+ }
+}
+
+UrlbarTestUtils.TestProvider = TestProvider;
diff --git a/browser/components/urlbar/tests/browser-tips/README.txt b/browser/components/urlbar/tests/browser-tips/README.txt
new file mode 100644
index 0000000000..04a7b09707
--- /dev/null
+++ b/browser/components/urlbar/tests/browser-tips/README.txt
@@ -0,0 +1,7 @@
+If you're running these tests and you get an error like this:
+
+FAIL head.js import threw an exception - Error opening input stream (invalid filename?): chrome://mochitests/content/browser/toolkit/mozapps/update/tests/browser/head.js
+
+Then run `mach test toolkit/mozapps/update/tests/browser` first. You can
+stop mach as soon as it starts the first test, but this is necessary so that
+mach builds the update tests in your objdir.
diff --git a/browser/components/urlbar/tests/browser-tips/browser.ini b/browser/components/urlbar/tests/browser-tips/browser.ini
new file mode 100644
index 0000000000..0c4bb95cc5
--- /dev/null
+++ b/browser/components/urlbar/tests/browser-tips/browser.ini
@@ -0,0 +1,17 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+[DEFAULT]
+support-files =
+ head.js
+
+[browser_interventions.js]
+[browser_picks.js]
+[browser_searchTips_interaction.js]
+[browser_searchTips.js]
+[browser_selection.js]
+[browser_updateAsk.js]
+[browser_updateRefresh.js]
+[browser_updateRestart.js]
+[browser_updateWeb.js]
diff --git a/browser/components/urlbar/tests/browser-tips/browser_interventions.js b/browser/components/urlbar/tests/browser-tips/browser_interventions.js
new file mode 100644
index 0000000000..87ee03e75d
--- /dev/null
+++ b/browser/components/urlbar/tests/browser-tips/browser_interventions.js
@@ -0,0 +1,209 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ UrlbarProviderInterventions:
+ "resource:///modules/UrlbarProviderInterventions.jsm",
+});
+
+add_task(async function init() {
+ makeProfileResettable();
+});
+
+// Tests the refresh tip.
+add_task(async function refresh() {
+ // Pick the tip, which should open the refresh dialog. Click its cancel
+ // button.
+ await checkIntervention({
+ searchString: SEARCH_STRINGS.REFRESH,
+ tip: UrlbarProviderInterventions.TIP_TYPE.REFRESH,
+ title:
+ "Restore default settings and remove old add-ons for optimal performance.",
+ button: /^Refresh .+…$/,
+ awaitCallback() {
+ return promiseAlertDialog("cancel", [
+ "chrome://global/content/resetProfile.xhtml",
+ "chrome://global/content/resetProfile.xul",
+ ]);
+ },
+ });
+});
+
+// Tests the clear tip.
+add_task(async function clear() {
+ // Pick the tip, which should open the refresh dialog. Click its cancel
+ // button.
+ await checkIntervention({
+ searchString: SEARCH_STRINGS.CLEAR,
+ tip: UrlbarProviderInterventions.TIP_TYPE.CLEAR,
+ title: "Clear your cache, cookies, history and more.",
+ button: "Choose What to Clear…",
+ awaitCallback() {
+ return promiseAlertDialog("cancel", [
+ "chrome://browser/content/sanitize.xhtml",
+ "chrome://browser/content/sanitize.xul",
+ ]);
+ },
+ });
+});
+
+// Tests the clear tip in a private window. The clear tip shouldn't appear in
+// private windows.
+add_task(async function clear_private() {
+ let win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+ // First, make sure the extension works in PBM by triggering a non-clear
+ // tip.
+ let result = (await awaitTip(SEARCH_STRINGS.REFRESH, win))[0];
+ Assert.strictEqual(
+ result.payload.type,
+ UrlbarProviderInterventions.TIP_TYPE.REFRESH
+ );
+
+ // Blur the urlbar so that the engagement is ended.
+ await UrlbarTestUtils.promisePopupClose(win, () => win.gURLBar.blur());
+
+ // Now do a search that would trigger the clear tip.
+ await awaitNoTip(SEARCH_STRINGS.CLEAR, win);
+
+ // Blur the urlbar so that the engagement is ended.
+ await UrlbarTestUtils.promisePopupClose(win, () => win.gURLBar.blur());
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+// Tests that if multiple interventions of the same type are seen in the same
+// engagement, only one instance is recorded in Telemetry.
+add_task(async function multipleInterventionsInOneEngagement() {
+ Services.telemetry.clearScalars();
+ let result = (await awaitTip(SEARCH_STRINGS.REFRESH, window))[0];
+ Assert.strictEqual(
+ result.payload.type,
+ UrlbarProviderInterventions.TIP_TYPE.REFRESH
+ );
+ result = (await awaitTip(SEARCH_STRINGS.CLEAR, window))[0];
+ Assert.strictEqual(
+ result.payload.type,
+ UrlbarProviderInterventions.TIP_TYPE.CLEAR
+ );
+ result = (await awaitTip(SEARCH_STRINGS.REFRESH, window))[0];
+ Assert.strictEqual(
+ result.payload.type,
+ UrlbarProviderInterventions.TIP_TYPE.REFRESH
+ );
+
+ // Blur the urlbar so that the engagement is ended.
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ // We should only record one impression for the Refresh tip. Although it was
+ // seen twice, it was in the same engagement.
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ `${UrlbarProviderInterventions.TIP_TYPE.REFRESH}-shown`,
+ 1
+ );
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ `${UrlbarProviderInterventions.TIP_TYPE.CLEAR}-shown`,
+ 1
+ );
+});
+
+add_task(async function tipsAreEnglishOnly() {
+ // Test that Interventions are working in en-US.
+ let result = (await awaitTip(SEARCH_STRINGS.REFRESH, window))[0];
+ Assert.strictEqual(
+ result.payload.type,
+ UrlbarProviderInterventions.TIP_TYPE.REFRESH
+ );
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+
+ // We will need to fetch new engines when we switch locales.
+ let enginesReloaded = SearchTestUtils.promiseSearchNotification(
+ "engines-reloaded"
+ );
+
+ const originalAvailable = Services.locale.availableLocales;
+ const originalRequested = Services.locale.requestedLocales;
+ Services.locale.availableLocales = ["en-US", "de"];
+ Services.locale.requestedLocales = ["de"];
+
+ registerCleanupFunction(async () => {
+ let enginesReloaded2 = SearchTestUtils.promiseSearchNotification(
+ "engines-reloaded"
+ );
+ Services.locale.requestedLocales = originalRequested;
+ Services.locale.availableLocales = originalAvailable;
+ await enginesReloaded2;
+ });
+
+ let appLocales = Services.locale.appLocalesAsBCP47;
+ Assert.equal(appLocales[0], "de");
+
+ await enginesReloaded;
+
+ // Interventions should no longer work in the new locale.
+ await awaitNoTip(SEARCH_STRINGS.CLEAR, window);
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+});
+
+/**
+ * Picks the help button from an Intervention. We spoof the Intervention in this
+ * test because our withDNSRedirect helper cannot handle the HTTPS SUMO links.
+ */
+add_task(async function pickHelpButton() {
+ const helpUrl = "http://example.com/";
+ let results = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/a" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TIP,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ type: UrlbarProviderInterventions.TIP_TYPE.CLEAR,
+ text: "This is a test tip.",
+ buttonText: "Done",
+ helpUrl,
+ }
+ ),
+ ];
+ let interventionProvider = new UrlbarTestUtils.TestProvider({
+ results,
+ priority: 2,
+ });
+ UrlbarProvidersManager.registerProvider(interventionProvider);
+
+ registerCleanupFunction(() => {
+ UrlbarProvidersManager.unregisterProvider(interventionProvider);
+ });
+
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ let [result, element] = await awaitTip(SEARCH_STRINGS.CLEAR);
+ Assert.strictEqual(
+ result.payload.type,
+ UrlbarProviderInterventions.TIP_TYPE.CLEAR
+ );
+
+ let helpButton = element._elements.get("helpButton");
+ Assert.ok(BrowserTestUtils.is_visible(helpButton));
+ EventUtils.synthesizeMouseAtCenter(helpButton, {});
+
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, helpUrl);
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ `${UrlbarProviderInterventions.TIP_TYPE.CLEAR}-help`,
+ 1
+ );
+ });
+});
diff --git a/browser/components/urlbar/tests/browser-tips/browser_picks.js b/browser/components/urlbar/tests/browser-tips/browser_picks.js
new file mode 100644
index 0000000000..9aacddfad0
--- /dev/null
+++ b/browser/components/urlbar/tests/browser-tips/browser_picks.js
@@ -0,0 +1,213 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests clicks and enter key presses on UrlbarUtils.RESULT_TYPE.TIP results.
+
+"use strict";
+
+const TIP_URL = "http://example.com/tip";
+const HELP_URL = "http://example.com/help";
+
+add_task(async function init() {
+ window.windowUtils.disableNonTestMouseEvents(true);
+ registerCleanupFunction(() => {
+ window.windowUtils.disableNonTestMouseEvents(false);
+ });
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.eventTelemetry.enabled", true]],
+ });
+});
+
+add_task(async function enter_mainButton_url() {
+ await doTest({ click: false, buttonUrl: TIP_URL });
+});
+
+add_task(async function enter_mainButton_noURL() {
+ await doTest({ click: false });
+});
+
+add_task(async function enter_helpButton() {
+ await doTest({ click: false, helpUrl: HELP_URL });
+});
+
+add_task(async function mouse_mainButton_url() {
+ await doTest({ click: true, buttonUrl: TIP_URL });
+});
+
+add_task(async function mouse_mainButton_noURL() {
+ await doTest({ click: true });
+});
+
+add_task(async function mouse_helpButton() {
+ await doTest({ click: true, helpUrl: HELP_URL });
+});
+
+// Clicks inside a tip but not on any button.
+add_task(async function mouse_insideTipButNotOnButtons() {
+ let results = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TIP,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ type: "test",
+ text: "This is a test tip.",
+ buttonText: "Done",
+ helpUrl: HELP_URL,
+ buttonUrl: TIP_URL,
+ }
+ ),
+ ];
+ let provider = new UrlbarTestUtils.TestProvider({ results, priority: 1 });
+ UrlbarProvidersManager.registerProvider(provider);
+
+ // Click inside the tip but outside the buttons. Nothing should happen. Make
+ // the result the heuristic to check that the selection on the main button
+ // isn't lost.
+ results[0].heuristic = true;
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ value: "test",
+ window,
+ fireInputEvent: true,
+ });
+ let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElementIndex(window),
+ 0,
+ "The main button's index should be selected initially"
+ );
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElement(window),
+ row._elements.get("tipButton"),
+ "The main button element should be selected initially"
+ );
+ EventUtils.synthesizeMouseAtCenter(row, {});
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 500));
+ Assert.ok(gURLBar.view.isOpen, "The view should remain open");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElementIndex(window),
+ 0,
+ "The main button's index should remain selected"
+ );
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElement(window),
+ row._elements.get("tipButton"),
+ "The main button element should remain selected"
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ UrlbarProvidersManager.unregisterProvider(provider);
+});
+
+/**
+ * Runs this test's main checks.
+ *
+ * @param {boolean} click
+ * Pass true to trigger a click, false to trigger an enter key.
+ * @param {string} buttonUrl
+ * Pass a URL if picking the main button should open a URL. Pass nothing if
+ * picking it should call provider.pickResult instead, or if you want to pick
+ * the help button instead of the main button.
+ * @param {string} helpUrl
+ * Pass a URL if you want to pick the help button. Pass nothing if you want
+ * to pick the main button instead.
+ */
+async function doTest({ click, buttonUrl = undefined, helpUrl = undefined }) {
+ // Open a new tab for the test if we expect to load a URL.
+ let tab;
+ if (buttonUrl || helpUrl) {
+ tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:blank",
+ });
+ }
+
+ // Add our test provider.
+ let provider = new UrlbarTestUtils.TestProvider({
+ results: [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TIP,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ type: "test",
+ text: "This is a test tip.",
+ buttonText: "Done",
+ buttonUrl,
+ helpUrl,
+ }
+ ),
+ ],
+ priority: 1,
+ });
+ UrlbarProvidersManager.registerProvider(provider);
+
+ // If we don't expect to load a URL, then override provider.pickResult so we
+ // can make sure it's called.
+ let pickedPromise =
+ !buttonUrl && !helpUrl
+ ? new Promise(resolve => (provider.pickResult = resolve))
+ : null;
+
+ // Do a search to show our tip result.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ value: "test",
+ window,
+ fireInputEvent: true,
+ });
+ let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
+ let mainButton = row._elements.get("tipButton");
+ let helpButton = row._elements.get("helpButton");
+ let target = helpUrl ? helpButton : mainButton;
+
+ // If we're picking the tip with the keyboard, arrow down to select the proper
+ // target.
+ if (!click) {
+ EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: helpUrl ? 2 : 1 });
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElement(window),
+ target,
+ `${target.className} should be selected.`
+ );
+ }
+
+ // Now pick the target and wait for provider.pickResult to be called if we
+ // don't expect to load a URL, or wait for the URL to load otherwise.
+ await Promise.all([
+ pickedPromise || BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser),
+ UrlbarTestUtils.promisePopupClose(window, () => {
+ if (click) {
+ EventUtils.synthesizeMouseAtCenter(target, {});
+ } else {
+ EventUtils.synthesizeKey("KEY_Enter");
+ }
+ }),
+ ]);
+
+ // Check telemetry.
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ helpUrl ? "test-help" : "test-picked",
+ 1
+ );
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ category: "urlbar",
+ method: "engagement",
+ object: click ? "click" : "enter",
+ value: "typed",
+ },
+ ],
+ { category: "urlbar" }
+ );
+
+ // Done.
+ UrlbarProvidersManager.unregisterProvider(provider);
+ if (tab) {
+ BrowserTestUtils.removeTab(tab);
+ }
+}
diff --git a/browser/components/urlbar/tests/browser-tips/browser_searchTips.js b/browser/components/urlbar/tests/browser-tips/browser_searchTips.js
new file mode 100644
index 0000000000..aeb2a6cd83
--- /dev/null
+++ b/browser/components/urlbar/tests/browser-tips/browser_searchTips.js
@@ -0,0 +1,305 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests the Search Tips feature, which displays a prompt to use the Urlbar on
+// the newtab page and on the user's default search engine's homepage.
+// Specifically, it tests that the Tips appear when they should be appearing.
+// This doesn't test the max-shown-count limit or the restriction on tips when
+// we show the default browser prompt because those require restarting the
+// browser.
+
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AppMenuNotifications: "resource://gre/modules/AppMenuNotifications.jsm",
+ HttpServer: "resource://testing-common/httpd.js",
+ ProfileAge: "resource://gre/modules/ProfileAge.jsm",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
+ UrlbarProviderSearchTips: "resource:///modules/UrlbarProviderSearchTips.jsm",
+});
+
+// These should match the same consts in UrlbarProviderSearchTips.jsm.
+const MAX_SHOWN_COUNT = 4;
+const LAST_UPDATE_THRESHOLD_MS = 24 * 60 * 60 * 1000;
+
+// We test some of the bigger Google domains.
+const GOOGLE_DOMAINS = [
+ "www.google.com",
+ "www.google.ca",
+ "www.google.co.uk",
+ "www.google.com.au",
+ "www.google.co.nz",
+];
+
+add_task(async function init() {
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ `browser.urlbar.tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.ONBOARD}`,
+ 0,
+ ],
+ [
+ `browser.urlbar.tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.REDIRECT}`,
+ 0,
+ ],
+ ],
+ });
+
+ // Write an old profile age so tips are actually shown.
+ let age = await ProfileAge();
+ let originalTimes = age._times;
+ let date = Date.now() - LAST_UPDATE_THRESHOLD_MS - 30000;
+ age._times = { created: date, firstUse: date };
+ await age.writeTimes();
+
+ // Remove update history and the current active update so tips are shown.
+ let updateRootDir = Services.dirsvc.get("UpdRootD", Ci.nsIFile);
+ let updatesFile = updateRootDir.clone();
+ updatesFile.append("updates.xml");
+ let activeUpdateFile = updateRootDir.clone();
+ activeUpdateFile.append("active-update.xml");
+ try {
+ updatesFile.remove(false);
+ } catch (e) {}
+ try {
+ activeUpdateFile.remove(false);
+ } catch (e) {}
+
+ let defaultEngine = await Services.search.getDefault();
+ let defaultEngineName = defaultEngine.name;
+ Assert.equal(defaultEngineName, "Google", "Default engine should be Google.");
+
+ registerCleanupFunction(async () => {
+ let age2 = await ProfileAge();
+ age2._times = originalTimes;
+ await age2.writeTimes();
+ await setDefaultEngine(defaultEngineName);
+ resetSearchTipsProvider();
+ });
+});
+
+// The onboarding tip should be shown on about:newtab.
+add_task(async function newtab() {
+ await checkTab(
+ window,
+ "about:newtab",
+ UrlbarProviderSearchTips.TIP_TYPE.ONBOARD
+ );
+});
+
+// The onboarding tip should be shown on about:home.
+add_task(async function home() {
+ await checkTab(
+ window,
+ "about:home",
+ UrlbarProviderSearchTips.TIP_TYPE.ONBOARD
+ );
+});
+
+// The redirect tip should be shown for www.google.com when it's the default
+// engine.
+add_task(async function google() {
+ await setDefaultEngine("Google");
+ for (let domain of GOOGLE_DOMAINS) {
+ await withDNSRedirect(domain, "/", async url => {
+ await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.REDIRECT);
+ });
+ }
+});
+
+// The redirect tip should be shown for www.google.com/webhp when it's the
+// default engine.
+add_task(async function googleWebhp() {
+ await setDefaultEngine("Google");
+ for (let domain of GOOGLE_DOMAINS) {
+ await withDNSRedirect(domain, "/webhp", async url => {
+ await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.REDIRECT);
+ });
+ }
+});
+
+// The redirect tip should be shown for the Google homepage when query strings
+// are appended.
+add_task(async function googleQueryString() {
+ await setDefaultEngine("Google");
+ for (let domain of GOOGLE_DOMAINS) {
+ await withDNSRedirect(domain, "/webhp", async url => {
+ await checkTab(
+ window,
+ `${url}?hl=en`,
+ UrlbarProviderSearchTips.TIP_TYPE.REDIRECT
+ );
+ });
+ }
+});
+
+// The redirect tip should not be shown on Google results pages.
+add_task(async function googleResults() {
+ await setDefaultEngine("Google");
+ for (let domain of GOOGLE_DOMAINS) {
+ await withDNSRedirect(domain, "/search", async url => {
+ await checkTab(
+ window,
+ `${url}?q=firefox`,
+ UrlbarProviderSearchTips.TIP_TYPE.NONE
+ );
+ });
+ }
+});
+
+// The redirect tip should not be shown for www.google.com when it's not the
+// default engine.
+add_task(async function googleNotDefault() {
+ await setDefaultEngine("Bing");
+ for (let domain of GOOGLE_DOMAINS) {
+ await withDNSRedirect(domain, "/", async url => {
+ await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.NONE);
+ });
+ }
+});
+
+// The redirect tip should not be shown for www.google.com/webhp when it's not
+// the default engine.
+add_task(async function googleWebhpNotDefault() {
+ await setDefaultEngine("Bing");
+ for (let domain of GOOGLE_DOMAINS) {
+ await withDNSRedirect(domain, "/webhp", async url => {
+ await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.NONE);
+ });
+ }
+});
+
+// The redirect tip should be shown for www.bing.com when it's the default
+// engine.
+add_task(async function bing() {
+ await setDefaultEngine("Bing");
+ await withDNSRedirect("www.bing.com", "/", async url => {
+ await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.REDIRECT);
+ });
+});
+
+// The redirect tip should be shown on the Bing homepage even when Bing appends
+// query strings.
+add_task(async function bingQueryString() {
+ await setDefaultEngine("Bing");
+ await withDNSRedirect("www.bing.com", "/", async url => {
+ await checkTab(
+ window,
+ `${url}?toWww=1`,
+ UrlbarProviderSearchTips.TIP_TYPE.REDIRECT
+ );
+ });
+});
+
+// The redirect tip should not be shown on Bing results pages.
+add_task(async function bingResults() {
+ await setDefaultEngine("Bing");
+ await withDNSRedirect("www.bing.com", "/search", async url => {
+ await checkTab(
+ window,
+ `${url}?q=firefox`,
+ UrlbarProviderSearchTips.TIP_TYPE.NONE
+ );
+ });
+});
+
+// The redirect tip should not be shown for www.bing.com when it's not the
+// default engine.
+add_task(async function bingNotDefault() {
+ await setDefaultEngine("Google");
+ await withDNSRedirect("www.bing.com", "/", async url => {
+ await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.NONE);
+ });
+});
+
+// The redirect tip should be shown for duckduckgo.com when it's the default
+// engine.
+add_task(async function ddg() {
+ await setDefaultEngine("DuckDuckGo");
+ await withDNSRedirect("duckduckgo.com", "/", async url => {
+ await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.REDIRECT);
+ });
+});
+
+// The redirect tip should be shown for start.duckduckgo.com when it's the
+// default engine.
+add_task(async function ddgStart() {
+ await setDefaultEngine("DuckDuckGo");
+ await withDNSRedirect("start.duckduckgo.com", "/", async url => {
+ await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.REDIRECT);
+ });
+});
+
+// The redirect tip should not be shown for duckduckgo.com when it's not the
+// default engine.
+add_task(async function ddgNotDefault() {
+ await setDefaultEngine("Google");
+ await withDNSRedirect("duckduckgo.com", "/", async url => {
+ await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.NONE);
+ });
+});
+
+// The redirect tip should not be shown for start.duckduckgo.com when it's not
+// the default engine.
+add_task(async function ddgStartNotDefault() {
+ await setDefaultEngine("Google");
+ await withDNSRedirect("start.duckduckgo.com", "/", async url => {
+ await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.NONE);
+ });
+});
+
+// The redirect tip should not be shown for duckduckgo.com/?q=foo, the search
+// results page, which happens to have the same domain and path as the home
+// page.
+add_task(async function ddgSearchResultsPage() {
+ await setDefaultEngine("DuckDuckGo");
+ await withDNSRedirect("duckduckgo.com", "/", async url => {
+ await checkTab(
+ window,
+ `${url}?q=test`,
+ UrlbarProviderSearchTips.TIP_TYPE.NONE
+ );
+ });
+});
+
+// The redirect tip should not be shown on a non-engine page.
+add_task(async function nonEnginePage() {
+ await checkTab(
+ window,
+ "http://example.com/",
+ UrlbarProviderSearchTips.TIP_TYPE.NONE
+ );
+});
+
+// Tips should be shown at most once per session regardless of their type.
+add_task(async function oncePerSession() {
+ await setDefaultEngine("Google");
+ await checkTab(
+ window,
+ "about:newtab",
+ UrlbarProviderSearchTips.TIP_TYPE.ONBOARD,
+ false
+ );
+ await checkTab(
+ window,
+ "about:newtab",
+ UrlbarProviderSearchTips.TIP_TYPE.NONE,
+ false
+ );
+ await withDNSRedirect("www.google.com", "/", async url => {
+ await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.NONE);
+ });
+});
+
+// The one-off search buttons should not be shown when
+// a search tip is shown even though the search string is empty.
+add_task(async function shortcut_buttons_with_tip() {
+ await checkTab(
+ window,
+ "about:newtab",
+ UrlbarProviderSearchTips.TIP_TYPE.ONBOARD
+ );
+});
diff --git a/browser/components/urlbar/tests/browser-tips/browser_searchTips_interaction.js b/browser/components/urlbar/tests/browser-tips/browser_searchTips_interaction.js
new file mode 100644
index 0000000000..7484749002
--- /dev/null
+++ b/browser/components/urlbar/tests/browser-tips/browser_searchTips_interaction.js
@@ -0,0 +1,620 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests the Search Tips feature, which displays a prompt to use the Urlbar on
+// the newtab page and on the user's default search engine's homepage.
+// Specifically, it tests that the Tips appear when they should be appearing.
+// This doesn't test the max-shown-count limit because it requires restarting
+// the browser.
+
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AppMenuNotifications: "resource://gre/modules/AppMenuNotifications.jsm",
+ HttpServer: "resource://testing-common/httpd.js",
+ ProfileAge: "resource://gre/modules/ProfileAge.jsm",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
+ UrlbarProviderSearchTips: "resource:///modules/UrlbarProviderSearchTips.jsm",
+});
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "clipboardHelper",
+ "@mozilla.org/widget/clipboardhelper;1",
+ "nsIClipboardHelper"
+);
+
+// These should match the same consts in UrlbarProviderSearchTips.jsm.
+const MAX_SHOWN_COUNT = 4;
+const LAST_UPDATE_THRESHOLD_MS = 24 * 60 * 60 * 1000;
+
+// We test some of the bigger Google domains.
+const GOOGLE_DOMAINS = [
+ "www.google.com",
+ "www.google.ca",
+ "www.google.co.uk",
+ "www.google.com.au",
+ "www.google.co.nz",
+];
+
+add_task(async function init() {
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ `browser.urlbar.tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.ONBOARD}`,
+ 0,
+ ],
+ [
+ `browser.urlbar.tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.REDIRECT}`,
+ 0,
+ ],
+ ],
+ });
+
+ // Write an old profile age so tips are actually shown.
+ let age = await ProfileAge();
+ let originalTimes = age._times;
+ let date = Date.now() - LAST_UPDATE_THRESHOLD_MS - 30000;
+ age._times = { created: date, firstUse: date };
+ await age.writeTimes();
+
+ // Remove update history and the current active update so tips are shown.
+ let updateRootDir = Services.dirsvc.get("UpdRootD", Ci.nsIFile);
+ let updatesFile = updateRootDir.clone();
+ updatesFile.append("updates.xml");
+ let activeUpdateFile = updateRootDir.clone();
+ activeUpdateFile.append("active-update.xml");
+ try {
+ updatesFile.remove(false);
+ } catch (e) {}
+ try {
+ activeUpdateFile.remove(false);
+ } catch (e) {}
+
+ let defaultEngine = await Services.search.getDefault();
+ let defaultEngineName = defaultEngine.name;
+ Assert.equal(defaultEngineName, "Google", "Default engine should be Google.");
+
+ registerCleanupFunction(async () => {
+ let age2 = await ProfileAge();
+ age2._times = originalTimes;
+ await age2.writeTimes();
+ await setDefaultEngine(defaultEngineName);
+ resetSearchTipsProvider();
+ });
+});
+
+// Picking the tip's button should cause the Urlbar to blank out and the tip to
+// be not to be shown again in any session. Telemetry should be updated.
+add_task(async function pickButton_onboard() {
+ UrlbarProviderSearchTips.disableTipsForCurrentSession = false;
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.eventTelemetry.enabled", true]],
+ });
+
+ Services.telemetry.clearEvents();
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:newtab",
+ waitForLoad: false,
+ });
+ await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.ONBOARD, false);
+
+ // Click the tip button.
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ let button = result.element.row._elements.get("tipButton");
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ });
+ gURLBar.blur();
+
+ // Check telemetry.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ `${UrlbarProviderSearchTips.TIP_TYPE.ONBOARD}-picked`,
+ 1
+ );
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ category: "urlbar",
+ method: "engagement",
+ object: "click",
+ value: "typed",
+ },
+ ],
+ { category: "urlbar" }
+ );
+
+ Assert.equal(
+ UrlbarPrefs.get(
+ `tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.ONBOARD}`
+ ),
+ MAX_SHOWN_COUNT,
+ "Onboarding tips are disabled after tip button is picked."
+ );
+ Assert.equal(gURLBar.value, "", "The Urlbar should be empty.");
+ resetSearchTipsProvider();
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+// Picking the tip's button should cause the Urlbar to blank out and the tip to
+// be not to be shown again in any session. Telemetry should be updated.
+add_task(async function pickButton_redirect() {
+ UrlbarProviderSearchTips.disableTipsForCurrentSession = false;
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.eventTelemetry.enabled", true]],
+ });
+ Services.telemetry.clearEvents();
+
+ await setDefaultEngine("Google");
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await withDNSRedirect("www.google.com", "/", async url => {
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, url);
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.REDIRECT, false);
+
+ // Click the tip button.
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ let button = result.element.row._elements.get("tipButton");
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ });
+ gURLBar.blur();
+ });
+ });
+
+ // Check telemetry.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ `${UrlbarProviderSearchTips.TIP_TYPE.REDIRECT}-picked`,
+ 1
+ );
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ category: "urlbar",
+ method: "engagement",
+ object: "click",
+ value: "typed",
+ },
+ ],
+ { category: "urlbar" }
+ );
+
+ Assert.equal(
+ UrlbarPrefs.get(
+ `tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.REDIRECT}`
+ ),
+ MAX_SHOWN_COUNT,
+ "Redirect tips are disabled after tip button is picked."
+ );
+ Assert.equal(gURLBar.value, "", "The Urlbar should be empty.");
+ resetSearchTipsProvider();
+ await SpecialPowers.popPrefEnv();
+});
+
+// Clicking in the input while the onboard tip is showing should have the same
+// effect as picking the tip.
+add_task(async function clickInInput_onboard() {
+ UrlbarProviderSearchTips.disableTipsForCurrentSession = false;
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.eventTelemetry.enabled", true]],
+ });
+
+ Services.telemetry.clearEvents();
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:newtab",
+ waitForLoad: false,
+ });
+ await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.ONBOARD, false);
+
+ // Click in the input.
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeMouseAtCenter(gURLBar.textbox.parentNode, {});
+ });
+ gURLBar.blur();
+
+ // Check telemetry.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ `${UrlbarProviderSearchTips.TIP_TYPE.ONBOARD}-picked`,
+ 1
+ );
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ category: "urlbar",
+ method: "engagement",
+ object: "click",
+ value: "typed",
+ },
+ ],
+ { category: "urlbar" }
+ );
+
+ Assert.equal(
+ UrlbarPrefs.get(
+ `tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.ONBOARD}`
+ ),
+ MAX_SHOWN_COUNT,
+ "Onboarding tips are disabled after tip button is picked."
+ );
+ Assert.equal(gURLBar.value, "", "The Urlbar should be empty.");
+ resetSearchTipsProvider();
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+// Pressing Ctrl+L (the open location command) while the onboard tip is showing
+// should have the same effect as picking the tip.
+add_task(async function openLocation_onboard() {
+ UrlbarProviderSearchTips.disableTipsForCurrentSession = false;
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.eventTelemetry.enabled", true]],
+ });
+
+ Services.telemetry.clearEvents();
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:newtab",
+ waitForLoad: false,
+ });
+ await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.ONBOARD, false);
+
+ // Trigger the open location command.
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ document.getElementById("Browser:OpenLocation").doCommand();
+ });
+ gURLBar.blur();
+
+ // Check telemetry.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ `${UrlbarProviderSearchTips.TIP_TYPE.ONBOARD}-picked`,
+ 1
+ );
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ },
+ ],
+ { category: "urlbar" }
+ );
+
+ Assert.equal(
+ UrlbarPrefs.get(
+ `tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.ONBOARD}`
+ ),
+ MAX_SHOWN_COUNT,
+ "Onboarding tips are disabled after tip button is picked."
+ );
+ Assert.equal(gURLBar.value, "", "The Urlbar should be empty.");
+ resetSearchTipsProvider();
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+// Clicking in the input while the redirect tip is showing should have the same
+// effect as picking the tip.
+add_task(async function clickInInput_redirect() {
+ UrlbarProviderSearchTips.disableTipsForCurrentSession = false;
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.eventTelemetry.enabled", true]],
+ });
+ Services.telemetry.clearEvents();
+
+ await setDefaultEngine("Google");
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await withDNSRedirect("www.google.com", "/", async url => {
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, url);
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.REDIRECT, false);
+
+ // Click in the input.
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeMouseAtCenter(gURLBar.textbox.parentNode, {});
+ });
+ gURLBar.blur();
+ });
+ });
+
+ // Check telemetry.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ `${UrlbarProviderSearchTips.TIP_TYPE.REDIRECT}-picked`,
+ 1
+ );
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ category: "urlbar",
+ method: "engagement",
+ object: "click",
+ value: "typed",
+ },
+ ],
+ { category: "urlbar" }
+ );
+
+ Assert.equal(
+ UrlbarPrefs.get(
+ `tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.REDIRECT}`
+ ),
+ MAX_SHOWN_COUNT,
+ "Redirect tips are disabled after tip button is picked."
+ );
+ Assert.equal(gURLBar.value, "", "The Urlbar should be empty.");
+ resetSearchTipsProvider();
+ await SpecialPowers.popPrefEnv();
+});
+
+// Pressing Ctrl+L (the open location command) while the redirect tip is showing
+// should have the same effect as picking the tip.
+add_task(async function openLocation_redirect() {
+ UrlbarProviderSearchTips.disableTipsForCurrentSession = false;
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.eventTelemetry.enabled", true]],
+ });
+ Services.telemetry.clearEvents();
+
+ await setDefaultEngine("Google");
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await withDNSRedirect("www.google.com", "/", async url => {
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, url);
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.REDIRECT, false);
+
+ // Trigger the open location command.
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ document.getElementById("Browser:OpenLocation").doCommand();
+ });
+ gURLBar.blur();
+ });
+ });
+
+ // Check telemetry.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ `${UrlbarProviderSearchTips.TIP_TYPE.REDIRECT}-picked`,
+ 1
+ );
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ },
+ ],
+ { category: "urlbar" }
+ );
+
+ Assert.equal(
+ UrlbarPrefs.get(
+ `tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.REDIRECT}`
+ ),
+ MAX_SHOWN_COUNT,
+ "Redirect tips are disabled after tip button is picked."
+ );
+ Assert.equal(gURLBar.value, "", "The Urlbar should be empty.");
+ resetSearchTipsProvider();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function pickingTipDoesNotDisableOtherKinds() {
+ UrlbarProviderSearchTips.disableTipsForCurrentSession = false;
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:newtab",
+ waitForLoad: false,
+ });
+ await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.ONBOARD, false);
+
+ // Click the tip button.
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ let button = result.element.row._elements.get("tipButton");
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ });
+
+ gURLBar.blur();
+ Assert.equal(
+ UrlbarPrefs.get(
+ `tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.ONBOARD}`
+ ),
+ MAX_SHOWN_COUNT,
+ "Onboarding tips are disabled after tip button is picked."
+ );
+
+ BrowserTestUtils.removeTab(tab);
+
+ // Simulate a new session.
+ UrlbarProviderSearchTips.disableTipsForCurrentSession = false;
+
+ // Onboarding tips should no longer be shown.
+ let tab2 = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:newtab",
+ waitForLoad: false,
+ });
+ await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.NONE);
+
+ // We should still show redirect tips.
+ await withDNSRedirect("www.google.com", "/", async url => {
+ await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.REDIRECT);
+ });
+
+ BrowserTestUtils.removeTab(tab2);
+ resetSearchTipsProvider();
+});
+
+// The tip shouldn't be shown when there's another notification present.
+add_task(async function notification() {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ let box = gBrowser.getNotificationBox();
+ let note = box.appendNotification(
+ "Test",
+ "urlbar-test",
+ null,
+ box.PRIORITY_INFO_HIGH,
+ null,
+ null,
+ null
+ );
+ // Give it a big persistence so it doesn't go away on page load.
+ note.persistence = 100;
+ await withDNSRedirect("www.google.com", "/", async url => {
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, url);
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.NONE);
+ box.removeNotification(note, true);
+ });
+ });
+ resetSearchTipsProvider();
+});
+
+// The tip should be shown when switching to a tab where it should be shown.
+add_task(async function tabSwitch() {
+ let tab = BrowserTestUtils.addTab(gBrowser, "about:newtab");
+ UrlbarProviderSearchTips.disableTipsForCurrentSession = false;
+ Services.telemetry.clearScalars();
+ await BrowserTestUtils.switchTab(gBrowser, tab);
+ await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.ONBOARD);
+ BrowserTestUtils.removeTab(tab);
+ resetSearchTipsProvider();
+});
+
+// The engagement event should be ended if the user ignores a tip.
+// See bug 1610024.
+add_task(async function ignoreEndsEngagement() {
+ await setDefaultEngine("Google");
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await withDNSRedirect("www.google.com", "/", async url => {
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, url);
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.REDIRECT, false);
+ // We're just looking for any target outside the Urlbar.
+ let spring = gURLBar.inputField
+ .closest("#nav-bar")
+ .querySelector("toolbarspring");
+ await UrlbarTestUtils.promisePopupClose(window, async () => {
+ await EventUtils.synthesizeMouseAtCenter(spring, {});
+ });
+ Assert.ok(
+ UrlbarProviderSearchTips.showedTipTypeInCurrentEngagement ==
+ UrlbarProviderSearchTips.TIP_TYPE.NONE,
+ "The engagement should have ended after the tip was ignored."
+ );
+ });
+ });
+ resetSearchTipsProvider();
+});
+
+add_task(async function pasteAndGo_url() {
+ await doPasteAndGoTest("http://example.com/", "http://example.com/");
+});
+
+add_task(async function pasteAndGo_nonURL() {
+ // Add a mock engine so we don't hit the network loading the SERP.
+ let engine = await Services.search.addEngineWithDetails("Test", {
+ template: "http://example.com/?search={searchTerms}",
+ });
+ let oldDefaultEngine = await Services.search.getDefault();
+ Services.search.setDefault(engine);
+
+ await doPasteAndGoTest(
+ "pasteAndGo_nonURL",
+ "http://example.com/?search=pasteAndGo_nonURL"
+ );
+
+ Services.search.setDefault(oldDefaultEngine);
+ await Services.search.removeEngine(engine);
+});
+
+async function doPasteAndGoTest(searchString, expectedURL) {
+ UrlbarProviderSearchTips.disableTipsForCurrentSession = false;
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:newtab",
+ waitForLoad: false,
+ });
+ await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.ONBOARD, false);
+
+ await SimpleTest.promiseClipboardChange(searchString, () => {
+ clipboardHelper.copyString(searchString);
+ });
+
+ let textBox = gURLBar.querySelector("moz-input-box");
+ let cxmenu = textBox.menupopup;
+ let cxmenuPromise = BrowserTestUtils.waitForEvent(cxmenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {
+ type: "contextmenu",
+ button: 2,
+ });
+ await cxmenuPromise;
+ let menuitem = textBox.getMenuItem("paste-and-go");
+
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ expectedURL
+ );
+ EventUtils.synthesizeMouseAtCenter(menuitem, {});
+ await browserLoadedPromise;
+ BrowserTestUtils.removeTab(tab);
+ resetSearchTipsProvider();
+}
+
+// Since we coupled the logic that decides whether to show the tip with our
+// gURLBar.search call, we should make sure search isn't called when
+// the conditions for a tip are met but the provider is disabled.
+add_task(async function noActionWhenDisabled() {
+ await setDefaultEngine("Bing");
+ await withDNSRedirect("www.bing.com", "/", async url => {
+ await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.REDIRECT);
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features",
+ false,
+ ],
+ ],
+ });
+
+ await withDNSRedirect("www.bing.com", "/", async url => {
+ Assert.ok(
+ !UrlbarTestUtils.isPopupOpen(window),
+ "The UrlbarView should not be open."
+ );
+ });
+
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/urlbar/tests/browser-tips/browser_selection.js b/browser/components/urlbar/tests/browser-tips/browser_selection.js
new file mode 100644
index 0000000000..b7834a8518
--- /dev/null
+++ b/browser/components/urlbar/tests/browser-tips/browser_selection.js
@@ -0,0 +1,284 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests keyboard selection within UrlbarUtils.RESULT_TYPE.TIP results.
+
+"use strict";
+
+const HELP_URL = "about:mozilla";
+const TIP_URL = "about:about";
+
+add_task(async function tipIsSecondResult() {
+ let results = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/a" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TIP,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ icon: "",
+ text: "This is a test intervention.",
+ buttonText: "Done",
+ type: "test",
+ helpUrl: HELP_URL,
+ buttonUrl: TIP_URL,
+ }
+ ),
+ ];
+
+ let provider = new UrlbarTestUtils.TestProvider({ results, priority: 1 });
+ UrlbarProvidersManager.registerProvider(provider);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ value: "test",
+ window,
+ });
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 2,
+ "There should be two results in the view."
+ );
+ let secondResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(
+ secondResult.type,
+ UrlbarUtils.RESULT_TYPE.TIP,
+ "The second result should be a tip."
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElementIndex(window),
+ 0,
+ "The first element should be selected."
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.ok(
+ UrlbarTestUtils.getSelectedElement(window).classList.contains(
+ "urlbarView-tip-button"
+ ),
+ "The selected element should be the tip button."
+ );
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElementIndex(window),
+ 1,
+ "The first element should be selected."
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.ok(
+ UrlbarTestUtils.getSelectedElement(window).classList.contains(
+ "urlbarView-tip-help"
+ ),
+ "The selected element should be the tip help button."
+ );
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 1,
+ "getSelectedRowIndex should return 1 even though the help button is selected."
+ );
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElementIndex(window),
+ 2,
+ "The third element should be selected."
+ );
+
+ // If this test is running alone, the one-offs will rebuild themselves when
+ // the view is opened above, and they may not be visible yet. Wait for the
+ // first one to become visible before trying to select it.
+ await TestUtils.waitForCondition(() => {
+ return (
+ gURLBar.view.oneOffSearchButtons.buttons.firstElementChild &&
+ BrowserTestUtils.is_visible(
+ gURLBar.view.oneOffSearchButtons.buttons.firstElementChild
+ )
+ );
+ }, "Waiting for first one-off to become visible.");
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await TestUtils.waitForCondition(() => {
+ return gURLBar.view.oneOffSearchButtons.selectedButton;
+ }, "Waiting for one-off to become selected.");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElementIndex(window),
+ -1,
+ "No results should be selected."
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ Assert.ok(
+ UrlbarTestUtils.getSelectedElement(window).classList.contains(
+ "urlbarView-tip-help"
+ ),
+ "The selected element should be the tip help button."
+ );
+
+ gURLBar.view.close();
+ UrlbarProvidersManager.unregisterProvider(provider);
+});
+
+add_task(async function tipIsOnlyResult() {
+ let results = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TIP,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ icon: "",
+ text: "This is a test intervention.",
+ buttonText: "Done",
+ type: "test",
+ helpUrl:
+ "https://support.mozilla.org/en-US/kb/delete-browsing-search-download-history-firefox",
+ }
+ ),
+ ];
+
+ let provider = new UrlbarTestUtils.TestProvider({ results, priority: 1 });
+ UrlbarProvidersManager.registerProvider(provider);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ value: "test",
+ window,
+ });
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 1,
+ "There should be one result in the view."
+ );
+ let firstResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ firstResult.type,
+ UrlbarUtils.RESULT_TYPE.TIP,
+ "The first and only result should be a tip."
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.ok(
+ UrlbarTestUtils.getSelectedElement(window).classList.contains(
+ "urlbarView-tip-button"
+ ),
+ "The selected element should be the tip button."
+ );
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElementIndex(window),
+ 0,
+ "The first element should be selected."
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.ok(
+ UrlbarTestUtils.getSelectedElement(window).classList.contains(
+ "urlbarView-tip-help"
+ ),
+ "The selected element should be the tip help button."
+ );
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElementIndex(window),
+ 1,
+ "The second element should be selected."
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElementIndex(window),
+ -1,
+ "There should be no selection."
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ Assert.ok(
+ UrlbarTestUtils.getSelectedElement(window).classList.contains(
+ "urlbarView-tip-help"
+ ),
+ "The selected element should be the tip help button."
+ );
+
+ gURLBar.view.close();
+ UrlbarProvidersManager.unregisterProvider(provider);
+});
+
+add_task(async function tipHasNoHelpButton() {
+ let results = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/a" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TIP,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ icon: "",
+ text: "This is a test intervention.",
+ buttonText: "Done",
+ type: "test",
+ }
+ ),
+ ];
+
+ let provider = new UrlbarTestUtils.TestProvider({ results, priority: 1 });
+ UrlbarProvidersManager.registerProvider(provider);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ value: "test",
+ window,
+ });
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 2,
+ "There should be two results in the view."
+ );
+ let secondResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(
+ secondResult.type,
+ UrlbarUtils.RESULT_TYPE.TIP,
+ "The second result should be a tip."
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElementIndex(window),
+ 0,
+ "The first element should be selected."
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.ok(
+ UrlbarTestUtils.getSelectedElement(window).classList.contains(
+ "urlbarView-tip-button"
+ ),
+ "The selected element should be the tip button."
+ );
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElementIndex(window),
+ 1,
+ "The first element should be selected."
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await TestUtils.waitForCondition(() => {
+ return gURLBar.view.oneOffSearchButtons.selectedButton;
+ }, "Waiting for one-off to become selected.");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElementIndex(window),
+ -1,
+ "No results should be selected."
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ Assert.ok(
+ UrlbarTestUtils.getSelectedElement(window).classList.contains(
+ "urlbarView-tip-button"
+ ),
+ "The selected element should be the tip button."
+ );
+
+ gURLBar.view.close();
+ UrlbarProvidersManager.unregisterProvider(provider);
+});
diff --git a/browser/components/urlbar/tests/browser-tips/browser_updateAsk.js b/browser/components/urlbar/tests/browser-tips/browser_updateAsk.js
new file mode 100644
index 0000000000..009aaf4609
--- /dev/null
+++ b/browser/components/urlbar/tests/browser-tips/browser_updateAsk.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Checks the UPDATE_ASK tip.
+//
+// The update parts of this test are adapted from:
+// https://searchfox.org/mozilla-central/source/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_downloadOptIn.js
+
+"use strict";
+
+let params = { queryString: "&invalidCompleteSize=1" };
+
+let downloadInfo = [];
+if (Services.prefs.getBoolPref(PREF_APP_UPDATE_BITS_ENABLED, false)) {
+ downloadInfo[0] = { patchType: "partial", bitsResult: "0" };
+} else {
+ downloadInfo[0] = { patchType: "partial", internalResult: "0" };
+}
+
+let preSteps = [
+ {
+ panelId: "checkingForUpdates",
+ checkActiveUpdate: null,
+ continueFile: CONTINUE_CHECK,
+ },
+ {
+ panelId: "downloadAndInstall",
+ checkActiveUpdate: null,
+ continueFile: null,
+ },
+];
+
+let postSteps = [
+ {
+ panelId: "downloading",
+ checkActiveUpdate: { state: STATE_DOWNLOADING },
+ continueFile: CONTINUE_DOWNLOAD,
+ downloadInfo,
+ },
+ {
+ panelId: "apply",
+ checkActiveUpdate: { state: STATE_PENDING },
+ continueFile: null,
+ },
+];
+
+add_task(async function test() {
+ // Disable the pref that automatically downloads and installs updates.
+ await UpdateUtils.setAppUpdateAutoEnabled(false);
+
+ // Set up the "download and install" update state.
+ await initUpdate(params);
+ UrlbarProviderInterventions.checkForBrowserUpdate(true);
+ await processUpdateSteps(preSteps);
+
+ // Pick the tip and continue with the mock update, which should attempt to
+ // restart the browser.
+ await doUpdateTest({
+ searchString: SEARCH_STRINGS.UPDATE,
+ tip: UrlbarProviderInterventions.TIP_TYPE.UPDATE_ASK,
+ title: /^A new version of .+ is available\.$/,
+ button: "Install and Restart to Update",
+ awaitCallback() {
+ return Promise.all([
+ processUpdateSteps(postSteps),
+ awaitAppRestartRequest(),
+ ]);
+ },
+ });
+});
diff --git a/browser/components/urlbar/tests/browser-tips/browser_updateRefresh.js b/browser/components/urlbar/tests/browser-tips/browser_updateRefresh.js
new file mode 100644
index 0000000000..c462a595b7
--- /dev/null
+++ b/browser/components/urlbar/tests/browser-tips/browser_updateRefresh.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Checks the UPDATE_REFRESH tip.
+//
+// The update parts of this test are adapted from:
+// https://searchfox.org/mozilla-central/source/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_check_noUpdate.js
+
+"use strict";
+
+let params = { queryString: "&noUpdates=1" };
+
+let preSteps = [
+ {
+ panelId: "checkingForUpdates",
+ checkActiveUpdate: null,
+ continueFile: CONTINUE_CHECK,
+ },
+ {
+ panelId: "noUpdatesFound",
+ checkActiveUpdate: null,
+ continueFile: null,
+ },
+];
+
+add_task(async function test() {
+ makeProfileResettable();
+
+ // Set up the "no updates" update state.
+ await initUpdate(params);
+ UrlbarProviderInterventions.checkForBrowserUpdate(true);
+ await processUpdateSteps(preSteps);
+
+ // Picking the tip should open the refresh dialog. Click its cancel
+ // button.
+ await doUpdateTest({
+ searchString: SEARCH_STRINGS.UPDATE,
+ tip: UrlbarProviderInterventions.TIP_TYPE.UPDATE_REFRESH,
+ title: /^.+ is up to date\. Trying to fix a problem\? Restore default settings and remove old add-ons for optimal performance\.$/,
+ button: /^Refresh .+…$/,
+ awaitCallback() {
+ return BrowserTestUtils.promiseAlertDialog(
+ "cancel",
+ "chrome://global/content/resetProfile.xhtml"
+ );
+ },
+ });
+});
diff --git a/browser/components/urlbar/tests/browser-tips/browser_updateRestart.js b/browser/components/urlbar/tests/browser-tips/browser_updateRestart.js
new file mode 100644
index 0000000000..9d6b5e48a5
--- /dev/null
+++ b/browser/components/urlbar/tests/browser-tips/browser_updateRestart.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Checks the UPDATE_RESTART tip.
+//
+// The update parts of this test are adapted from:
+// https://searchfox.org/mozilla-central/source/toolkit/mozapps/update/tests/browser/browser_aboutDialog_bc_downloaded_staged.js
+
+"use strict";
+
+let params = {
+ queryString: "&invalidCompleteSize=1",
+ backgroundUpdate: true,
+ continueFile: CONTINUE_STAGING,
+ waitForUpdateState: STATE_APPLIED,
+};
+
+let preSteps = [
+ {
+ panelId: "apply",
+ checkActiveUpdate: { state: STATE_APPLIED },
+ continueFile: null,
+ },
+];
+
+add_task(async function test() {
+ // Enable the pref that automatically downloads and installs updates.
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_APP_UPDATE_STAGING_ENABLED, true]],
+ });
+
+ // Set up the "apply" update state.
+ await initUpdate(params);
+ UrlbarProviderInterventions.checkForBrowserUpdate(true);
+ await processUpdateSteps(preSteps);
+
+ // Picking the tip should attempt to restart the browser.
+ await doUpdateTest({
+ searchString: SEARCH_STRINGS.UPDATE,
+ tip: UrlbarProviderInterventions.TIP_TYPE.UPDATE_RESTART,
+ title: /^The latest .+ is downloaded and ready to install\.$/,
+ button: "Restart to Update",
+ awaitCallback: awaitAppRestartRequest,
+ });
+});
diff --git a/browser/components/urlbar/tests/browser-tips/browser_updateWeb.js b/browser/components/urlbar/tests/browser-tips/browser_updateWeb.js
new file mode 100644
index 0000000000..4db9fb6019
--- /dev/null
+++ b/browser/components/urlbar/tests/browser-tips/browser_updateWeb.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Checks the UPDATE_WEB tip.
+//
+// The update parts of this test are adapted from:
+// https://searchfox.org/mozilla-central/source/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_check_unsupported.js
+
+"use strict";
+
+let params = { queryString: "&unsupported=1" };
+
+let preSteps = [
+ {
+ panelId: "checkingForUpdates",
+ checkActiveUpdate: null,
+ continueFile: CONTINUE_CHECK,
+ },
+ {
+ panelId: "unsupportedSystem",
+ checkActiveUpdate: null,
+ continueFile: null,
+ },
+];
+
+add_task(async function test() {
+ // Set up the "unsupported update" update state.
+ await initUpdate(params);
+ UrlbarProviderInterventions.checkForBrowserUpdate(true);
+ await processUpdateSteps(preSteps);
+
+ // Picking the tip should open the download page in a new tab.
+ let downloadTab = await doUpdateTest({
+ searchString: SEARCH_STRINGS.UPDATE,
+ tip: UrlbarProviderInterventions.TIP_TYPE.UPDATE_WEB,
+ title: /^Get the latest .+ browser\.$/,
+ button: "Download Now",
+ awaitCallback() {
+ return BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "https://www.mozilla.org/firefox/new/"
+ );
+ },
+ });
+
+ Assert.equal(gBrowser.selectedTab, downloadTab);
+ BrowserTestUtils.removeTab(downloadTab);
+});
diff --git a/browser/components/urlbar/tests/browser-tips/head.js b/browser/components/urlbar/tests/browser-tips/head.js
new file mode 100644
index 0000000000..f76377b80e
--- /dev/null
+++ b/browser/components/urlbar/tests/browser-tips/head.js
@@ -0,0 +1,754 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This directory contains tests that check tips and interventions, and in
+// particular the update-related interventions.
+// We mock updates by using the test helpers in
+// toolkit/mozapps/update/tests/browser.
+
+"use strict";
+
+/* import-globals-from ../../../../../toolkit/mozapps/update/tests/browser/head.js */
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/mozapps/update/tests/browser/head.js",
+ this
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ HttpServer: "resource://testing-common/httpd.js",
+ ResetProfile: "resource://gre/modules/ResetProfile.jsm",
+ UrlbarProviderInterventions:
+ "resource:///modules/UrlbarProviderInterventions.jsm",
+ UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm",
+ UrlbarResult: "resource:///modules/UrlbarResult.jsm",
+ TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(this, "UrlbarTestUtils", () => {
+ const { UrlbarTestUtils: module } = ChromeUtils.import(
+ "resource://testing-common/UrlbarTestUtils.jsm"
+ );
+ module.init(this);
+ return module;
+});
+
+XPCOMUtils.defineLazyGetter(this, "SearchTestUtils", () => {
+ const { SearchTestUtils: module } = ChromeUtils.import(
+ "resource://testing-common/SearchTestUtils.jsm"
+ );
+ module.init(this);
+ return module;
+});
+
+// For each intervention type, a search string that trigger the intervention.
+const SEARCH_STRINGS = {
+ CLEAR: "firefox history",
+ REFRESH: "firefox slow",
+ UPDATE: "firefox update",
+};
+
+add_task(async function init() {
+ registerCleanupFunction(() => {
+ // We need to reset the provider's appUpdater.status between tests so that
+ // each test doesn't interfere with the next.
+ UrlbarProviderInterventions.resetAppUpdater();
+ });
+});
+
+/**
+ * Override our binary path so that the update lock doesn't think more than one
+ * instance of this test is running.
+ * This is a heavily pared down copy of the function in xpcshellUtilsAUS.js.
+ */
+function adjustGeneralPaths() {
+ let dirProvider = {
+ getFile(aProp, aPersistent) {
+ // Set the value of persistent to false so when this directory provider is
+ // unregistered it will revert back to the original provider.
+ aPersistent.value = false;
+ // The sync manager only uses XRE_EXECUTABLE_FILE, so that's all we need
+ // to override, we won't bother handling anything else.
+ if (aProp == XRE_EXECUTABLE_FILE) {
+ // The temp directory that the mochitest runner creates is unique per
+ // test, so its path can serve to provide the unique key that the update
+ // sync manager requires (it doesn't need for this to be the actual
+ // path to any real file, it's only used as an opaque string).
+ let tempPath = gEnv.get("MOZ_PROCESS_LOG");
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.initWithPath(tempPath);
+ return file;
+ }
+ return null;
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIDirectoryServiceProvider"]),
+ };
+
+ let ds = Services.dirsvc.QueryInterface(Ci.nsIDirectoryService);
+ try {
+ ds.QueryInterface(Ci.nsIProperties).undefine(XRE_EXECUTABLE_FILE);
+ } catch (_ex) {
+ // We only override one property, so we have nothing to do if that fails.
+ return;
+ }
+ ds.registerProvider(dirProvider);
+ registerCleanupFunction(() => {
+ ds.unregisterProvider(dirProvider);
+ // Reset the update lock once again so that we know the lock we're
+ // interested in here will be closed properly (normally that happens during
+ // XPCOM shutdown, but that isn't consistent during tests).
+ let syncManager = Cc[
+ "@mozilla.org/updates/update-sync-manager;1"
+ ].getService(Ci.nsIUpdateSyncManager);
+ syncManager.resetLock();
+ });
+
+ // Now that we've overridden the directory provider, the name of the update
+ // lock needs to be changed to match the overridden path.
+ let syncManager = Cc["@mozilla.org/updates/update-sync-manager;1"].getService(
+ Ci.nsIUpdateSyncManager
+ );
+ syncManager.resetLock();
+}
+
+/**
+ * Initializes a mock app update. Adapted from runAboutDialogUpdateTest:
+ * https://searchfox.org/mozilla-central/source/toolkit/mozapps/update/tests/browser/head.js
+ *
+ * @param {object} params
+ * See the files in toolkit/mozapps/update/tests/browser.
+ */
+async function initUpdate(params) {
+ gEnv.set("MOZ_TEST_SLOW_SKIP_UPDATE_STAGE", "1");
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [PREF_APP_UPDATE_DISABLEDFORTESTING, false],
+ [PREF_APP_UPDATE_URL_MANUAL, gDetailsURL],
+ ],
+ });
+
+ adjustGeneralPaths();
+ await setupTestUpdater();
+
+ let queryString = params.queryString ? params.queryString : "";
+ let updateURL =
+ URL_HTTP_UPDATE_SJS +
+ "?detailsURL=" +
+ gDetailsURL +
+ queryString +
+ getVersionParams();
+ if (params.backgroundUpdate) {
+ setUpdateURL(updateURL);
+ gAUS.checkForBackgroundUpdates();
+ if (params.continueFile) {
+ await continueFileHandler(params.continueFile);
+ }
+ if (params.waitForUpdateState) {
+ let whichUpdate =
+ params.waitForUpdateState == STATE_DOWNLOADING
+ ? "downloadingUpdate"
+ : "readyUpdate";
+ await TestUtils.waitForCondition(
+ () =>
+ gUpdateManager[whichUpdate] &&
+ gUpdateManager[whichUpdate].state == params.waitForUpdateState,
+ "Waiting for update state: " + params.waitForUpdateState,
+ undefined,
+ 200
+ ).catch(e => {
+ // Instead of throwing let the check below fail the test so the panel
+ // ID and the expected panel ID is printed in the log.
+ logTestInfo(e);
+ });
+ // Display the UI after the update state equals the expected value.
+ Assert.equal(
+ gUpdateManager[whichUpdate].state,
+ params.waitForUpdateState,
+ "The update state value should equal " + params.waitForUpdateState
+ );
+ }
+ } else {
+ updateURL += "&slowUpdateCheck=1&useSlowDownloadMar=1";
+ setUpdateURL(updateURL);
+ }
+}
+
+/**
+ * Performs steps in a mock update. Adapted from runAboutDialogUpdateTest:
+ * https://searchfox.org/mozilla-central/source/toolkit/mozapps/update/tests/browser/head.js
+ *
+ * @param {array} steps
+ * See the files in toolkit/mozapps/update/tests/browser.
+ */
+async function processUpdateSteps(steps) {
+ for (let step of steps) {
+ await processUpdateStep(step);
+ }
+}
+
+/**
+ * Performs a step in a mock update. Adapted from runAboutDialogUpdateTest:
+ * https://searchfox.org/mozilla-central/source/toolkit/mozapps/update/tests/browser/head.js
+ *
+ * @param {object} step
+ * See the files in toolkit/mozapps/update/tests/browser.
+ */
+async function processUpdateStep(step) {
+ if (typeof step == "function") {
+ step();
+ return;
+ }
+
+ const { panelId, checkActiveUpdate, continueFile, downloadInfo } = step;
+ if (checkActiveUpdate) {
+ let whichUpdate =
+ checkActiveUpdate.state == STATE_DOWNLOADING
+ ? "downloadingUpdate"
+ : "readyUpdate";
+ await TestUtils.waitForCondition(
+ () => gUpdateManager[whichUpdate],
+ "Waiting for active update"
+ );
+ Assert.ok(
+ !!gUpdateManager[whichUpdate],
+ "There should be an active update"
+ );
+ Assert.equal(
+ gUpdateManager[whichUpdate].state,
+ checkActiveUpdate.state,
+ "The active update state should equal " + checkActiveUpdate.state
+ );
+ } else {
+ Assert.ok(
+ !gUpdateManager.readyUpdate,
+ "There should not be a ready update"
+ );
+ Assert.ok(
+ !gUpdateManager.downloadingUpdate,
+ "There should not be a downloadingUpdate update"
+ );
+ }
+
+ if (panelId == "downloading") {
+ for (let i = 0; i < downloadInfo.length; ++i) {
+ let data = downloadInfo[i];
+ // The About Dialog tests always specify a continue file.
+ await continueFileHandler(continueFile);
+ let patch = getPatchOfType(
+ data.patchType,
+ gUpdateManager.downloadingUpdate
+ );
+ // The update is removed early when the last download fails so check
+ // that there is a patch before proceeding.
+ let isLastPatch = i == downloadInfo.length - 1;
+ if (!isLastPatch || patch) {
+ let resultName = data.bitsResult ? "bitsResult" : "internalResult";
+ patch.QueryInterface(Ci.nsIWritablePropertyBag);
+ await TestUtils.waitForCondition(
+ () => patch.getProperty(resultName) == data[resultName],
+ "Waiting for expected patch property " +
+ resultName +
+ " value: " +
+ data[resultName],
+ undefined,
+ 200
+ ).catch(e => {
+ // Instead of throwing let the check below fail the test so the
+ // property value and the expected property value is printed in
+ // the log.
+ logTestInfo(e);
+ });
+ Assert.equal(
+ patch.getProperty(resultName),
+ data[resultName],
+ "The patch property " +
+ resultName +
+ " value should equal " +
+ data[resultName]
+ );
+ }
+ }
+ } else if (continueFile) {
+ await continueFileHandler(continueFile);
+ }
+}
+
+/**
+ * Checks an intervention tip. This works by starting a search that should
+ * trigger a tip, picks the tip, and waits for the tip's action to happen.
+ *
+ * @param {string} searchString
+ * The search string.
+ * @param {string} tip
+ * The expected tip type.
+ * @param {string/regexp} title
+ * The expected tip title.
+ * @param {string/regexp} button
+ * The expected button title.
+ * @param {function} awaitCallback
+ * A function that checks the tip's action. Should return a promise (or be
+ * async).
+ * @returns {object}
+ * The value returned from `awaitCallback`.
+ */
+async function doUpdateTest({
+ searchString,
+ tip,
+ title,
+ button,
+ awaitCallback,
+} = {}) {
+ // Do a search that triggers the tip.
+ let [result, element] = await awaitTip(searchString);
+ Assert.strictEqual(result.payload.type, tip, "Tip type");
+ await element.ownerDocument.l10n.translateFragment(element);
+
+ let actualTitle = element._elements.get("title").textContent;
+ if (typeof title == "string") {
+ Assert.equal(actualTitle, title, "Title string");
+ } else {
+ // regexp
+ Assert.ok(title.test(actualTitle), "Title regexp");
+ }
+
+ let actualButton = element._elements.get("tipButton").textContent;
+ if (typeof button == "string") {
+ Assert.equal(actualButton, button, "Button string");
+ } else {
+ // regexp
+ Assert.ok(button.test(actualButton), "Button regexp");
+ }
+
+ Assert.ok(
+ BrowserTestUtils.is_visible(element._elements.get("helpButton")),
+ "Help button visible"
+ );
+
+ // Pick the tip and wait for the action.
+ let values = await Promise.all([awaitCallback(), pickTip()]);
+
+ // Check telemetry.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ `${tip}-shown`,
+ 1
+ );
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ `${tip}-picked`,
+ 1
+ );
+
+ return values[0] || null;
+}
+
+/**
+ * Starts a search and asserts that the second result is a tip.
+ *
+ * @param {string} searchString
+ * The search string.
+ * @param {window} win
+ * The window.
+ * @returns {[result, element]}
+ * The result and its element in the DOM.
+ */
+async function awaitTip(searchString, win = window) {
+ let context = await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: searchString,
+ waitForFocus,
+ fireInputEvent: true,
+ });
+ Assert.ok(context.results.length >= 2);
+ let result = context.results[1];
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.TIP);
+ let element = await UrlbarTestUtils.waitForAutocompleteResultAt(win, 1);
+ return [result, element];
+}
+
+/**
+ * Picks the current tip's button. The view should be open and the second
+ * result should be a tip.
+ */
+async function pickTip() {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ let button = result.element.row._elements.get("tipButton");
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ });
+}
+
+/**
+ * Waits for the quit-application-requested notification and cancels it (so that
+ * the app isn't actually restarted).
+ */
+async function awaitAppRestartRequest() {
+ await TestUtils.topicObserved(
+ "quit-application-requested",
+ (cancelQuit, data) => {
+ if (data == "restart") {
+ cancelQuit.QueryInterface(Ci.nsISupportsPRBool).data = true;
+ return true;
+ }
+ return false;
+ }
+ );
+}
+
+/**
+ * Sets up the profile so that it can be reset.
+ */
+function makeProfileResettable() {
+ // Make reset possible.
+ let profileService = Cc["@mozilla.org/toolkit/profile-service;1"].getService(
+ Ci.nsIToolkitProfileService
+ );
+ let currentProfileDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ let profileName = "mochitest-test-profile-temp-" + Date.now();
+ let tempProfile = profileService.createProfile(
+ currentProfileDir,
+ profileName
+ );
+ Assert.ok(
+ ResetProfile.resetSupported(),
+ "Should be able to reset from mochitest's temporary profile once it's in the profile manager."
+ );
+
+ registerCleanupFunction(() => {
+ tempProfile.remove(false);
+ Assert.ok(
+ !ResetProfile.resetSupported(),
+ "Shouldn't be able to reset from mochitest's temporary profile once removed from the profile manager."
+ );
+ });
+}
+
+/**
+ * Starts a search that should trigger a tip, picks the tip, and waits for the
+ * tip's action to happen.
+ *
+ * @param {string} searchString
+ * The search string.
+ * @param {TIPS} tip
+ * The expected tip type.
+ * @param {string} title
+ * The expected tip title.
+ * @param {string} button
+ * The expected button title.
+ * @param {function} awaitCallback
+ * A function that checks the tip's action. Should return a promise (or be
+ * async).
+ * @returns {*}
+ * The value returned from `awaitCallback`.
+ */
+function checkIntervention({
+ searchString,
+ tip,
+ title,
+ button,
+ awaitCallback,
+} = {}) {
+ // Opening modal dialogs confuses focus on Linux just after them, thus run
+ // these checks in separate tabs to better isolate them.
+ return BrowserTestUtils.withNewTab("about:blank", async () => {
+ // Do a search that triggers the tip.
+ let [result, element] = await awaitTip(searchString);
+ Assert.strictEqual(result.payload.type, tip);
+ await element.ownerDocument.l10n.translateFragment(element);
+
+ let actualTitle = element._elements.get("title").textContent;
+ if (typeof title == "string") {
+ Assert.equal(actualTitle, title, "Title string");
+ } else {
+ // regexp
+ Assert.ok(title.test(actualTitle), "Title regexp");
+ }
+
+ let actualButton = element._elements.get("tipButton").textContent;
+ if (typeof button == "string") {
+ Assert.equal(actualButton, button, "Button string");
+ } else {
+ // regexp
+ Assert.ok(button.test(actualButton), "Button regexp");
+ }
+
+ Assert.ok(BrowserTestUtils.is_visible(element._elements.get("helpButton")));
+
+ let values = await Promise.all([awaitCallback(), pickTip()]);
+ Assert.ok(true, "Refresh dialog opened");
+
+ // Ensure the urlbar is closed so that the engagement is ended.
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ `${tip}-shown`,
+ 1
+ );
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ `${tip}-picked`,
+ 1
+ );
+
+ return values[0] || null;
+ });
+}
+
+/**
+ * Starts a search and asserts that there are no tips.
+ *
+ * @param {string} searchString
+ * The search string.
+ * @param {Window} win
+ */
+async function awaitNoTip(searchString, win = window) {
+ let context = await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: searchString,
+ waitForFocus,
+ fireInputEvent: true,
+ });
+ for (let result of context.results) {
+ Assert.notEqual(result.type, UrlbarUtils.RESULT_TYPE.TIP);
+ }
+}
+
+/**
+ * Copied from BrowserTestUtils.jsm, but lets you listen for any one of multiple
+ * dialog URIs instead of only one.
+ * @param {string} buttonAction
+ * What button should be pressed on the alert dialog.
+ * @param {array} uris
+ * The URIs for the alert dialogs.
+ * @param {function} [func]
+ * An optional callback.
+ */
+async function promiseAlertDialogOpen(buttonAction, uris, func) {
+ let win = await BrowserTestUtils.domWindowOpened(null, async aWindow => {
+ // The test listens for the "load" event which guarantees that the alert
+ // class has already been added (it is added when "DOMContentLoaded" is
+ // fired).
+ await BrowserTestUtils.waitForEvent(aWindow, "load");
+
+ return uris.includes(aWindow.document.documentURI);
+ });
+
+ if (func) {
+ await func(win);
+ return win;
+ }
+
+ let dialog = win.document.querySelector("dialog");
+ dialog.getButton(buttonAction).click();
+
+ return win;
+}
+
+/**
+ * Copied from BrowserTestUtils.jsm, but lets you listen for any one of multiple
+ * dialog URIs instead of only one.
+ * @param {string} buttonAction
+ * What button should be pressed on the alert dialog.
+ * @param {array} uris
+ * The URIs for the alert dialogs.
+ * @param {function} [func]
+ * An optional callback.
+ */
+async function promiseAlertDialog(buttonAction, uris, func) {
+ let win = await promiseAlertDialogOpen(buttonAction, uris, func);
+ return BrowserTestUtils.windowClosed(win);
+}
+
+/**
+ * Search tips helper. Asserts that a particular search tip is shown or that no
+ * search tip is shown.
+ *
+ * @param {window} win
+ * A browser window.
+ * @param {UrlbarProviderSearchTips.TIP_TYPE} expectedTip
+ * The expected search tip. Pass a falsey value (like zero) for none.
+ * @param {boolean} closeView
+ * If true, this function closes the urlbar view before returning.
+ */
+async function checkTip(win, expectedTip, closeView = true) {
+ if (!expectedTip) {
+ // Wait a bit for the tip to not show up.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 100));
+ Assert.ok(!win.gURLBar.view.isOpen);
+ return;
+ }
+
+ // Wait for the view to open, and then check the tip result.
+ await UrlbarTestUtils.promisePopupOpen(win, () => {});
+ Assert.ok(true, "View opened");
+ Assert.equal(UrlbarTestUtils.getResultCount(win), 1);
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(win, 0);
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.TIP);
+ let heuristic;
+ let title;
+ let name = Services.search.defaultEngine.name;
+ switch (expectedTip) {
+ case UrlbarProviderSearchTips.TIP_TYPE.ONBOARD:
+ heuristic = true;
+ title =
+ `Type less, find more: Search ${name} right from your ` +
+ `address bar.`;
+ break;
+ case UrlbarProviderSearchTips.TIP_TYPE.REDIRECT:
+ heuristic = false;
+ title =
+ `Start your search in the address bar to see suggestions from ` +
+ `${name} and your browsing history.`;
+ break;
+ }
+ Assert.equal(result.heuristic, heuristic);
+ Assert.equal(result.displayed.title, title);
+ Assert.equal(
+ result.element.row._elements.get("tipButton").textContent,
+ `Okay, Got It`
+ );
+ Assert.ok(
+ BrowserTestUtils.is_hidden(result.element.row._elements.get("helpButton"))
+ );
+
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ `${expectedTip}-shown`,
+ 1
+ );
+
+ Assert.ok(
+ !UrlbarTestUtils.getOneOffSearchButtonsVisible(window),
+ "One-offs should be hidden when showing a search tip"
+ );
+
+ if (closeView) {
+ await UrlbarTestUtils.promisePopupClose(win);
+ }
+}
+
+/**
+ * Search tips helper. Opens a foreground tab and asserts that a particular
+ * search tip is shown or that no search tip is shown.
+ *
+ * @param {window} win
+ * A browser window.
+ * @param {string} url
+ * The URL to load in a new foreground tab.
+ * @param {UrlbarProviderSearchTips.TIP_TYPE} expectedTip
+ * The expected search tip. Pass a falsey value (like zero) for none.
+ * @param {boolean} reset
+ * If true, the search tips provider will be reset before this function
+ * returns. See resetSearchTipsProvider.
+ */
+async function checkTab(win, url, expectedTip, reset = true) {
+ // BrowserTestUtils.withNewTab always waits for tab load, which hangs on
+ // about:newtab for some reason, so don't use it.
+ let shownCount;
+ if (expectedTip) {
+ shownCount = UrlbarPrefs.get(`tipShownCount.${expectedTip}`);
+ }
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser: win.gBrowser,
+ url,
+ waitForLoad: url != "about:newtab",
+ });
+
+ await checkTip(win, expectedTip, true);
+ if (expectedTip) {
+ Assert.equal(
+ UrlbarPrefs.get(`tipShownCount.${expectedTip}`),
+ shownCount + 1,
+ "The shownCount pref should have been incremented by one."
+ );
+ }
+
+ if (reset) {
+ resetSearchTipsProvider();
+ }
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+/**
+ * This lets us visit www.google.com (for example) and have it redirect to
+ * our test HTTP server instead of visiting the actual site.
+ * @param {string} domain
+ * The domain to which we are redirecting.
+ * @param {string} path
+ * The pathname on the domain.
+ * @param {function} callback
+ * Executed when the test suite thinks `domain` is loaded.
+ */
+async function withDNSRedirect(domain, path, callback) {
+ // Some domains have special security requirements, like www.bing.com. We
+ // need to override them to successfully load them. This part is adapted from
+ // testing/marionette/cert.js.
+ const certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+ ].getService(Ci.nsICertOverrideService);
+ Services.prefs.setBoolPref(
+ "network.stricttransportsecurity.preloadlist",
+ false
+ );
+ Services.prefs.setIntPref("security.cert_pinning.enforcement_level", 0);
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ true
+ );
+
+ // Now set network.dns.localDomains to redirect the domain to localhost and
+ // set up an HTTP server.
+ Services.prefs.setCharPref("network.dns.localDomains", domain);
+
+ let server = new HttpServer();
+ server.registerPathHandler(path, (req, resp) => {
+ resp.write(`Test! http://${domain}${path}`);
+ });
+ server.start(-1);
+ server.identity.setPrimary("http", domain, server.identity.primaryPort);
+ let url = `http://${domain}:${server.identity.primaryPort}${path}`;
+
+ await callback(url);
+
+ // Reset network.dns.localDomains and stop the server.
+ Services.prefs.clearUserPref("network.dns.localDomains");
+ await new Promise(resolve => server.stop(resolve));
+
+ // Reset the security stuff.
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ false
+ );
+ Services.prefs.clearUserPref("network.stricttransportsecurity.preloadlist");
+ Services.prefs.clearUserPref("security.cert_pinning.enforcement_level");
+ const sss = Cc["@mozilla.org/ssservice;1"].getService(
+ Ci.nsISiteSecurityService
+ );
+ sss.clearAll();
+ sss.clearPreloads();
+}
+
+function resetSearchTipsProvider() {
+ Services.prefs.clearUserPref(
+ `browser.urlbar.tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.ONBOARD}`
+ );
+ Services.prefs.clearUserPref(
+ `browser.urlbar.tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.REDIRECT}`
+ );
+ UrlbarProviderSearchTips.disableTipsForCurrentSession = false;
+}
+
+async function setDefaultEngine(name) {
+ let engine = (await Services.search.getEngines()).find(e => e.name == name);
+ Assert.ok(engine);
+ await Services.search.setDefault(engine);
+}
diff --git a/browser/components/urlbar/tests/browser/POSTSearchEngine.xml b/browser/components/urlbar/tests/browser/POSTSearchEngine.xml
new file mode 100644
index 0000000000..8b387ea9ae
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/POSTSearchEngine.xml
@@ -0,0 +1,6 @@
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
+ <ShortName>POST Search</ShortName>
+ <Url type="text/html" method="POST" template="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/print_postdata.sjs">
+ <Param name="searchterms" value="{searchTerms}"/>
+ </Url>
+</OpenSearchDescription>
diff --git a/browser/components/urlbar/tests/browser/authenticate.sjs b/browser/components/urlbar/tests/browser/authenticate.sjs
new file mode 100644
index 0000000000..58da655cf9
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/authenticate.sjs
@@ -0,0 +1,220 @@
+function handleRequest(request, response)
+{
+ try {
+ reallyHandleRequest(request, response);
+ } catch (e) {
+ response.setStatusLine("1.0", 200, "AlmostOK");
+ response.write("Error handling request: " + e);
+ }
+}
+
+
+function reallyHandleRequest(request, response) {
+ var match;
+ var requestAuth = true, requestProxyAuth = true;
+
+ // Allow the caller to drive how authentication is processed via the query.
+ // Eg, http://localhost:8888/authenticate.sjs?user=foo&realm=bar
+ // The extra ? allows the user/pass/realm checks to succeed if the name is
+ // at the beginning of the query string.
+ var query = "?" + request.queryString;
+
+ var expected_user = "", expected_pass = "", realm = "mochitest";
+ var proxy_expected_user = "", proxy_expected_pass = "", proxy_realm = "mochi-proxy";
+ var huge = false, plugin = false, anonymous = false;
+ var authHeaderCount = 1;
+ // user=xxx
+ match = /[^_]user=([^&]*)/.exec(query);
+ if (match)
+ expected_user = match[1];
+
+ // pass=xxx
+ match = /[^_]pass=([^&]*)/.exec(query);
+ if (match)
+ expected_pass = match[1];
+
+ // realm=xxx
+ match = /[^_]realm=([^&]*)/.exec(query);
+ if (match)
+ realm = match[1];
+
+ // proxy_user=xxx
+ match = /proxy_user=([^&]*)/.exec(query);
+ if (match)
+ proxy_expected_user = match[1];
+
+ // proxy_pass=xxx
+ match = /proxy_pass=([^&]*)/.exec(query);
+ if (match)
+ proxy_expected_pass = match[1];
+
+ // proxy_realm=xxx
+ match = /proxy_realm=([^&]*)/.exec(query);
+ if (match)
+ proxy_realm = match[1];
+
+ // huge=1
+ match = /huge=1/.exec(query);
+ if (match)
+ huge = true;
+
+ // plugin=1
+ match = /plugin=1/.exec(query);
+ if (match)
+ plugin = true;
+
+ // multiple=1
+ match = /multiple=([^&]*)/.exec(query);
+ if (match)
+ authHeaderCount = match[1]+0;
+
+ // anonymous=1
+ match = /anonymous=1/.exec(query);
+ if (match)
+ anonymous = true;
+
+ // Look for an authentication header, if any, in the request.
+ //
+ // EG: Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
+ //
+ // This test only supports Basic auth. The value sent by the client is
+ // "username:password", obscured with base64 encoding.
+
+ var actual_user = "", actual_pass = "", authHeader, authPresent = false;
+ if (request.hasHeader("Authorization")) {
+ authPresent = true;
+ authHeader = request.getHeader("Authorization");
+ match = /Basic (.+)/.exec(authHeader);
+ if (match.length != 2)
+ throw "Couldn't parse auth header: " + authHeader;
+
+ var userpass = base64ToString(match[1]); // no atob() :-(
+ match = /(.*):(.*)/.exec(userpass);
+ if (match.length != 3)
+ throw "Couldn't decode auth header: " + userpass;
+ actual_user = match[1];
+ actual_pass = match[2];
+ }
+
+ var proxy_actual_user = "", proxy_actual_pass = "";
+ if (request.hasHeader("Proxy-Authorization")) {
+ authHeader = request.getHeader("Proxy-Authorization");
+ match = /Basic (.+)/.exec(authHeader);
+ if (match.length != 2)
+ throw "Couldn't parse auth header: " + authHeader;
+
+ var userpass = base64ToString(match[1]); // no atob() :-(
+ match = /(.*):(.*)/.exec(userpass);
+ if (match.length != 3)
+ throw "Couldn't decode auth header: " + userpass;
+ proxy_actual_user = match[1];
+ proxy_actual_pass = match[2];
+ }
+
+ // Don't request authentication if the credentials we got were what we
+ // expected.
+ if (expected_user == actual_user &&
+ expected_pass == actual_pass) {
+ requestAuth = false;
+ }
+ if (proxy_expected_user == proxy_actual_user &&
+ proxy_expected_pass == proxy_actual_pass) {
+ requestProxyAuth = false;
+ }
+
+ if (anonymous) {
+ if (authPresent) {
+ response.setStatusLine("1.0", 400, "Unexpected authorization header found");
+ } else {
+ response.setStatusLine("1.0", 200, "Authorization header not found");
+ }
+ } else {
+ if (requestProxyAuth) {
+ response.setStatusLine("1.0", 407, "Proxy authentication required");
+ for (i = 0; i < authHeaderCount; ++i)
+ response.setHeader("Proxy-Authenticate", "basic realm=\"" + proxy_realm + "\"", true);
+ } else if (requestAuth) {
+ response.setStatusLine("1.0", 401, "Authentication required");
+ for (i = 0; i < authHeaderCount; ++i)
+ response.setHeader("WWW-Authenticate", "basic realm=\"" + realm + "\"", true);
+ } else {
+ response.setStatusLine("1.0", 200, "OK");
+ }
+ }
+
+ response.setHeader("Content-Type", "application/xhtml+xml", false);
+ response.write("<html xmlns='http://www.w3.org/1999/xhtml'>");
+ response.write("<p>Login: <span id='ok'>" + (requestAuth ? "FAIL" : "PASS") + "</span></p>\n");
+ response.write("<p>Proxy: <span id='proxy'>" + (requestProxyAuth ? "FAIL" : "PASS") + "</span></p>\n");
+ response.write("<p>Auth: <span id='auth'>" + authHeader + "</span></p>\n");
+ response.write("<p>User: <span id='user'>" + actual_user + "</span></p>\n");
+ response.write("<p>Pass: <span id='pass'>" + actual_pass + "</span></p>\n");
+
+ if (huge) {
+ response.write("<div style='display: none'>");
+ for (i = 0; i < 100000; i++) {
+ response.write("123456789\n");
+ }
+ response.write("</div>");
+ response.write("<span id='footnote'>This is a footnote after the huge content fill</span>");
+ }
+
+ if (plugin) {
+ response.write("<embed id='embedtest' style='width: 400px; height: 100px;' " +
+ "type='application/x-test'></embed>\n");
+ }
+
+ response.write("</html>");
+}
+
+
+// base64 decoder
+//
+// Yoinked from extensions/xml-rpc/src/nsXmlRpcClient.js because btoa()
+// doesn't seem to exist. :-(
+/* Convert Base64 data to a string */
+const toBinaryTable = [
+ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
+ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
+ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63,
+ 52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1,
+ -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14,
+ 15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1,
+ -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40,
+ 41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1
+];
+const base64Pad = '=';
+
+function base64ToString(data) {
+
+ var result = '';
+ var leftbits = 0; // number of bits decoded, but yet to be appended
+ var leftdata = 0; // bits decoded, but yet to be appended
+
+ // Convert one by one.
+ for (var i = 0; i < data.length; i++) {
+ var c = toBinaryTable[data.charCodeAt(i) & 0x7f];
+ var padding = (data[i] == base64Pad);
+ // Skip illegal characters and whitespace
+ if (c == -1) continue;
+
+ // Collect data into leftdata, update bitcount
+ leftdata = (leftdata << 6) | c;
+ leftbits += 6;
+
+ // If we have 8 or more bits, append 8 bits to the result
+ if (leftbits >= 8) {
+ leftbits -= 8;
+ // Append if not padding.
+ if (!padding)
+ result += String.fromCharCode((leftdata >> leftbits) & 0xff);
+ leftdata &= (1 << leftbits) - 1;
+ }
+ }
+
+ // If there are any bits left, the base64 string was corrupted
+ if (leftbits)
+ throw Components.Exception('Corrupted base64 string');
+
+ return result;
+}
diff --git a/browser/components/urlbar/tests/browser/browser.ini b/browser/components/urlbar/tests/browser/browser.ini
new file mode 100644
index 0000000000..09b29034a8
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser.ini
@@ -0,0 +1,297 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+[DEFAULT]
+support-files =
+ dummy_page.html
+ head.js
+ head-common.js
+
+[browser_aboutHomeLoading.js]
+skip-if = tsan # Intermittently times out, see 1622698 (frequent on TSan).
+[browser_action_searchengine.js]
+[browser_action_searchengine_alias.js]
+[browser_autocomplete_a11y_label.js]
+support-files =
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+[browser_autocomplete_autoselect.js]
+[browser_autocomplete_cursor.js]
+[browser_autocomplete_edit_completed.js]
+[browser_autocomplete_enter_race.js]
+[browser_autocomplete_no_title.js]
+[browser_autocomplete_readline_navigation.js]
+skip-if = os != "mac" # Mac only feature
+[browser_autocomplete_tag_star_visibility.js]
+[browser_autoFill_backspaced.js]
+[browser_autoFill_canonize.js]
+[browser_autoFill_caretNotAtEnd.js]
+[browser_autoFill_firstResult.js]
+[browser_autoFill_paste.js]
+[browser_autoFill_placeholder.js]
+[browser_autoFill_preserve.js]
+[browser_autoFill_trimURLs.js]
+[browser_autoFill_typed.js]
+[browser_autoFill_undo.js]
+[browser_autoOpen.js]
+[browser_blanking.js]
+support-files =
+ file_blank_but_not_blank.html
+[browser_canonizeURL.js]
+support-files =
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+[browser_caret_navigation.js]
+[browser_closePanelOnClick.js]
+[browser_content_opener.js]
+[browser_copy_during_load.js]
+support-files =
+ slow-page.sjs
+[browser_copying.js]
+support-files =
+ authenticate.sjs
+[browser_customizeMode.js]
+[browser_cutting.js]
+[browser_decode.js]
+[browser_delete.js]
+[browser_deleteAllText.js]
+[browser_display_selectedAction_Extensions.js]
+[browser_dns_first_for_single_words.js]
+skip-if = verify && os == 'linux' # Bug 1581635
+[browser_downArrowKeySearch.js]
+[browser_dragdropURL.js]
+[browser_dynamicResults.js]
+support-files =
+ dynamicResult0.css
+ dynamicResult1.css
+[browser_edit_invalid_url.js]
+[browser_enter.js]
+[browser_enterAfterMouseOver.js]
+[browser_focusedCmdK.js]
+[browser_handleCommand_fallback.js]
+[browser_hashChangeProxyState.js]
+[browser_heuristicNotAddedFirst.js]
+[browser_ime_composition.js]
+[browser_inputHistory.js]
+support-files =
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+[browser_inputHistory_emptystring.js]
+[browser_keepStateAcrossTabSwitches.js]
+[browser_keywordBookmarklets.js]
+[browser_keyword_override.js]
+[browser_keywordSearch.js]
+support-files =
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+[browser_keywordSearch_postData.js]
+support-files =
+ POSTSearchEngine.xml
+ print_postdata.sjs
+[browser_keyword_select_and_type.js]
+[browser_keyword.js]
+support-files =
+ print_postdata.sjs
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+[browser_loadRace.js]
+[browser_locationBarCommand.js]
+skip-if = (os == 'mac' && os_version == '10.14') # bug 1554807
+[browser_locationBarExternalLoad.js]
+[browser_locationchange_urlbar_edit_dos.js]
+support-files =
+ file_urlbar_edit_dos.html
+[browser_new_tab_urlbar_reset.js]
+[browser_oneOffs_contextMenu.js]
+support-files =
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+[browser_oneOffs_heuristicRestyle.js]
+[browser_oneOffs_keyModifiers.js]
+support-files =
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+[browser_oneOffs_searchSuggestions.js]
+support-files =
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+ searchSuggestionEngine2.xml
+[browser_oneOffs_settings.js]
+[browser_oneOffs.js]
+support-files =
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+[browser_pasteAndGo.js]
+[browser_percent_encoded.js]
+[browser_placeholder.js]
+support-files =
+ searchSuggestionEngine.xml
+ searchSuggestionEngine2.xml
+ searchSuggestionEngine.sjs
+[browser_populateAfterPushState.js]
+[browser_primary_selection_safe_on_new_tab.js]
+[browser_privateBrowsingWindowChange.js]
+[browser_raceWithTabs.js]
+[browser_redirect_error.js]
+support-files = redirect_error.sjs
+[browser_remoteness_switch.js]
+run-if = e10s
+[browser_remotetab.js]
+[browser_remove_match.js]
+[browser_removeUnsafeProtocolsFromURLBarPaste.js]
+[browser_restoreEmptyInput.js]
+[browser_result_onSelection.js]
+[browser_resultSpan.js]
+[browser_retainedResultsOnFocus.js]
+[browser_revert.js]
+[browser_searchFunction.js]
+[browser_searchHistoryLimit.js]
+[browser_searchMode_alias_replacement.js]
+support-files =
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+[browser_searchMode_autofill.js]
+[browser_searchMode_clickLink.js]
+support-files =
+ dummy_page.html
+[browser_searchMode_engineRemoval.js]
+[browser_searchMode_excludeResults.js]
+[browser_searchMode_heuristic.js]
+[browser_searchMode_indicator.js]
+support-files =
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+[browser_searchMode_localOneOffs_actionText.js]
+support-files =
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+[browser_searchMode_no_results.js]
+[browser_searchMode_oneOffButton.js]
+[browser_searchMode_pickResult.js]
+[browser_searchMode_preview.js]
+[browser_searchMode_sessionStore.js]
+skip-if = os == 'mac' && debug && verify # bug 1671045
+[browser_searchMode_setURI.js]
+[browser_searchMode_suggestions.js]
+support-files =
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+ searchSuggestionEngineMany.xml
+[browser_searchMode_switchTabs.js]
+[browser_searchSettings.js]
+[browser_searchSingleWordNotification.js]
+[browser_searchSuggestions.js]
+support-files =
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+[browser_selectionKeyNavigation.js]
+[browser_selectStaleResults.js]
+support-files =
+ searchSuggestionEngineSlow.xml
+ searchSuggestionEngine.sjs
+[browser_searchTelemetry.js]
+support-files =
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+[browser_separatePrivateDefault.js]
+support-files =
+ POSTSearchEngine.xml
+ print_postdata.sjs
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+ searchSuggestionEngine2.xml
+[browser_separatePrivateDefault_differentEngine.js]
+support-files =
+ POSTSearchEngine.xml
+ print_postdata.sjs
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+ searchSuggestionEngine2.xml
+[browser_speculative_connect.js]
+support-files =
+ searchSuggestionEngine2.xml
+ searchSuggestionEngine.sjs
+[browser_speculative_connect_not_with_client_cert.js]
+[browser_stop.js]
+skip-if = os == 'mac' # macosx1014 fails due to 1485288
+[browser_stop_pending.js]
+support-files =
+ slow-page.sjs
+[browser_stopSearchOnSelection.js]
+support-files =
+ searchSuggestionEngineSlow.xml
+ searchSuggestionEngine.sjs
+[browser_suggestedIndex.js]
+[browser_switchTab_closesUrlbarPopup.js]
+[browser_switchTab_decodeuri.js]
+[browser_switchTab_override.js]
+[browser_switchToTab_closes_newtab.js]
+[browser_switchToTab_fullUrl_repeatedKeydown.js]
+[browser_switchToTabHavingURI_aOpenParams.js]
+[browser_tabKeyBehavior.js]
+[browser_tabMatchesInAwesomebar_perwindowpb.js]
+[browser_tabMatchesInAwesomebar.js]
+skip-if = fission && os == 'linux' && debug # bug 1590880
+support-files =
+ moz.png
+[browser_tabToSearch.js]
+[browser_textruns.js]
+[browser_tokenAlias.js]
+[browser_top_sites.js]
+[browser_top_sites_private.js]
+[browser_typed_value.js]
+[browser_updateForDomainCompletion.js]
+[browser_updateRows.js]
+[browser_urlbar_event_telemetry.js]
+support-files =
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+[browser_urlbar_selection.js]
+skip-if = (os == 'mac') # bug 1570474
+[browser_urlbar_telemetry_dynamic.js]
+support-files =
+ urlbarTelemetryUrlbarDynamic.css
+[browser_urlbar_telemetry_extension.js]
+tags = search-telemetry
+[browser_urlbar_telemetry_places.js]
+tags = search-telemetry
+[browser_urlbar_telemetry_remotetab.js]
+tags = search-telemetry
+[browser_urlbar_telemetry_searchmode.js]
+tags = search-telemetry
+support-files =
+ urlbarTelemetrySearchSuggestions.sjs
+ urlbarTelemetrySearchSuggestions.xml
+[browser_urlbar_telemetry_tabtosearch.js]
+tags = search-telemetry
+[browser_urlbar_telemetry_tip.js]
+tags = search-telemetry
+[browser_urlbar_telemetry_topsite.js]
+tags = search-telemetry
+[browser_urlbar_telemetry.js]
+tags = search-telemetry
+support-files =
+ urlbarTelemetrySearchSuggestions.sjs
+ urlbarTelemetrySearchSuggestions.xml
+[browser_UrlbarInput_formatValue.js]
+[browser_UrlbarInput_hiddenFocus.js]
+[browser_UrlbarInput_overflow.js]
+[browser_UrlbarInput_overflow_resize.js]
+[browser_UrlbarInput_setURI.js]
+[browser_UrlbarInput_tooltip.js]
+[browser_UrlbarInput_trimURLs.js]
+
+[browser_userTypedValue.js]
+support-files = file_userTypedValue.html
+[browser_valueOnTabSwitch.js]
+[browser_view_emptyResultSet.js]
+[browser_view_resultDisplay.js]
+[browser_view_resultTypes_display.js]
+support-files =
+ print_postdata.sjs
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+[browser_waitForLoadOrTimeout.js]
+skip-if = tsan # Bug 1683730
+[browser_whereToOpen.js]
diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_formatValue.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_formatValue.js
new file mode 100644
index 0000000000..00c659f62b
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_formatValue.js
@@ -0,0 +1,174 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Checks that the url formatter properly recognizes the host and de-emphasizes
+// the rest of the url.
+
+/**
+ * Tests a given url.
+ * The de-emphasized parts must be wrapped in "<" and ">" chars.
+ * @param {string} aExpected The url to test.
+ * @param {string} aClobbered [optional] Normally the url is de-emphasized
+ * in-place, thus it's enough to pass aExpected. Though, in some cases
+ * the formatter may decide to replace the url with a fixed one, because
+ * it can't properly guess a host. In that case aClobbered is the
+ * expected de-emphasized value.
+ * @param {boolean} synthesizeInput [optional] Whether to synthesize an input
+ * event to test.
+ */
+function testVal(aExpected, aClobbered = null, synthesizeInput = false) {
+ let str = aExpected.replace(/[<>]/g, "");
+ if (synthesizeInput) {
+ gURLBar.focus();
+ gURLBar.select();
+ EventUtils.sendString(str);
+ Assert.equal(
+ gURLBar.editor.rootElement.textContent,
+ str,
+ "Url is not highlighted"
+ );
+ gBrowser.selectedBrowser.focus();
+ } else {
+ gURLBar.value = str;
+ }
+
+ let selectionController = gURLBar.editor.selectionController;
+ let selection = selectionController.getSelection(
+ selectionController.SELECTION_URLSECONDARY
+ );
+ let value = gURLBar.editor.rootElement.textContent;
+ let result = "";
+ for (let i = 0; i < selection.rangeCount; i++) {
+ let range = selection.getRangeAt(i).toString();
+ let pos = value.indexOf(range);
+ result += value.substring(0, pos) + "<" + range + ">";
+ value = value.substring(pos + range.length);
+ }
+ result += value;
+ Assert.equal(
+ result,
+ aClobbered || aExpected,
+ "Correct part of the url is de-emphasized" +
+ (synthesizeInput ? " (with input simulation)" : "")
+ );
+
+ // Now re-test synthesizing input.
+ if (!synthesizeInput) {
+ testVal(aExpected, aClobbered, true);
+ }
+}
+
+function test() {
+ const prefname = "browser.urlbar.formatting.enabled";
+
+ registerCleanupFunction(function() {
+ Services.prefs.clearUserPref(prefname);
+ gURLBar.setURI();
+ });
+
+ gBrowser.selectedBrowser.focus();
+
+ testVal("<https://>mozilla.org");
+ testVal("<https://>mözilla.org");
+ testVal("<https://>mozilla.imaginatory");
+
+ testVal("<https://www.>mozilla.org");
+ testVal("<https://sub.>mozilla.org");
+ testVal("<https://sub1.sub2.sub3.>mozilla.org");
+ testVal("<www.>mozilla.org");
+ testVal("<sub.>mozilla.org");
+ testVal("<sub1.sub2.sub3.>mozilla.org");
+ testVal("<mozilla.com.>mozilla.com");
+ testVal("<https://mozilla.com:mozilla.com@>mozilla.com");
+ testVal("<mozilla.com:mozilla.com@>mozilla.com");
+
+ testVal("<ftp.>mozilla.org");
+ testVal("<ftp://ftp.>mozilla.org");
+
+ testVal("<https://sub.>mozilla.org");
+ testVal("<https://sub1.sub2.sub3.>mozilla.org");
+ testVal("<https://user:pass@sub1.sub2.sub3.>mozilla.org");
+ testVal("<https://user:pass@>mozilla.org");
+ testVal("<user:pass@sub1.sub2.sub3.>mozilla.org");
+ testVal("<user:pass@>mozilla.org");
+
+ testVal("<https://>mozilla.org< >");
+ testVal("mozilla.org< >");
+
+ testVal("<https://>mozilla.org</file.ext>");
+ testVal("<https://>mozilla.org</sub/file.ext>");
+ testVal("<https://>mozilla.org</sub/file.ext?foo>");
+ testVal("<https://>mozilla.org</sub/file.ext?foo&bar>");
+ testVal("<https://>mozilla.org</sub/file.ext?foo&bar#top>");
+ testVal("<https://>mozilla.org</sub/file.ext?foo&bar#top>");
+ testVal("foo.bar<?q=test>");
+ testVal("foo.bar<#mozilla.org>");
+ testVal("foo.bar<?somewhere.mozilla.org>");
+ testVal("foo.bar<?@mozilla.org>");
+ testVal("foo.bar<#x@mozilla.org>");
+ testVal("foo.bar<#@x@mozilla.org>");
+ testVal("foo.bar<?x@mozilla.org>");
+ testVal("foo.bar<?@x@mozilla.org>");
+ testVal("<foo.bar@x@>mozilla.org");
+ testVal("<foo.bar@:baz@>mozilla.org");
+ testVal("<foo.bar:@baz@>mozilla.org");
+ testVal("<foo.bar@:ba:z@>mozilla.org");
+ testVal("<foo.:bar:@baz@>mozilla.org");
+ testVal(
+ "foopy:\\blah@somewhere.com//whatever/",
+ "foopy</blah@somewhere.com//whatever/>"
+ );
+
+ testVal("<https://sub.>mozilla.org<:666/file.ext>");
+ testVal("<sub.>mozilla.org<:666/file.ext>");
+ testVal("localhost<:666/file.ext>");
+
+ let IPs = [
+ "192.168.1.1",
+ "[::]",
+ "[::1]",
+ "[1::]",
+ "[::]",
+ "[::1]",
+ "[1::]",
+ "[1:2:3:4:5:6:7::]",
+ "[::1:2:3:4:5:6:7]",
+ "[1:2:a:B:c:D:e:F]",
+ "[1::8]",
+ "[1:2::8]",
+ "[fe80::222:19ff:fe11:8c76]",
+ "[0000:0123:4567:89AB:CDEF:abcd:ef00:0000]",
+ "[::192.168.1.1]",
+ "[1::0.0.0.0]",
+ "[1:2::255.255.255.255]",
+ "[1:2:3::255.255.255.255]",
+ "[1:2:3:4::255.255.255.255]",
+ "[1:2:3:4:5::255.255.255.255]",
+ "[1:2:3:4:5:6:255.255.255.255]",
+ ];
+ IPs.forEach(function(IP) {
+ testVal(IP);
+ testVal(IP + "</file.ext>");
+ testVal(IP + "<:666/file.ext>");
+ testVal("<https://>" + IP);
+ testVal(`<https://>${IP}</file.ext>`);
+ testVal(`<https://user:pass@>${IP}<:666/file.ext>`);
+ testVal(`<user:pass@>${IP}<:666/file.ext>`);
+ testVal(`user:\\pass@${IP}/`, `user</pass@${IP}/>`);
+ });
+
+ testVal("mailto:admin@mozilla.org");
+ testVal("gopher://mozilla.org/");
+ testVal("about:config");
+ testVal("jar:http://mozilla.org/example.jar!/");
+ testVal("view-source:http://mozilla.org/");
+ testVal("foo9://mozilla.org/");
+ testVal("foo+://mozilla.org/");
+ testVal("foo.://mozilla.org/");
+ testVal("foo-://mozilla.org/");
+
+ // Disable formatting.
+ Services.prefs.setBoolPref(prefname, false);
+
+ testVal("https://mozilla.org");
+}
diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_hiddenFocus.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_hiddenFocus.js
new file mode 100644
index 0000000000..70e43e6c42
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_hiddenFocus.js
@@ -0,0 +1,21 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ registerCleanupFunction(async function() {
+ BrowserTestUtils.removeTab(tab);
+ gURLBar.setURI();
+ });
+
+ gURLBar.blur();
+ ok(!gURLBar.focused, "url bar is not focused");
+ ok(!gURLBar.hasAttribute("focused"), "url bar is not visibly focused");
+ gURLBar.setHiddenFocus();
+ ok(gURLBar.focused, "url bar is focused");
+ ok(!gURLBar.hasAttribute("focused"), "url bar is not visibly focused");
+ gURLBar.removeHiddenFocus();
+ ok(gURLBar.focused, "url bar is focused");
+ ok(gURLBar.hasAttribute("focused"), "url bar is visibly focused");
+});
diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow.js
new file mode 100644
index 0000000000..eab30a175d
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow.js
@@ -0,0 +1,156 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+async function testVal(aExpected, overflowSide = "") {
+ info(`Testing ${aExpected}`);
+ try {
+ gURLBar.setURI(makeURI(aExpected));
+ } catch (ex) {
+ if (ex.result != Cr.NS_ERROR_MALFORMED_URI) {
+ throw ex;
+ }
+ // For values without a protocol fallback to setting the raw value.
+ gURLBar.value = aExpected;
+ }
+
+ Assert.equal(
+ gURLBar.selectionStart,
+ gURLBar.selectionEnd,
+ "Selection sanity check"
+ );
+
+ gURLBar.focus();
+ Assert.equal(
+ document.activeElement,
+ gURLBar.inputField,
+ "URL Bar should be focused"
+ );
+ Assert.equal(
+ gURLBar.valueFormatter.scheme.value,
+ "",
+ "Check the scheme value"
+ );
+ Assert.equal(
+ getComputedStyle(gURLBar.valueFormatter.scheme).visibility,
+ "hidden",
+ "Check the scheme box visibility"
+ );
+
+ gURLBar.blur();
+ await window.promiseDocumentFlushed(() => {});
+ // The attribute doesn't always change, so we can't use waitForAttribute.
+ await TestUtils.waitForCondition(
+ () => gURLBar.getAttribute("textoverflow") === overflowSide
+ );
+
+ let scheme = aExpected.match(/^([a-z]+:\/{0,2})/)?.[1] || "";
+ // We strip http, so we should not show the scheme for it.
+ if (
+ scheme == "http://" &&
+ Services.prefs.getBoolPref("browser.urlbar.trimURLs", true)
+ ) {
+ scheme = "";
+ }
+
+ Assert.equal(
+ gURLBar.valueFormatter.scheme.value,
+ scheme,
+ "Check the scheme value"
+ );
+ let isOverflowed =
+ gURLBar.inputField.scrollWidth > gURLBar.inputField.clientWidth;
+ Assert.equal(isOverflowed, !!overflowSide, "Check The input field overflow");
+ Assert.equal(
+ gURLBar.getAttribute("textoverflow"),
+ overflowSide,
+ "Check the textoverflow attribute"
+ );
+ if (overflowSide) {
+ let side = gURLBar.getAttribute("domaindir") == "ltr" ? "right" : "left";
+ Assert.equal(side, overflowSide, "Check the overflow side");
+ Assert.equal(
+ getComputedStyle(gURLBar.valueFormatter.scheme).visibility,
+ scheme && isOverflowed && overflowSide == "left" ? "visible" : "hidden",
+ "Check the scheme box visibility"
+ );
+
+ info("Focus, change scroll position and blur, to ensure proper restore");
+ gURLBar.focus();
+ EventUtils.synthesizeKey("KEY_End");
+ gURLBar.blur();
+ await window.promiseDocumentFlushed(() => {});
+ // The attribute doesn't always change, so we can't use waitForAttribute.
+ await TestUtils.waitForCondition(
+ () => gURLBar.getAttribute("textoverflow") === overflowSide
+ );
+
+ Assert.equal(side, overflowSide, "Check the overflow side");
+ Assert.equal(
+ getComputedStyle(gURLBar.valueFormatter.scheme).visibility,
+ scheme && isOverflowed && overflowSide == "left" ? "visible" : "hidden",
+ "Check the scheme box visibility"
+ );
+ }
+}
+
+add_task(async function() {
+ // We use a new tab for the test to be sure all the tab switching and loading
+ // is complete before starting, otherwise onLocationChange for this tab could
+ // override the value we set with an empty value.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ registerCleanupFunction(function() {
+ gURLBar.setURI();
+ BrowserTestUtils.removeTab(tab);
+ });
+
+ let lotsOfSpaces = "%20".repeat(200);
+
+ // اسماء.شبكة
+ let rtlDomain =
+ "\u0627\u0633\u0645\u0627\u0621\u002e\u0634\u0628\u0643\u0629";
+ let rtlChar = "\u0627";
+
+ // Mix the direction of the tests to cover more cases, and to ensure the
+ // textoverflow attribute changes every time, because tewtVal waits for that.
+ await testVal(`https://mozilla.org/${lotsOfSpaces}/test/`, "right");
+ await testVal(`https://mozilla.org/`);
+ await testVal(`https://${rtlDomain}/${lotsOfSpaces}/test/`, "left");
+ await testVal(`https://mozilla.org:8888/${lotsOfSpaces}/test/`, "right");
+ await testVal(`https://${rtlDomain}:8888/${lotsOfSpaces}/test/`, "left");
+
+ await testVal(`ftp://mozilla.org/${lotsOfSpaces}/test/`, "right");
+ await testVal(`ftp://${rtlDomain}/${lotsOfSpaces}/test/`, "left");
+ await testVal(`ftp://mozilla.org/`);
+
+ await testVal(`http://${rtlDomain}/${lotsOfSpaces}/test/`, "left");
+ await testVal(`http://mozilla.org/`);
+ await testVal(`http://mozilla.org/${lotsOfSpaces}/test/`, "right");
+ await testVal(`http://${rtlDomain}:8888/${lotsOfSpaces}/test/`, "left");
+ await testVal(`http://[::1]/${rtlChar}/${lotsOfSpaces}/test/`, "right");
+
+ info("Test with formatting disabled");
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.formatting.enabled", false],
+ ["browser.urlbar.trimURLs", false],
+ ],
+ });
+
+ await testVal(`https://mozilla.org/`);
+ await testVal(`https://${rtlDomain}/${lotsOfSpaces}/test/`, "left");
+ await testVal(`https://mozilla.org/${lotsOfSpaces}/test/`, "right");
+
+ info("Test with trimURLs disabled");
+ await testVal(`http://${rtlDomain}/${lotsOfSpaces}/test/`, "left");
+
+ await SpecialPowers.popPrefEnv();
+
+ info("Tests without protocol");
+ await testVal(`mozilla.org/${lotsOfSpaces}/test/`, "right");
+ await testVal(`mozilla.org/`);
+ await testVal(`${rtlDomain}/${lotsOfSpaces}/test/`, "left");
+ await testVal(`mozilla.org:8888/${lotsOfSpaces}/test/`, "right");
+ await testVal(`${rtlDomain}:8888/${lotsOfSpaces}/test/`, "left");
+ await testVal(`[::1]/${rtlChar}/${lotsOfSpaces}/test/`, "right");
+});
diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow_resize.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow_resize.js
new file mode 100644
index 0000000000..050961abe0
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow_resize.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+async function testVal(win, url) {
+ info(`Testing ${url}`);
+ win.gURLBar.setURI(makeURI(url));
+
+ let urlbar = win.gURLBar;
+ urlbar.blur();
+
+ for (let width of [1000, 800]) {
+ win.resizeTo(width, 500);
+ await win.promiseDocumentFlushed(() => {});
+ Assert.greater(
+ urlbar.inputField.scrollWidth,
+ urlbar.inputField.clientWidth,
+ "Check The input field overflows"
+ );
+ // Resize is handled on a timer, so we must wait for it.
+ await TestUtils.waitForCondition(
+ () => urlbar.inputField.scrollLeft == urlbar.inputField.scrollLeftMax,
+ "The urlbar input field is completely scrolled to the end"
+ );
+ await TestUtils.waitForCondition(
+ () => urlbar.getAttribute("textoverflow") == "left",
+ "Wait for the textoverflow attribute"
+ );
+ }
+}
+
+add_task(async function() {
+ // We use a new tab for the test to be sure all the tab switching and loading
+ // is complete before starting, otherwise onLocationChange for this tab could
+ // override the value we set with an empty value.
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ registerCleanupFunction(() => BrowserTestUtils.closeWindow(win));
+
+ let lotsOfSpaces = "%20".repeat(200);
+
+ // اسماء.شبكة
+ let rtlDomain =
+ "\u0627\u0633\u0645\u0627\u0621\u002e\u0634\u0628\u0643\u0629";
+
+ // Mix the direction of the tests to cover more cases, and to ensure the
+ // textoverflow attribute changes every time, because tewtVal waits for that.
+ await testVal(win, `https://${rtlDomain}/${lotsOfSpaces}/test/`);
+
+ info("Test with formatting and trimurl disabled");
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.formatting.enabled", false],
+ ["browser.urlbar.trimURLs", false],
+ ],
+ });
+
+ await testVal(win, `https://${rtlDomain}/${lotsOfSpaces}/test/`);
+ await testVal(win, `http://${rtlDomain}/${lotsOfSpaces}/test/`);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_setURI.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_setURI.js
new file mode 100644
index 0000000000..3aabe1ecac
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_setURI.js
@@ -0,0 +1,125 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ waitForExplicitFinish();
+
+ // avoid prompting about phishing
+ Services.prefs.setIntPref(phishyUserPassPref, 32);
+ registerCleanupFunction(function() {
+ Services.prefs.clearUserPref(phishyUserPassPref);
+ });
+
+ nextTest();
+}
+
+const phishyUserPassPref = "network.http.phishy-userpass-length";
+
+function nextTest() {
+ let testCase = tests.shift();
+ if (testCase) {
+ testCase(function() {
+ executeSoon(nextTest);
+ });
+ } else {
+ executeSoon(finish);
+ }
+}
+
+var tests = [
+ function revert(next) {
+ loadTabInWindow(window, function(tab) {
+ gURLBar.handleRevert();
+ is(
+ gURLBar.value,
+ "example.com",
+ "URL bar had user/pass stripped after reverting"
+ );
+ gBrowser.removeTab(tab);
+ next();
+ });
+ },
+ function customize(next) {
+ // Need to wait for delayedStartup for the customization part of the test,
+ // since that's where BrowserToolboxCustomizeDone is set.
+ BrowserTestUtils.openNewBrowserWindow().then(function(win) {
+ loadTabInWindow(win, function() {
+ openToolbarCustomizationUI(function() {
+ closeToolbarCustomizationUI(function() {
+ is(
+ win.gURLBar.value,
+ "example.com",
+ "URL bar had user/pass stripped after customize"
+ );
+ win.close();
+ next();
+ }, win);
+ }, win);
+ });
+ });
+ },
+ function pageloaderror(next) {
+ loadTabInWindow(window, function(tab) {
+ // Load a new URL and then immediately stop it, to simulate a page load
+ // error.
+ BrowserTestUtils.loadURI(tab.linkedBrowser, "http://test1.example.com");
+ tab.linkedBrowser.stop();
+ is(
+ gURLBar.value,
+ "example.com",
+ "URL bar had user/pass stripped after load error"
+ );
+ gBrowser.removeTab(tab);
+ next();
+ });
+ },
+];
+
+function loadTabInWindow(win, callback) {
+ info("Loading tab");
+ let url = "http://user:pass@example.com/";
+ let tab = (win.gBrowser.selectedTab = BrowserTestUtils.addTab(
+ win.gBrowser,
+ url
+ ));
+ BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, url).then(() => {
+ info("Tab loaded");
+ is(
+ win.gURLBar.value,
+ "example.com",
+ "URL bar had user/pass stripped initially"
+ );
+ callback(tab);
+ }, true);
+}
+
+function openToolbarCustomizationUI(aCallback, aBrowserWin) {
+ if (!aBrowserWin) {
+ aBrowserWin = window;
+ }
+
+ aBrowserWin.gCustomizeMode.enter();
+
+ aBrowserWin.gNavToolbox.addEventListener(
+ "customizationready",
+ function() {
+ executeSoon(function() {
+ aCallback(aBrowserWin);
+ });
+ },
+ { once: true }
+ );
+}
+
+function closeToolbarCustomizationUI(aCallback, aBrowserWin) {
+ aBrowserWin.gNavToolbox.addEventListener(
+ "aftercustomization",
+ function() {
+ executeSoon(aCallback);
+ },
+ { once: true }
+ );
+
+ aBrowserWin.gCustomizeMode.exit();
+}
diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_tooltip.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_tooltip.js
new file mode 100644
index 0000000000..5c182d1b92
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_tooltip.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function synthesizeMouseOver(element) {
+ info("synthesize mouseover");
+ let promise = BrowserTestUtils.waitForEvent(element, "mouseover");
+ EventUtils.synthesizeMouseAtCenter(document.documentElement, {
+ type: "mouseout",
+ });
+ EventUtils.synthesizeMouseAtCenter(element, { type: "mouseover" });
+ EventUtils.synthesizeMouseAtCenter(element, { type: "mousemove" });
+ return promise;
+}
+
+function synthesizeMouseOut(element) {
+ info("synthesize mouseout");
+ let promise = BrowserTestUtils.waitForEvent(element, "mouseout");
+ EventUtils.synthesizeMouseAtCenter(element, { type: "mouseover" });
+ EventUtils.synthesizeMouseAtCenter(element, { type: "mouseout" });
+ EventUtils.synthesizeMouseAtCenter(document.documentElement, {
+ type: "mousemove",
+ });
+ return promise;
+}
+
+async function expectTooltip(text) {
+ if (!gURLBar._overflowing && !gURLBar._inOverflow) {
+ info("waiting for overflow event");
+ await BrowserTestUtils.waitForEvent(gURLBar.inputField, "overflow");
+ }
+
+ let tooltip = document.getElementById("aHTMLTooltip");
+ let element = gURLBar.inputField;
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(tooltip, "popupshown");
+ await synthesizeMouseOver(element);
+ info("awaiting for tooltip popup");
+ await popupShownPromise;
+
+ is(element.getAttribute("title"), text, "title attribute has expected text");
+ is(tooltip.textContent, text, "tooltip shows expected text");
+
+ await synthesizeMouseOut(element);
+}
+
+async function expectNoTooltip() {
+ if (gURLBar._overflowing || gURLBar._inOverflow) {
+ info("waiting for underflow event");
+ await BrowserTestUtils.waitForEvent(gURLBar.inputField, "underflow");
+ }
+
+ let element = gURLBar.inputField;
+ await synthesizeMouseOver(element);
+
+ is(element.getAttribute("title"), null, "title attribute shouldn't be set");
+
+ await synthesizeMouseOut(element);
+}
+
+add_task(async function() {
+ window.windowUtils.disableNonTestMouseEvents(true);
+ registerCleanupFunction(() => {
+ window.windowUtils.disableNonTestMouseEvents(false);
+ });
+
+ // Ensure the URL bar is neither focused nor hovered before we start.
+ gBrowser.selectedBrowser.focus();
+ await synthesizeMouseOut(gURLBar.inputField);
+
+ gURLBar.value = "short string";
+ await expectNoTooltip();
+
+ let longURL = "http://longurl.com/" + "foobar/".repeat(30);
+ gURLBar.value = longURL;
+ is(
+ gURLBar.inputField.value,
+ longURL.replace(/^http:\/\//, ""),
+ "Urlbar value has http:// stripped"
+ );
+ await expectTooltip(longURL);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_trimURLs.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_trimURLs.js
new file mode 100644
index 0000000000..3c1f3ea8e4
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_trimURLs.js
@@ -0,0 +1,127 @@
+add_task(async function() {
+ const PREF_TRIMURLS = "browser.urlbar.trimURLs";
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ registerCleanupFunction(async function() {
+ BrowserTestUtils.removeTab(tab);
+ Services.prefs.clearUserPref(PREF_TRIMURLS);
+ gURLBar.setURI();
+ });
+
+ // Avoid search service sync init warnings due to URIFixup, when running the
+ // test alone.
+ await Services.search.init();
+
+ Services.prefs.setBoolPref(PREF_TRIMURLS, true);
+
+ testVal("http://mozilla.org/", "mozilla.org");
+ testVal("https://mozilla.org/", "https://mozilla.org");
+ testVal("http://mözilla.org/", "mözilla.org");
+ // This isn't a valid public suffix, thus we should untrim it or it would
+ // end up doing a search.
+ testVal("http://mozilla.imaginatory/");
+ testVal("http://www.mozilla.org/", "www.mozilla.org");
+ testVal("http://sub.mozilla.org/", "sub.mozilla.org");
+ testVal("http://sub1.sub2.sub3.mozilla.org/", "sub1.sub2.sub3.mozilla.org");
+ testVal("http://mozilla.org/file.ext", "mozilla.org/file.ext");
+ testVal("http://mozilla.org/sub/", "mozilla.org/sub/");
+
+ testVal("http://ftp.mozilla.org/", "ftp.mozilla.org");
+ testVal("http://ftp1.mozilla.org/", "ftp1.mozilla.org");
+ testVal("http://ftp42.mozilla.org/", "ftp42.mozilla.org");
+ testVal("http://ftpx.mozilla.org/", "ftpx.mozilla.org");
+ testVal("ftp://ftp.mozilla.org/", "ftp://ftp.mozilla.org");
+ testVal("ftp://ftp1.mozilla.org/", "ftp://ftp1.mozilla.org");
+ testVal("ftp://ftp42.mozilla.org/", "ftp://ftp42.mozilla.org");
+ testVal("ftp://ftpx.mozilla.org/", "ftp://ftpx.mozilla.org");
+
+ testVal("https://user:pass@mozilla.org/", "https://user:pass@mozilla.org");
+ testVal("https://user@mozilla.org/", "https://user@mozilla.org");
+ testVal("http://user:pass@mozilla.org/", "user:pass@mozilla.org");
+ testVal("http://user@mozilla.org/", "user@mozilla.org");
+ testVal("http://sub.mozilla.org:666/", "sub.mozilla.org:666");
+
+ testVal("https://[fe80::222:19ff:fe11:8c76]/file.ext");
+ testVal("http://[fe80::222:19ff:fe11:8c76]/", "[fe80::222:19ff:fe11:8c76]");
+ testVal("https://user:pass@[fe80::222:19ff:fe11:8c76]:666/file.ext");
+ testVal(
+ "http://user:pass@[fe80::222:19ff:fe11:8c76]:666/file.ext",
+ "user:pass@[fe80::222:19ff:fe11:8c76]:666/file.ext"
+ );
+
+ testVal("mailto:admin@mozilla.org");
+ testVal("gopher://mozilla.org/");
+ testVal("about:config");
+ testVal("jar:http://mozilla.org/example.jar!/");
+ testVal("view-source:http://mozilla.org/");
+
+ // Behaviour for hosts with no dots depends on the whitelist:
+ let fixupWhitelistPref = "browser.fixup.domainwhitelist.localhost";
+ Services.prefs.setBoolPref(fixupWhitelistPref, false);
+ testVal("http://localhost");
+ Services.prefs.setBoolPref(fixupWhitelistPref, true);
+ testVal("http://localhost", "localhost");
+ Services.prefs.clearUserPref(fixupWhitelistPref);
+
+ testVal("http:// invalid url");
+
+ testVal("http://someotherhostwithnodots");
+
+ // This host is whitelisted, it can be trimmed.
+ testVal("http://localhost/ foo bar baz", "localhost/ foo bar baz");
+
+ // This is not trimmed because it's not in the domain whitelist.
+ testVal(
+ "http://localhost.localdomain/ foo bar baz",
+ "http://localhost.localdomain/ foo bar baz"
+ );
+
+ Services.prefs.setBoolPref(PREF_TRIMURLS, false);
+
+ testVal("http://mozilla.org/");
+
+ Services.prefs.setBoolPref(PREF_TRIMURLS, true);
+
+ let promiseLoaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ "http://example.com/"
+ );
+ BrowserTestUtils.loadURI(gBrowser, "http://example.com/");
+ await promiseLoaded;
+
+ await testCopy("example.com", "http://example.com/");
+
+ gURLBar.setPageProxyState("invalid");
+ gURLBar.valueIsTyped = true;
+ await testCopy("example.com", "example.com");
+});
+
+function testVal(originalValue, targetValue) {
+ gURLBar.value = originalValue;
+ gURLBar.valueIsTyped = false;
+ let trimmedValue = UrlbarPrefs.get("trimURLs")
+ ? BrowserUtils.trimURL(originalValue)
+ : originalValue;
+ Assert.equal(gURLBar.value, trimmedValue, "url bar value set");
+ // Now focus the urlbar and check the inputField value is properly set.
+ gURLBar.focus();
+ Assert.equal(
+ gURLBar.value,
+ targetValue || originalValue,
+ "Check urlbar value on focus"
+ );
+ // On blur we should trim again.
+ gURLBar.blur();
+ Assert.equal(gURLBar.value, trimmedValue, "Check urlbar value on blur");
+}
+
+function testCopy(originalValue, targetValue) {
+ return SimpleTest.promiseClipboardChange(targetValue, () => {
+ Assert.equal(gURLBar.value, originalValue, "url bar copy value set");
+ gURLBar.focus();
+ gURLBar.select();
+ goDoCommand("cmd_copy");
+ });
+}
diff --git a/browser/components/urlbar/tests/browser/browser_aboutHomeLoading.js b/browser/components/urlbar/tests/browser/browser_aboutHomeLoading.js
new file mode 100644
index 0000000000..7ba9ed328c
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_aboutHomeLoading.js
@@ -0,0 +1,189 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This tests ensures the urlbar is cleared properly when about:home is visited.
+ */
+
+"use strict";
+
+const { SessionSaver } = ChromeUtils.import(
+ "resource:///modules/sessionstore/SessionSaver.jsm"
+);
+const { TabStateFlusher } = ChromeUtils.import(
+ "resource:///modules/sessionstore/TabStateFlusher.jsm"
+);
+
+/**
+ * Test what happens if loading a URL that should clear the
+ * location bar after a parent process URL.
+ */
+add_task(async function clearURLBarAfterParentProcessURL() {
+ let tab = await new Promise(resolve => {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "about:preferences"
+ );
+ let newTabBrowser = gBrowser.getBrowserForTab(gBrowser.selectedTab);
+ newTabBrowser.addEventListener(
+ "Initialized",
+ async function() {
+ resolve(gBrowser.selectedTab);
+ },
+ { capture: true, once: true }
+ );
+ });
+ document.getElementById("home-button").click();
+ await BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ HomePage.get()
+ );
+ is(gURLBar.value, "", "URL bar should be empty");
+ is(
+ tab.linkedBrowser.userTypedValue,
+ null,
+ "The browser should have no recorded userTypedValue"
+ );
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * Same as above, but open the tab without passing the URL immediately
+ * which changes behaviour in tabbrowser.xml.
+ */
+add_task(async function clearURLBarAfterParentProcessURLInExistingTab() {
+ let tab = await new Promise(resolve => {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+ let newTabBrowser = gBrowser.getBrowserForTab(gBrowser.selectedTab);
+ newTabBrowser.addEventListener(
+ "Initialized",
+ async function() {
+ resolve(gBrowser.selectedTab);
+ },
+ { capture: true, once: true }
+ );
+ BrowserTestUtils.loadURI(newTabBrowser, "about:preferences");
+ });
+ document.getElementById("home-button").click();
+ await BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ HomePage.get()
+ );
+ is(gURLBar.value, "", "URL bar should be empty");
+ is(
+ tab.linkedBrowser.userTypedValue,
+ null,
+ "The browser should have no recorded userTypedValue"
+ );
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * Load about:home directly from an about:newtab page. Because it is an
+ * 'initial' page, we need to treat this specially if the user actually
+ * loads a page like this from the URL bar.
+ */
+add_task(async function clearURLBarAfterManuallyLoadingAboutHome() {
+ let promiseTabOpenedAndSwitchedTo = BrowserTestUtils.switchTab(
+ gBrowser,
+ () => {}
+ );
+ // This opens about:newtab:
+ BrowserOpenTab();
+ let tab = await promiseTabOpenedAndSwitchedTo;
+ is(gURLBar.value, "", "URL bar should be empty");
+ is(tab.linkedBrowser.userTypedValue, null, "userTypedValue should be null");
+
+ gURLBar.value = "about:home";
+ gURLBar.select();
+ let aboutHomeLoaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ "about:home"
+ );
+ EventUtils.sendKey("return");
+ await aboutHomeLoaded;
+
+ is(gURLBar.value, "", "URL bar should be empty");
+ is(tab.linkedBrowser.userTypedValue, null, "userTypedValue should be null");
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * Ensure we don't show 'about:home' in the URL bar temporarily in new tabs
+ * while we're switching remoteness (when the URL we're loading and the
+ * default content principal are different).
+ */
+add_task(async function dontTemporarilyShowAboutHome() {
+ requestLongerTimeout(2);
+
+ await SpecialPowers.pushPrefEnv({ set: [["browser.startup.page", 1]] });
+ let windowOpenedPromise = BrowserTestUtils.waitForNewWindow();
+ let win = OpenBrowserWindow();
+ await windowOpenedPromise;
+ let promiseTabSwitch = BrowserTestUtils.switchTab(win.gBrowser, () => {});
+ win.BrowserOpenTab();
+ await promiseTabSwitch;
+ is(win.gBrowser.visibleTabs.length, 2, "2 tabs opened");
+ await TabStateFlusher.flush(win.gBrowser.selectedBrowser);
+ await BrowserTestUtils.closeWindow(win);
+ ok(SessionStore.getClosedWindowCount(), "Should have a closed window");
+
+ await SessionSaver.run();
+
+ windowOpenedPromise = BrowserTestUtils.waitForNewWindow();
+ win = SessionStore.undoCloseWindow(0);
+ await windowOpenedPromise;
+ let wpl = {
+ onLocationChange() {
+ is(win.gURLBar.value, "", "URL bar value should stay empty.");
+ },
+ };
+ win.gBrowser.addProgressListener(wpl);
+
+ if (win.gBrowser.visibleTabs.length < 2) {
+ await BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "TabOpen");
+ }
+ let otherTab = win.gBrowser.selectedTab.previousElementSibling;
+ let tabLoaded = BrowserTestUtils.browserLoaded(
+ otherTab.linkedBrowser,
+ false,
+ "about:home"
+ );
+ await BrowserTestUtils.switchTab(win.gBrowser, otherTab);
+ await tabLoaded;
+ win.gBrowser.removeProgressListener(wpl);
+ is(win.gURLBar.value, "", "URL bar value should be empty.");
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Test that if the Home Button is clicked after a user has typed
+ * some value into the URL bar, that the URL bar is cleared if
+ * the homepage is one of the initial pages set.
+ */
+add_task(async function() {
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "http://example.com",
+ gBrowser,
+ },
+ async browser => {
+ const TYPED_VALUE = "This string should get cleared";
+ gURLBar.value = TYPED_VALUE;
+ browser.userTypedValue = TYPED_VALUE;
+
+ document.getElementById("home-button").click();
+ await BrowserTestUtils.browserLoaded(browser, false, HomePage.get());
+ is(gURLBar.value, "", "URL bar should be empty");
+ is(
+ browser.userTypedValue,
+ null,
+ "The browser should have no recorded userTypedValue"
+ );
+ }
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_action_searchengine.js b/browser/components/urlbar/tests/browser/browser_action_searchengine.js
new file mode 100644
index 0000000000..68bef141e5
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_action_searchengine.js
@@ -0,0 +1,127 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that a search result has the correct attributes and visits the
+ * expected URL for the engine.
+ */
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.search.separatePrivateDefault.ui.enabled", true],
+ ["browser.search.separatePrivateDefault", false],
+ ],
+ });
+
+ const engine = await Services.search.addEngineWithDetails("MozSearch", {
+ method: "GET",
+ template: "http://example.com/?q={searchTerms}",
+ });
+ const engine2 = await Services.search.addEngineWithDetails(
+ "MozSearchPrivate",
+ {
+ method: "GET",
+ template: "http://example.com/private?q={searchTerms}",
+ }
+ );
+ let originalEngine = await Services.search.getDefault();
+ await Services.search.setDefault(engine);
+
+ registerCleanupFunction(async function() {
+ await Services.search.setDefault(originalEngine);
+ await Services.search.removeEngine(engine);
+ await Services.search.removeEngine(engine2);
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.formHistory.clear();
+ });
+});
+
+async function testSearch(win, expectedName, expectedBaseUrl) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "open a search",
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(win, 0);
+
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "Should have type search"
+ );
+ Assert.deepEqual(
+ result.searchParams,
+ {
+ engine: expectedName,
+ keyword: undefined,
+ query: "open a search",
+ suggestion: undefined,
+ inPrivateWindow: undefined,
+ isPrivateEngine: undefined,
+ },
+ "Should have the correct result parameters."
+ );
+
+ Assert.equal(
+ result.image,
+ UrlbarUtils.ICON.SEARCH_GLASS,
+ "Should have the search icon image"
+ );
+
+ let tabPromise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ let element = await UrlbarTestUtils.waitForAutocompleteResultAt(win, 0);
+ EventUtils.synthesizeMouseAtCenter(element, {}, win);
+ await tabPromise;
+
+ Assert.equal(
+ win.gBrowser.selectedBrowser.currentURI.spec,
+ expectedBaseUrl + "?q=open+a+search",
+ "Should have loaded the correct page"
+ );
+}
+
+add_task(async function test_search_normal_window() {
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:mozilla"
+ );
+
+ registerCleanupFunction(async function() {
+ try {
+ BrowserTestUtils.removeTab(tab);
+ } catch (ex) {
+ /* tab may have already been closed in case of failure */
+ }
+ });
+
+ await testSearch(window, "MozSearch", "http://example.com/");
+});
+
+add_task(async function test_search_private_window_no_separate_default() {
+ const win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+
+ registerCleanupFunction(async function() {
+ await BrowserTestUtils.closeWindow(win);
+ });
+
+ await testSearch(win, "MozSearch", "http://example.com/");
+});
+
+add_task(async function test_search_private_window() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.separatePrivateDefault", true]],
+ });
+
+ let engine = Services.search.getEngineByName("MozSearchPrivate");
+ let originalEngine = await Services.search.getDefaultPrivate();
+ await Services.search.setDefaultPrivate(engine);
+
+ registerCleanupFunction(async () => {
+ await BrowserTestUtils.closeWindow(win);
+ await Services.search.setDefaultPrivate(originalEngine);
+ });
+
+ const win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+
+ await testSearch(win, "MozSearchPrivate", "http://example.com/private");
+});
diff --git a/browser/components/urlbar/tests/browser/browser_action_searchengine_alias.js b/browser/components/urlbar/tests/browser/browser_action_searchengine_alias.js
new file mode 100644
index 0000000000..b399b66450
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_action_searchengine_alias.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that search result obtained using a search keyword gives an entry with
+ * the correct attributes and visits the expected URL for the engine.
+ */
+
+add_task(async function() {
+ const ICON_URI =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAA" +
+ "CQkWg2AAABGklEQVQoz2NgGB6AnZ1dUlJSXl4eSDIyMhLW4Ovr%2B%2Fr168uXL69Zs4YoG%2BL" +
+ "i4i5dusTExMTGxsbNzd3f37937976%2BnpmZmagbHR09J49e5YvX66kpATVEBYW9ubNm2nTphkb" +
+ "G7e2tp44cQLIuHfvXm5urpaWFlDKysqqu7v73LlzECMYIiIiHj58mJCQoKKicvXq1bS0NKBgW1v" +
+ "bjh074uPjgeqAXE1NzSdPnvDz84M0AEUvXLgAsW379u1z5swBen3jxo2zZ892cHB4%2BvQp0KlA" +
+ "fwI1cHJyghQFBwfv2rULokFXV%2FfixYu7d%2B8GGqGgoMDKyrpu3br9%2B%2FcDuXl5eVA%2FA" +
+ "EWBfoWHAdAYoNuAYQ0XAeoUERFhGDYAAPoUaT2dfWJuAAAAAElFTkSuQmCC";
+ await Services.search.addEngineWithDetails("MozSearch", {
+ iconURL: ICON_URI,
+ alias: "moz",
+ method: "GET",
+ template: "http://example.com/?q={searchTerms}",
+ });
+ let engine = Services.search.getEngineByName("MozSearch");
+ let originalEngine = await Services.search.getDefault();
+ await Services.search.setDefault(engine);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:mozilla"
+ );
+
+ // Disable autofill so mozilla.org isn't autofilled below.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.autoFill", false]],
+ });
+
+ registerCleanupFunction(async function() {
+ await Services.search.setDefault(originalEngine);
+ await Services.search.removeEngine(engine);
+ try {
+ BrowserTestUtils.removeTab(tab);
+ } catch (ex) {
+ /* tab may have already been closed in case of failure */
+ }
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.formHistory.clear();
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "moz",
+ });
+ Assert.equal(gURLBar.value, "moz", "Value should be unchanged");
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "moz open a search",
+ });
+ // Wait for the second new search that starts when search mode is entered.
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: engine.name,
+ entry: "typed",
+ });
+ Assert.equal(gURLBar.value, "open a search", "value should be query");
+
+ let tabPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await tabPromise;
+
+ Assert.equal(
+ gBrowser.selectedBrowser.currentURI.spec,
+ "http://example.com/?q=open+a+search",
+ "Should have loaded the correct page"
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_backspaced.js b/browser/components/urlbar/tests/browser/browser_autoFill_backspaced.js
new file mode 100644
index 0000000000..92da82dfaf
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_autoFill_backspaced.js
@@ -0,0 +1,262 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* This test ensures that backspacing autoFilled values still allows to
+ * confirm the remaining value.
+ */
+
+"use strict";
+
+async function test_autocomplete(data) {
+ let { desc, typed, autofilled, modified, keys, type, onAutoFill } = data;
+ info(desc);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: typed,
+ fireInputEvent: true,
+ });
+ Assert.equal(gURLBar.value, autofilled, "autofilled value is as expected");
+ if (onAutoFill) {
+ onAutoFill();
+ }
+
+ info("Synthesizing keys");
+ for (let key of keys) {
+ let args = Array.isArray(key) ? key : [key];
+ EventUtils.synthesizeKey(...args);
+ }
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ Assert.equal(gURLBar.value, modified, "backspaced value is as expected");
+
+ Assert.greater(
+ UrlbarTestUtils.getResultCount(window),
+ 0,
+ "Should get at least 1 result"
+ );
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+
+ Assert.equal(result.type, type, "Should have the correct result type");
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ gURLBar.blur();
+}
+
+add_task(async function() {
+ registerCleanupFunction(async function() {
+ Services.prefs.clearUserPref("browser.urlbar.autoFill");
+ gURLBar.handleRevert();
+ await PlacesUtils.history.clear();
+ });
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", true);
+
+ await PlacesTestUtils.addVisits([
+ "http://example.com/",
+ "http://example.com/foo",
+ ]);
+
+ await test_autocomplete({
+ desc: "DELETE the autofilled part should search",
+ typed: "exam",
+ autofilled: "example.com/",
+ modified: "exam",
+ keys: ["KEY_Delete"],
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ });
+ await test_autocomplete({
+ desc: "DELETE the final slash should visit",
+ typed: "example.com",
+ autofilled: "example.com/",
+ modified: "example.com",
+ keys: ["KEY_Delete"],
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ });
+
+ await test_autocomplete({
+ desc: "BACK_SPACE the autofilled part should search",
+ typed: "exam",
+ autofilled: "example.com/",
+ modified: "exam",
+ keys: ["KEY_Backspace"],
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ });
+ await test_autocomplete({
+ desc: "BACK_SPACE the final slash should visit",
+ typed: "example.com",
+ autofilled: "example.com/",
+ modified: "example.com",
+ keys: ["KEY_Backspace"],
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ });
+
+ await test_autocomplete({
+ desc: "DELETE the autofilled part, then BACK_SPACE, should search",
+ typed: "exam",
+ autofilled: "example.com/",
+ modified: "exa",
+ keys: ["KEY_Delete", "KEY_Backspace"],
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ });
+ await test_autocomplete({
+ desc: "DELETE the final slash, then BACK_SPACE, should search",
+ typed: "example.com",
+ autofilled: "example.com/",
+ modified: "example.co",
+ keys: ["KEY_Delete", "KEY_Backspace"],
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ });
+
+ await test_autocomplete({
+ desc: "BACK_SPACE the autofilled part, then BACK_SPACE, should search",
+ typed: "exam",
+ autofilled: "example.com/",
+ modified: "exa",
+ keys: ["KEY_Backspace", "KEY_Backspace"],
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ });
+ await test_autocomplete({
+ desc: "BACK_SPACE the final slash, then BACK_SPACE, should search",
+ typed: "example.com",
+ autofilled: "example.com/",
+ modified: "example.co",
+ keys: ["KEY_Backspace", "KEY_Backspace"],
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ });
+
+ await test_autocomplete({
+ desc: "BACK_SPACE after blur should search",
+ typed: "ex",
+ autofilled: "example.com/",
+ modified: "e",
+ keys: ["KEY_Backspace"],
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ onAutoFill: () => {
+ gURLBar.blur();
+ gURLBar.focus();
+ Assert.equal(
+ gURLBar.selectionStart,
+ gURLBar.value.length,
+ "blur/focus should not change selection"
+ );
+ Assert.equal(
+ gURLBar.selectionEnd,
+ gURLBar.value.length,
+ "blur/focus should not change selection"
+ );
+ },
+ });
+ await test_autocomplete({
+ desc: "DELETE after blur should search",
+ typed: "ex",
+ autofilled: "example.com/",
+ modified: "e",
+ keys: ["KEY_ArrowLeft", "KEY_Delete"],
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ onAutoFill: () => {
+ gURLBar.blur();
+ gURLBar.focus();
+ Assert.equal(
+ gURLBar.selectionStart,
+ gURLBar.value.length,
+ "blur/focus should not change selection"
+ );
+ Assert.equal(
+ gURLBar.selectionEnd,
+ gURLBar.value.length,
+ "blur/focus should not change selection"
+ );
+ },
+ });
+ await test_autocomplete({
+ desc: "double BACK_SPACE after blur should search",
+ typed: "exa",
+ autofilled: "example.com/",
+ modified: "e",
+ keys: ["KEY_Backspace", "KEY_Backspace"],
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ onAutoFill: () => {
+ gURLBar.blur();
+ gURLBar.focus();
+ Assert.equal(
+ gURLBar.selectionStart,
+ gURLBar.value.length,
+ "blur/focus should not change selection"
+ );
+ Assert.equal(
+ gURLBar.selectionEnd,
+ gURLBar.value.length,
+ "blur/focus should not change selection"
+ );
+ },
+ });
+
+ await test_autocomplete({
+ desc:
+ "Right arrow key and then backspace should delete the backslash and not re-trigger autofill",
+ typed: "ex",
+ autofilled: "example.com/",
+ modified: "example.com",
+ keys: ["KEY_ArrowRight", "KEY_Backspace"],
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ });
+
+ await test_autocomplete({
+ desc:
+ "Right arrow key, selecting the last few characters using the keyboard, and then backspace should delete the characters and not re-trigger autofill",
+ typed: "ex",
+ autofilled: "example.com/",
+ modified: "example.c",
+ keys: [
+ "KEY_ArrowRight",
+ ["KEY_ArrowLeft", { shiftKey: true }],
+ ["KEY_ArrowLeft", { shiftKey: true }],
+ ["KEY_ArrowLeft", { shiftKey: true }],
+ "KEY_Backspace",
+ ],
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ });
+
+ await test_autocomplete({
+ desc:
+ "End and then backspace should delete the backslash and not re-trigger autofill",
+ typed: "ex",
+ autofilled: "example.com/",
+ modified: "example.com",
+ keys: [
+ AppConstants.platform == "macosx"
+ ? ["KEY_ArrowRight", { metaKey: true }]
+ : "KEY_End",
+ "KEY_Backspace",
+ ],
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ });
+
+ await test_autocomplete({
+ desc:
+ "Clicking in the input after the text and then backspace should delete the backslash and not re-trigger autofill",
+ typed: "ex",
+ autofilled: "example.com/",
+ modified: "example.com",
+ keys: ["KEY_Backspace"],
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ onAutoFill: () => {
+ // This assumes that the center of the input is to the right of the end
+ // of the text, so the caret is placed at the end of the text on click.
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ },
+ });
+
+ await test_autocomplete({
+ desc:
+ "Selecting the next result and then backspace should delete the last character and not re-trigger autofill",
+ typed: "ex",
+ autofilled: "example.com/",
+ modified: "example.com/fo",
+ keys: ["KEY_ArrowDown", "KEY_Backspace"],
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ });
+
+ await PlacesUtils.history.clear();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_canonize.js b/browser/components/urlbar/tests/browser/browser_autoFill_canonize.js
new file mode 100644
index 0000000000..099fc90417
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_autoFill_canonize.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* This test ensures that pressing ctrl+enter bypasses the autoFilled
+ * value, and only considers what the user typed (but not just enter).
+ */
+
+async function test_autocomplete(data) {
+ let { desc, typed, autofilled, modified, waitForUrl, keys } = data;
+ info(desc);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: typed,
+ });
+ Assert.equal(gURLBar.value, autofilled, "autofilled value is as expected");
+
+ let promiseLoad = BrowserTestUtils.waitForDocLoadAndStopIt(
+ waitForUrl,
+ gBrowser.selectedBrowser
+ );
+
+ keys.forEach(([key, mods]) => EventUtils.synthesizeKey(key, mods));
+
+ Assert.equal(gURLBar.value, modified, "value is as expected");
+
+ await promiseLoad;
+ gURLBar.blur();
+}
+
+add_task(async function() {
+ registerCleanupFunction(async function() {
+ Services.prefs.clearUserPref("browser.urlbar.autoFill");
+ gURLBar.handleRevert();
+ await PlacesUtils.history.clear();
+ });
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", true);
+
+ // Add a typed visit, so it will be autofilled.
+ await PlacesTestUtils.addVisits({
+ uri: "http://example.com/",
+ transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ });
+
+ await test_autocomplete({
+ desc: "CTRL+ENTER on the autofilled part should use autofill",
+ typed: "exam",
+ autofilled: "example.com/",
+ modified: "example.com",
+ waitForUrl: "http://example.com/",
+ keys: [["KEY_Enter"]],
+ });
+
+ await test_autocomplete({
+ desc: "CTRL+ENTER on the autofilled part should bypass autofill",
+ typed: "exam",
+ autofilled: "example.com/",
+ modified: "https://www.exam.com",
+ waitForUrl: "https://www.exam.com/",
+ keys: [["KEY_Enter", { ctrlKey: true }]],
+ });
+});
diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_caretNotAtEnd.js b/browser/components/urlbar/tests/browser/browser_autoFill_caretNotAtEnd.js
new file mode 100644
index 0000000000..570a1c2c8c
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_autoFill_caretNotAtEnd.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function noAutofillWhenCaretNotAtEnd() {
+ gURLBar.focus();
+
+ // Add a visit that can be autofilled.
+ await PlacesUtils.history.clear();
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com/",
+ },
+ ]);
+
+ // Fill the input with xample.
+ gURLBar.inputField.value = "xample";
+
+ // Move the caret to the beginning and type e.
+ gURLBar.selectionStart = 0;
+ gURLBar.selectionEnd = 0;
+ EventUtils.sendString("e");
+
+ // Check the first result and input.
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(!result.autofill, "The first result should not be autofill");
+
+ Assert.equal(gURLBar.value, "example");
+ Assert.equal(gURLBar.selectionStart, 1);
+ Assert.equal(gURLBar.selectionEnd, 1);
+
+ await PlacesUtils.history.clear();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_firstResult.js b/browser/components/urlbar/tests/browser/browser_autoFill_firstResult.js
new file mode 100644
index 0000000000..a425ddd4a7
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_autoFill_firstResult.js
@@ -0,0 +1,199 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test makes sure that autofilling the first result of a new search works
+// correctly: autofill happens when it should and doesn't when it shouldn't.
+
+"use strict";
+
+add_task(async function init() {
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+ await PlacesTestUtils.addVisits(["http://example.com/"]);
+
+ // Disable placeholder completion. The point of this test is to make sure the
+ // first result is autofilled (or not) correctly. Autofilling the placeholder
+ // before the search starts interferes with that.
+ gURLBar._enableAutofillPlaceholder = false;
+ registerCleanupFunction(async () => {
+ gURLBar._enableAutofillPlaceholder = true;
+ });
+});
+
+// The first result should be autofilled when all conditions are met. This also
+// does a sanity check to make sure that placeholder autofill is correctly
+// disabled, which is helpful for all tasks here and is why this one is first.
+add_task(async function successfulAutofill() {
+ // Do a simple search that should autofill. This will also set up the
+ // autofill placeholder string, which next we make sure is *not* autofilled.
+ await doInitialAutofillSearch();
+
+ // As a sanity check, do another search to make sure the placeholder is *not*
+ // autofilled. Make sure it's not autofilled by checking the input value and
+ // selection *before* the search completes. If placeholder autofill was not
+ // correctly disabled, then these assertions will fail.
+
+ gURLBar.value = "exa";
+ UrlbarTestUtils.fireInputEvent(window);
+
+ // before the search completes: no autofill
+ Assert.equal(gURLBar.value, "exa");
+ Assert.equal(gURLBar.selectionStart, "exa".length);
+ Assert.equal(gURLBar.selectionEnd, "exa".length);
+
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ // after the search completes: successful autofill
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(gURLBar.value, "example.com/");
+ Assert.equal(gURLBar.selectionStart, "exa".length);
+ Assert.equal(gURLBar.selectionEnd, "example.com/".length);
+});
+
+// The first result should not be autofilled when it's not an autofill result.
+add_task(async function firstResultNotAutofill() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ fireInputEvent: true,
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(!details.autofill);
+ Assert.equal(gURLBar.value, "foo");
+ Assert.equal(gURLBar.selectionStart, "foo".length);
+ Assert.equal(gURLBar.selectionEnd, "foo".length);
+});
+
+// The first result should *not* be autofilled when the placeholder is not
+// selected, the selection is empty, and the caret is *not* at the end of the
+// search string.
+add_task(async function caretNotAtEndOfSearchString() {
+ // To set up the placeholder, do an initial search that triggers autofill.
+ await doInitialAutofillSearch();
+
+ // Now do another search but set the caret to somewhere else besides the end
+ // of the new search string.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ selectionStart: "exa".length,
+ selectionEnd: "exa".length,
+ });
+
+ // The first result should be an autofill result, but it should not have been
+ // autofilled.
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(gURLBar.value, "exam");
+ Assert.equal(gURLBar.selectionStart, "exa".length);
+ Assert.equal(gURLBar.selectionEnd, "exa".length);
+
+ await cleanUp();
+});
+
+// The first result should *not* be autofilled when the placeholder is not
+// selected, the selection is *not* empty, and the caret is at the end of the
+// search string.
+add_task(async function selectionNotEmpty() {
+ // To set up the placeholder, do an initial search that triggers autofill.
+ await doInitialAutofillSearch();
+
+ // Now do another search. Set the selection end at the end of the search
+ // string, but make the selection non-empty.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ selectionStart: "exa".length,
+ selectionEnd: "exam".length,
+ });
+
+ // The first result should be an autofill result, but it should not have been
+ // autofilled.
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(gURLBar.value, "exam");
+ Assert.equal(gURLBar.selectionStart, "exa".length);
+ Assert.equal(gURLBar.selectionEnd, "exam".length);
+
+ await cleanUp();
+});
+
+// The first result should be autofilled when the placeholder is not selected,
+// the selection is empty, and the caret is at the end of the search string.
+add_task(async function successfulAutofillAfterSettingPlaceholder() {
+ // To set up the placeholder, do an initial search that triggers autofill.
+ await doInitialAutofillSearch();
+
+ // Now do another search.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ selectionStart: "exam".length,
+ selectionEnd: "exam".length,
+ });
+
+ // It should be autofilled.
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(gURLBar.value, "example.com/");
+ Assert.equal(gURLBar.selectionStart, "exam".length);
+ Assert.equal(gURLBar.selectionEnd, "example.com/".length);
+
+ await cleanUp();
+});
+
+// The first result should be autofilled when the placeholder *is* selected --
+// more precisely, when the portion of the placeholder after the new search
+// string is selected.
+add_task(async function successfulAutofillPlaceholderSelected() {
+ // To set up the placeholder, do an initial search that triggers autofill.
+ await doInitialAutofillSearch();
+
+ // Now do another search and select the portion of the placeholder after the
+ // new search string.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ selectionStart: "exam".length,
+ selectionEnd: "example.com/".length,
+ });
+
+ // It should be autofilled.
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(gURLBar.value, "example.com/");
+ Assert.equal(gURLBar.selectionStart, "exam".length);
+ Assert.equal(gURLBar.selectionEnd, "example.com/".length);
+
+ await cleanUp();
+});
+
+async function doInitialAutofillSearch() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "ex",
+ fireInputEvent: true,
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(gURLBar.value, "example.com/");
+ Assert.equal(gURLBar.selectionStart, "ex".length);
+ Assert.equal(gURLBar.selectionEnd, "example.com/".length);
+}
+
+async function cleanUp() {
+ // In some cases above, a test task searches for "exam" at the end, and then
+ // the next task searches for "ex". Autofill results will not be allowed in
+ // the next task in that case since the old search string starts with the new
+ // search string. To prevent one task from interfering with the next, do a
+ // search that changes the search string. Also close the popup while we're
+ // here, although that's not really necessary.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "reset last search string",
+ });
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeKey("KEY_Escape");
+ });
+}
diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_paste.js b/browser/components/urlbar/tests/browser/browser_autoFill_paste.js
new file mode 100644
index 0000000000..47d92cb7d3
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_autoFill_paste.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test checks we don't autofill on paste.
+
+"use strict";
+
+add_task(async function test() {
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+ await PlacesTestUtils.addVisits(["http://example.com/"]);
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ });
+
+ // Search for "e". It should autofill to example.com/.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "e",
+ fireInputEvent: true,
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(gURLBar.value, "example.com/");
+ Assert.equal(gURLBar.selectionStart, "e".length);
+ Assert.equal(gURLBar.selectionEnd, "example.com/".length);
+
+ // Now paste.
+ await selectAndPaste("ex");
+
+ // Nothing should have been autofilled.
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(!details.autofill);
+ Assert.equal(gURLBar.value, "ex");
+ Assert.equal(gURLBar.selectionStart, "ex".length);
+ Assert.equal(gURLBar.selectionEnd, "ex".length);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_placeholder.js b/browser/components/urlbar/tests/browser/browser_autoFill_placeholder.js
new file mode 100644
index 0000000000..7da2230ad1
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_autoFill_placeholder.js
@@ -0,0 +1,257 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test makes sure that the autofill placeholder value is autofilled
+// correctly. The placeholder is a string that we immediately autofill when a
+// search starts and before its first result arrives in order to prevent text
+// flicker in the input.
+//
+// Because this test specifically checks autofill *before* searches complete, we
+// can't use promiseAutocompleteResultPopup() or other helpers that wait for
+// searches to complete. Instead the test uses fireInputEvent() to trigger
+// placeholder autofill and then immediately checks autofill status.
+
+"use strict";
+
+add_task(async function init() {
+ await cleanUp();
+});
+
+add_task(async function origin() {
+ await PlacesTestUtils.addVisits("http://example.com/");
+
+ // Do an initial search that triggers autofill so that the placeholder has an
+ // initial value.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "ex",
+ fireInputEvent: true,
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(gURLBar.value, "example.com/");
+ Assert.equal(gURLBar.selectionStart, "ex".length);
+ Assert.equal(gURLBar.selectionEnd, "example.com/".length);
+
+ await searchAndCheck("exa", "example.com/");
+ await searchAndCheck("EXAM", "EXAMple.com/");
+ await searchAndCheck("eXaMp", "eXaMple.com/");
+ await searchAndCheck("exampl", "example.com/");
+
+ await cleanUp();
+});
+
+add_task(async function tokenAlias() {
+ // We have built-in engine aliases that may conflict with the one we choose
+ // here in terms of autofill, so be careful and choose a weird alias.
+ await Services.search.addEngineWithDetails("Test", {
+ alias: "@__example",
+ template: "http://example.com/?search={searchTerms}",
+ });
+ registerCleanupFunction(async function() {
+ let engine = Services.search.getEngineByName("Test");
+ await Services.search.removeEngine(engine);
+ });
+
+ // Do an initial search that triggers autofill so that the placeholder has an
+ // initial value.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@__ex",
+ fireInputEvent: true,
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(gURLBar.value, "@__example ");
+ Assert.equal(gURLBar.selectionStart, "@__ex".length);
+ Assert.equal(gURLBar.selectionEnd, "@__example ".length);
+
+ await searchAndCheck("@__exa", "@__example ");
+ await searchAndCheck("@__EXAM", "@__EXAMple ");
+ await searchAndCheck("@__eXaMp", "@__eXaMple ");
+ await searchAndCheck("@__exampl", "@__example ");
+
+ await cleanUp();
+});
+
+add_task(async function noMatch1() {
+ await PlacesTestUtils.addVisits("http://example.com/");
+
+ // Do an initial search that triggers autofill so that the placeholder has an
+ // initial value.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "ex",
+ fireInputEvent: true,
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(gURLBar.value, "example.com/");
+ Assert.equal(gURLBar.selectionStart, "ex".length);
+ Assert.equal(gURLBar.selectionEnd, "example.com/".length);
+
+ // Search with a string that does not match the placeholder. Placeholder
+ // autofill shouldn't happen.
+ gURLBar.value = "moz";
+ UrlbarTestUtils.fireInputEvent(window);
+ Assert.equal(gURLBar.value, "moz");
+ Assert.equal(gURLBar.selectionStart, "moz".length);
+ Assert.equal(gURLBar.selectionEnd, "moz".length);
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ // Search for "ex" again. It should be autofilled. Placeholder autofill
+ // won't happen. It's not important for this test to check that.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "ex",
+ fireInputEvent: true,
+ });
+ details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(gURLBar.value, "example.com/");
+ Assert.equal(gURLBar.selectionStart, "ex".length);
+ Assert.equal(gURLBar.selectionEnd, "example.com/".length);
+
+ // Placeholder autofill should work again for example.com searches.
+ await searchAndCheck("exa", "example.com/");
+ await searchAndCheck("EXAM", "EXAMple.com/");
+ await searchAndCheck("eXaMp", "eXaMple.com/");
+ await searchAndCheck("exampl", "example.com/");
+
+ await cleanUp();
+});
+
+add_task(async function noMatch2() {
+ await PlacesTestUtils.addVisits([
+ "http://mozilla.org/",
+ "http://example.com/",
+ ]);
+
+ // Do an initial search that triggers autofill so that the placeholder has an
+ // initial value.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "moz",
+ fireInputEvent: true,
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(gURLBar.value, "mozilla.org/");
+ Assert.equal(gURLBar.selectionStart, "moz".length);
+ Assert.equal(gURLBar.selectionEnd, "mozilla.org/".length);
+
+ // Search with a string that does not match the placeholder but does trigger
+ // autofill. Placeholder autofill shouldn't happen.
+ gURLBar.value = "ex";
+ UrlbarTestUtils.fireInputEvent(window);
+ Assert.equal(gURLBar.value, "ex");
+ Assert.equal(gURLBar.selectionStart, "ex".length);
+ Assert.equal(gURLBar.selectionEnd, "ex".length);
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ Assert.equal(gURLBar.value, "example.com/");
+ Assert.equal(gURLBar.selectionStart, "ex".length);
+ Assert.equal(gURLBar.selectionEnd, "example.com/".length);
+
+ // Do some searches that should trigger placeholder autofill.
+ await searchAndCheck("exa", "example.com/");
+ await searchAndCheck("EXAm", "EXAmple.com/");
+
+ // Search for "moz" again. It should be autofilled. Placeholder autofill
+ // shouldn't happen.
+ gURLBar.value = "moz";
+ UrlbarTestUtils.fireInputEvent(window);
+ Assert.equal(gURLBar.value, "moz");
+ Assert.equal(gURLBar.selectionStart, "moz".length);
+ Assert.equal(gURLBar.selectionEnd, "moz".length);
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ Assert.equal(gURLBar.value, "mozilla.org/");
+ Assert.equal(gURLBar.selectionStart, "moz".length);
+ Assert.equal(gURLBar.selectionEnd, "mozilla.org/".length);
+
+ // Do some searches that should trigger placeholder autofill.
+ await searchAndCheck("mozi", "mozilla.org/");
+ await searchAndCheck("MOZil", "MOZilla.org/");
+
+ await cleanUp();
+});
+
+add_task(async function clear_placeholder_for_keyword_or_alias() {
+ info("Clear the autofill placeholder if a keyword is typed");
+ await PlacesTestUtils.addVisits("http://example.com/");
+ await PlacesUtils.keywords.insert({
+ keyword: "ex",
+ url: "http://somekeyword.com/",
+ });
+ let engine = await Services.search.addEngineWithDetails("AutofillTest", {
+ alias: "exam",
+ template: "http://example.com/?search={searchTerms}",
+ });
+ registerCleanupFunction(async function() {
+ await PlacesUtils.keywords.remove("ex");
+ await Services.search.removeEngine(engine);
+ });
+
+ // Do an initial search that triggers autofill so that the placeholder has an
+ // initial value.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "e",
+ fireInputEvent: true,
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(gURLBar.value, "example.com/");
+ Assert.equal(gURLBar.selectionStart, "e".length);
+ Assert.equal(gURLBar.selectionEnd, "example.com/".length);
+
+ // The values are initially autofilled on input, then the placeholder is
+ // removed when the first non-autofill result arrives.
+
+ // Matches the keyword.
+ await searchAndCheck("ex", "example.com/", "ex");
+ await searchAndCheck("EXA", "EXAmple.com/", "EXAmple.com/");
+ // Matches the alias.
+
+ await searchAndCheck("eXaM", "eXaMple.com/", "eXaMple.com/");
+ await searchAndCheck("examp", "example.com/", "example.com/");
+
+ await cleanUp();
+});
+
+async function searchAndCheck(
+ searchString,
+ expectedAutofillValue,
+ onCompleteValue = ""
+) {
+ gURLBar.value = searchString;
+
+ // Placeholder autofill is done on input, so fire an input event. As the
+ // comment at the top of this file says, we can't use
+ // promiseAutocompleteResultPopup() or other helpers that wait for searches to
+ // complete because we are specifically checking autofill before the search
+ // completes.
+ UrlbarTestUtils.fireInputEvent(window);
+
+ // Check the input value and selection immediately, before waiting on the
+ // search to complete.
+ Assert.equal(gURLBar.value, expectedAutofillValue);
+ Assert.equal(gURLBar.selectionStart, searchString.length);
+ Assert.equal(gURLBar.selectionEnd, expectedAutofillValue.length);
+
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ if (onCompleteValue) {
+ // Check the final value after the results arrived.
+ Assert.equal(gURLBar.value, onCompleteValue);
+ Assert.equal(gURLBar.selectionStart, searchString.length);
+ Assert.equal(gURLBar.selectionEnd, onCompleteValue.length);
+ }
+}
+
+async function cleanUp() {
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeKey("KEY_Escape");
+ });
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+}
diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_preserve.js b/browser/components/urlbar/tests/browser/browser_autoFill_preserve.js
new file mode 100644
index 0000000000..bfd7f6e20e
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_autoFill_preserve.js
@@ -0,0 +1,260 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test makes sure that a few of aspects of autofill are correctly
+// preserved:
+//
+// * Autofill should preserve the user's case. If you type ExA, it should be
+// autofilled to ExAmple.com/, not example.com/.
+// * When you key down and then back up to the autofill result, autofill should
+// be restored, with the text selection and the user's case both preserved.
+// * When you key down/up so that no result is selected, the value that the
+// user typed to trigger autofill should be restored in the input.
+
+"use strict";
+
+add_task(async function init() {
+ await cleanUp();
+});
+
+add_task(async function origin() {
+ await PlacesTestUtils.addVisits([
+ "http://example.com/",
+ "http://mozilla.org/example",
+ ]);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "ExA",
+ fireInputEvent: true,
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(gURLBar.value, "ExAmple.com/");
+ Assert.equal(gURLBar.selectionStart, "ExA".length);
+ Assert.equal(gURLBar.selectionEnd, "ExAmple.com/".length);
+ checkKeys([
+ ["KEY_ArrowDown", "http://mozilla.org/example", 1],
+ ["KEY_ArrowDown", "ExA", -1],
+ ["KEY_ArrowUp", "http://mozilla.org/example", 1],
+ ["KEY_ArrowUp", "ExAmple.com/", 0],
+ ["KEY_ArrowUp", "ExA", -1],
+ ["KEY_ArrowDown", "ExAmple.com/", 0],
+ ]);
+ await cleanUp();
+});
+
+add_task(async function originPort() {
+ await PlacesTestUtils.addVisits([
+ "http://example.com:8888/",
+ "http://mozilla.org/example",
+ ]);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "ExA",
+ fireInputEvent: true,
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(gURLBar.value, "ExAmple.com:8888/");
+ Assert.equal(gURLBar.selectionStart, "ExA".length);
+ Assert.equal(gURLBar.selectionEnd, "ExAmple.com:8888/".length);
+ checkKeys([
+ ["KEY_ArrowDown", "http://mozilla.org/example", 1],
+ ["KEY_ArrowDown", "ExA", -1],
+ ["KEY_ArrowUp", "http://mozilla.org/example", 1],
+ ["KEY_ArrowUp", "ExAmple.com:8888/", 0],
+ ["KEY_ArrowUp", "ExA", -1],
+ ["KEY_ArrowDown", "ExAmple.com:8888/", 0],
+ ]);
+ await cleanUp();
+});
+
+add_task(async function originScheme() {
+ await PlacesTestUtils.addVisits([
+ "http://example.com/",
+ "http://mozilla.org/example",
+ ]);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "http://ExA",
+ fireInputEvent: true,
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(gURLBar.value, "http://ExAmple.com/");
+ Assert.equal(gURLBar.selectionStart, "http://ExA".length);
+ Assert.equal(gURLBar.selectionEnd, "http://ExAmple.com/".length);
+ checkKeys([
+ ["KEY_ArrowDown", "http://mozilla.org/example", 1],
+ ["KEY_ArrowDown", "http://ExA", -1],
+ ["KEY_ArrowUp", "http://mozilla.org/example", 1],
+ ["KEY_ArrowUp", "http://ExAmple.com/", 0],
+ ["KEY_ArrowUp", "http://ExA", -1],
+ ["KEY_ArrowDown", "http://ExAmple.com/", 0],
+ ]);
+ await cleanUp();
+});
+
+add_task(async function originPortScheme() {
+ await PlacesTestUtils.addVisits([
+ "http://example.com:8888/",
+ "http://mozilla.org/example",
+ ]);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "http://ExA",
+ fireInputEvents: true,
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(gURLBar.value, "http://ExAmple.com:8888/");
+ Assert.equal(gURLBar.selectionStart, "http://ExA".length);
+ Assert.equal(gURLBar.selectionEnd, "http://ExAmple.com:8888/".length);
+ checkKeys([
+ ["KEY_ArrowDown", "http://mozilla.org/example", 1],
+ ["KEY_ArrowDown", "http://ExA", -1],
+ ["KEY_ArrowUp", "http://mozilla.org/example", 1],
+ ["KEY_ArrowUp", "http://ExAmple.com:8888/", 0],
+ ["KEY_ArrowUp", "http://ExA", -1],
+ ["KEY_ArrowDown", "http://ExAmple.com:8888/", 0],
+ ]);
+ await cleanUp();
+});
+
+add_task(async function url() {
+ await PlacesTestUtils.addVisits([
+ "http://example.com/foo",
+ "http://example.com/foo",
+ "http://example.com/fff",
+ ]);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "ExAmple.com/f",
+ fireInputEvent: true,
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(gURLBar.value, "ExAmple.com/foo");
+ Assert.equal(gURLBar.selectionStart, "ExAmple.com/f".length);
+ Assert.equal(gURLBar.selectionEnd, "ExAmple.com/foo".length);
+ checkKeys([
+ ["KEY_ArrowDown", "http://example.com/fff", 1],
+ ["KEY_ArrowDown", "ExAmple.com/f", -1],
+ ["KEY_ArrowUp", "http://example.com/fff", 1],
+ ["KEY_ArrowUp", "ExAmple.com/foo", 0],
+ ["KEY_ArrowUp", "ExAmple.com/f", -1],
+ ["KEY_ArrowDown", "ExAmple.com/foo", 0],
+ ]);
+ await cleanUp();
+});
+
+add_task(async function urlPort() {
+ await PlacesTestUtils.addVisits([
+ "http://example.com:8888/foo",
+ "http://example.com:8888/foo",
+ "http://example.com:8888/fff",
+ ]);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "ExAmple.com:8888/f",
+ fireInputEvent: true,
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(gURLBar.value, "ExAmple.com:8888/foo");
+ Assert.equal(gURLBar.selectionStart, "ExAmple.com:8888/f".length);
+ Assert.equal(gURLBar.selectionEnd, "ExAmple.com:8888/foo".length);
+ checkKeys([
+ ["KEY_ArrowDown", "http://example.com:8888/fff", 1],
+ ["KEY_ArrowDown", "ExAmple.com:8888/f", -1],
+ ["KEY_ArrowUp", "http://example.com:8888/fff", 1],
+ ["KEY_ArrowUp", "ExAmple.com:8888/foo", 0],
+ ["KEY_ArrowUp", "ExAmple.com:8888/f", -1],
+ ["KEY_ArrowDown", "ExAmple.com:8888/foo", 0],
+ ]);
+ await cleanUp();
+});
+
+add_task(async function tokenAlias() {
+ await Services.search.addEngineWithDetails("Test", {
+ alias: "@example",
+ template: "http://example.com/?search={searchTerms}",
+ });
+ registerCleanupFunction(async function() {
+ let engine = Services.search.getEngineByName("Test");
+ await Services.search.removeEngine(engine);
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@ExA",
+ fireInputEvent: true,
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(gURLBar.value, "@ExAmple ");
+ Assert.equal(gURLBar.selectionStart, "@ExA".length);
+ Assert.equal(gURLBar.selectionEnd, "@ExAmple ".length);
+ // Token aliases (1) hide the one-off buttons and (2) show only a single
+ // result, the "Search with" result for the alias's engine, so there's no way
+ // to key up/down to change the selection, so this task doesn't check key
+ // presses like the others do.
+ await cleanUp();
+});
+
+// This test is a little different from the others. It backspaces over the
+// autofilled substring and checks that autofill is *not* preserved.
+add_task(async function backspaceNoAutofill() {
+ await PlacesTestUtils.addVisits([
+ "http://example.com/",
+ "http://example.com/",
+ "http://mozilla.org/example",
+ ]);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "ExA",
+ fireInputEvent: true,
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(gURLBar.value, "ExAmple.com/");
+ Assert.equal(gURLBar.selectionStart, "ExA".length);
+ Assert.equal(gURLBar.selectionEnd, "ExAmple.com/".length);
+
+ EventUtils.synthesizeKey("KEY_Backspace");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(!details.autofill);
+ Assert.equal(gURLBar.value, "ExA");
+ Assert.equal(gURLBar.selectionStart, "ExA".length);
+ Assert.equal(gURLBar.selectionEnd, "ExA".length);
+
+ let heuristicValue = "ExA";
+
+ checkKeys([
+ ["KEY_ArrowDown", "http://example.com/", 1],
+ ["KEY_ArrowDown", "http://mozilla.org/example", 2],
+ ["KEY_ArrowDown", "ExA", -1],
+ ["KEY_ArrowUp", "http://mozilla.org/example", 2],
+ ["KEY_ArrowUp", "http://example.com/", 1],
+ ["KEY_ArrowUp", heuristicValue, 0],
+ ["KEY_ArrowUp", "ExA", -1],
+ ["KEY_ArrowDown", heuristicValue, 0],
+ ]);
+
+ await cleanUp();
+});
+
+function checkKeys(testTuples) {
+ for (let [key, value, selectedIndex] of testTuples) {
+ EventUtils.synthesizeKey(key);
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), selectedIndex);
+ Assert.equal(gURLBar.untrimmedValue, value);
+ }
+}
+
+async function cleanUp() {
+ EventUtils.synthesizeKey("KEY_Escape");
+ await UrlbarTestUtils.promisePopupClose(window);
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+}
diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_trimURLs.js b/browser/components/urlbar/tests/browser/browser_autoFill_trimURLs.js
new file mode 100644
index 0000000000..1d4b805681
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_autoFill_trimURLs.js
@@ -0,0 +1,183 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test ensures that autoFilled values are not trimmed, unless the user
+// selects from the autocomplete popup.
+
+"use strict";
+
+add_task(async function setup() {
+ const PREF_TRIMURL = "browser.urlbar.trimURLs";
+ const PREF_AUTOFILL = "browser.urlbar.autoFill";
+
+ registerCleanupFunction(async function() {
+ Services.prefs.clearUserPref(PREF_TRIMURL);
+ Services.prefs.clearUserPref(PREF_AUTOFILL);
+ await PlacesUtils.history.clear();
+ gURLBar.handleRevert();
+ });
+ Services.prefs.setBoolPref(PREF_TRIMURL, true);
+ Services.prefs.setBoolPref(PREF_AUTOFILL, true);
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+
+ // Adding a tab would hit switch-to-tab, so it's safer to just add a visit.
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://www.autofilltrimurl.com/whatever",
+ },
+ {
+ uri: "https://www.secureautofillurl.com/whatever",
+ },
+ ]);
+});
+
+async function promiseSearch(searchtext) {
+ gURLBar.focus();
+ gURLBar.inputField.value = searchtext.substr(0, searchtext.length - 1);
+ EventUtils.sendString(searchtext.substr(-1, 1));
+ await UrlbarTestUtils.promiseSearchComplete(window);
+}
+
+async function promiseTestResult(test) {
+ info(`Searching for '${test.search}'`);
+
+ await promiseSearch(test.search);
+
+ Assert.equal(
+ gURLBar.inputField.value,
+ test.autofilledValue,
+ `Autofilled value is as expected for search '${test.search}'`
+ );
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+
+ Assert.equal(
+ result.displayed.title,
+ test.resultListDisplayTitle,
+ `Autocomplete result should have displayed title as expected for search '${test.search}'`
+ );
+
+ Assert.equal(
+ result.displayed.action,
+ test.resultListActionText,
+ `Autocomplete action text should be as expected for search '${test.search}'`
+ );
+
+ Assert.equal(
+ result.type,
+ test.resultListType,
+ `Autocomplete result should have searchengine for the type for search '${test.search}'`
+ );
+
+ Assert.equal(
+ !!result.searchParams,
+ !!test.searchParams,
+ "Should have search params if expected"
+ );
+ if (test.searchParams) {
+ let definedParams = {};
+ for (let [k, v] of Object.entries(result.searchParams)) {
+ if (v !== undefined) {
+ definedParams[k] = v;
+ }
+ }
+ Assert.deepEqual(
+ definedParams,
+ test.searchParams,
+ "Shoud have the correct search params"
+ );
+ } else {
+ Assert.equal(
+ result.url,
+ test.finalCompleteValue,
+ "Should have the correct URL/finalCompleteValue"
+ );
+ }
+}
+
+const tests = [
+ {
+ search: "http://",
+ autofilledValue: "http://",
+ resultListDisplayTitle: "http://",
+ resultListActionText: "Search with Google",
+ resultListType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ searchParams: {
+ engine: "Google",
+ query: "http://",
+ },
+ },
+ {
+ search: "https://",
+ autofilledValue: "https://",
+ resultListDisplayTitle: "https://",
+ resultListActionText: "Search with Google",
+ resultListType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ searchParams: {
+ engine: "Google",
+ query: "https://",
+ },
+ },
+ {
+ search: "au",
+ autofilledValue: "autofilltrimurl.com/",
+ resultListDisplayTitle: "www.autofilltrimurl.com",
+ resultListActionText: "Visit",
+ resultListType: UrlbarUtils.RESULT_TYPE.URL,
+ finalCompleteValue: "http://www.autofilltrimurl.com/",
+ },
+ {
+ search: "http://au",
+ autofilledValue: "http://autofilltrimurl.com/",
+ resultListDisplayTitle: "www.autofilltrimurl.com",
+ resultListActionText: "Visit",
+ resultListType: UrlbarUtils.RESULT_TYPE.URL,
+ finalCompleteValue: "http://www.autofilltrimurl.com/",
+ },
+ {
+ search: "sec",
+ autofilledValue: "secureautofillurl.com/",
+ resultListDisplayTitle: "https://www.secureautofillurl.com",
+ resultListActionText: "Visit",
+ resultListType: UrlbarUtils.RESULT_TYPE.URL,
+ finalCompleteValue: "https://www.secureautofillurl.com/",
+ },
+ {
+ search: "https://sec",
+ autofilledValue: "https://secureautofillurl.com/",
+ resultListDisplayTitle: "https://www.secureautofillurl.com",
+ resultListActionText: "Visit",
+ resultListType: UrlbarUtils.RESULT_TYPE.URL,
+ finalCompleteValue: "https://www.secureautofillurl.com/",
+ },
+];
+
+add_task(async function autofill_tests() {
+ for (let test of tests) {
+ await promiseTestResult(test);
+ }
+});
+
+add_task(async function autofill_complete_domain() {
+ await promiseSearch("http://www.autofilltrimurl.com");
+ Assert.equal(
+ gURLBar.inputField.value,
+ "http://www.autofilltrimurl.com/",
+ "Should have the correct autofill value"
+ );
+
+ // Now ensure selecting from the popup correctly trims.
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 2,
+ "Should have the correct matches"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.equal(
+ gURLBar.inputField.value,
+ "www.autofilltrimurl.com/whatever",
+ "Should have applied trim correctly"
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_typed.js b/browser/components/urlbar/tests/browser/browser_autoFill_typed.js
new file mode 100644
index 0000000000..03be7d5424
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_autoFill_typed.js
@@ -0,0 +1,179 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test makes sure that autofill works as expected when typing, character
+// by character.
+
+"use strict";
+
+add_task(async function init() {
+ await cleanUp();
+});
+
+add_task(async function origin() {
+ await PlacesTestUtils.addVisits(["http://example.com/"]);
+ // all lowercase
+ await typeAndCheck([
+ ["e", "example.com/"],
+ ["x", "example.com/"],
+ ["a", "example.com/"],
+ ["m", "example.com/"],
+ ["p", "example.com/"],
+ ["l", "example.com/"],
+ ["e", "example.com/"],
+ [".", "example.com/"],
+ ["c", "example.com/"],
+ ["o", "example.com/"],
+ ["m", "example.com/"],
+ ["/", "example.com/"],
+ ]);
+ gURLBar.value = "";
+ // mixed case
+ await typeAndCheck([
+ ["E", "Example.com/"],
+ ["x", "Example.com/"],
+ ["A", "ExAmple.com/"],
+ ["m", "ExAmple.com/"],
+ ["P", "ExAmPle.com/"],
+ ["L", "ExAmPLe.com/"],
+ ["e", "ExAmPLe.com/"],
+ [".", "ExAmPLe.com/"],
+ ["C", "ExAmPLe.Com/"],
+ ["o", "ExAmPLe.Com/"],
+ ["M", "ExAmPLe.CoM/"],
+ ["/", "ExAmPLe.CoM/"],
+ ]);
+ await cleanUp();
+});
+
+add_task(async function url() {
+ await PlacesTestUtils.addVisits(["http://example.com/foo/bar"]);
+ // all lowercase
+ await typeAndCheck([
+ ["e", "example.com/"],
+ ["x", "example.com/"],
+ ["a", "example.com/"],
+ ["m", "example.com/"],
+ ["p", "example.com/"],
+ ["l", "example.com/"],
+ ["e", "example.com/"],
+ [".", "example.com/"],
+ ["c", "example.com/"],
+ ["o", "example.com/"],
+ ["m", "example.com/"],
+ ["/", "example.com/"],
+ ["f", "example.com/foo/"],
+ ["o", "example.com/foo/"],
+ ["o", "example.com/foo/"],
+ ["/", "example.com/foo/"],
+ ["b", "example.com/foo/bar"],
+ ["a", "example.com/foo/bar"],
+ ["r", "example.com/foo/bar"],
+ ]);
+ gURLBar.value = "";
+ // mixed case
+ await typeAndCheck([
+ ["E", "Example.com/"],
+ ["x", "Example.com/"],
+ ["A", "ExAmple.com/"],
+ ["m", "ExAmple.com/"],
+ ["P", "ExAmPle.com/"],
+ ["L", "ExAmPLe.com/"],
+ ["e", "ExAmPLe.com/"],
+ [".", "ExAmPLe.com/"],
+ ["C", "ExAmPLe.Com/"],
+ ["o", "ExAmPLe.Com/"],
+ ["M", "ExAmPLe.CoM/"],
+ ["/", "ExAmPLe.CoM/"],
+ ["f", "ExAmPLe.CoM/foo/"],
+ ["o", "ExAmPLe.CoM/foo/"],
+ ["o", "ExAmPLe.CoM/foo/"],
+ ["/", "ExAmPLe.CoM/foo/"],
+ ["b", "ExAmPLe.CoM/foo/bar"],
+ ["a", "ExAmPLe.CoM/foo/bar"],
+ ["r", "ExAmPLe.CoM/foo/bar"],
+ ]);
+ await cleanUp();
+});
+
+add_task(async function tokenAlias() {
+ // We have built-in engine aliases that may conflict with the one we choose
+ // here in terms of autofill, so be careful and choose a weird alias.
+ await Services.search.addEngineWithDetails("Test", {
+ alias: "@__example",
+ template: "http://example.com/?search={searchTerms}",
+ });
+ registerCleanupFunction(async function() {
+ let engine = Services.search.getEngineByName("Test");
+ await Services.search.removeEngine(engine);
+ });
+ // all lowercase
+ await typeAndCheck([
+ ["@", "@"],
+ ["_", "@__example "],
+ ["_", "@__example "],
+ ["e", "@__example "],
+ ["x", "@__example "],
+ ["a", "@__example "],
+ ["m", "@__example "],
+ ["p", "@__example "],
+ ["l", "@__example "],
+ ["e", "@__example "],
+ ]);
+ gURLBar.value = "";
+ // mixed case
+ await typeAndCheck([
+ ["@", "@"],
+ ["_", "@__example "],
+ ["_", "@__example "],
+ ["E", "@__Example "],
+ ["x", "@__Example "],
+ ["A", "@__ExAmple "],
+ ["m", "@__ExAmple "],
+ ["P", "@__ExAmPle "],
+ ["L", "@__ExAmPLe "],
+ ["e", "@__ExAmPLe "],
+ ]);
+ await cleanUp();
+});
+
+async function typeAndCheck(values) {
+ gURLBar.focus();
+ for (let i = 0; i < values.length; i++) {
+ let [char, expectedInputValue] = values[i];
+ info(
+ `Typing: i=${i} char=${char} ` +
+ `substring="${expectedInputValue.substring(0, i + 1)}"`
+ );
+ EventUtils.synthesizeKey(char);
+ if (i == 0 && char == "@") {
+ // A single "@" doesn't trigger autofill, so skip the checks below. (It
+ // shows all the @ aliases.)
+ continue;
+ }
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ let restIsSpaces = !expectedInputValue.substring(i + 1).trim();
+ Assert.equal(gURLBar.value, expectedInputValue);
+ Assert.equal(gURLBar.selectionStart, i + 1);
+ Assert.equal(gURLBar.selectionEnd, expectedInputValue.length);
+ if (restIsSpaces) {
+ // Autofilled @ aliases have a trailing space. We should check that the
+ // space is autofilled when each preceding character is typed, but once
+ // the final non-space char is typed, autofill actually stops and the
+ // trailing space is not autofilled. (Which is maybe not the way it
+ // should work...) Skip the check below.
+ continue;
+ }
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ }
+}
+
+async function cleanUp() {
+ gURLBar.value = "";
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeKey("KEY_Escape");
+ });
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+}
diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_undo.js b/browser/components/urlbar/tests/browser/browser_autoFill_undo.js
new file mode 100644
index 0000000000..c233da80f2
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_autoFill_undo.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test checks the behavior of text undo (Ctrl-Z, cmd_undo) in regard to
+// autofill.
+
+"use strict";
+
+add_task(async function test() {
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+ await PlacesTestUtils.addVisits(["http://example.com/"]);
+
+ // Search for "ex". It should autofill to example.com/.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "ex",
+ fireInputEvent: true,
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(gURLBar.value, "example.com/");
+ Assert.equal(gURLBar.selectionStart, "ex".length);
+ Assert.equal(gURLBar.selectionEnd, "example.com/".length);
+
+ // Type an x.
+ EventUtils.synthesizeKey("x");
+
+ // Nothing should have been autofilled.
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(!details.autofill);
+ Assert.equal(gURLBar.value, "exx");
+ Assert.equal(gURLBar.selectionStart, "exx".length);
+ Assert.equal(gURLBar.selectionEnd, "exx".length);
+
+ // Undo the typed x.
+ goDoCommand("cmd_undo");
+
+ // The text should be restored to ex[ample.com/] (with the part in brackets
+ // autofilled and selected).
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(gURLBar.value, "example.com/");
+ Assert.ok(!details.autofill, "Autofill should not be set.");
+ Assert.equal(gURLBar.selectionStart, "ex".length);
+ Assert.equal(gURLBar.selectionEnd, "example.com/".length);
+
+ await PlacesUtils.history.clear();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_autoOpen.js b/browser/components/urlbar/tests/browser/browser_autoOpen.js
new file mode 100644
index 0000000000..bd43e52565
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_autoOpen.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+async function checkOpensOnFocus(win = window) {
+ // The view should not open when the input is focused programmatically.
+ win.gURLBar.blur();
+ win.gURLBar.focus();
+ Assert.ok(!win.gURLBar.view.isOpen, "check urlbar panel is not open");
+ win.gURLBar.blur();
+
+ // Check the keyboard shortcut.
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ win.document.getElementById("Browser:OpenLocation").doCommand();
+ });
+ await UrlbarTestUtils.promisePopupClose(win, () => {
+ win.gURLBar.blur();
+ });
+
+ // Focus with the mouse.
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ EventUtils.synthesizeMouseAtCenter(win.gURLBar.inputField, {}, win);
+ });
+ await UrlbarTestUtils.promisePopupClose(win, () => {
+ win.gURLBar.blur();
+ });
+}
+
+add_task(async function setUp() {
+ // Add some history for the empty panel.
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://mochi.test:8888/",
+ transition: PlacesUtils.history.TRANSITIONS.TYPED,
+ },
+ ]);
+ registerCleanupFunction(() => PlacesUtils.history.clear());
+});
+
+add_task(async function test() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async browser => {
+ await checkOpensOnFocus();
+ }
+ );
+});
+
+add_task(async function newtabAndHome() {
+ for (let url of ["about:newtab", "about:home"]) {
+ // withNewTab randomly hangs on these pages when waitForLoad = true (the
+ // default), so pass false.
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url, waitForLoad: false },
+ async browser => {
+ // We don't wait for load, but we must ensure to be on the expected url.
+ await TestUtils.waitForCondition(
+ () => gBrowser.currentURI.spec == url,
+ "Ensure we're on the expected page"
+ );
+ await checkOpensOnFocus();
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "http://example.com/" },
+ async otherBrowser => {
+ await checkOpensOnFocus();
+ // Switch back to about:newtab/home.
+ await BrowserTestUtils.switchTab(
+ gBrowser,
+ gBrowser.getTabForBrowser(browser)
+ );
+ await checkOpensOnFocus();
+ // Switch back to example.com.
+ await BrowserTestUtils.switchTab(
+ gBrowser,
+ gBrowser.getTabForBrowser(otherBrowser)
+ );
+ await checkOpensOnFocus();
+ }
+ );
+ // After example.com closes, about:newtab/home is selected again.
+ await checkOpensOnFocus();
+ // Load example.com in the same tab.
+ BrowserTestUtils.loadURI(
+ gBrowser.selectedBrowser,
+ "http://example.com/"
+ );
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await checkOpensOnFocus();
+ }
+ );
+ }
+});
diff --git a/browser/components/urlbar/tests/browser/browser_autocomplete_a11y_label.js b/browser/components/urlbar/tests/browser/browser_autocomplete_a11y_label.js
new file mode 100644
index 0000000000..0f8652248b
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_autocomplete_a11y_label.js
@@ -0,0 +1,155 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This test ensures that we produce good labels for a11y purposes.
+ */
+
+const SUGGEST_ALL_PREF = "browser.search.suggest.enabled";
+const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches";
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+
+async function getResultText(element) {
+ await initAccessibilityService();
+ await BrowserTestUtils.waitForCondition(() =>
+ accService.getAccessibleFor(element)
+ );
+ let accessible = accService.getAccessibleFor(element);
+ return accessible.name;
+}
+
+let accService;
+async function initAccessibilityService() {
+ if (accService) {
+ return;
+ }
+ accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+ if (Services.appinfo.accessibilityEnabled) {
+ return;
+ }
+
+ async function promiseInitOrShutdown(init = true) {
+ await new Promise(resolve => {
+ let observe = (subject, topic, data) => {
+ Services.obs.removeObserver(observe, "a11y-init-or-shutdown");
+ // "1" indicates that the accessibility service is initialized.
+ if (data === (init ? "1" : "0")) {
+ resolve();
+ }
+ };
+ Services.obs.addObserver(observe, "a11y-init-or-shutdown");
+ });
+ }
+ await promiseInitOrShutdown(true);
+ registerCleanupFunction(async () => {
+ accService = null;
+ await promiseInitOrShutdown(false);
+ });
+}
+
+add_task(async function switchToTab() {
+ let tab = BrowserTestUtils.addTab(gBrowser, "about:robots");
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "% robots",
+ });
+
+ let index = 0;
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index);
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ "Should have a switch tab result"
+ );
+
+ let element = await UrlbarTestUtils.waitForAutocompleteResultAt(
+ window,
+ index
+ );
+ is(
+ await getResultText(element),
+ "about: robots— Switch to Tab",
+ "Result a11y label should be: <title>— Switch to Tab"
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ gURLBar.handleRevert();
+ gBrowser.removeTab(tab);
+});
+
+add_task(async function searchSuggestions() {
+ let engine = await SearchTestUtils.promiseNewSearchEngine(
+ getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME
+ );
+ let oldDefaultEngine = await Services.search.getDefault();
+ await Services.search.setDefault(engine);
+ Services.prefs.setBoolPref(SUGGEST_ALL_PREF, true);
+ let suggestionsEnabled = Services.prefs.getBoolPref(SUGGEST_URLBAR_PREF);
+ Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, true);
+ registerCleanupFunction(async function() {
+ await Services.search.setDefault(oldDefaultEngine);
+ Services.prefs.clearUserPref(SUGGEST_ALL_PREF);
+ Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, suggestionsEnabled);
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ });
+ let length = await UrlbarTestUtils.getResultCount(window);
+ // Don't assume that the search doesn't match history or bookmarks left around
+ // by earlier tests.
+ Assert.greaterOrEqual(
+ length,
+ 3,
+ "Should get at least heuristic result + two search suggestions"
+ );
+ // The first expected search is the search term itself since the heuristic
+ // result will come before the search suggestions.
+ // The extra spaces are here due to bug 1550644.
+ let searchTerm = "foo ";
+ let expectedSearches = [searchTerm, "foo foo", "foo bar"];
+ for (let i = 0; i < length; i++) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ if (result.type === UrlbarUtils.RESULT_TYPE.SEARCH) {
+ Assert.greaterOrEqual(
+ expectedSearches.length,
+ 0,
+ "Should still have expected searches remaining"
+ );
+
+ let element = await UrlbarTestUtils.waitForAutocompleteResultAt(
+ window,
+ i
+ );
+ let selected = element.hasAttribute("selected");
+ if (!selected) {
+ // Simulate the result being selected so we see the expanded text.
+ element.toggleAttribute("selected", true);
+ }
+ if (result.searchParams.inPrivateWindow) {
+ Assert.equal(
+ await getResultText(element),
+ searchTerm + "— Search in a Private Window",
+ "Check result label"
+ );
+ } else {
+ let suggestion = expectedSearches.shift();
+ Assert.equal(
+ await getResultText(element),
+ suggestion +
+ "— Search with browser_searchSuggestionEngine searchSuggestionEngine.xml",
+ "Check result label"
+ );
+ }
+ if (!selected) {
+ element.toggleAttribute("selected", false);
+ }
+ }
+ }
+ Assert.ok(!expectedSearches.length);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_autocomplete_autoselect.js b/browser/components/urlbar/tests/browser/browser_autocomplete_autoselect.js
new file mode 100644
index 0000000000..750cbb01a9
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_autocomplete_autoselect.js
@@ -0,0 +1,128 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that the first item is correctly autoselected and some navigation
+ * around the results list.
+ */
+
+function repeat(limit, func) {
+ for (let i = 0; i < limit; i++) {
+ func(i);
+ }
+}
+
+function assertSelected(index) {
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ index,
+ "Should have selected the correct item"
+ );
+ // Also check the "selected" attribute, to ensure it is not a "fake" selection
+ // due to binding misbehaviors.
+ let element = UrlbarTestUtils.getSelectedRow(window);
+ Assert.ok(
+ element.hasAttribute("selected"),
+ "Should have the selected attribute on the row element"
+ );
+
+ // This is true because although both the listbox and the one-offs can have
+ // selections, the test doesn't check that.
+ Assert.equal(
+ UrlbarTestUtils.getOneOffSearchButtons(window).selectedButton,
+ null,
+ "A result is selected, so the one-offs should not have a selection"
+ );
+}
+
+function assertSelected_one_off(index) {
+ Assert.equal(
+ UrlbarTestUtils.getOneOffSearchButtons(window).selectedButtonIndex,
+ index,
+ "Expected one-off button should be selected"
+ );
+
+ // This is true because although both the listbox and the one-offs can have
+ // selections, the test doesn't check that.
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ -1,
+ "A one-off is selected, so the listbox should not have a selection"
+ );
+}
+
+add_task(async function() {
+ let maxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults");
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:mozilla"
+ );
+ registerCleanupFunction(async function() {
+ await PlacesUtils.history.clear();
+ BrowserTestUtils.removeTab(tab);
+ });
+
+ let visits = [];
+ repeat(maxResults, i => {
+ visits.push({
+ uri: makeURI("http://example.com/autocomplete/?" + i),
+ });
+ });
+ await PlacesTestUtils.addVisits(visits);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "example.com/autocomplete",
+ fireInputEvent: true,
+ });
+
+ let resultCount = await UrlbarTestUtils.getResultCount(window);
+
+ Assert.equal(
+ resultCount,
+ maxResults,
+ "Should get the expected amount of results"
+ );
+ assertSelected(0);
+
+ info("Key Down to select the next item");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ assertSelected(1);
+
+ info("Key Down maxResults-1 times should select the first one-off");
+ repeat(maxResults - 1, () => EventUtils.synthesizeKey("KEY_ArrowDown"));
+ assertSelected_one_off(0);
+
+ info("Key Down numButtons-1 should select the last one-off");
+ let numButtons = UrlbarTestUtils.getOneOffSearchButtons(
+ window
+ ).getSelectableButtons(true).length;
+ repeat(numButtons - 1, () => EventUtils.synthesizeKey("KEY_ArrowDown"));
+ assertSelected_one_off(numButtons - 1);
+
+ info("Key Down twice more should select the second result");
+ repeat(2, () => EventUtils.synthesizeKey("KEY_ArrowDown"));
+ assertSelected(1);
+
+ info("Key Down maxResults + numButtons times should wrap around");
+ repeat(maxResults + numButtons, () =>
+ EventUtils.synthesizeKey("KEY_ArrowDown")
+ );
+ assertSelected(1);
+
+ info("Key Up maxResults + numButtons times should wrap around the other way");
+ repeat(maxResults + numButtons, () =>
+ EventUtils.synthesizeKey("KEY_ArrowUp")
+ );
+ assertSelected(1);
+
+ info("Page Up will go up the list, but not wrap");
+ EventUtils.synthesizeKey("KEY_PageUp");
+ assertSelected(0);
+
+ info("Page Up again will wrap around to the end of the list");
+ EventUtils.synthesizeKey("KEY_PageUp");
+ assertSelected(maxResults - 1);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_autocomplete_cursor.js b/browser/components/urlbar/tests/browser/browser_autocomplete_cursor.js
new file mode 100644
index 0000000000..5e0081a92c
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_autocomplete_cursor.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests the cursor remains in the right place when a new window is opened.
+ */
+
+add_task(async function test_windowSwitch() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:mozilla"
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "www.mozilla.org",
+ });
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
+
+ gURLBar.focus();
+ gURLBar.inputField.setSelectionRange(4, 4);
+
+ let newWindow = await BrowserTestUtils.openNewBrowserWindow();
+
+ await BrowserTestUtils.closeWindow(newWindow);
+
+ Assert.equal(
+ document.activeElement,
+ gURLBar.inputField,
+ "URL Bar should be focused"
+ );
+ Assert.equal(gURLBar.selectionStart, 4, "Should not have moved the cursor");
+ Assert.equal(gURLBar.selectionEnd, 4, "Should not have selected anything");
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_autocomplete_edit_completed.js b/browser/components/urlbar/tests/browser/browser_autocomplete_edit_completed.js
new file mode 100644
index 0000000000..51e1972dcc
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_autocomplete_edit_completed.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests selecting a result, and editing the value of that autocompleted result.
+ */
+
+add_task(async function() {
+ await PlacesUtils.history.clear();
+
+ await PlacesTestUtils.addVisits([
+ { uri: makeURI("http://example.com/foo") },
+ { uri: makeURI("http://example.com/foo/bar") },
+ ]);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+ registerCleanupFunction(async function() {
+ BrowserTestUtils.removeTab(tab);
+ await PlacesUtils.history.clear();
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "http://example.com",
+ });
+
+ const initialIndex = UrlbarTestUtils.getSelectedRowIndex(window);
+
+ info("Key Down to select the next item.");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+
+ let nextIndex = initialIndex + 1;
+ let nextResult = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ nextIndex
+ );
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ nextIndex,
+ "Should have selected the next item"
+ );
+ Assert.equal(
+ gURLBar.untrimmedValue,
+ nextResult.url,
+ "Should have completed the URL"
+ );
+
+ info("Press backspace");
+ EventUtils.synthesizeKey("KEY_Backspace");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ let editedValue = gURLBar.value;
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ initialIndex,
+ "Should have selected the initialIndex again"
+ );
+ Assert.notEqual(editedValue, nextResult.url, "The URL has changed.");
+
+ let docLoad = BrowserTestUtils.waitForDocLoadAndStopIt(
+ "http://" + editedValue,
+ gBrowser.selectedBrowser
+ );
+
+ info("Press return to load edited URL.");
+
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeKey("KEY_Enter");
+ });
+
+ await docLoad;
+});
diff --git a/browser/components/urlbar/tests/browser/browser_autocomplete_enter_race.js b/browser/components/urlbar/tests/browser/browser_autocomplete_enter_race.js
new file mode 100644
index 0000000000..7fda51305a
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_autocomplete_enter_race.js
@@ -0,0 +1,178 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests what happens when the enter key is pressed quickly after entering text.
+ */
+
+// The order of these tests matters!
+
+add_task(async function setup() {
+ let bm = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.com/?q=%s",
+ title: "test",
+ });
+ registerCleanupFunction(async function() {
+ await PlacesUtils.bookmarks.remove(bm);
+ });
+ await PlacesUtils.keywords.insert({
+ keyword: "keyword",
+ url: "http://example.com/?q=%s",
+ });
+ // Needs at least one success.
+ ok(true, "Setup complete");
+});
+
+add_task(
+ taskWithNewTab(async function test_keyword() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "keyword bear",
+ });
+ gURLBar.focus();
+ EventUtils.sendString("d");
+ EventUtils.synthesizeKey("KEY_Enter");
+ info("wait for the page to load");
+ await BrowserTestUtils.browserLoaded(
+ gBrowser.selectedTab.linkedBrowser,
+ false,
+ "http://example.com/?q=beard"
+ );
+ })
+);
+
+add_task(
+ taskWithNewTab(async function test_sametext() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "example.com",
+ fireInputEvent: true,
+ });
+
+ // Simulate re-entering the same text searched the last time. This may happen
+ // through a copy paste, but clipboard handling is not much reliable, so just
+ // fire an input event.
+ info("synthesize input event");
+ let event = document.createEvent("Events");
+ event.initEvent("input", true, true);
+ gURLBar.inputField.dispatchEvent(event);
+ EventUtils.synthesizeKey("KEY_Enter");
+
+ info("wait for the page to load");
+ await BrowserTestUtils.browserLoaded(
+ gBrowser.selectedTab.linkedBrowser,
+ false,
+ "http://example.com/"
+ );
+ })
+);
+
+add_task(
+ taskWithNewTab(async function test_after_empty_search() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ gURLBar.focus();
+ gURLBar.value = "e";
+ EventUtils.synthesizeKey("x");
+ EventUtils.synthesizeKey("KEY_Enter");
+
+ info("wait for the page to load");
+ await BrowserTestUtils.browserLoaded(
+ gBrowser.selectedTab.linkedBrowser,
+ false,
+ "http://example.com/"
+ );
+ })
+);
+
+add_task(
+ taskWithNewTab(async function test_disabled_ac() {
+ // Disable autocomplete.
+ let suggestHistory = Preferences.get("browser.urlbar.suggest.history");
+ Preferences.set("browser.urlbar.suggest.history", false);
+ let suggestBookmarks = Preferences.get("browser.urlbar.suggest.bookmark");
+ Preferences.set("browser.urlbar.suggest.bookmark", false);
+ let suggestOpenPages = Preferences.get("browser.urlbar.suggest.openpage");
+ Preferences.set("browser.urlbar.suggest.openpage", false);
+
+ await Services.search.addEngineWithDetails("MozSearch", {
+ method: "GET",
+ template: "http://example.com/?q={searchTerms}",
+ });
+ let engine = Services.search.getEngineByName("MozSearch");
+ let originalEngine = await Services.search.getDefault();
+ await Services.search.setDefault(engine);
+
+ async function cleanup() {
+ Preferences.set("browser.urlbar.suggest.history", suggestHistory);
+ Preferences.set("browser.urlbar.suggest.bookmark", suggestBookmarks);
+ Preferences.set("browser.urlbar.suggest.openpage", suggestOpenPages);
+
+ await Services.search.setDefault(originalEngine);
+ let mozSearchEngine = Services.search.getEngineByName("MozSearch");
+ if (mozSearchEngine) {
+ await Services.search.removeEngine(mozSearchEngine);
+ }
+ }
+ registerCleanupFunction(cleanup);
+
+ gURLBar.focus();
+ gURLBar.value = "e";
+ EventUtils.sendString("x");
+ EventUtils.synthesizeKey("KEY_Enter");
+
+ info("wait for the page to load");
+ await BrowserTestUtils.browserLoaded(
+ gBrowser.selectedTab.linkedBrowser,
+ false,
+ "http://example.com/?q=ex"
+ );
+ await cleanup();
+ })
+);
+
+add_task(
+ taskWithNewTab(async function test_delay() {
+ // This is needed to clear the current value, otherwise autocomplete may think
+ // the user removed text from the end.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ // Set a large delay.
+ const TIMEOUT = 3000;
+ let delay = UrlbarPrefs.get("delay");
+ UrlbarPrefs.set("delay", TIMEOUT);
+ registerCleanupFunction(function() {
+ UrlbarPrefs.set("delay", delay);
+ });
+
+ let start = Cu.now();
+ gURLBar.focus();
+ gURLBar.value = "e";
+ EventUtils.sendString("x");
+ EventUtils.synthesizeKey("KEY_Enter");
+ info("wait for the page to load");
+ await BrowserTestUtils.browserLoaded(
+ gBrowser.selectedTab.linkedBrowser,
+ false,
+ "http://example.com/"
+ );
+ Assert.ok(Cu.now() - start < TIMEOUT);
+ })
+);
+
+// The main reason for running each test task in a new tab that's closed when
+// the task finishes is to avoid switch-to-tab results.
+function taskWithNewTab(fn) {
+ return async function() {
+ await BrowserTestUtils.withNewTab("about:blank", fn);
+ };
+}
diff --git a/browser/components/urlbar/tests/browser/browser_autocomplete_no_title.js b/browser/components/urlbar/tests/browser/browser_autocomplete_no_title.js
new file mode 100644
index 0000000000..13c9fc3fb5
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_autocomplete_no_title.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test ensures that we display just the domain name when a URL result doesn't
+ * have a title.
+ */
+
+add_task(async function() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:mozilla"
+ );
+ await PlacesUtils.history.clear();
+ const uri = "http://bug1060642.example.com/beards/are/pretty/great";
+ await PlacesTestUtils.addVisits([{ uri, title: "" }]);
+ registerCleanupFunction(async function() {
+ await PlacesUtils.history.clear();
+ BrowserTestUtils.removeTab(tab);
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "bug1060642",
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(
+ result.displayed.title,
+ "bug1060642.example.com",
+ "Result title should be as expected"
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_autocomplete_readline_navigation.js b/browser/components/urlbar/tests/browser/browser_autocomplete_readline_navigation.js
new file mode 100644
index 0000000000..399612738d
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_autocomplete_readline_navigation.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests navigation between results using ctrl-n/p.
+ */
+
+function repeat(limit, func) {
+ for (let i = 0; i < limit; i++) {
+ func(i);
+ }
+}
+
+function assertSelected(index) {
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ index,
+ "Should have the correct item selected"
+ );
+
+ // This is true because although both the listbox and the one-offs can have
+ // selections, the test doesn't check that.
+ Assert.equal(
+ UrlbarTestUtils.getOneOffSearchButtons(window).selectedButton,
+ null,
+ "A result is selected, so the one-offs should not have a selection"
+ );
+}
+
+add_task(async function() {
+ let maxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults");
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:mozilla"
+ );
+ registerCleanupFunction(async function() {
+ await PlacesUtils.history.clear();
+ BrowserTestUtils.removeTab(tab);
+ });
+
+ let visits = [];
+ repeat(maxResults, i => {
+ visits.push({
+ uri: makeURI("http://example.com/autocomplete/?" + i),
+ });
+ });
+ await PlacesTestUtils.addVisits(visits);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "example.com/autocomplete",
+ });
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, maxResults - 1);
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ maxResults,
+ "Should get maxResults=" + maxResults + " results"
+ );
+ assertSelected(0);
+
+ info("Ctrl-n to select the next item");
+ EventUtils.synthesizeKey("n", { ctrlKey: true });
+ assertSelected(1);
+
+ info("Ctrl-p to select the previous item");
+ EventUtils.synthesizeKey("p", { ctrlKey: true });
+ assertSelected(0);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_autocomplete_tag_star_visibility.js b/browser/components/urlbar/tests/browser/browser_autocomplete_tag_star_visibility.js
new file mode 100644
index 0000000000..a2dc30fdcc
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_autocomplete_tag_star_visibility.js
@@ -0,0 +1,167 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests for the bookmark star being correct displayed for results matching
+ * tags.
+ */
+
+add_task(async function() {
+ registerCleanupFunction(async function() {
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+
+ async function addTagItem(tagName) {
+ let url = `http://example.com/this/is/tagged/${tagName}`;
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url,
+ title: `test ${tagName}`,
+ });
+ PlacesUtils.tagging.tagURI(Services.io.newURI(url), [tagName]);
+ await PlacesTestUtils.addVisits({
+ uri: url,
+ title: `Test page with tag ${tagName}`,
+ });
+ }
+
+ // We use different tags for each part of the test, as otherwise the
+ // autocomplete code tries to be smart by using the previously cached element
+ // without updating it (since all parameters it knows about are the same).
+
+ let testcases = [
+ {
+ description: "Test with suggest.bookmark=true",
+ tagName: "tagtest1",
+ prefs: {
+ "suggest.bookmark": true,
+ },
+ input: "tagtest1",
+ expected: {
+ typeImageVisible: true,
+ },
+ },
+ {
+ description: "Test with suggest.bookmark=false",
+ tagName: "tagtest2",
+ prefs: {
+ "suggest.bookmark": false,
+ },
+ input: "tagtest2",
+ expected: {
+ typeImageVisible: false,
+ },
+ },
+ {
+ description: "Test with suggest.bookmark=true (again)",
+ tagName: "tagtest3",
+ prefs: {
+ "suggest.bookmark": true,
+ },
+ input: "tagtest3",
+ expected: {
+ typeImageVisible: true,
+ },
+ },
+ {
+ description: "Test with bookmark restriction token",
+ tagName: "tagtest4",
+ prefs: {
+ "suggest.bookmark": true,
+ },
+ input: "* tagtest4",
+ expected: {
+ typeImageVisible: true,
+ },
+ },
+ {
+ description: "Test with history restriction token",
+ tagName: "tagtest5",
+ prefs: {
+ "suggest.bookmark": true,
+ },
+ input: "^ tagtest5",
+ expected: {
+ typeImageVisible: false,
+ },
+ },
+ {
+ description: "Test partial tag and casing",
+ tagName: "tagtest6",
+ prefs: {
+ "suggest.bookmark": true,
+ },
+ input: "TeSt6",
+ expected: {
+ typeImageVisible: true,
+ },
+ },
+ ];
+
+ for (let testcase of testcases) {
+ info(`Test case: ${testcase.description}`);
+
+ await addTagItem(testcase.tagName);
+ for (let prefName of Object.keys(testcase.prefs)) {
+ Services.prefs.setBoolPref(
+ `browser.urlbar.${prefName}`,
+ testcase.prefs[prefName]
+ );
+ }
+
+ let context = await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: testcase.input,
+ });
+
+ // If testcase.input triggers local search mode, there won't be a heuristic.
+ let resultIndex =
+ context.searchMode && !context.searchMode.engineName ? 0 : 1;
+
+ Assert.greaterOrEqual(
+ UrlbarTestUtils.getResultCount(window),
+ resultIndex + 1,
+ `Should be at least ${resultIndex + 1} results`
+ );
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ resultIndex
+ );
+
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.URL,
+ "Should have a URL result type"
+ );
+ // The Quantum Bar differs from the legacy urlbar in the fact that, if
+ // bookmarks are filtered out, it won't show tags for history results.
+ let expected_tags = !testcase.expected.typeImageVisible
+ ? []
+ : [testcase.tagName];
+ Assert.deepEqual(
+ result.tags,
+ expected_tags,
+ "Should have the expected tag"
+ );
+
+ if (testcase.expected.typeImageVisible) {
+ Assert.equal(
+ result.displayed.typeIcon,
+ 'url("chrome://browser/skin/bookmark-12.svg")',
+ "Should have the star image displayed or not as expected"
+ );
+ } else {
+ Assert.equal(
+ result.displayed.typeIcon,
+ "none",
+ "Should have the star image displayed or not as expected"
+ );
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ gURLBar.handleRevert();
+ }
+});
diff --git a/browser/components/urlbar/tests/browser/browser_blanking.js b/browser/components/urlbar/tests/browser/browser_blanking.js
new file mode 100644
index 0000000000..3cfb1d3c1e
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_blanking.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URL = `${TEST_BASE_URL}file_blank_but_not_blank.html`;
+
+add_task(async function() {
+ for (let page of gInitialPages) {
+ if (page == "about:newtab") {
+ // New tab preloading makes this a pain to test, so skip
+ continue;
+ }
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, page);
+ ok(
+ !gURLBar.value,
+ "The URL bar should be empty if we load a plain " + page + " page."
+ );
+ BrowserTestUtils.removeTab(tab);
+ }
+});
+
+add_task(async function() {
+ // The test was originally to check that reloading of a javascript: URL could
+ // throw an error and empty the URL bar. This situation can no longer happen
+ // as in bug 836567 we set document.URL to active document's URL on navigation
+ // to a javascript: URL; reloading after that will simply reload the original
+ // active document rather than the javascript: URL itself. But we can still
+ // verify that the URL bar's value is correct.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+ is(gURLBar.value, TEST_URL, "The URL bar should match the URI");
+ let browserLoaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ SpecialPowers.spawn(tab.linkedBrowser, [], function() {
+ content.document.querySelector("a").click();
+ });
+ await browserLoaded;
+ is(
+ gURLBar.value,
+ TEST_URL,
+ "The URL bar should be the previous active document's URI."
+ );
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function() {
+ // This is sync, so by the time we return we should have changed the URL bar.
+ content.location.reload();
+ }).catch(e => {
+ // Ignore expected exception.
+ });
+ is(
+ gURLBar.value,
+ TEST_URL,
+ "The URL bar should still be the previous active document's URI."
+ );
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_canonizeURL.js b/browser/components/urlbar/tests/browser/browser_canonizeURL.js
new file mode 100644
index 0000000000..e15f6a393e
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_canonizeURL.js
@@ -0,0 +1,255 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests turning non-url-looking values typed in the input field into proper URLs.
+ */
+
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+
+add_task(async function checkCtrlWorks() {
+ registerCleanupFunction(async function() {
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.formHistory.clear();
+ });
+
+ let defaultEngine = await Services.search.getDefault();
+ let testcases = [
+ ["example", "https://www.example.com/", { ctrlKey: true }],
+ // Check that a direct load is not overwritten by a previous canonization.
+ ["http://example.com/test/", "http://example.com/test/", {}],
+ ["ex-ample", "https://www.ex-ample.com/", { ctrlKey: true }],
+ [" example ", "https://www.example.com/", { ctrlKey: true }],
+ [" example/foo ", "https://www.example.com/foo", { ctrlKey: true }],
+ [
+ " example/foo bar ",
+ "https://www.example.com/foo%20bar",
+ { ctrlKey: true },
+ ],
+ ["example.net", "http://example.net/", { ctrlKey: true }],
+ ["http://example", "http://example/", { ctrlKey: true }],
+ ["example:8080", "http://example:8080/", { ctrlKey: true }],
+ ["ex-ample.foo", "http://ex-ample.foo/", { ctrlKey: true }],
+ ["example.foo/bar ", "http://example.foo/bar", { ctrlKey: true }],
+ ["1.1.1.1", "http://1.1.1.1/", { ctrlKey: true }],
+ ["ftp.example.bar", "http://ftp.example.bar/", { ctrlKey: true }],
+ [
+ "ex ample",
+ defaultEngine.getSubmission("ex ample", null, "keyword").uri.spec,
+ { ctrlKey: true },
+ ],
+ ];
+
+ if (Services.prefs.getBoolPref("network.ftp.enabled")) {
+ // Include FTP testcase only if FTP protocol handler is enabled, otherwise
+ // the test would hang on external application chooser popup.
+ testcases.push(["ftp://example", "ftp://example/", { ctrlKey: true }]);
+ }
+
+ // Disable autoFill for this test, since it could mess up the results.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.autoFill", false],
+ ["browser.urlbar.ctrlCanonizesURLs", true],
+ ],
+ });
+
+ for (let [inputValue, expectedURL, options] of testcases) {
+ info(`Testing input string: "${inputValue}" - expected: "${expectedURL}"`);
+ let promiseLoad = BrowserTestUtils.waitForDocLoadAndStopIt(
+ expectedURL,
+ gBrowser.selectedBrowser
+ );
+ let promiseStopped = BrowserTestUtils.browserStopped(
+ gBrowser.selectedBrowser,
+ undefined,
+ true
+ );
+ gURLBar.focus();
+ gURLBar.inputField.value = inputValue.slice(0, -1);
+ EventUtils.sendString(inputValue.slice(-1));
+ EventUtils.synthesizeKey("KEY_Enter", options);
+ await Promise.all([promiseLoad, promiseStopped]);
+ }
+});
+
+add_task(async function checkPrefTurnsOffCanonize() {
+ // Add a dummy search engine to avoid hitting the network.
+ let engine = await SearchTestUtils.promiseNewSearchEngine(
+ getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME
+ );
+ let oldDefaultEngine = await Services.search.getDefault();
+ await Services.search.setDefault(engine);
+ registerCleanupFunction(async () =>
+ Services.search.setDefault(oldDefaultEngine)
+ );
+
+ // Ensure we don't end up loading something in the current tab becuase it's empty:
+ let initialTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "about:mozilla",
+ });
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.ctrlCanonizesURLs", false]],
+ });
+
+ let newURL = "http://mochi.test:8888/?terms=example";
+ // On MacOS CTRL+Enter is not supposed to open in a new tab, because it uses
+ // CMD+Enter for that.
+ let promiseLoaded =
+ AppConstants.platform == "macosx"
+ ? BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false, newURL)
+ : BrowserTestUtils.waitForNewTab(gBrowser);
+
+ gURLBar.focus();
+ gURLBar.selectionStart = gURLBar.selectionEnd =
+ gURLBar.inputField.value.length;
+ gURLBar.inputField.value = "exampl";
+ EventUtils.sendString("e");
+ EventUtils.synthesizeKey("KEY_Enter", { ctrlKey: true });
+
+ await promiseLoaded;
+ if (AppConstants.platform == "macosx") {
+ Assert.equal(
+ initialTab.linkedBrowser.currentURI.spec,
+ newURL,
+ "Original tab should have navigated"
+ );
+ } else {
+ Assert.equal(
+ initialTab.linkedBrowser.currentURI.spec,
+ "about:mozilla",
+ "Original tab shouldn't have navigated"
+ );
+ Assert.equal(
+ gBrowser.selectedBrowser.currentURI.spec,
+ newURL,
+ "New tab should have navigated"
+ );
+ }
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeTab(gBrowser.selectedTab, { animate: false });
+ }
+});
+
+add_task(async function autofill() {
+ // Re-enable autofill and canonization.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.autoFill", true],
+ ["browser.urlbar.ctrlCanonizesURLs", true],
+ ],
+ });
+
+ // Quantumbar automatically disables autofill when the old search string
+ // starts with the new search string, so to make sure that doesn't happen and
+ // that earlier tests don't conflict with this one, start a new search for
+ // some other string.
+ gURLBar.select();
+ EventUtils.sendString("blah");
+
+ // Add a visit that will be autofilled.
+ await PlacesUtils.history.clear();
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com/",
+ transition: PlacesUtils.history.TRANSITIONS.TYPED,
+ },
+ ]);
+
+ let testcases = [
+ ["ex", "https://www.ex.com/", { ctrlKey: true }],
+ // Check that a direct load is not overwritten by a previous canonization.
+ ["ex", "http://example.com/", {}],
+ // search alias
+ ["@goo", "https://www.goo.com/", { ctrlKey: true }],
+ ];
+
+ function promiseAutofill() {
+ return BrowserTestUtils.waitForEvent(gURLBar.inputField, "select");
+ }
+
+ for (let [inputValue, expectedURL, options] of testcases) {
+ let promiseLoad = BrowserTestUtils.waitForDocLoadAndStopIt(
+ expectedURL,
+ gBrowser.selectedBrowser
+ );
+ gURLBar.select();
+ let autofillPromise = promiseAutofill();
+ EventUtils.sendString(inputValue);
+ await autofillPromise;
+ EventUtils.synthesizeKey("KEY_Enter", options);
+ await promiseLoad;
+
+ // Here again, make sure autofill isn't disabled for the next search. See
+ // the comment above.
+ gURLBar.select();
+ EventUtils.sendString("blah");
+ }
+
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function() {
+ info(
+ "Test whether canonization is disabled until the ctrl key is releasing if the key was used to paste text into urlbar"
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.ctrlCanonizesURLs", true]],
+ });
+
+ info("Paste the word to the urlbar");
+ const testWord = "example";
+ simulatePastingToUrlbar(testWord);
+ is(gURLBar.value, testWord, "Paste the test word correctly");
+
+ info("Send enter key while pressing the ctrl key");
+ EventUtils.synthesizeKey("VK_RETURN", { type: "keydown", ctrlKey: true });
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ is(
+ gBrowser.selectedBrowser.documentURI.spec,
+ `http://mochi.test:8888/?terms=${testWord}`,
+ "The loaded url is not canonized"
+ );
+
+ EventUtils.synthesizeKey("VK_CONTROL", { type: "keyup" });
+});
+
+add_task(async function() {
+ info("Test whether canonization is enabled again after releasing the ctrl");
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.ctrlCanonizesURLs", true]],
+ });
+
+ info("Paste the word to the urlbar");
+ const testWord = "example";
+ simulatePastingToUrlbar(testWord);
+ is(gURLBar.value, testWord, "Paste the test word correctly");
+
+ info("Release the ctrl key befoer typing Enter key");
+ EventUtils.synthesizeKey("VK_CONTROL", { type: "keyup" });
+
+ info("Send enter key with the ctrl");
+ const onLoad = BrowserTestUtils.waitForDocLoadAndStopIt(
+ `https://www.${testWord}.com/`,
+ gBrowser.selectedBrowser
+ );
+ EventUtils.synthesizeKey("VK_RETURN", { type: "keydown", ctrlKey: true });
+ await onLoad;
+ info("The loaded url is canonized");
+});
+
+function simulatePastingToUrlbar(text) {
+ gURLBar.focus();
+
+ const keyForPaste = document
+ .getElementById("key_paste")
+ .getAttribute("key")
+ .toLowerCase();
+ EventUtils.synthesizeKey(keyForPaste, { type: "keydown", ctrlKey: true });
+
+ gURLBar.select();
+ EventUtils.sendString(text);
+}
diff --git a/browser/components/urlbar/tests/browser/browser_caret_navigation.js b/browser/components/urlbar/tests/browser/browser_caret_navigation.js
new file mode 100644
index 0000000000..890acd30d6
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_caret_navigation.js
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that up and down keys move the caret on certain platforms, and that
+ * opening the popup doesn't change the caret position.
+ */
+
+add_task(async function() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "This is a generic sentence",
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ const INITIAL_SELECTION_START = 3;
+ const INITIAL_SELECTION_END = 10;
+ gURLBar.selectionStart = INITIAL_SELECTION_START;
+ gURLBar.selectionEnd = INITIAL_SELECTION_END;
+
+ if (AppConstants.platform == "macosx" || AppConstants.platform == "linux") {
+ if (AppConstants.platform == "linux") {
+ await checkCaretMoves(
+ "KEY_ArrowUp",
+ INITIAL_SELECTION_START,
+ "Selection should be collapsed to its start"
+ );
+
+ gURLBar.selectionStart = INITIAL_SELECTION_START;
+ gURLBar.selectionEnd = INITIAL_SELECTION_END;
+ await checkCaretMoves(
+ "KEY_ArrowDown",
+ INITIAL_SELECTION_END,
+ "Selection should be collapsed to its end"
+ );
+ }
+
+ await checkCaretMoves(
+ "KEY_ArrowDown",
+ gURLBar.value.length,
+ "Caret should have moved to the end"
+ );
+ await checkPopupOpens("KEY_ArrowDown");
+
+ await checkCaretMoves(
+ "KEY_ArrowUp",
+ 0,
+ "Caret should have moved to the start"
+ );
+ await checkPopupOpens("KEY_ArrowUp");
+ } else {
+ await checkPopupOpens("KEY_ArrowDown");
+ await checkPopupOpens("KEY_ArrowUp");
+ }
+});
+
+async function checkCaretMoves(key, pos, msg) {
+ checkIfKeyStartsQuery(key, false);
+ Assert.equal(
+ UrlbarTestUtils.isPopupOpen(window),
+ false,
+ `${key}: Popup shouldn't be open`
+ );
+ Assert.equal(
+ gURLBar.selectionStart,
+ gURLBar.selectionEnd,
+ `${key}: Input selection should be empty`
+ );
+ Assert.equal(gURLBar.selectionStart, pos, `${key}: ${msg}`);
+}
+
+async function checkPopupOpens(key) {
+ // Store current selection and check it doesn't change.
+ let selectionStart = gURLBar.selectionStart;
+ let selectionEnd = gURLBar.selectionEnd;
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ checkIfKeyStartsQuery(key, true);
+ });
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 0,
+ `${key}: Heuristic result should be selected`
+ );
+ Assert.equal(
+ gURLBar.selectionStart,
+ selectionStart,
+ `${key}: Input selection start should not change`
+ );
+ Assert.equal(
+ gURLBar.selectionEnd,
+ selectionEnd,
+ `${key}: Input selection end should not change`
+ );
+ await UrlbarTestUtils.promisePopupClose(window);
+}
+
+function checkIfKeyStartsQuery(key, shouldStartQuery) {
+ let queryStarted = false;
+ let queryListener = {
+ onQueryStarted() {
+ queryStarted = true;
+ },
+ };
+ gURLBar.controller.addQueryListener(queryListener);
+ EventUtils.synthesizeKey(key);
+ gURLBar.eventBufferer.replayDeferredEvents(false);
+ gURLBar.controller.removeQueryListener(queryListener);
+ Assert.equal(
+ queryStarted,
+ shouldStartQuery,
+ `${key}: Should${shouldStartQuery ? "" : "n't"} have started a query`
+ );
+}
diff --git a/browser/components/urlbar/tests/browser/browser_closePanelOnClick.js b/browser/components/urlbar/tests/browser/browser_closePanelOnClick.js
new file mode 100644
index 0000000000..f7d88ffbf5
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_closePanelOnClick.js
@@ -0,0 +1,30 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This tests that the urlbar panel closes when clicking certain ui elements.
+ */
+
+"use strict";
+
+add_task(async function() {
+ await BrowserTestUtils.withNewTab("about:robots", async () => {
+ for (let elt of [
+ gBrowser.selectedBrowser,
+ gBrowser.tabContainer,
+ document.querySelector("#nav-bar toolbarspring"),
+ ]) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value: "dummy",
+ });
+ // Must have at least one test.
+ Assert.ok(!!elt, "Found a valid element: " + (elt.id || elt.localName));
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeNativeMouseClickAtCenter(elt)
+ );
+ }
+ });
+});
diff --git a/browser/components/urlbar/tests/browser/browser_content_opener.js b/browser/components/urlbar/tests/browser/browser_content_opener.js
new file mode 100644
index 0000000000..1f7b80b21f
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_content_opener.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function() {
+ await BrowserTestUtils.withNewTab(
+ TEST_BASE_URL + "dummy_page.html",
+ async function(browser) {
+ let windowOpenedPromise = BrowserTestUtils.waitForNewWindow();
+ await SpecialPowers.spawn(browser, [], function() {
+ content.window.open("", "_BLANK", "toolbar=no,height=300,width=500");
+ });
+ let newWin = await windowOpenedPromise;
+ is(
+ newWin.gURLBar.value,
+ "about:blank",
+ "Should be displaying about:blank for the opened window."
+ );
+ await BrowserTestUtils.closeWindow(newWin);
+ }
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_copy_during_load.js b/browser/components/urlbar/tests/browser/browser_copy_during_load.js
new file mode 100644
index 0000000000..b24cbe077d
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_copy_during_load.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that copying from the urlbar page works correctly after a result is
+// confirmed but takes a while to load.
+
+add_task(async function() {
+ const SLOW_PAGE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://www.example.com"
+ ) + "slow-page.sjs";
+
+ await BrowserTestUtils.withNewTab(gBrowser, async tab => {
+ gURLBar.focus();
+ gURLBar.value = SLOW_PAGE;
+ let promise = TestUtils.waitForCondition(
+ () => gURLBar.getAttribute("pageproxystate") == "invalid"
+ );
+ EventUtils.synthesizeKey("KEY_Enter");
+ info("wait for the initial conditions");
+ await promise;
+
+ info("Copy the whole url");
+ await SimpleTest.promiseClipboardChange(SLOW_PAGE, () => {
+ gURLBar.select();
+ goDoCommand("cmd_copy");
+ });
+
+ info("Copy the initial part of the url, as a different valid url");
+ await SimpleTest.promiseClipboardChange(
+ SLOW_PAGE.substring(0, SLOW_PAGE.indexOf("slow-page.sjs")),
+ () => {
+ gURLBar.selectionStart = 0;
+ gURLBar.selectionEnd = gURLBar.value.indexOf("slow-page.sjs");
+ goDoCommand("cmd_copy");
+ }
+ );
+
+ // This is apparently necessary to avoid a timeout on mochitest shutdown(!?)
+ let browserStoppedPromise = BrowserTestUtils.browserStopped(
+ gBrowser,
+ null,
+ true
+ );
+ BrowserStop();
+ await browserStoppedPromise;
+ });
+});
diff --git a/browser/components/urlbar/tests/browser/browser_copying.js b/browser/components/urlbar/tests/browser/browser_copying.js
new file mode 100644
index 0000000000..c1858cffa8
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_copying.js
@@ -0,0 +1,386 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function getUrl(hostname, file) {
+ return (
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ hostname
+ ) + file
+ );
+}
+
+add_task(async function() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ registerCleanupFunction(function() {
+ gBrowser.removeTab(tab);
+ gURLBar.setURI();
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.trimURLs", true],
+ // avoid prompting about phishing
+ ["network.http.phishy-userpass-length", 32],
+ ],
+ });
+
+ for (let testCase of tests) {
+ if (testCase.setup) {
+ await testCase.setup();
+ }
+
+ if (testCase.loadURL) {
+ info(`Loading : ${testCase.loadURL}`);
+ let expectedLoad = testCase.expectedLoad || testCase.loadURL;
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, testCase.loadURL);
+ await BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ expectedLoad
+ );
+ } else if (testCase.setURL) {
+ gURLBar.value = testCase.setURL;
+ }
+ if (testCase.setURL || testCase.loadURL) {
+ gURLBar.valueIsTyped = !!testCase.setURL;
+ is(
+ gURLBar.value,
+ testCase.expectedURL,
+ "url bar value set to " + gURLBar.value
+ );
+ }
+
+ gURLBar.focus();
+ if (testCase.expectedValueOnFocus) {
+ Assert.equal(
+ gURLBar.value,
+ testCase.expectedValueOnFocus,
+ "Check value on focus"
+ );
+ }
+ await testCopy(testCase.copyVal, testCase.copyExpected);
+ gURLBar.blur();
+
+ if (testCase.cleanup) {
+ await testCase.cleanup();
+ }
+ }
+});
+
+var tests = [
+ // pageproxystate="invalid"
+ {
+ setURL: "http://example.com/",
+ expectedURL: "example.com",
+ copyExpected: "example.com",
+ },
+ {
+ copyVal: "<e>xample.com",
+ copyExpected: "e",
+ },
+ {
+ copyVal: "<e>x<a>mple.com",
+ copyExpected: "ea",
+ },
+ {
+ copyVal: "<e><xa>mple.com",
+ copyExpected: "exa",
+ },
+ {
+ copyVal: "<e><xa>mple.co<m>",
+ copyExpected: "exam",
+ },
+ {
+ copyVal: "<e><xample.co><m>",
+ copyExpected: "example.com",
+ },
+
+ // pageproxystate="valid" from this point on (due to the load)
+ {
+ loadURL: "http://example.com/",
+ expectedURL: "example.com",
+ copyExpected: "http://example.com/",
+ },
+ {
+ copyVal: "<example.co>m",
+ copyExpected: "example.co",
+ },
+ {
+ copyVal: "e<x>ample.com",
+ copyExpected: "x",
+ },
+ {
+ copyVal: "<e>xample.com",
+ copyExpected: "e",
+ },
+ {
+ copyVal: "<e>xample.co<m>",
+ copyExpected: "em",
+ },
+ {
+ copyVal: "<exam><ple.com>",
+ copyExpected: "example.com",
+ },
+
+ {
+ loadURL: "http://example.com/foo",
+ expectedURL: "example.com/foo",
+ copyExpected: "http://example.com/foo",
+ },
+ {
+ copyVal: "<example.com>/foo",
+ copyExpected: "http://example.com",
+ },
+ {
+ copyVal: "<example>.com/foo",
+ copyExpected: "example",
+ },
+
+ // Test that userPass is stripped out
+ {
+ loadURL: getUrl(
+ "http://user:pass@mochi.test:8888",
+ "authenticate.sjs?user=user&pass=pass"
+ ),
+ expectedURL: getUrl(
+ "mochi.test:8888",
+ "authenticate.sjs?user=user&pass=pass"
+ ),
+ copyExpected: getUrl(
+ "http://mochi.test:8888",
+ "authenticate.sjs?user=user&pass=pass"
+ ),
+ },
+
+ // Test escaping
+ {
+ loadURL: "http://example.com/()%28%29%C3%A9",
+ expectedURL: "example.com/()()\xe9",
+ copyExpected: "http://example.com/()%28%29%C3%A9",
+ },
+ {
+ copyVal: "<example.com/(>)()\xe9",
+ copyExpected: "http://example.com/(",
+ },
+ {
+ copyVal: "e<xample.com/(>)()\xe9",
+ copyExpected: "xample.com/(",
+ },
+
+ {
+ loadURL: "http://example.com/%C3%A9%C3%A9",
+ expectedURL: "example.com/\xe9\xe9",
+ copyExpected: "http://example.com/%C3%A9%C3%A9",
+ },
+ {
+ copyVal: "e<xample.com/\xe9>\xe9",
+ copyExpected: "xample.com/\xe9",
+ },
+ {
+ copyVal: "<example.com/\xe9>\xe9",
+ copyExpected: "http://example.com/\xe9",
+ },
+ {
+ // Note: it seems BrowserTestUtils.loadURI fails for unicode domains
+ loadURL: "http://sub2.xn--lt-uia.mochi.test:8888/foo",
+ expectedURL: "sub2.ält.mochi.test:8888/foo",
+ copyExpected: "http://sub2.ält.mochi.test:8888/foo",
+ },
+ {
+ copyVal: "s<ub2.ält.mochi.test:8888/f>oo",
+ copyExpected: "ub2.ält.mochi.test:8888/f",
+ },
+ {
+ copyVal: "<sub2.ält.mochi.test:8888/f>oo",
+ copyExpected: "http://sub2.ält.mochi.test:8888/f",
+ },
+
+ {
+ loadURL: "http://example.com/?%C3%B7%C3%B7",
+ expectedURL: "example.com/?\xf7\xf7",
+ copyExpected: "http://example.com/?%C3%B7%C3%B7",
+ },
+ {
+ copyVal: "e<xample.com/?\xf7>\xf7",
+ copyExpected: "xample.com/?\xf7",
+ },
+ {
+ copyVal: "<example.com/?\xf7>\xf7",
+ copyExpected: "http://example.com/?\xf7",
+ },
+ {
+ loadURL: "http://example.com/a%20test",
+ expectedURL: "example.com/a test",
+ copyExpected: "http://example.com/a%20test",
+ },
+ {
+ loadURL: "http://example.com/a%E3%80%80test",
+ expectedURL: "example.com/a%E3%80%80test",
+ copyExpected: "http://example.com/a%E3%80%80test",
+ },
+ {
+ loadURL: "http://example.com/a%20%C2%A0test",
+ expectedURL: "example.com/a %C2%A0test",
+ copyExpected: "http://example.com/a%20%C2%A0test",
+ },
+ {
+ loadURL: "http://example.com/%20%20%20",
+ expectedURL: "example.com/%20%20%20",
+ copyExpected: "http://example.com/%20%20%20",
+ },
+ {
+ loadURL: "http://example.com/%E3%80%80%E3%80%80",
+ expectedURL: "example.com/%E3%80%80%E3%80%80",
+ copyExpected: "http://example.com/%E3%80%80%E3%80%80",
+ },
+
+ // Loading of javascript: URI results in previous URI, so if the previous
+ // entry changes, change this one too!
+ {
+ loadURL: "javascript:('%C3%A9%20%25%50')",
+ expectedLoad: "http://example.com/%E3%80%80%E3%80%80",
+ expectedURL: "example.com/%E3%80%80%E3%80%80",
+ copyExpected: "http://example.com/%E3%80%80%E3%80%80",
+ },
+
+ // data: URIs shouldn't be encoded
+ {
+ loadURL: "data:text/html,(%C3%A9%20%25%50)",
+ expectedURL: "data:text/html,(%C3%A9 %25P)",
+ copyExpected: "data:text/html,(%C3%A9 %25P)",
+ },
+ {
+ copyVal: "<data:text/html,(>%C3%A9 %25P)",
+ copyExpected: "data:text/html,(",
+ },
+ {
+ copyVal: "<data:text/html,(%C3%A9 %25P>)",
+ copyExpected: "data:text/html,(%C3%A9 %25P",
+ },
+
+ {
+ async setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.decodeURLsOnCopy", true]],
+ });
+ },
+ async cleanup() {
+ await SpecialPowers.popPrefEnv();
+ },
+ loadURL:
+ "http://example.com/%D0%B1%D0%B8%D0%BE%D0%B3%D1%80%D0%B0%D1%84%D0%B8%D1%8F",
+ expectedURL: "example.com/биография",
+ copyExpected: "http://example.com/биография",
+ },
+ {
+ copyVal: "<example.com/би>ография",
+ copyExpected: "http://example.com/би",
+ },
+
+ {
+ async setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.decodeURLsOnCopy", true]],
+ });
+ // Setup a valid intranet url that resolves but is not yet known.
+ const proxyService = Cc[
+ "@mozilla.org/network/protocol-proxy-service;1"
+ ].getService(Ci.nsIProtocolProxyService);
+ let proxyInfo = proxyService.newProxyInfo(
+ "http",
+ "localhost",
+ 8888,
+ "",
+ "",
+ 0,
+ 4096,
+ null
+ );
+ this._proxyFilter = {
+ applyFilter(channel, defaultProxyInfo, callback) {
+ callback.onProxyFilterResult(
+ channel.URI.host === "mytest" ? proxyInfo : defaultProxyInfo
+ );
+ },
+ };
+ proxyService.registerChannelFilter(this._proxyFilter, 0);
+ registerCleanupFunction(() => {
+ if (this._proxyFilter) {
+ proxyService.unregisterChannelFilter(this._proxyFilter);
+ }
+ });
+ },
+ async cleanup() {
+ await SpecialPowers.popPrefEnv();
+ const proxyService = Cc[
+ "@mozilla.org/network/protocol-proxy-service;1"
+ ].getService(Ci.nsIProtocolProxyService);
+ proxyService.unregisterChannelFilter(this._proxyFilter);
+ this._proxyFilter = null;
+ },
+ loadURL: "http://mytest/",
+ expectedURL: "mytest",
+ expectedValueOnFocus: "http://mytest/",
+ copyExpected: "http://mytest/",
+ },
+
+ {
+ async setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.decodeURLsOnCopy", true]],
+ });
+ },
+ async cleanup() {
+ await SpecialPowers.popPrefEnv();
+ },
+ loadURL: "https://example.com/",
+ expectedURL: "https://example.com",
+ copyExpected: "https://example.com",
+ },
+];
+
+function testCopy(copyVal, targetValue) {
+ info("Expecting copy of: " + targetValue);
+
+ if (copyVal) {
+ let offsets = [];
+ while (true) {
+ let startBracket = copyVal.indexOf("<");
+ let endBracket = copyVal.indexOf(">");
+ if (startBracket == -1 && endBracket == -1) {
+ break;
+ }
+ if (startBracket > endBracket || startBracket == -1) {
+ offsets = [];
+ break;
+ }
+ offsets.push([startBracket, endBracket - 1]);
+ copyVal = copyVal.replace("<", "").replace(">", "");
+ }
+ if (!offsets.length || copyVal != gURLBar.value) {
+ ok(false, "invalid copyVal: " + copyVal);
+ }
+ gURLBar.selectionStart = offsets[0][0];
+ gURLBar.selectionEnd = offsets[0][1];
+ if (offsets.length > 1) {
+ let sel = gURLBar.editor.selection;
+ let r0 = sel.getRangeAt(0);
+ let node0 = r0.startContainer;
+ sel.removeAllRanges();
+ offsets.map(function(startEnd) {
+ let range = r0.cloneRange();
+ range.setStart(node0, startEnd[0]);
+ range.setEnd(node0, startEnd[1]);
+ sel.addRange(range);
+ });
+ }
+ } else {
+ gURLBar.select();
+ }
+
+ return SimpleTest.promiseClipboardChange(targetValue, () =>
+ goDoCommand("cmd_copy")
+ );
+}
diff --git a/browser/components/urlbar/tests/browser/browser_customizeMode.js b/browser/components/urlbar/tests/browser/browser_customizeMode.js
new file mode 100644
index 0000000000..0ed26644cc
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_customizeMode.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test checks that the left/right arrow keys and home/end keys work in
+// the urlbar after customize mode starts and ends.
+
+"use strict";
+
+add_task(async function test() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await startCustomizing(win);
+ await endCustomizing(win);
+
+ let urlbar = win.gURLBar;
+
+ let value = "example";
+ urlbar.value = value;
+ urlbar.focus();
+ urlbar.selectionEnd = value.length;
+ urlbar.selectionStart = value.length;
+
+ // left
+ EventUtils.synthesizeKey("KEY_ArrowLeft", {}, win);
+ Assert.equal(urlbar.selectionStart, value.length - 1);
+ Assert.equal(urlbar.selectionEnd, value.length - 1);
+
+ // home
+ if (AppConstants.platform == "macosx") {
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { metaKey: true }, win);
+ } else {
+ EventUtils.synthesizeKey("KEY_Home", {}, win);
+ }
+ Assert.equal(urlbar.selectionStart, 0);
+ Assert.equal(urlbar.selectionEnd, 0);
+
+ // right
+ EventUtils.synthesizeKey("KEY_ArrowRight", {}, win);
+ Assert.equal(urlbar.selectionStart, 1);
+ Assert.equal(urlbar.selectionEnd, 1);
+
+ // end
+ if (AppConstants.platform == "macosx") {
+ EventUtils.synthesizeKey("KEY_ArrowRight", { metaKey: true }, win);
+ } else {
+ EventUtils.synthesizeKey("KEY_End", {}, win);
+ }
+ Assert.equal(urlbar.selectionStart, value.length);
+ Assert.equal(urlbar.selectionEnd, value.length);
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+async function startCustomizing(win = window) {
+ if (win.document.documentElement.getAttribute("customizing") != "true") {
+ let eventPromise = BrowserTestUtils.waitForEvent(
+ win.gNavToolbox,
+ "customizationready"
+ );
+ win.gCustomizeMode.enter();
+ await eventPromise;
+ }
+}
+
+async function endCustomizing(win = window) {
+ if (win.document.documentElement.getAttribute("customizing") == "true") {
+ let eventPromise = BrowserTestUtils.waitForEvent(
+ win.gNavToolbox,
+ "aftercustomization"
+ );
+ win.gCustomizeMode.exit();
+ await eventPromise;
+ }
+}
diff --git a/browser/components/urlbar/tests/browser/browser_cutting.js b/browser/components/urlbar/tests/browser/browser_cutting.js
new file mode 100644
index 0000000000..31b51751d9
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_cutting.js
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ gURLBar.focus();
+ gURLBar.inputField.value = "https://example.com/";
+ gURLBar.selectionStart = 4;
+ gURLBar.selectionEnd = 5;
+ goDoCommand("cmd_cut");
+ is(
+ gURLBar.inputField.value,
+ "http://example.com/",
+ "location bar value after cutting 's' from https"
+ );
+ gURLBar.handleRevert();
+}
diff --git a/browser/components/urlbar/tests/browser/browser_decode.js b/browser/components/urlbar/tests/browser/browser_decode.js
new file mode 100644
index 0000000000..4dc199c1f7
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_decode.js
@@ -0,0 +1,144 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// This test makes sure (1) you can't break the urlbar by typing particular JSON
+// or JS fragments into it, (2) urlbar.textValue shows URLs unescaped, and (3)
+// the urlbar also shows the URLs embedded in action URIs unescaped. See bug
+// 1233672.
+
+add_task(async function injectJSON() {
+ let inputStrs = [
+ 'http://example.com/ ", "url": "bar',
+ "http://example.com/\\",
+ 'http://example.com/"',
+ 'http://example.com/","url":"evil.com',
+ "http://mozilla.org/\\u0020",
+ 'http://www.mozilla.org/","url":1e6,"some-key":"foo',
+ 'http://www.mozilla.org/","url":null,"some-key":"foo',
+ 'http://www.mozilla.org/","url":["foo","bar"],"some-key":"foo',
+ ];
+ for (let inputStr of inputStrs) {
+ await checkInput(inputStr);
+ }
+ gURLBar.value = "";
+ gURLBar.handleRevert();
+ gURLBar.blur();
+});
+
+add_task(function losslessDecode() {
+ let urlNoScheme = "example.com/\u30a2\u30a4\u30a6\u30a8\u30aa";
+ let url = "http://" + urlNoScheme;
+ const result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url }
+ );
+ gURLBar.setValueFromResult(result);
+ // Since this is directly setting textValue, it is expected to be trimmed.
+ Assert.equal(
+ gURLBar.inputField.value,
+ urlNoScheme,
+ "The string displayed in the textbox should not be escaped"
+ );
+ gURLBar.value = "";
+ gURLBar.handleRevert();
+ gURLBar.blur();
+});
+
+add_task(async function actionURILosslessDecode() {
+ let urlNoScheme = "example.com/\u30a2\u30a4\u30a6\u30a8\u30aa";
+ let url = "http://" + urlNoScheme;
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: url,
+ });
+
+ // At this point the heuristic result is selected but the urlbar's value is
+ // simply `url`. Key down and back around until the heuristic result is
+ // selected again.
+ do {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ } while (UrlbarTestUtils.getSelectedRowIndex(window) != 0);
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.URL,
+ "Should have selected a result of URL type"
+ );
+
+ Assert.equal(
+ gURLBar.inputField.value,
+ urlNoScheme,
+ "The string displayed in the textbox should not be escaped"
+ );
+
+ gURLBar.value = "";
+ gURLBar.handleRevert();
+ gURLBar.blur();
+});
+
+add_task(async function test_resultsDisplayDecoded() {
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.formHistory.clear();
+
+ await PlacesTestUtils.addVisits("http://example.com/%E9%A1%B5");
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "example",
+ });
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(
+ result.displayed.url,
+ "http://example.com/\u9875",
+ "Should be displayed the correctly unescaped URL"
+ );
+});
+
+async function checkInput(inputStr) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: inputStr,
+ });
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+
+ // URL matches have their param.urls fixed up.
+ let fixupInfo = Services.uriFixup.getFixupURIInfo(
+ inputStr,
+ Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS |
+ Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP
+ );
+ let expectedVisitURL = fixupInfo.fixedURI.spec;
+
+ Assert.equal(result.url, expectedVisitURL, "Should have the correct URL");
+ Assert.equal(
+ result.title,
+ inputStr.replace("\\", "/"),
+ "Should have the correct title"
+ );
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.URL,
+ "Should have be a result of type URL"
+ );
+
+ Assert.equal(
+ result.displayed.title,
+ inputStr.replace("\\", "/"),
+ "Should be displaying the correct text"
+ );
+ let [action] = await document.l10n.formatValues([
+ { id: "urlbar-result-action-visit" },
+ ]);
+ Assert.equal(
+ result.displayed.action,
+ action,
+ "Should be displaying the correct action text"
+ );
+}
diff --git a/browser/components/urlbar/tests/browser/browser_delete.js b/browser/components/urlbar/tests/browser/browser_delete.js
new file mode 100644
index 0000000000..48fce657ad
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_delete.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test deleting the start of urls works correctly.
+ */
+
+add_task(async function() {
+ let bm = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://bug1105244.example.com/",
+ title: "test",
+ });
+
+ registerCleanupFunction(async function() {
+ await PlacesUtils.bookmarks.remove(bm);
+ });
+
+ await BrowserTestUtils.withNewTab("about:blank", testDelete);
+});
+
+function sendHome() {
+ // unclear why VK_HOME doesn't work on Mac, but it doesn't...
+ if (AppConstants.platform == "macosx") {
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { metaKey: true });
+ } else {
+ EventUtils.synthesizeKey("KEY_Home");
+ }
+}
+
+function sendDelete() {
+ EventUtils.synthesizeKey("KEY_Delete");
+}
+
+async function testDelete() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "bug1105244",
+ });
+
+ // move to the start.
+ sendHome();
+
+ // delete the first few chars - each delete should operate on the input field.
+ await UrlbarTestUtils.promisePopupOpen(window, sendDelete);
+ Assert.equal(gURLBar.inputField.value, "ug1105244.example.com/");
+ sendDelete();
+ Assert.equal(gURLBar.inputField.value, "g1105244.example.com/");
+}
diff --git a/browser/components/urlbar/tests/browser/browser_deleteAllText.js b/browser/components/urlbar/tests/browser/browser_deleteAllText.js
new file mode 100644
index 0000000000..5b355fa477
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_deleteAllText.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test makes sure that deleting all text in the input doesn't mess up
+// subsequent searches.
+
+"use strict";
+
+add_task(async function test() {
+ await runTest();
+ // Setting suggest.topsites to false disables the view's autoOpen behavior,
+ // which changes this test's outcomes.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.topsites", false]],
+ });
+ info("Running the test with autoOpen disabled.");
+ await runTest();
+ await SpecialPowers.popPrefEnv();
+});
+
+async function runTest() {
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+ await PlacesTestUtils.addVisits([
+ "http://example.com/",
+ "http://mozilla.org/",
+ ]);
+
+ // Do an initial search for "x".
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "x",
+ fireInputEvent: true,
+ });
+ await checkResults();
+
+ await deleteInput();
+
+ // Type "x". A new search should start. Don't use
+ // promiseAutocompleteResultPopup, which has some logic that starts the search
+ // manually in certain conditions. We want to specifically check that the
+ // input event causes UrlbarInput to start a new search on its own. If it
+ // doesn't, then the test will hang here on promiseSearchComplete.
+ EventUtils.synthesizeKey("x");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await checkResults();
+
+ // Now repeat the backspace + x two more times. Same thing should happen.
+ for (let i = 0; i < 2; i++) {
+ await deleteInput();
+ EventUtils.synthesizeKey("x");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await checkResults();
+ }
+
+ await deleteInput();
+ // autoOpen opened the panel, so we need to close it.
+ gURLBar.view.close();
+}
+
+async function checkResults() {
+ Assert.equal(await UrlbarTestUtils.getResultCount(window), 2);
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(details.type, UrlbarUtils.RESULT_TYPE.SEARCH);
+ Assert.equal(details.searchParams.query, "x");
+ details = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(details.type, UrlbarUtils.RESULT_TYPE.URL);
+ Assert.equal(details.url, "http://example.com/");
+}
+
+async function deleteInput() {
+ if (UrlbarPrefs.get("suggest.topsites")) {
+ // The popup should remain open and show top sites.
+ while (gURLBar.value.length) {
+ EventUtils.synthesizeKey("KEY_Backspace");
+ }
+ Assert.ok(
+ gURLBar.view.isOpen,
+ "View should remain open when deleting all input text"
+ );
+ let queryContext = await UrlbarTestUtils.promiseSearchComplete(window);
+ Assert.notEqual(
+ queryContext.results.length,
+ 0,
+ "View should show results when deleting all input text"
+ );
+ Assert.equal(
+ queryContext.searchString,
+ "",
+ "Results should be for the empty search string (i.e. top sites) when deleting all input text"
+ );
+ } else {
+ // Deleting all text should close the view.
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ while (gURLBar.value.length) {
+ EventUtils.synthesizeKey("KEY_Backspace");
+ }
+ });
+ }
+}
diff --git a/browser/components/urlbar/tests/browser/browser_display_selectedAction_Extensions.js b/browser/components/urlbar/tests/browser/browser_display_selectedAction_Extensions.js
new file mode 100644
index 0000000000..d3a51ede76
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_display_selectedAction_Extensions.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests for the presence of selected action text "Extensions:" in the URL bar.
+ */
+
+add_task(async function testSwitchToTabTextDisplay() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ omnibox: {
+ keyword: "omniboxtest",
+ },
+
+ background() {
+ /* global browser */
+ browser.omnibox.setDefaultSuggestion({
+ description: "doit",
+ });
+ // Just do nothing for this test.
+ },
+ },
+ });
+
+ await extension.startup();
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "omniboxtest ",
+ fireInputEvent: true,
+ });
+
+ // The "Extension:" label appears after a key down followed by a key up
+ // back to the extension result.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+
+ // Checks to see if "Extension:" text in URL bar is visible
+ const extensionText = document.getElementById("urlbar-label-extension");
+ Assert.ok(BrowserTestUtils.is_visible(extensionText));
+ Assert.equal(extensionText.value, "Extension:");
+
+ // Check to see if all other labels are hidden
+ const allLabels = document.getElementById("urlbar-label-box").children;
+ for (let label of allLabels) {
+ if (label != extensionText) {
+ Assert.ok(BrowserTestUtils.is_hidden(label));
+ }
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+ await extension.unload();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_dns_first_for_single_words.js b/browser/components/urlbar/tests/browser/browser_dns_first_for_single_words.js
new file mode 100644
index 0000000000..5a294afcbc
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_dns_first_for_single_words.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Checks that if browser.fixup.dns_first_for_single_words pref is set, we pass
+// the original search string to the docshell and not a search url.
+
+add_task(async function test() {
+ const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
+ const sandbox = sinon.createSandbox();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.fixup.dns_first_for_single_words", true]],
+ });
+
+ registerCleanupFunction(sandbox.restore);
+
+ /**
+ * Tests the given search string.
+ * @param {string} str The search string
+ * @param {boolean} passthrough whether the value should be passed unchanged
+ * to the docshell that will first execute a DNS request.
+ */
+ async function testVal(str, passthrough) {
+ sandbox.stub(gURLBar, "_loadURL").callsFake(url => {
+ if (passthrough) {
+ Assert.equal(url, str, "Should pass the unmodified search string");
+ } else {
+ Assert.ok(url.startsWith("http"), "Should pass an url");
+ }
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: str,
+ });
+ EventUtils.synthesizeKey("KEY_Enter");
+ sandbox.restore();
+ }
+
+ await testVal("test", true);
+ await testVal("te-st", true);
+ await testVal("test ", true);
+ await testVal(" test", true);
+ await testVal(" test", true);
+ await testVal("test.test", true);
+ await testVal("test test", false);
+ // This is not a single word host, though it contains one. At a certain point
+ // we may evaluate to increase coverage of the feature to also ask for this.
+ await testVal("test/test", false);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_downArrowKeySearch.js b/browser/components/urlbar/tests/browser/browser_downArrowKeySearch.js
new file mode 100644
index 0000000000..15273b4285
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_downArrowKeySearch.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Checks that pressing the down arrow key starts the proper searches, depending
+// on the input value/state.
+
+"use strict";
+
+add_task(async function init() {
+ await PlacesUtils.history.clear();
+ // Enough vists to get this site into Top Sites.
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits("http://example.com/");
+ }
+
+ await updateTopSites(
+ sites => sites && sites[0] && sites[0].url == "http://example.com/"
+ );
+ registerCleanupFunction(async function() {
+ await PlacesUtils.history.clear();
+ });
+});
+
+add_task(async function url() {
+ await BrowserTestUtils.withNewTab("http://example.com/", async () => {
+ gURLBar.focus();
+ gURLBar.selectionEnd = gURLBar.untrimmedValue.length;
+ gURLBar.selectionStart = gURLBar.untrimmedValue.length;
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), 0);
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(details.url, "http://example.com/");
+ Assert.equal(gURLBar.value, "example.com/");
+ await UrlbarTestUtils.promisePopupClose(window);
+ });
+});
+
+add_task(async function userTyping() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), 0);
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(details.type, UrlbarUtils.RESULT_TYPE.SEARCH);
+ Assert.ok(details.searchParams);
+ Assert.equal(details.searchParams.query, "foo");
+ Assert.equal(gURLBar.value, "foo");
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+add_task(async function empty() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), -1);
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(details.url, "http://example.com/");
+ Assert.equal(gURLBar.value, "");
+});
+
+add_task(async function new_window() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ win.gURLBar.focus();
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(win), -1);
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(win, 0);
+ Assert.equal(details.url, "http://example.com/");
+ Assert.equal(win.gURLBar.value, "");
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_dragdropURL.js b/browser/components/urlbar/tests/browser/browser_dragdropURL.js
new file mode 100644
index 0000000000..c4a4a04749
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_dragdropURL.js
@@ -0,0 +1,108 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests for draging and dropping to the Urlbar.
+ */
+
+const TEST_URL = "data:text/html,a test page";
+
+add_task(async function test_setup() {
+ // Stop search-engine loads from hitting the network.
+ await Services.search.addEngineWithDetails("MozSearch", {
+ method: "GET",
+ template: "http://example.com/?q={searchTerms}",
+ });
+ let originalEngine = await Services.search.getDefault();
+ let engine = Services.search.getEngineByName("MozSearch");
+ await Services.search.setDefault(engine);
+
+ registerCleanupFunction(async function cleanup() {
+ while (gBrowser.tabs.length > 1) {
+ BrowserTestUtils.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]);
+ }
+ await Services.search.setDefault(originalEngine);
+ await Services.search.removeEngine(engine);
+ });
+});
+
+/**
+ * Simulates a drop on the URL bar input field.
+ * The drag source must be something different from the URL bar, so we pick the
+ * home button somewhat arbitrarily.
+ * @param {object} content a {type, data} object representing the DND content.
+ */
+function simulateURLBarDrop(content) {
+ EventUtils.synthesizeDrop(
+ document.getElementById("home-button"), // Dragstart element.
+ gURLBar.inputField, // Drop element.
+ [[content]], // Drag data.
+ "copy",
+ window
+ );
+}
+
+add_task(async function checkDragURL() {
+ await BrowserTestUtils.withNewTab(TEST_URL, function(browser) {
+ info("Check dragging a normal url to the urlbar");
+ const DRAG_URL = "http://www.example.com/";
+ simulateURLBarDrop({ type: "text/plain", data: DRAG_URL });
+ Assert.equal(
+ gURLBar.value,
+ TEST_URL,
+ "URL bar value should not have changed"
+ );
+ Assert.equal(
+ gBrowser.selectedBrowser.userTypedValue,
+ null,
+ "Stored URL bar value should not have changed"
+ );
+ });
+});
+
+add_task(async function checkDragForbiddenURL() {
+ await BrowserTestUtils.withNewTab(TEST_URL, function(browser) {
+ // See also browser_removeUnsafeProtocolsFromURLBarPaste.js for other
+ // examples. In general we trust that function, we pick some testcases to
+ // ensure we disallow dropping trimmed text.
+ for (let url of [
+ "chrome://browser/content/aboutDialog.xhtml",
+ "file:///",
+ "javascript:",
+ "javascript:void(0)",
+ "java\r\ns\ncript:void(0)",
+ " javascript:void(0)",
+ "\u00A0java\nscript:void(0)",
+ "javascript:document.domain",
+ "javascript:javascript:alert('hi!')",
+ ]) {
+ info(`Check dragging "{$url}" to the URL bar`);
+ simulateURLBarDrop({ type: "text/plain", data: url });
+ Assert.notEqual(
+ gURLBar.value,
+ url,
+ `Shouldn't be allowed to drop ${url} on URL bar`
+ );
+ }
+ });
+});
+
+add_task(async function checkDragText() {
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ info("Check dragging multi word text to the urlbar");
+ const TEXT = "Firefox is awesome";
+ const TEXT_URL = "http://example.com/?q=Firefox+is+awesome";
+ let promiseLoad = BrowserTestUtils.browserLoaded(browser, false, TEXT_URL);
+ simulateURLBarDrop({ type: "text/plain", data: TEXT });
+ await promiseLoad;
+
+ info("Check dragging single word text to the urlbar");
+ const WORD = "Firefox";
+ const WORD_URL = "http://example.com/?q=Firefox";
+ promiseLoad = BrowserTestUtils.browserLoaded(browser, false, WORD_URL);
+ simulateURLBarDrop({ type: "text/plain", data: WORD });
+ await promiseLoad;
+ });
+});
diff --git a/browser/components/urlbar/tests/browser/browser_dynamicResults.js b/browser/components/urlbar/tests/browser/browser_dynamicResults.js
new file mode 100644
index 0000000000..97126f35c3
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_dynamicResults.js
@@ -0,0 +1,740 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This tests dynamic results.
+ */
+
+"use strict";
+
+const DYNAMIC_TYPE_NAME = "test";
+
+const DYNAMIC_TYPE_VIEW_TEMPLATE = {
+ stylesheet: getRootDirectory(gTestPath) + "dynamicResult0.css",
+ children: [
+ {
+ name: "selectable",
+ tag: "span",
+ attributes: {
+ selectable: "true",
+ },
+ },
+ {
+ name: "text",
+ tag: "span",
+ },
+ {
+ name: "buttonBox",
+ tag: "span",
+ children: [
+ {
+ name: "button1",
+ tag: "span",
+ attributes: {
+ role: "button",
+ },
+ },
+ {
+ name: "button2",
+ tag: "span",
+ attributes: {
+ role: "button",
+ },
+ },
+ ],
+ },
+ ],
+};
+
+const DUMMY_PAGE =
+ "http://example.com/browser/browser/base/content/test/general/dummy_page.html";
+
+// Tests the dynamic type registration functions and stylesheet loading.
+add_task(async function registration() {
+ // Get our test stylesheet URIs.
+ let stylesheetURIs = [];
+ for (let i = 0; i < 2; i++) {
+ stylesheetURIs.push(
+ Services.io.newURI(getRootDirectory(gTestPath) + `dynamicResult${i}.css`)
+ );
+ }
+
+ // Maps from dynamic type names to their type.
+ let viewTemplatesByName = {
+ foo: {
+ stylesheet: stylesheetURIs[0].spec,
+ children: [
+ {
+ name: "text",
+ tag: "span",
+ },
+ ],
+ },
+ bar: {
+ stylesheet: stylesheetURIs[1].spec,
+ children: [
+ {
+ name: "icon",
+ tag: "span",
+ },
+ {
+ name: "button",
+ tag: "span",
+ attributes: {
+ role: "button",
+ },
+ },
+ ],
+ },
+ };
+
+ // First, open another window so that multiple windows are open when we add
+ // the types so we can verify below that the stylesheets are added to all open
+ // windows.
+ let newWindows = [];
+ newWindows.push(await BrowserTestUtils.openNewBrowserWindow());
+
+ // Add the test dynamic types.
+ for (let [name, viewTemplate] of Object.entries(viewTemplatesByName)) {
+ UrlbarResult.addDynamicResultType(name);
+ UrlbarView.addDynamicViewTemplate(name, viewTemplate);
+ }
+
+ // Get them back to make sure they were added.
+ for (let name of Object.keys(viewTemplatesByName)) {
+ let actualType = UrlbarResult.getDynamicResultType(name);
+ // Types are currently just empty objects.
+ Assert.deepEqual(actualType, {}, "Types should match");
+ }
+
+ // Their stylesheets should have been applied to all open windows. There's no
+ // good way to check this because:
+ //
+ // * nsIStyleSheetService has a function that returns whether a stylesheet has
+ // been loaded, but it's global and not per window.
+ // * nsIDOMWindowUtils has functions to load stylesheets but not one to check
+ // whether a stylesheet has been loaded.
+ // * document.stylesheets only contains stylesheets in the DOM.
+ //
+ // So instead we set a CSS variable on #urlbar in each of our stylesheets and
+ // check that it's present.
+ function getCSSVariables(windows) {
+ let valuesByWindow = new Map();
+ for (let win of windows) {
+ let values = [];
+ valuesByWindow.set(window, values);
+ for (let i = 0; i < stylesheetURIs.length; i++) {
+ let value = win
+ .getComputedStyle(gURLBar.panel)
+ .getPropertyValue(`--testDynamicResult${i}`);
+ values.push((value || "").trim());
+ }
+ }
+ return valuesByWindow;
+ }
+ function checkCSSVariables(windows) {
+ for (let values of getCSSVariables(windows).values()) {
+ for (let i = 0; i < stylesheetURIs.length; i++) {
+ if (values[i].trim() !== `ok${i}`) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ // The stylesheets are loaded asyncly, so we need to poll for it.
+ await TestUtils.waitForCondition(() =>
+ checkCSSVariables(BrowserWindowTracker.orderedWindows)
+ );
+ Assert.ok(true, "Stylesheets loaded in all open windows");
+
+ // Open another window to make sure the stylesheets are loaded in it after we
+ // added the new dynamic types.
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+ newWindows.push(newWin);
+ await TestUtils.waitForCondition(() => checkCSSVariables([newWin]));
+ Assert.ok(true, "Stylesheets loaded in new window");
+
+ // Remove the dynamic types.
+ for (let name of Object.keys(viewTemplatesByName)) {
+ UrlbarView.removeDynamicViewTemplate(name);
+ UrlbarResult.removeDynamicResultType(name);
+ let actualType = UrlbarResult.getDynamicResultType(name);
+ Assert.equal(actualType, null, "Type should be unregistered");
+ }
+
+ // The stylesheets should be removed from all windows.
+ let valuesByWindow = getCSSVariables(BrowserWindowTracker.orderedWindows);
+ for (let values of valuesByWindow.values()) {
+ for (let i = 0; i < stylesheetURIs.length; i++) {
+ Assert.ok(!values[i], "Stylesheet should be removed");
+ }
+ }
+
+ // Close the new windows.
+ for (let win of newWindows) {
+ await BrowserTestUtils.closeWindow(win);
+ }
+});
+
+// Tests that the view is created correctly from the view template.
+add_task(async function viewCreated() {
+ await withDynamicTypeProvider(async () => {
+ // Do a search.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ waitForFocus: SimpleTest.waitForFocus,
+ });
+
+ // Get the row.
+ let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1);
+ Assert.equal(
+ row.result.type,
+ UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ "row.result.type"
+ );
+ Assert.equal(
+ row.getAttribute("dynamicType"),
+ DYNAMIC_TYPE_NAME,
+ "row[dynamicType]"
+ );
+ let inner = row.querySelector(".urlbarView-row-inner");
+ Assert.ok(inner, ".urlbarView-row-inner should exist");
+
+ // Check the DOM.
+ checkDOM(inner, DYNAMIC_TYPE_VIEW_TEMPLATE.children);
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ });
+});
+
+// Tests that the view is updated correctly.
+add_task(async function viewUpdated() {
+ await withDynamicTypeProvider(async () => {
+ // Test a few different search strings. The dynamic result view will be
+ // updated to reflect the current string.
+ for (let searchString of ["test", "some other string", "and another"]) {
+ // Do a search.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: searchString,
+ waitForFocus: SimpleTest.waitForFocus,
+ });
+
+ let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1);
+ let text = row.querySelector(
+ `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-text`
+ );
+
+ // The view's call to provider.getViewUpdate is async, so we need to make
+ // sure the update has been applied before continuing to avoid
+ // intermittent failures.
+ await TestUtils.waitForCondition(
+ () => text.getAttribute("searchString") == searchString
+ );
+
+ // The "searchString" attribute of these elements should be updated.
+ let elementNames = ["selectable", "text", "button1", "button2"];
+ for (let name of elementNames) {
+ let element = row.querySelector(
+ `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-${name}`
+ );
+ Assert.equal(
+ element.getAttribute("searchString"),
+ searchString,
+ 'element.getAttribute("searchString")'
+ );
+ }
+
+ // text.textContent should be updated.
+ Assert.equal(
+ text.textContent,
+ `result.payload.searchString is: ${searchString}`,
+ "text.textContent"
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ }
+ });
+});
+
+// Tests that selection correctly moves through buttons and selectables in a
+// dynamic result.
+add_task(async function selection() {
+ await withDynamicTypeProvider(async () => {
+ // Add a visit so we have at least one result after the dynamic result.
+ await PlacesUtils.history.clear();
+ await PlacesTestUtils.addVisits("http://example.com/test");
+
+ // Do a search.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ waitForFocus: SimpleTest.waitForFocus,
+ });
+
+ // Sanity check that the dynamic result is at index 1.
+ let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1);
+ Assert.equal(
+ row.result.type,
+ UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ "row.result.type"
+ );
+
+ // The heuristic result will be selected. Arrow down from the heuristic
+ // through all the selectable elements in the dynamic result.
+ let selectables = ["selectable", "button1", "button2"];
+ for (let name of selectables) {
+ let element = row.querySelector(
+ `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-${name}`
+ );
+ Assert.ok(element, "Sanity check element");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElement(window),
+ element,
+ `Selected element: ${name}`
+ );
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 1,
+ "Row at index 1 selected"
+ );
+ Assert.equal(UrlbarTestUtils.getSelectedRow(window), row, "Row selected");
+ }
+
+ // Arrow down again to select the result after the dynamic result.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 2,
+ "Row at index 2 selected"
+ );
+ Assert.notEqual(
+ UrlbarTestUtils.getSelectedRow(window),
+ row,
+ "Row is not selected"
+ );
+
+ // Arrow back up through the dynamic result.
+ for (let name of selectables.reverse()) {
+ let element = row.querySelector(
+ `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-${name}`
+ );
+ Assert.ok(element, "Sanity check element");
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElement(window),
+ element,
+ `Selected element: ${name}`
+ );
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 1,
+ "Row at index 1 selected"
+ );
+ Assert.equal(UrlbarTestUtils.getSelectedRow(window), row, "Row selected");
+ }
+
+ // Arrow up again to select the heuristic result.
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 0,
+ "Row at index 0 selected"
+ );
+ Assert.notEqual(
+ UrlbarTestUtils.getSelectedRow(window),
+ row,
+ "Row is not selected"
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ await PlacesUtils.history.clear();
+ });
+});
+
+// Tests picking elements in a dynamic result.
+add_task(async function pick() {
+ await withDynamicTypeProvider(async provider => {
+ let selectables = ["selectable", "button1", "button2"];
+ for (let i = 0; i < selectables.length; i++) {
+ let selectable = selectables[i];
+
+ // Do a search.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ waitForFocus: SimpleTest.waitForFocus,
+ });
+
+ // Sanity check that the dynamic result is at index 1.
+ let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1);
+ Assert.equal(
+ row.result.type,
+ UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ "row.result.type"
+ );
+
+ // The heuristic result will be selected. Arrow down from the heuristic
+ // to the selectable element.
+ let element = row.querySelector(
+ `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-${selectable}`
+ );
+ Assert.ok(element, "Sanity check element");
+ EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: i + 1 });
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElement(window),
+ element,
+ `Selected element: ${name}`
+ );
+
+ // Pick the element.
+ let pickPromise = provider.promisePick();
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Enter")
+ );
+ let [result, pickedElement] = await pickPromise;
+ Assert.equal(result, row.result, "Picked result");
+ Assert.equal(pickedElement, element, "Picked element");
+ }
+ });
+});
+
+// Tests picking elements in a dynamic result.
+add_task(async function shouldNavigate() {
+ /**
+ * A dummy provider that providers results with a `shouldNavigate` property.
+ */
+ class TestShouldNavigateProvider extends TestProvider {
+ /**
+ * @param {object} context - Data regarding the context of the query.
+ * @param {function} addCallback - Function to add a result to the query.
+ */
+ async startQuery(context, addCallback) {
+ for (let result of this._results) {
+ result.payload.searchString = context.searchString;
+ result.payload.shouldNavigate = true;
+ result.payload.url = DUMMY_PAGE;
+ addCallback(this, result);
+ }
+ }
+ }
+
+ await withDynamicTypeProvider(async provider => {
+ // Do a search.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ waitForFocus: SimpleTest.waitForFocus,
+ });
+
+ // Sanity check that the dynamic result is at index 1.
+ let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1);
+ Assert.equal(
+ row.result.type,
+ UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ "row.result.type"
+ );
+
+ // The heuristic result will be selected. Arrow down from the heuristic
+ // to the selectable element.
+ let element = row.querySelector(
+ `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-selectable`
+ );
+ Assert.ok(element, "Sanity check element");
+ EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: 1 });
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElement(window),
+ element,
+ `Selected element: ${name}`
+ );
+
+ // Pick the element.
+ let pickPromise = provider.promisePick();
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Enter")
+ );
+ // Verify that pickResult was still called.
+ let [result, pickedElement] = await pickPromise;
+ Assert.equal(result, row.result, "Picked result");
+ Assert.equal(pickedElement, element, "Picked element");
+
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ is(
+ gBrowser.currentURI.spec,
+ DUMMY_PAGE,
+ "We navigated to payload.url when result selected"
+ );
+
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "about:home");
+ await BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ "about:home"
+ );
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ waitForFocus: SimpleTest.waitForFocus,
+ });
+
+ row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1);
+ element = row.querySelector(
+ `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-selectable`
+ );
+
+ pickPromise = provider.promisePick();
+ EventUtils.synthesizeMouseAtCenter(element, {});
+ [result, pickedElement] = await pickPromise;
+ Assert.equal(result, row.result, "Picked result");
+ Assert.equal(pickedElement, element, "Picked element");
+
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ is(
+ gBrowser.currentURI.spec,
+ DUMMY_PAGE,
+ "We navigated to payload.url when result is clicked"
+ );
+ }, new TestShouldNavigateProvider());
+});
+
+// Tests applying highlighting to a dynamic result.
+add_task(async function highlighting() {
+ /**
+ * Provides a dynamic result with highlighted text.
+ */
+ class TestHighlightProvider extends TestProvider {
+ startQuery(context, addCallback) {
+ let result = Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ ...UrlbarResult.payloadAndSimpleHighlights(context.tokens, {
+ dynamicType: DYNAMIC_TYPE_NAME,
+ text: ["Test title", UrlbarUtils.HIGHLIGHT.SUGGESTED],
+ })
+ ),
+ { suggestedIndex: 1 }
+ );
+ addCallback(this, result);
+ }
+
+ getViewUpdate(result, idsByName) {
+ return {};
+ }
+ }
+
+ // Test that highlighting is applied.
+ await withDynamicTypeProvider(async () => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ waitForFocus: SimpleTest.waitForFocus,
+ });
+
+ let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1);
+ Assert.equal(
+ row.result.type,
+ UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ "row.result.type"
+ );
+ let parentTextNode = row.querySelector(
+ `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-text`
+ );
+ let highlightedTextNode = row.querySelector(
+ `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-text > strong`
+ );
+ Assert.equal(parentTextNode.firstChild.textContent, "Test");
+ Assert.equal(
+ highlightedTextNode.textContent,
+ " title",
+ "The highlighting was applied successfully."
+ );
+ }, new TestHighlightProvider());
+
+ /**
+ * Provides a dynamic result with highlighted text that is then overridden.
+ */
+ class TestHighlightProviderOveridden extends TestHighlightProvider {
+ getViewUpdate(result, idsByName) {
+ return {
+ text: {
+ textContent: "Test title",
+ },
+ };
+ }
+ }
+
+ // Test that highlighting is not applied when overridden from getViewUpdate.
+ await withDynamicTypeProvider(async () => {
+ // Do a search.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ waitForFocus: SimpleTest.waitForFocus,
+ });
+
+ let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1);
+ Assert.equal(
+ row.result.type,
+ UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ "row.result.type"
+ );
+ let parentTextNode = row.querySelector(
+ `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-text`
+ );
+ let highlightedTextNode = row.querySelector(
+ `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-text > strong`
+ );
+ Assert.equal(
+ parentTextNode.firstChild.textContent,
+ "Test title",
+ "No highlighting was applied"
+ );
+ Assert.ok(!highlightedTextNode, "The <strong> child node was deleted.");
+ }, new TestHighlightProviderOveridden());
+});
+
+/**
+ * Provides a dynamic result.
+ */
+class TestProvider extends UrlbarTestUtils.TestProvider {
+ constructor() {
+ super({
+ results: [
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ dynamicType: DYNAMIC_TYPE_NAME,
+ }
+ ),
+ { suggestedIndex: 1 }
+ ),
+ ],
+ });
+ }
+
+ async startQuery(context, addCallback) {
+ for (let result of this._results) {
+ result.payload.searchString = context.searchString;
+ addCallback(this, result);
+ }
+ }
+
+ getViewUpdate(result, idsByName) {
+ for (let child of DYNAMIC_TYPE_VIEW_TEMPLATE.children) {
+ Assert.ok(idsByName.get(child.name), `idsByName contains ${child.name}`);
+ }
+
+ return {
+ selectable: {
+ textContent: "Selectable",
+ attributes: {
+ searchString: result.payload.searchString,
+ },
+ },
+ text: {
+ textContent: `result.payload.searchString is: ${result.payload.searchString}`,
+ attributes: {
+ searchString: result.payload.searchString,
+ },
+ },
+ button1: {
+ textContent: "Button 1",
+ attributes: {
+ searchString: result.payload.searchString,
+ },
+ },
+ button2: {
+ textContent: "Button 2",
+ attributes: {
+ searchString: result.payload.searchString,
+ },
+ },
+ };
+ }
+
+ pickResult(result, element) {
+ if (this._pickPromiseResolve) {
+ this._pickPromiseResolve([result, element]);
+ delete this._pickPromiseResolve;
+ delete this._pickPromise;
+ }
+ }
+
+ promisePick() {
+ this._pickPromise = new Promise(resolve => {
+ this._pickPromiseResolve = resolve;
+ });
+ return this._pickPromise;
+ }
+}
+
+/**
+ * Provides a dynamic result.
+ * @param {object} callback - Function that runs the body of the test.
+ * @param {object} provider - The dummy provider to use.
+ */
+async function withDynamicTypeProvider(
+ callback,
+ provider = new TestProvider()
+) {
+ // Add a dynamic result type.
+ UrlbarResult.addDynamicResultType(DYNAMIC_TYPE_NAME);
+ UrlbarView.addDynamicViewTemplate(
+ DYNAMIC_TYPE_NAME,
+ DYNAMIC_TYPE_VIEW_TEMPLATE
+ );
+
+ // Add a provider of the dynamic type.
+ UrlbarProvidersManager.registerProvider(provider);
+
+ await callback(provider);
+
+ // Clean up.
+ UrlbarProvidersManager.unregisterProvider(provider);
+ UrlbarView.removeDynamicViewTemplate(DYNAMIC_TYPE_NAME);
+ UrlbarResult.removeDynamicResultType(DYNAMIC_TYPE_NAME);
+}
+
+function checkDOM(parentNode, expectedChildren) {
+ info(
+ `checkDOM: Checking parentNode id=${parentNode.id} className=${parentNode.className}`
+ );
+ for (let i = 0; i < expectedChildren.length; i++) {
+ let child = expectedChildren[i];
+ let actualChild = parentNode.children[i];
+ info(`checkDOM: Checking expected child: ${JSON.stringify(child)}`);
+ Assert.ok(actualChild, "actualChild should exist");
+ Assert.equal(actualChild.tagName, child.tag, "child.tag");
+ Assert.equal(actualChild.getAttribute("name"), child.name, "child.name");
+ Assert.ok(
+ actualChild.classList.contains(
+ `urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-${child.name}`
+ ),
+ "child.name should be in classList"
+ );
+ // We have to use startsWith/endsWith since the middle of the ID is a random
+ // number.
+ Assert.ok(actualChild.id.startsWith("urlbarView-row-"));
+ Assert.ok(
+ actualChild.id.endsWith(child.name),
+ "The child was assigned the correct ID."
+ );
+ for (let [name, value] of Object.entries(child.attributes || {})) {
+ Assert.equal(actualChild.getAttribute(name), value, `attribute: ${name}`);
+ }
+ for (let name of child.classList || []) {
+ Assert.ok(actualChild.classList.contains(name), `classList: ${name}`);
+ }
+ if (child.children) {
+ checkDOM(actualChild, child.children);
+ }
+ }
+}
diff --git a/browser/components/urlbar/tests/browser/browser_edit_invalid_url.js b/browser/components/urlbar/tests/browser/browser_edit_invalid_url.js
new file mode 100644
index 0000000000..bbebc0f940
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_edit_invalid_url.js
@@ -0,0 +1,91 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Checks that we trim invalid urls when they are selected, so that if the user
+// modifies the selected url, or just closes the results pane, we do a visit
+// rather than searching for the trimmed string.
+
+const url = BrowserUtils.trimURLProtocol + "invalid.somehost/mytest";
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.trimURLs", true]],
+ });
+ await PlacesTestUtils.addVisits(url);
+ registerCleanupFunction(PlacesUtils.history.clear);
+});
+
+add_task(async function test_escape() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "invalid",
+ });
+ // Look for our result.
+ let resultCount = UrlbarTestUtils.getResultCount(window);
+ Assert.greater(resultCount, 1, "There should be at least two results");
+ for (let i = 0; i < resultCount; i++) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ info(`Result at ${i} has url ${result.url}`);
+ if (result.url.startsWith(url)) {
+ break;
+ }
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+ Assert.equal(
+ gURLBar.value,
+ url,
+ "The string displayed in the textbox should be the untrimmed url"
+ );
+ // Close the results pane by ESC.
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeKey("KEY_Escape");
+ });
+ // Confirm the result and check the loaded page.
+ let promise = waitforLoadURL();
+ EventUtils.synthesizeKey("KEY_Enter");
+ let loadedUrl = await promise;
+ Assert.equal(loadedUrl, url, "Should try to load a url");
+});
+
+add_task(async function test_edit_url() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "invalid",
+ });
+ // Look for our result.
+ let resultCount = UrlbarTestUtils.getResultCount(window);
+ Assert.greater(resultCount, 1, "There should be at least two results");
+ for (let i = 1; i < resultCount; i++) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ info(`Result at ${i} has url ${result.url}`);
+ if (result.url.startsWith(url)) {
+ break;
+ }
+ }
+ Assert.equal(
+ gURLBar.value,
+ url,
+ "The string displayed in the textbox should be the untrimmed url"
+ );
+ // Modify the url.
+ EventUtils.synthesizeKey("2");
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.URL, "Should visit a url");
+ Assert.equal(result.url, url + "2", "Should visit the modified url");
+
+ // Confirm the result and check the loaded page.
+ let promise = waitforLoadURL();
+ EventUtils.synthesizeKey("KEY_Enter");
+ let loadedUrl = await promise;
+ Assert.equal(loadedUrl, url + "2", "Should try to load the modified url");
+});
+
+async function waitforLoadURL() {
+ let sandbox = sinon.createSandbox();
+ let loadedUrl = await new Promise(resolve =>
+ sandbox.stub(gURLBar, "_loadURL").callsFake(resolve)
+ );
+ sandbox.restore();
+ return loadedUrl;
+}
diff --git a/browser/components/urlbar/tests/browser/browser_enter.js b/browser/components/urlbar/tests/browser/browser_enter.js
new file mode 100644
index 0000000000..567ccfa834
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_enter.js
@@ -0,0 +1,268 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_VALUE = "example.com/\xF7?\xF7";
+const START_VALUE = "example.com/%C3%B7?%C3%B7";
+
+add_task(async function setup() {
+ const engine = await SearchTestUtils.promiseNewSearchEngine(
+ getRootDirectory(gTestPath) + "searchSuggestionEngine.xml"
+ );
+ engine.alias = "@default";
+
+ const defaultEngine = Services.search.defaultEngine;
+ Services.search.defaultEngine = engine;
+
+ registerCleanupFunction(async function() {
+ Services.search.defaultEngine = defaultEngine;
+ });
+});
+
+add_task(async function returnKeypress() {
+ info("Simple return keypress");
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, START_VALUE);
+
+ gURLBar.focus();
+ EventUtils.synthesizeKey("KEY_Enter");
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ // Check url bar and selected tab.
+ is(
+ gURLBar.value,
+ TEST_VALUE,
+ "Urlbar should preserve the value on return keypress"
+ );
+ is(gBrowser.selectedTab, tab, "New URL was loaded in the current tab");
+
+ // Cleanup.
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function altReturnKeypress() {
+ info("Alt+Return keypress");
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, START_VALUE);
+
+ let tabOpenPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabOpen"
+ );
+ gURLBar.focus();
+ EventUtils.synthesizeKey("KEY_Enter", { altKey: true });
+
+ // wait for the new tab to appear.
+ await tabOpenPromise;
+
+ // Check url bar and selected tab.
+ is(
+ gURLBar.value,
+ TEST_VALUE,
+ "Urlbar should preserve the value on return keypress"
+ );
+ isnot(gBrowser.selectedTab, tab, "New URL was loaded in a new tab");
+
+ // Cleanup.
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function altGrReturnKeypress() {
+ info("AltGr+Return keypress");
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, START_VALUE);
+
+ let tabOpenPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabOpen"
+ );
+ gURLBar.focus();
+ EventUtils.synthesizeKey("KEY_Enter", { altGraphKey: true });
+
+ // wait for the new tab to appear.
+ await tabOpenPromise;
+
+ // Check url bar and selected tab.
+ is(
+ gURLBar.value,
+ TEST_VALUE,
+ "Urlbar should preserve the value on return keypress"
+ );
+ isnot(gBrowser.selectedTab, tab, "New URL was loaded in a new tab");
+
+ // Cleanup.
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function searchOnEnterNoPick() {
+ info("Search on Enter without picking a urlbar result");
+
+ // Why is BrowserTestUtils.openNewForegroundTab not causing the bug?
+ let promiseTabOpened = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabOpen"
+ );
+ EventUtils.synthesizeMouseAtCenter(gBrowser.tabContainer.newTabButton, {});
+ let openEvent = await promiseTabOpened;
+ let tab = openEvent.target;
+
+ let loadPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ null,
+ true
+ );
+ gURLBar.focus();
+ gURLBar.value = "test test";
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+
+ Assert.ok(
+ gBrowser.selectedBrowser.currentURI.spec.endsWith("test+test"),
+ "Should have loaded the correct page"
+ );
+ Assert.equal(
+ gBrowser.selectedBrowser.currentURI.spec,
+ gURLBar.untrimmedValue,
+ "The location should have changed"
+ );
+
+ // Cleanup.
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function searchOnEnterSoon() {
+ info("Search on Enter as soon as typing a char");
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ START_VALUE
+ );
+
+ const onLoad = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ const onBeforeUnload = SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ () => {
+ return new Promise(resolve => {
+ content.window.addEventListener("beforeunload", () => {
+ resolve();
+ });
+ });
+ }
+ );
+ const onResult = SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ return new Promise(resolve => {
+ content.window.addEventListener("keyup", () => {
+ resolve("keyup");
+ });
+ content.window.addEventListener("unload", () => {
+ resolve("unload");
+ });
+ });
+ });
+
+ // Focus on the input field in urlbar.
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ const ownerDocument = gBrowser.selectedBrowser.ownerDocument;
+ is(
+ ownerDocument.activeElement,
+ gURLBar.inputField,
+ "The input field in urlbar has focus"
+ );
+
+ info("Keydown a char and Enter");
+ EventUtils.synthesizeKey("x", { type: "keydown" });
+ EventUtils.synthesizeKey("KEY_Enter", { type: "keydown" });
+
+ // Wait for beforeUnload event in the content.
+ await onBeforeUnload;
+ is(
+ ownerDocument.activeElement,
+ gURLBar.inputField,
+ "The input field in urlbar still has focus"
+ );
+
+ // Check the caret position.
+ Assert.equal(
+ gURLBar.selectionStart,
+ gURLBar.value.length,
+ "The selectionStart indicates at ending of the value"
+ );
+ Assert.equal(
+ gURLBar.selectionEnd,
+ gURLBar.value.length,
+ "The selectionEnd indicates at ending of the value"
+ );
+
+ // Keyup both key as soon as beforeUnload event happens.
+ EventUtils.synthesizeKey("x", { type: "keyup" });
+ EventUtils.synthesizeKey("KEY_Enter", { type: "keyup" });
+
+ // Wait for moving the focus.
+ await TestUtils.waitForCondition(
+ () => ownerDocument.activeElement === gBrowser.selectedBrowser
+ );
+ info("The focus is moved to the browser");
+
+ // Check whether keyup event is not captured before unload event happens.
+ const result = await onResult;
+ is(result, "unload", "Keyup event is not captured.");
+
+ // Check the caret position again.
+ Assert.equal(
+ gURLBar.selectionStart,
+ 0,
+ "The selectionStart indicates at beginning of the value"
+ );
+ Assert.equal(
+ gURLBar.selectionEnd,
+ 0,
+ "The selectionEnd indicates at beginning of the value"
+ );
+
+ // Cleanup.
+ await onLoad;
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function searchByMultipleEnters() {
+ info("Search on Enter after selecting the search engine by Enter");
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ START_VALUE
+ );
+
+ info("Select a search engine by Enter key");
+ gURLBar.focus();
+ gURLBar.select();
+ EventUtils.sendString("@default");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await TestUtils.waitForCondition(
+ () => gURLBar.searchMode,
+ "Wait until entering search mode"
+ );
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: "browser_searchSuggestionEngine searchSuggestionEngine.xml",
+ entry: "keywordoffer",
+ });
+ const ownerDocument = gBrowser.selectedBrowser.ownerDocument;
+ is(
+ ownerDocument.activeElement,
+ gURLBar.inputField,
+ "The input field in urlbar has focus"
+ );
+
+ info("Search by Enter key");
+ const onLoad = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ EventUtils.sendString("mozilla");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await onLoad;
+ is(
+ ownerDocument.activeElement,
+ gBrowser.selectedBrowser,
+ "The focus is moved to the browser"
+ );
+
+ // Cleanup.
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_enterAfterMouseOver.js b/browser/components/urlbar/tests/browser/browser_enterAfterMouseOver.js
new file mode 100644
index 0000000000..3629774c73
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_enterAfterMouseOver.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that enter works correctly after a mouse over.
+ */
+
+function repeat(limit, func) {
+ for (let i = 0; i < limit; i++) {
+ func(i);
+ }
+}
+
+async function promiseAutoComplete(inputText) {
+ gURLBar.focus();
+ gURLBar.value = inputText.slice(0, -1);
+ EventUtils.sendString(inputText.slice(-1));
+ await UrlbarTestUtils.promiseSearchComplete(window);
+}
+
+function assertSelected(index) {
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ index,
+ "Should have the correct index selected"
+ );
+}
+
+let gMaxResults;
+
+add_task(async function() {
+ registerCleanupFunction(async function() {
+ await PlacesUtils.history.clear();
+ });
+
+ await PlacesUtils.history.clear();
+
+ gMaxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults");
+
+ let visits = [];
+ repeat(gMaxResults, i => {
+ visits.push({
+ uri: makeURI("http://example.com/autocomplete/?" + i),
+ });
+ });
+ await PlacesTestUtils.addVisits(visits);
+
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank");
+ await promiseAutoComplete("http://example.com/autocomplete/");
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ gMaxResults,
+ "Should have got the correct amount of results"
+ );
+
+ let initiallySelected = UrlbarTestUtils.getSelectedRowIndex(window);
+
+ info("Key Down to select the next item");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ assertSelected(initiallySelected + 1);
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ initiallySelected + 1
+ );
+ let expectedURL = result.url;
+
+ Assert.equal(
+ gURLBar.untrimmedValue,
+ expectedURL,
+ "Value in the URL bar should be updated by keyboard selection"
+ );
+
+ // Verify that what we're about to do changes the selectedIndex:
+ Assert.notEqual(
+ initiallySelected + 1,
+ 3,
+ "Shouldn't be changing the selectedIndex to the same index we keyboard-selected."
+ );
+
+ let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 3);
+ EventUtils.synthesizeMouseAtCenter(element, { type: "mousemove" });
+
+ await UrlbarTestUtils.promisePopupClose(window, async () => {
+ let openedExpectedPage = BrowserTestUtils.waitForDocLoadAndStopIt(
+ expectedURL,
+ gBrowser.selectedBrowser
+ );
+ EventUtils.synthesizeKey("KEY_Enter");
+ await openedExpectedPage;
+ });
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_focusedCmdK.js b/browser/components/urlbar/tests/browser/browser_focusedCmdK.js
new file mode 100644
index 0000000000..c03689d392
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_focusedCmdK.js
@@ -0,0 +1,15 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function() {
+ // Test that Ctrl/Cmd + K will focus the url bar
+ let focusPromise = BrowserTestUtils.waitForEvent(gURLBar, "focus");
+ document.documentElement.focus();
+ EventUtils.synthesizeKey("k", { accelKey: true });
+ await focusPromise;
+ Assert.equal(
+ document.activeElement,
+ gURLBar.inputField,
+ "URL Bar should be focused"
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_handleCommand_fallback.js b/browser/components/urlbar/tests/browser/browser_handleCommand_fallback.js
new file mode 100644
index 0000000000..51cec4e8be
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_handleCommand_fallback.js
@@ -0,0 +1,151 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the fallback paths of handleCommand (no view and no previous
+ * result) work consistently against the normal case of picking the heuristic
+ * result.
+ */
+
+const TEST_STRINGS = [
+ "test",
+ "test/",
+ "test.com",
+ "test.invalid",
+ "moz",
+ "moz test",
+ "@moz test",
+ "keyword",
+ "keyword test",
+ "test/test/",
+ "test /test/",
+];
+
+add_task(async function() {
+ // Disable autofill so mozilla.org isn't autofilled below.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.autoFill", false]],
+ });
+
+ sandbox = sinon.createSandbox();
+ let engine = await Services.search.addEngineWithDetails("MozSearch", {
+ alias: "moz",
+ method: "GET",
+ template: "http://example.com/?q={searchTerms}",
+ });
+ let engine2 = await Services.search.addEngineWithDetails("MozSearch2", {
+ alias: "@moz",
+ method: "GET",
+ template: "http://example.com/?q={searchTerms}",
+ });
+ let bm = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.com/?q=%s",
+ title: "test",
+ });
+ await PlacesUtils.keywords.insert({
+ keyword: "keyword",
+ url: "http://example.com/?q=%s",
+ });
+ registerCleanupFunction(async () => {
+ sandbox.restore();
+ await Services.search.removeEngine(engine);
+ await Services.search.removeEngine(engine2);
+ await PlacesUtils.bookmarks.remove(bm);
+ await UrlbarTestUtils.formHistory.clear();
+ });
+
+ async function promiseLoadURL() {
+ return new Promise(resolve => {
+ sandbox.stub(gURLBar, "_loadURL").callsFake(function() {
+ sandbox.restore();
+ // The last arguments are optional and apply only to some cases, so we
+ // could not use deepEqual with them.
+ resolve(Array.from(arguments).slice(0, 3));
+ });
+ });
+ }
+
+ // Run the string through a normal search where the user types the string
+ // and confirms the heuristic result, store the arguments to _loadURL, then
+ // confirm the same string without a view and without an input event, and
+ // compare the arguments.
+ for (let value of TEST_STRINGS) {
+ info("Input the value normally and Enter.");
+ let promise = promiseLoadURL();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value,
+ });
+ EventUtils.synthesizeKey("KEY_Enter");
+ let args = await promise;
+ Assert.ok(args.length, "Sanity check");
+ info("Close the panel and confirm again.");
+ promise = promiseLoadURL();
+ await UrlbarTestUtils.promisePopupClose(window);
+ EventUtils.synthesizeKey("KEY_Enter");
+ Assert.deepEqual(await promise, args, "Check arguments are coherent");
+
+ info("Set the value directly and Enter.");
+ // To properly testing the original value we must be out of search mode.
+ if (gURLBar.searchMode) {
+ await UrlbarTestUtils.exitSearchMode(window);
+ // Exiting search mode may reopen the panel.
+ await UrlbarTestUtils.promisePopupClose(window);
+ }
+ promise = promiseLoadURL();
+ gURLBar.value = value;
+ let spy = sinon.spy(UrlbarUtils, "getHeuristicResultFor");
+ EventUtils.synthesizeKey("KEY_Enter");
+ spy.restore();
+ Assert.ok(spy.called, "invoked getHeuristicResultFor");
+ Assert.deepEqual(await promise, args, "Check arguments are coherent");
+ gURLBar.handleRevert();
+ }
+});
+
+// This is testing the final fallback case that may happen when we can't
+// get a heuristic result, maybe because the Places database is corrupt.
+add_task(async function no_heuristic_test() {
+ sandbox = sinon.createSandbox();
+
+ let stub = sandbox
+ .stub(UrlbarUtils, "getHeuristicResultFor")
+ .callsFake(async function() {
+ throw new Error("I failed!");
+ });
+
+ registerCleanupFunction(async () => {
+ sandbox.restore();
+ await UrlbarTestUtils.formHistory.clear();
+ });
+
+ async function promiseLoadURL() {
+ return new Promise(resolve => {
+ sandbox.stub(gURLBar, "_loadURL").callsFake(function() {
+ sandbox.restore();
+ // The last arguments are optional and apply only to some cases, so we
+ // could not use deepEqual with them.
+ resolve(Array.from(arguments).slice(0, 3));
+ });
+ });
+ }
+
+ // Run the string through a normal search where the user types the string
+ // and confirms the heuristic result, store the arguments to _loadURL, then
+ // confirm the same string without a view and without an input event, and
+ // compare the arguments.
+ for (let value of TEST_STRINGS) {
+ // To properly testing the original value we must be out of search mode.
+ if (gURLBar.searchMode) {
+ await UrlbarTestUtils.exitSearchMode(window);
+ }
+ let promise = promiseLoadURL();
+ gURLBar.value = value;
+ EventUtils.synthesizeKey("KEY_Enter");
+ Assert.ok(stub.called, "invoked getHeuristicResultFor");
+ // The first argument to _loadURL should always be a valid url, so this
+ // should never throw.
+ new URL((await promise)[0]);
+ }
+});
diff --git a/browser/components/urlbar/tests/browser/browser_hashChangeProxyState.js b/browser/components/urlbar/tests/browser/browser_hashChangeProxyState.js
new file mode 100644
index 0000000000..d6d0cf0e3c
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_hashChangeProxyState.js
@@ -0,0 +1,148 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that navigating through both the URL bar and using in-page hash- or ref-
+ * based links and back or forward navigation updates the URL bar and identity block correctly.
+ */
+add_task(async function() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.navigation.requireUserInteraction", false]],
+ });
+ let baseURL = `${TEST_BASE_URL}dummy_page.html`;
+ let url = baseURL + "#foo";
+ await BrowserTestUtils.withNewTab({ gBrowser, url }, async function(browser) {
+ let identityBox = document.getElementById("identity-box");
+ let expectedURL = url;
+
+ let verifyURLBarState = testType => {
+ is(
+ gURLBar.value,
+ expectedURL,
+ "URL bar visible value should be correct " + testType
+ );
+ is(
+ gURLBar.untrimmedValue,
+ expectedURL,
+ "URL bar value should be correct " + testType
+ );
+ ok(
+ identityBox.classList.contains("verifiedDomain"),
+ "Identity box should know we're doing SSL " + testType
+ );
+ is(
+ gURLBar.getAttribute("pageproxystate"),
+ "valid",
+ "URL bar is in valid page proxy state"
+ );
+ };
+
+ verifyURLBarState("at the beginning");
+
+ let locationChangePromise;
+ let resolveLocationChangePromise;
+ let expectURL = urlTemp => {
+ expectedURL = urlTemp;
+ locationChangePromise = new Promise(
+ r => (resolveLocationChangePromise = r)
+ );
+ };
+ let wpl = {
+ onLocationChange(unused, unused2, location) {
+ is(location.spec, expectedURL, "Got the expected URL");
+ resolveLocationChangePromise();
+ },
+ };
+ gBrowser.addProgressListener(wpl);
+
+ expectURL(baseURL + "#foo");
+ gURLBar.select();
+ EventUtils.sendKey("return");
+
+ await locationChangePromise;
+ verifyURLBarState("after hitting enter on the same URL a second time");
+
+ expectURL(baseURL + "#bar");
+ gURLBar.value = expectedURL;
+ gURLBar.select();
+ EventUtils.sendKey("return");
+
+ await locationChangePromise;
+ verifyURLBarState("after a URL bar hash navigation");
+
+ expectURL(baseURL + "#foo");
+ await SpecialPowers.spawn(browser, [], function() {
+ let a = content.document.createElement("a");
+ a.href = "#foo";
+ a.textContent = "Foo Link";
+ content.document.body.appendChild(a);
+ a.click();
+ });
+
+ await locationChangePromise;
+ verifyURLBarState("after a page link hash navigation");
+
+ expectURL(baseURL + "#bar");
+ gBrowser.goBack();
+
+ await locationChangePromise;
+ verifyURLBarState("after going back");
+
+ expectURL(baseURL + "#foo");
+ gBrowser.goForward();
+
+ await locationChangePromise;
+ verifyURLBarState("after going forward");
+
+ expectURL(baseURL + "#foo");
+ gURLBar.select();
+ EventUtils.sendKey("return");
+
+ await locationChangePromise;
+ verifyURLBarState("after hitting enter on the same URL");
+
+ gBrowser.removeProgressListener(wpl);
+ });
+});
+
+/**
+ * Check that initial secure loads that swap remoteness
+ * get the correct page icon when finished.
+ */
+add_task(async function() {
+ // Ensure there's no preloaded newtab browser, since that'll not fire a load event.
+ NewTabPagePreloading.removePreloadedBrowser(window);
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:newtab"
+ );
+ let url = `${TEST_BASE_URL}dummy_page.html#foo`;
+ gURLBar.value = url;
+ gURLBar.select();
+ EventUtils.sendKey("return");
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ is(
+ gURLBar.value,
+ url,
+ "URL bar visible value should be correct when the page loads from about:newtab"
+ );
+ is(
+ gURLBar.untrimmedValue,
+ url,
+ "URL bar value should be correct when the page loads from about:newtab"
+ );
+ let identityBox = document.getElementById("identity-box");
+ ok(
+ identityBox.classList.contains("verifiedDomain"),
+ "Identity box should know we're doing SSL when the page loads from about:newtab"
+ );
+ is(
+ gURLBar.getAttribute("pageproxystate"),
+ "valid",
+ "URL bar is in valid page proxy state when SSL page with hash loads from about:newtab"
+ );
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_heuristicNotAddedFirst.js b/browser/components/urlbar/tests/browser/browser_heuristicNotAddedFirst.js
new file mode 100644
index 0000000000..7a25235d34
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_heuristicNotAddedFirst.js
@@ -0,0 +1,165 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// When the heuristic result is not the first result added, it should still be
+// selected.
+
+"use strict";
+
+// When the heuristic result is not the first result added, it should still be
+// selected.
+add_task(async function slowHeuristicSelected() {
+ // First, add a provider that adds a heuristic result on a delay. Both this
+ // provider and the one below have a high priority so that only they are used
+ // during the test.
+ let engine = await Services.search.getDefault();
+ let heuristicResult = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ {
+ suggestion: "test",
+ engine: engine.name,
+ }
+ );
+ heuristicResult.heuristic = true;
+ let heuristicProvider = new UrlbarTestUtils.TestProvider({
+ results: [heuristicResult],
+ name: "heuristicProvider",
+ priority: Infinity,
+ addTimeout: 500,
+ });
+ UrlbarProvidersManager.registerProvider(heuristicProvider);
+
+ // Second, add another provider that adds a non-heuristic result immediately
+ // with suggestedIndex = 1.
+ let nonHeuristicResult = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TIP,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ text: "This is a test tip.",
+ buttonText: "Done",
+ type: "test",
+ helpUrl: "http://example.com/",
+ }
+ );
+ nonHeuristicResult.suggestedIndex = 1;
+ let nonHeuristicProvider = new UrlbarTestUtils.TestProvider({
+ results: [nonHeuristicResult],
+ name: "nonHeuristicProvider",
+ priority: Infinity,
+ });
+ UrlbarProvidersManager.registerProvider(nonHeuristicProvider);
+
+ // Do a search.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ value: "test",
+ window,
+ });
+
+ // The first result should be the heuristic and it should be selected.
+ let actualHeuristic = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(actualHeuristic.type, UrlbarUtils.RESULT_TYPE.SEARCH);
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElement(window),
+ actualHeuristic.element.row
+ );
+ Assert.equal(UrlbarTestUtils.getSelectedElementIndex(window), 0);
+
+ // Check the second result for good measure.
+ let actualNonHeuristic = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ 1
+ );
+ Assert.equal(actualNonHeuristic.type, UrlbarUtils.RESULT_TYPE.TIP);
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ UrlbarProvidersManager.unregisterProvider(heuristicProvider);
+ UrlbarProvidersManager.unregisterProvider(nonHeuristicProvider);
+});
+
+// When the heuristic result is not the first result added but a one-off search
+// button is already selected, the heuristic result should not steal the
+// selection from the one-off button.
+add_task(async function oneOffRemainsSelected() {
+ // First, add a provider that adds a heuristic result on a delay. Both this
+ // provider and the one below have a high priority so that only they are used
+ // during the test.
+ let engine = await Services.search.getDefault();
+ let heuristicResult = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ {
+ suggestion: "test",
+ engine: engine.name,
+ }
+ );
+ heuristicResult.heuristic = true;
+ let heuristicProvider = new UrlbarTestUtils.TestProvider({
+ results: [heuristicResult],
+ name: "heuristicProvider",
+ priority: Infinity,
+ addTimeout: 500,
+ });
+ UrlbarProvidersManager.registerProvider(heuristicProvider);
+
+ // Second, add another provider that adds a non-heuristic result immediately
+ // with suggestedIndex = 1.
+ let nonHeuristicResult = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TIP,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ text: "This is a test tip.",
+ buttonText: "Done",
+ type: "test",
+ helpUrl: "http://example.com/",
+ }
+ );
+ nonHeuristicResult.suggestedIndex = 1;
+ let nonHeuristicProvider = new UrlbarTestUtils.TestProvider({
+ results: [nonHeuristicResult],
+ name: "nonHeuristicProvider",
+ priority: Infinity,
+ });
+ UrlbarProvidersManager.registerProvider(nonHeuristicProvider);
+
+ // Do a search but don't wait for it to finish.
+ let searchPromise = UrlbarTestUtils.promiseAutocompleteResultPopup({
+ value: "test",
+ window,
+ });
+
+ // When the view opens, press the up arrow key to select the one-off search
+ // settings button. There's no point in selecting instead the non-heuristic
+ // result because once we do that, the search is canceled, and the heuristic
+ // result will never be added.
+ await UrlbarTestUtils.promisePopupOpen(window, () => {});
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+
+ // Wait for the search to finish.
+ await searchPromise;
+
+ // The first result should be the heuristic.
+ let actualHeuristic = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(actualHeuristic.type, UrlbarUtils.RESULT_TYPE.SEARCH);
+
+ // Check the second result for good measure.
+ let actualNonHeuristic = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ 1
+ );
+ Assert.equal(actualNonHeuristic.type, UrlbarUtils.RESULT_TYPE.TIP);
+
+ // No result should be selected.
+ Assert.equal(UrlbarTestUtils.getSelectedElement(window), null);
+ Assert.equal(UrlbarTestUtils.getSelectedElementIndex(window), -1);
+
+ // The one-off settings button should be selected.
+ Assert.equal(
+ gURLBar.view.oneOffSearchButtons.selectedButton,
+ gURLBar.view.oneOffSearchButtons.settingsButtonCompact
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ UrlbarProvidersManager.unregisterProvider(heuristicProvider);
+ UrlbarProvidersManager.unregisterProvider(nonHeuristicProvider);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_ime_composition.js b/browser/components/urlbar/tests/browser/browser_ime_composition.js
new file mode 100644
index 0000000000..94df57fc7f
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_ime_composition.js
@@ -0,0 +1,287 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests ime composition handling.
+
+function composeAndCheckPanel(string, isPopupOpen) {
+ EventUtils.synthesizeCompositionChange({
+ composition: {
+ string,
+ clauses: [
+ {
+ length: string.length,
+ attr: Ci.nsITextInputProcessor.ATTR_RAW_CLAUSE,
+ },
+ ],
+ },
+ caret: { start: string.length, length: 0 },
+ key: { key: string ? string[string.length - 1] : "KEY_Backspace" },
+ });
+ Assert.equal(
+ UrlbarTestUtils.isPopupOpen(window),
+ isPopupOpen,
+ "Check panel open state"
+ );
+}
+
+add_task(async function() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0]],
+ });
+
+ await PlacesUtils.history.clear();
+ // Add at least one typed entry for the empty results set. Also clear history
+ // so that this can be over the autofill threshold.
+ await PlacesTestUtils.addVisits({
+ uri: "http://mozilla.org/",
+ transition: PlacesUtils.history.TRANSITIONS.TYPED,
+ });
+ // Add a bookmark to ensure we autofill the engine domain for tab-to-search.
+ let bm = await PlacesUtils.bookmarks.insert({
+ url: "http://example.com/",
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ });
+ let engine = await Services.search.addEngineWithDetails("Test", {
+ alias: "@test",
+ template: `http://example.com/?search={searchTerms}`,
+ });
+ let originalEngine = await Services.search.getDefault();
+ await Services.search.setDefault(engine);
+
+ registerCleanupFunction(async () => {
+ await Services.search.setDefault(originalEngine);
+ await Services.search.removeEngine(engine);
+ await PlacesUtils.bookmarks.remove(bm);
+ await PlacesUtils.history.clear();
+ });
+
+ // Test both pref values.
+ for (let val of [true, false]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.imeCompositionClosesPanel", val]],
+ });
+ await test_composition(val);
+ await test_composition_searchMode_preview(val);
+ await test_composition_tabToSearch(val);
+ }
+});
+
+async function test_composition(compositionClosesPanel) {
+ gURLBar.focus();
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ info("Check the panel state during composition");
+ composeAndCheckPanel("I", false);
+ Assert.equal(gURLBar.value, "I", "Check urlbar value");
+ composeAndCheckPanel("In", false);
+ Assert.equal(gURLBar.value, "In", "Check urlbar value");
+
+ info("Committing composition should open the panel.");
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeComposition({
+ type: "compositioncommitasis",
+ key: { key: "KEY_Enter" },
+ });
+ });
+ Assert.equal(gURLBar.value, "In", "Check urlbar value");
+
+ info("Check the panel state starting from an open panel.");
+ Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Popup should be open");
+ composeAndCheckPanel("t", !compositionClosesPanel);
+ Assert.equal(gURLBar.value, "Int", "Check urlbar value");
+ composeAndCheckPanel("te", !compositionClosesPanel);
+ Assert.equal(gURLBar.value, "Inte", "Check urlbar value");
+
+ // Committing composition should open the popup.
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeComposition({
+ type: "compositioncommitasis",
+ key: { key: "KEY_Enter" },
+ });
+ });
+ Assert.equal(gURLBar.value, "Inte", "Check urlbar value");
+
+ info("If composition is cancelled, the value shouldn't be changed.");
+ Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Popup should be open");
+ composeAndCheckPanel("r", !compositionClosesPanel);
+ Assert.equal(gURLBar.value, "Inter", "Check urlbar value");
+ composeAndCheckPanel("", !compositionClosesPanel);
+ Assert.equal(gURLBar.value, "Inte", "Check urlbar value");
+ // Canceled compositionend should reopen the popup.
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeComposition({
+ type: "compositioncommit",
+ data: "",
+ key: { key: "KEY_Escape" },
+ });
+ });
+ Assert.equal(gURLBar.value, "Inte", "Check urlbar value");
+
+ info(
+ "If composition replaces some characters and canceled, the search string should be the latest value."
+ );
+ Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Popup should be open");
+ EventUtils.synthesizeKey("VK_LEFT", { shiftKey: true });
+ EventUtils.synthesizeKey("VK_LEFT", { shiftKey: true });
+ composeAndCheckPanel("t", !compositionClosesPanel);
+ Assert.equal(gURLBar.value, "Int", "Check urlbar value");
+ composeAndCheckPanel("te", !compositionClosesPanel);
+ Assert.equal(gURLBar.value, "Inte", "Check urlbar value");
+ composeAndCheckPanel("", !compositionClosesPanel);
+ Assert.equal(gURLBar.value, "In", "Check urlbar value");
+
+ // Canceled compositionend should search the result with the latest value.
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeComposition({
+ type: "compositioncommitasis",
+ key: { key: "KEY_Escape" },
+ });
+ });
+ Assert.equal(gURLBar.value, "In", "Check urlbar value");
+
+ Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Popup should be open");
+ info(
+ "Removing all characters should leave the popup open, Esc should then close it."
+ );
+ EventUtils.synthesizeKey("KEY_Backspace", {});
+ EventUtils.synthesizeKey("KEY_Backspace", {});
+ Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Popup should be open");
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeKey("KEY_Escape", {});
+ });
+ Assert.equal(gURLBar.value, "", "Check urlbar value");
+
+ info("Composition which is canceled shouldn't cause opening the popup.");
+ Assert.ok(!UrlbarTestUtils.isPopupOpen(window), "Popup should be closed");
+ composeAndCheckPanel("I", false);
+ Assert.equal(gURLBar.value, "I", "Check urlbar value");
+ composeAndCheckPanel("In", false);
+ Assert.equal(gURLBar.value, "In", "Check urlbar value");
+ composeAndCheckPanel("", false);
+ Assert.equal(gURLBar.value, "", "Check urlbar value");
+
+ info("Canceled compositionend shouldn't open the popup if it was closed.");
+ await UrlbarTestUtils.promisePopupClose(window);
+ EventUtils.synthesizeComposition({
+ type: "compositioncommitasis",
+ key: { key: "KEY_Escape" },
+ });
+ Assert.ok(!UrlbarTestUtils.isPopupOpen(window), "Popup should be closed");
+ Assert.equal(gURLBar.value, "", "Check urlbar value");
+
+ info("Down key should open the popup even if the editor is empty.");
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {});
+ });
+ Assert.equal(gURLBar.value, "", "Check urlbar value");
+
+ info(
+ "If popup is open at starting composition, the popup should be reopened after composition anyway."
+ );
+ Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Popup should be open");
+ composeAndCheckPanel("I", !compositionClosesPanel);
+ Assert.equal(gURLBar.value, "I", "Check urlbar value");
+ composeAndCheckPanel("In", !compositionClosesPanel);
+ Assert.equal(gURLBar.value, "In", "Check urlbar value");
+ composeAndCheckPanel("", !compositionClosesPanel);
+ Assert.equal(gURLBar.value, "", "Check urlbar value");
+ // A canceled compositionend should open the popup if it was open.
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeComposition({
+ type: "compositioncommitasis",
+ key: { key: "KEY_Escape" },
+ });
+ });
+ Assert.equal(gURLBar.value, "", "Check urlbar value");
+
+ info("Type normally, and hit escape, the popup should be closed.");
+ Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Popup should be open");
+ EventUtils.synthesizeKey("I", {});
+ EventUtils.synthesizeKey("n", {});
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeKey("KEY_Escape", {});
+ });
+ Assert.equal(gURLBar.value, "In", "Check urlbar value");
+ // Clear typed chars.
+ EventUtils.synthesizeKey("KEY_Backspace", {});
+ EventUtils.synthesizeKey("KEY_Backspace", {});
+ Assert.equal(gURLBar.value, "", "Check urlbar value");
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeKey("KEY_Escape", {});
+ });
+
+ info("With autofill, compositionstart shouldn't open the popup");
+ Assert.ok(!UrlbarTestUtils.isPopupOpen(window), "Popup should be closed");
+ composeAndCheckPanel("M", false);
+ Assert.equal(gURLBar.value, "M", "Check urlbar value");
+ composeAndCheckPanel("Mo", false);
+ Assert.equal(gURLBar.value, "Mo", "Check urlbar value");
+ // Committing composition should open the popup.
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeComposition({
+ type: "compositioncommitasis",
+ key: { key: "KEY_Enter" },
+ });
+ });
+ Assert.equal(gURLBar.value, "Mozilla.org/", "Check urlbar value");
+}
+
+async function test_composition_searchMode_preview(compositionClosesPanel) {
+ info("Check Search Mode preview is retained by composition");
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@",
+ });
+
+ while (gURLBar.searchMode?.engineName != "Test") {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
+ }
+ let expectedSearchMode = {
+ engineName: "Test",
+ isPreview: true,
+ entry: "keywordoffer",
+ };
+ await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode);
+ composeAndCheckPanel("I", !compositionClosesPanel);
+ Assert.equal(gURLBar.value, "I", "Check urlbar value");
+ if (!compositionClosesPanel) {
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ }
+ // Test that we are in confirmed search mode.
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: "Test",
+ entry: "keywordoffer",
+ });
+ await UrlbarTestUtils.exitSearchMode(window);
+}
+
+async function test_composition_tabToSearch(compositionClosesPanel) {
+ info("Check Tab-to-Search is retained by composition");
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exa",
+ });
+
+ while (gURLBar.searchMode?.engineName != "Test") {
+ EventUtils.synthesizeKey("KEY_Tab", {}, window);
+ }
+ let expectedSearchMode = {
+ engineName: "Test",
+ isPreview: true,
+ entry: "tabtosearch",
+ };
+ await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode);
+ composeAndCheckPanel("I", !compositionClosesPanel);
+ Assert.equal(gURLBar.value, "I", "Check urlbar value");
+ if (!compositionClosesPanel) {
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ }
+ // Test that we are in confirmed search mode.
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: "Test",
+ entry: "tabtosearch",
+ });
+ await UrlbarTestUtils.exitSearchMode(window);
+}
diff --git a/browser/components/urlbar/tests/browser/browser_inputHistory.js b/browser/components/urlbar/tests/browser/browser_inputHistory.js
new file mode 100644
index 0000000000..191032a78d
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_inputHistory.js
@@ -0,0 +1,361 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This tests the urlbar adaptive behavior powered by input history.
+ */
+
+"use strict";
+
+async function bumpScore(uri, searchString, counts, useMouseClick = false) {
+ if (counts.visits) {
+ let visits = new Array(counts.visits).fill(uri);
+ await PlacesTestUtils.addVisits(visits);
+ }
+ if (counts.picks) {
+ for (let i = 0; i < counts.picks; ++i) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: searchString,
+ });
+ let promise = BrowserTestUtils.waitForDocLoadAndStopIt(
+ uri,
+ gBrowser.selectedBrowser
+ );
+ // Look for the expected uri.
+ while (gURLBar.untrimmedValue != uri) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+ if (useMouseClick) {
+ let element = UrlbarTestUtils.getSelectedRow(window);
+ EventUtils.synthesizeMouseAtCenter(element, {});
+ } else {
+ EventUtils.synthesizeKey("KEY_Enter");
+ }
+ await promise;
+ }
+ }
+ await PlacesTestUtils.promiseAsyncUpdates();
+}
+
+async function decayInputHistory() {
+ PlacesUtils.history.decayFrecency();
+ await PlacesTestUtils.promiseAsyncUpdates();
+}
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // We don't want autofill to influence this test.
+ ["browser.urlbar.autoFill", false],
+ ],
+ });
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+});
+
+add_task(async function test_adaptive_with_search_terms() {
+ let url1 = "http://site.tld/1";
+ let url2 = "http://site.tld/2";
+
+ info("Same visit count, same picks, one partial match, one exact match");
+ await PlacesUtils.history.clear();
+ await bumpScore(url1, "si", { visits: 3, picks: 3 });
+ await bumpScore(url2, "site", { visits: 3, picks: 3 });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "si",
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.url, url1, "Check first result");
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2);
+ Assert.equal(result.url, url2, "Check second result");
+
+ info(
+ "Same visit count, same picks, one partial match, one exact match, invert"
+ );
+ await PlacesUtils.history.clear();
+ await bumpScore(url1, "site", { visits: 3, picks: 3 });
+ await bumpScore(url2, "si", { visits: 3, picks: 3 });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "si",
+ });
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.url, url2, "Check first result");
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2);
+ Assert.equal(result.url, url1, "Check second result");
+
+ info("Same visit count, different picks, both exact match");
+ await PlacesUtils.history.clear();
+ await bumpScore(url1, "si", { visits: 3, picks: 3 });
+ await bumpScore(url2, "si", { visits: 3, picks: 1 });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "si",
+ });
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.url, url1, "Check first result");
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2);
+ Assert.equal(result.url, url2, "Check second result");
+
+ info("Same visit count, different picks, both exact match, invert");
+ await PlacesUtils.history.clear();
+ await bumpScore(url1, "si", { visits: 3, picks: 1 });
+ await bumpScore(url2, "si", { visits: 3, picks: 3 });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "si",
+ });
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.url, url2, "Check first result");
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2);
+ Assert.equal(result.url, url1, "Check second result");
+
+ info("Same visit count, different picks, both partial match");
+ await PlacesUtils.history.clear();
+ await bumpScore(url1, "site", { visits: 3, picks: 3 });
+ await bumpScore(url2, "site", { visits: 3, picks: 1 });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "si",
+ });
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.url, url1, "Check first result");
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2);
+ Assert.equal(result.url, url2, "Check second result");
+
+ info("Same visit count, different picks, both partial match, invert");
+ await PlacesUtils.history.clear();
+ await bumpScore(url1, "site", { visits: 3, picks: 1 });
+ await bumpScore(url2, "site", { visits: 3, picks: 3 });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "si",
+ });
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.url, url2, "Check first result");
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2);
+ Assert.equal(result.url, url1, "Check second result");
+});
+
+add_task(async function test_adaptive_with_decay() {
+ let url1 = "http://site.tld/1";
+ let url2 = "http://site.tld/2";
+
+ info("Same visit count, same picks, both exact match, decay first");
+ await PlacesUtils.history.clear();
+ await bumpScore(url1, "si", { visits: 3, picks: 3 });
+ await decayInputHistory();
+ await bumpScore(url2, "si", { visits: 3, picks: 3 });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "si",
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.url, url2, "Check first result");
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2);
+ Assert.equal(result.url, url1, "Check second result");
+
+ info("Same visit count, same picks, both exact match, decay second");
+ await PlacesUtils.history.clear();
+ await bumpScore(url2, "si", { visits: 3, picks: 3 });
+ await decayInputHistory();
+ await bumpScore(url1, "si", { visits: 3, picks: 3 });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "si",
+ });
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.url, url1, "Check first result");
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2);
+ Assert.equal(result.url, url2, "Check second result");
+});
+
+add_task(async function test_adaptive_limited() {
+ let url1 = "http://site.tld/1";
+ let url2 = "http://site.tld/2";
+
+ info("Same visit count, same picks, both exact match, decay first");
+ await PlacesUtils.history.clear();
+ await bumpScore(url1, "si", { visits: 3, picks: 3 });
+ await decayInputHistory();
+ await bumpScore(url2, "si", { visits: 3, picks: 3 });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "si",
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.url, url2, "Check first result");
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2);
+ Assert.equal(result.url, url1, "Check second result");
+
+ info("Same visit count, same picks, both exact match, decay second");
+ await PlacesUtils.history.clear();
+ await bumpScore(url2, "si", { visits: 3, picks: 3 });
+ await decayInputHistory();
+ await bumpScore(url1, "si", { visits: 3, picks: 3 });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "si",
+ });
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.url, url1, "Check first result");
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2);
+ Assert.equal(result.url, url2, "Check second result");
+});
+
+add_task(async function test_adaptive_limited() {
+ info(
+ "Adaptive results should be added at the top up to maxRichResults / 4, then enqueued"
+ );
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ // Add as many adaptive results as maxRichResults.
+ let n = UrlbarPrefs.get("maxRichResults");
+ let urls = Array(n)
+ .fill(0)
+ .map((e, i) => "http://site.tld/" + i);
+ for (let url of urls) {
+ await bumpScore(url, "site", { visits: 1, picks: 1 });
+ }
+
+ // Add a matching bookmark with an higher frecency.
+ let url = "http://site.bookmark.tld/";
+ await PlacesTestUtils.addVisits(url);
+ let bm = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "test_site_book",
+ url,
+ });
+
+ let expectedBookmarkIndex = Math.floor(n / 4) + 2;
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "site",
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ expectedBookmarkIndex
+ );
+ Assert.equal(result.url, url, "Check bookmarked result");
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, n - 1);
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ n,
+ "Check all the results are filled"
+ );
+ Assert.ok(
+ result.url.startsWith("http://site.tld"),
+ "Check last adaptive result"
+ );
+ await PlacesUtils.bookmarks.remove(bm);
+});
+
+add_task(async function test_adaptive_behaviors() {
+ info(
+ "Check adaptive results are not provided regardless of the requested behavior"
+ );
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ // Add an adaptive entry.
+ await bumpScore("http://site.tld/1", "site", { visits: 1, picks: 1 });
+
+ let url = "http://bookmarked.site.tld/1";
+ let bm = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "test_book",
+ url,
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Search only bookmarks.
+ ["browser.urlbar.suggest.bookmarks", true],
+ ["browser.urlbar.suggest.history", false],
+ ],
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "site",
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.url, url, "Check bookmarked result");
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 2,
+ "Check there are no unexpected results"
+ );
+
+ await PlacesUtils.bookmarks.remove(bm);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_adaptive_mouse() {
+ info("Check adaptive results are updated on mouse picks");
+ await PlacesUtils.history.clear();
+
+ let url1 = "http://site.tld/1";
+ let url2 = "http://site.tld/2";
+
+ info("Same visit count, different picks");
+ await PlacesUtils.history.clear();
+ await bumpScore(url1, "site", { visits: 3, picks: 3 }, true);
+ await bumpScore(url2, "site", { visits: 3, picks: 1 }, true);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "si",
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.url, url1, "Check first result");
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2);
+ Assert.equal(result.url, url2, "Check second result");
+
+ info("Same visit count, different picks, invert");
+ await PlacesUtils.history.clear();
+ await bumpScore(url1, "site", { visits: 3, picks: 1 }, true);
+ await bumpScore(url2, "site", { visits: 3, picks: 3 }, true);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "si",
+ });
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.url, url2, "Check first result");
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2);
+ Assert.equal(result.url, url1, "Check second result");
+});
+
+add_task(async function test_adaptive_searchmode() {
+ info("Check adaptive history is not shown in search mode.");
+
+ let suggestionsEngine = await SearchTestUtils.promiseNewSearchEngine(
+ getRootDirectory(gTestPath) + "searchSuggestionEngine.xml"
+ );
+
+ let url1 = "http://site.tld/1";
+ let url2 = "http://site.tld/2";
+
+ info("Sanity check: adaptive history is shown for a normal search.");
+ await PlacesUtils.history.clear();
+ await bumpScore(url1, "site", { visits: 3, picks: 3 }, true);
+ await bumpScore(url2, "site", { visits: 3, picks: 1 }, true);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "si",
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.url, url1, "Check first result");
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2);
+ Assert.equal(result.url, url2, "Check second result");
+
+ info("Entering search mode.");
+ // enterSearchMode checks internally that our site.tld URLs are not shown.
+ await UrlbarTestUtils.enterSearchMode(window, {
+ engineName: suggestionsEngine.name,
+ });
+});
diff --git a/browser/components/urlbar/tests/browser/browser_inputHistory_emptystring.js b/browser/components/urlbar/tests/browser/browser_inputHistory_emptystring.js
new file mode 100644
index 0000000000..8fc44329ff
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_inputHistory_emptystring.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This tests input history in cases where the search string is empty.
+ * In the future we may want to not account for these, but for now they are
+ * stored with an empty input field.
+ */
+
+"use strict";
+
+async function checkInputHistory(len = 0) {
+ await PlacesUtils.withConnectionWrapper(
+ "test::checkInputHistory",
+ async db => {
+ let rows = await db.executeCached(`SELECT input FROM moz_inputhistory`);
+ Assert.equal(rows.length, len, "There should only be 1 entry");
+ if (len) {
+ Assert.equal(rows[0].getResultByIndex(0), "", "Input should be empty");
+ }
+ }
+ );
+}
+
+async function clearInputHistory() {
+ await PlacesUtils.withConnectionWrapper("test::clearInputHistory", db => {
+ return db.executeCached(`DELETE FROM moz_inputhistory`);
+ });
+}
+
+const TEST_URL = "http://example.com/";
+
+async function do_test(openFn, pickMethod) {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:blank",
+ },
+ async function(browser) {
+ await clearInputHistory();
+ await openFn();
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ let promise = BrowserTestUtils.waitForDocLoadAndStopIt(TEST_URL, browser);
+ if (pickMethod == "keyboard") {
+ info(`Test pressing Enter`);
+ EventUtils.sendKey("down");
+ EventUtils.sendKey("return");
+ } else {
+ info("Test with click");
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ EventUtils.synthesizeMouseAtCenter(result.element.row, {});
+ }
+ await promise;
+ await checkInputHistory(1);
+ }
+ );
+}
+
+add_task(async function setup() {
+ await PlacesUtils.history.clear();
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits(TEST_URL);
+ }
+
+ await updateTopSites(sites => sites && sites[0] && sites[0].url == TEST_URL);
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ });
+});
+
+add_task(async function test_history_no_search_terms() {
+ for (let pickMethod of ["keyboard", "mouse"]) {
+ // If a testFn returns false, it will be skipped.
+ for (let openFn of [
+ () => {
+ info("Test opening panel with down key");
+ gURLBar.focus();
+ EventUtils.sendKey("down");
+ },
+ async () => {
+ info("Test opening panel on focus");
+ gURLBar.blur();
+ EventUtils.synthesizeMouseAtCenter(gURLBar.textbox, {});
+ },
+ async () => {
+ info("Test opening panel on focus on a page");
+ let selectedBrowser = gBrowser.selectedBrowser;
+ // A page other than TEST_URL must be loaded, or the first Top Site
+ // result will be a switch-to-tab result and page won't be reloaded when
+ // the result is selected.
+ BrowserTestUtils.loadURI(selectedBrowser, "http://example.org/");
+ await BrowserTestUtils.browserLoaded(selectedBrowser);
+ gURLBar.blur();
+ EventUtils.synthesizeMouseAtCenter(gURLBar.textbox, {});
+ },
+ ]) {
+ await do_test(openFn, pickMethod);
+ }
+ }
+});
diff --git a/browser/components/urlbar/tests/browser/browser_keepStateAcrossTabSwitches.js b/browser/components/urlbar/tests/browser/browser_keepStateAcrossTabSwitches.js
new file mode 100644
index 0000000000..4e21f232b4
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_keepStateAcrossTabSwitches.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Verify user typed text remains in the URL bar when tab switching, even when
+ * loads fail.
+ */
+add_task(async function validURL() {
+ let input = "i-definitely-dont-exist.example.com";
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/"
+ );
+ let browser = tab.linkedBrowser;
+ // Note: Waiting on content document not being hidden because new tab pages can be preloaded,
+ // in which case no load events fire.
+ await SpecialPowers.spawn(browser, [], async () => {
+ await ContentTaskUtils.waitForCondition(() => {
+ return content.document && !content.document.hidden;
+ });
+ });
+ let errorPageLoaded = BrowserTestUtils.waitForErrorPage(browser);
+ gURLBar.value = input;
+ gURLBar.select();
+ EventUtils.sendKey("return");
+ await errorPageLoaded;
+ is(gURLBar.value, input, "Text is still in URL bar");
+ await BrowserTestUtils.switchTab(gBrowser, tab.previousElementSibling);
+ await BrowserTestUtils.switchTab(gBrowser, tab);
+ is(gURLBar.value, input, "Text is still in URL bar after tab switch");
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * Invalid URIs fail differently (that is, immediately, in the loadURI call)
+ * if keyword searches are turned off. Test that this works, too.
+ */
+add_task(async function invalidURL() {
+ let input = "To be or not to be-that is the question";
+ await SpecialPowers.pushPrefEnv({ set: [["keyword.enabled", false]] });
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank",
+ false
+ );
+ let browser = tab.linkedBrowser;
+ // Note: Waiting on content document not being hidden because new tab pages can be preloaded,
+ // in which case no load events fire.
+ await SpecialPowers.spawn(browser, [], async () => {
+ await ContentTaskUtils.waitForCondition(() => {
+ return content.document && !content.document.hidden;
+ });
+ });
+ let errorPageLoaded = BrowserTestUtils.waitForErrorPage(tab.linkedBrowser);
+ gURLBar.value = input;
+ gURLBar.select();
+ EventUtils.sendKey("return");
+ await errorPageLoaded;
+ is(gURLBar.value, input, "Text is still in URL bar");
+ is(tab.linkedBrowser.userTypedValue, input, "Text still stored on browser");
+ await BrowserTestUtils.switchTab(gBrowser, tab.previousElementSibling);
+ await BrowserTestUtils.switchTab(gBrowser, tab);
+ is(gURLBar.value, input, "Text is still in URL bar after tab switch");
+ is(tab.linkedBrowser.userTypedValue, input, "Text still stored on browser");
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_keyword.js b/browser/components/urlbar/tests/browser/browser_keyword.js
new file mode 100644
index 0000000000..8b6d84ec39
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_keyword.js
@@ -0,0 +1,240 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This tests that keywords are displayed and handled correctly.
+ */
+
+async function promise_first_result(inputText) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: inputText,
+ });
+
+ return UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+}
+
+function assertURL(result, expectedUrl, keyword, input, postData) {
+ Assert.equal(result.url, expectedUrl, "Should have the correct URL");
+ if (postData) {
+ Assert.equal(
+ NetUtil.readInputStreamToString(
+ result.postData,
+ result.postData.available()
+ ),
+ postData,
+ "Should have the correct postData"
+ );
+ }
+}
+
+const TEST_URL = `${TEST_BASE_URL}print_postdata.sjs`;
+
+add_task(async function setup() {
+ await PlacesUtils.keywords.insert({
+ keyword: "get",
+ url: TEST_URL + "?q=%s",
+ });
+ await PlacesUtils.keywords.insert({
+ keyword: "post",
+ url: TEST_URL,
+ postData: "q=%s",
+ });
+ await PlacesUtils.keywords.insert({
+ keyword: "question?",
+ url: TEST_URL + "?q2=%s",
+ });
+ await PlacesUtils.keywords.insert({
+ keyword: "?question",
+ url: TEST_URL + "?q3=%s",
+ });
+ // Avoid fetching search suggestions.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.searches", false]],
+ });
+ let engine = await SearchTestUtils.promiseNewSearchEngine(
+ getRootDirectory(gTestPath) + "searchSuggestionEngine.xml"
+ );
+ let defaultEngine = Services.search.defaultEngine;
+ Services.search.defaultEngine = engine;
+
+ registerCleanupFunction(async function() {
+ Services.search.defaultEngine = defaultEngine;
+ await PlacesUtils.keywords.remove("get");
+ await PlacesUtils.keywords.remove("post");
+ await PlacesUtils.keywords.remove("question?");
+ await PlacesUtils.keywords.remove("?question");
+ while (gBrowser.tabs.length > 1) {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+ });
+});
+
+add_task(async function test_display_keyword_without_query() {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla");
+
+ // Test a keyword that also has blank spaces to ensure they are ignored as well.
+ let result = await promise_first_result("get ");
+
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.KEYWORD,
+ "Should have a keyword result"
+ );
+ Assert.equal(
+ result.displayed.title,
+ "https://example.com/browser/browser/components/urlbar/tests/browser/print_postdata.sjs?q=",
+ "Node should contain the url of the bookmark"
+ );
+ let [action] = await document.l10n.formatValues([
+ { id: "urlbar-result-action-visit" },
+ ]);
+ Assert.equal(result.displayed.action, action, "Should have visit indicated");
+});
+
+add_task(async function test_keyword_using_get() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:mozilla"
+ );
+
+ let result = await promise_first_result("get something");
+
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.KEYWORD,
+ "Should have a keyword result"
+ );
+ Assert.equal(
+ result.displayed.title,
+ "example.com: something",
+ "Node should contain the name of the bookmark and query"
+ );
+ Assert.ok(!result.displayed.action, "Should have an empty action");
+
+ assertURL(result, TEST_URL + "?q=something", "get", "get something");
+
+ let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
+
+ // Click on the result
+ info("Normal click on result");
+ let tabPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ EventUtils.synthesizeMouseAtCenter(element, {});
+ await tabPromise;
+ Assert.equal(
+ tab.linkedBrowser.currentURI.spec,
+ TEST_URL + "?q=something",
+ "Tab should have loaded from clicking on result"
+ );
+
+ // Middle-click on the result
+ info("Middle-click on result");
+ result = await promise_first_result("get somethingmore");
+
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.KEYWORD,
+ "Should have a keyword result"
+ );
+
+ assertURL(result, TEST_URL + "?q=somethingmore", "get", "get somethingmore");
+
+ tabPromise = BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "TabOpen");
+ element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
+ EventUtils.synthesizeMouseAtCenter(element, { button: 1 });
+ let tabOpenEvent = await tabPromise;
+ let newTab = tabOpenEvent.target;
+ await BrowserTestUtils.browserLoaded(newTab.linkedBrowser);
+ Assert.equal(
+ newTab.linkedBrowser.currentURI.spec,
+ TEST_URL + "?q=somethingmore",
+ "Tab should have loaded from middle-clicking on result"
+ );
+});
+
+add_task(async function test_keyword_using_post() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:mozilla"
+ );
+
+ let result = await promise_first_result("post something");
+
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.KEYWORD,
+ "Should have a keyword result"
+ );
+ Assert.equal(
+ result.displayed.title,
+ "example.com: something",
+ "Node should contain the name of the bookmark and query"
+ );
+ Assert.ok(!result.displayed.action, "Should have an empty action");
+
+ assertURL(result, TEST_URL, "post", "post something", "q=something");
+
+ // Click on the result
+ info("Normal click on result");
+ let tabPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
+ EventUtils.synthesizeMouseAtCenter(element, {});
+ info("waiting for tab");
+ await tabPromise;
+ Assert.equal(
+ tab.linkedBrowser.currentURI.spec,
+ TEST_URL,
+ "Tab should have loaded from clicking on result"
+ );
+
+ let postData = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ async function() {
+ return content.document.body.textContent;
+ }
+ );
+ Assert.equal(postData, "q=something", "post data was submitted correctly");
+});
+
+add_task(async function test_keyword_with_question_mark() {
+ // TODO Bug 1517140: keywords containing restriction chars should not be
+ // allowed, or properly supported.
+ let result = await promise_first_result("question?");
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "Result should be a search"
+ );
+ Assert.equal(result.searchParams.query, "question?", "Check search query");
+
+ result = await promise_first_result("question? something");
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.KEYWORD,
+ "Result should be a keyword"
+ );
+ Assert.equal(result.keyword, "question?", "Check search query");
+
+ result = await promise_first_result("?question");
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "Result should be a search"
+ );
+ Assert.equal(result.searchParams.query, "question", "Check search query");
+
+ result = await promise_first_result("?question something");
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "Result should be a search"
+ );
+ Assert.equal(
+ result.searchParams.query,
+ "question something",
+ "Check search query"
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_keywordBookmarklets.js b/browser/components/urlbar/tests/browser/browser_keywordBookmarklets.js
new file mode 100644
index 0000000000..97220fc268
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_keywordBookmarklets.js
@@ -0,0 +1,133 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function setup() {
+ let bm = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "bookmarklet",
+ url: "javascript:'%sx'%20",
+ });
+ await PlacesUtils.keywords.insert({ keyword: "bm", url: bm.url });
+ registerCleanupFunction(async function() {
+ await PlacesUtils.bookmarks.remove(bm);
+ });
+
+ let testFns = [
+ function() {
+ info("Type keyword and immediately press enter");
+ gURLBar.value = "bm";
+ gURLBar.focus();
+ EventUtils.synthesizeKey("KEY_Enter");
+ return "x";
+ },
+ function() {
+ info("Type keyword with searchstring and immediately press enter");
+ gURLBar.value = "bm a";
+ gURLBar.focus();
+ EventUtils.synthesizeKey("KEY_Enter");
+ return "ax";
+ },
+ async function() {
+ info("Search keyword, then press enter");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "bm",
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(result.title, "javascript:'x' ", "Check title");
+ EventUtils.synthesizeKey("KEY_Enter");
+ return "x";
+ },
+ async function() {
+ info("Search keyword with searchstring, then press enter");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "bm a",
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(result.title, "javascript:'ax' ", "Check title");
+ EventUtils.synthesizeKey("KEY_Enter");
+ return "ax";
+ },
+ async function() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "bm",
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(result.title, "javascript:'x' ", "Check title");
+ let element = UrlbarTestUtils.getSelectedRow(window);
+ EventUtils.synthesizeMouseAtCenter(element, {});
+ return "x";
+ },
+ async function() {
+ info("Search keyword with searchstring, then click");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "bm a",
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(result.title, "javascript:'ax' ", "Check title");
+ let element = UrlbarTestUtils.getSelectedRow(window);
+ EventUtils.synthesizeMouseAtCenter(element, {});
+ return "ax";
+ },
+ ];
+ for (let testFn of testFns) {
+ await do_test(testFn);
+ }
+});
+
+async function do_test(loadFn) {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ },
+ async browser => {
+ let originalPrincipal = gBrowser.contentPrincipal;
+ let originalPrincipalURI = await getPrincipalURI(browser);
+
+ let promise = BrowserTestUtils.waitForContentEvent(browser, "pageshow");
+ const expectedTextContent = await loadFn();
+ info("Awaiting pageshow event");
+ await promise;
+ // URI should not change when we run a javascript: URL.
+ Assert.equal(gBrowser.currentURI.spec, "about:blank");
+ const textContent = await ContentTask.spawn(browser, [], function() {
+ return content.document.documentElement.textContent;
+ });
+ Assert.equal(textContent, expectedTextContent);
+
+ let newPrincipalURI = await getPrincipalURI(browser);
+ Assert.equal(
+ newPrincipalURI,
+ originalPrincipalURI,
+ "content has the same principal"
+ );
+
+ // In e10s, null principals don't round-trip so the same null principal sent
+ // from the child will be a new null principal. Verify that this is the
+ // case.
+ if (browser.isRemoteBrowser) {
+ Assert.ok(
+ originalPrincipal.isNullPrincipal &&
+ gBrowser.contentPrincipal.isNullPrincipal,
+ "both principals should be null principals in the parent"
+ );
+ } else {
+ Assert.ok(
+ gBrowser.contentPrincipal.equals(originalPrincipal),
+ "javascript bookmarklet should inherit principal"
+ );
+ }
+ }
+ );
+}
+
+function getPrincipalURI(browser) {
+ return SpecialPowers.spawn(browser, [], function() {
+ return content.document.nodePrincipal.spec;
+ });
+}
diff --git a/browser/components/urlbar/tests/browser/browser_keywordSearch.js b/browser/components/urlbar/tests/browser/browser_keywordSearch.js
new file mode 100644
index 0000000000..5ba5805589
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_keywordSearch.js
@@ -0,0 +1,61 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ **/
+
+var gTests = [
+ {
+ name: "normal search (search service)",
+ text: "test search",
+ expectText: "test+search",
+ },
+ {
+ name: "?-prefixed search (search service)",
+ text: "? foo ",
+ expectText: "foo",
+ },
+];
+
+add_task(async function setup() {
+ let engine = await SearchTestUtils.promiseNewSearchEngine(
+ getRootDirectory(gTestPath) + "searchSuggestionEngine.xml"
+ );
+ let oldDefaultEngine = await Services.search.getDefault();
+ await Services.search.setDefault(engine);
+ registerCleanupFunction(async function() {
+ await Services.search.setDefault(oldDefaultEngine);
+ });
+});
+
+add_task(async function() {
+ // Test both directly setting a value and pressing enter, or setting the
+ // value through input events, like the user would do.
+ const setValueFns = [
+ value => {
+ gURLBar.value = value;
+ },
+ value => {
+ return UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value,
+ });
+ },
+ ];
+
+ for (let test of gTests) {
+ info("Testing: " + test.name);
+ await BrowserTestUtils.withNewTab({ gBrowser }, async browser => {
+ for (let setValueFn of setValueFns) {
+ gURLBar.select();
+ await setValueFn(test.text);
+ EventUtils.synthesizeKey("KEY_Enter");
+
+ let expectedUrl = "http://mochi.test:8888/?terms=" + test.expectText;
+ info("Waiting for load: " + expectedUrl);
+ await BrowserTestUtils.browserLoaded(browser, false, expectedUrl);
+ // At least one test.
+ Assert.equal(browser.currentURI.spec, expectedUrl);
+ }
+ });
+ }
+});
diff --git a/browser/components/urlbar/tests/browser/browser_keywordSearch_postData.js b/browser/components/urlbar/tests/browser/browser_keywordSearch_postData.js
new file mode 100644
index 0000000000..8484cd778d
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_keywordSearch_postData.js
@@ -0,0 +1,78 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ **/
+
+var gTests = [
+ {
+ name: "single word search (search service)",
+ text: "pizza",
+ expectText: "pizza",
+ },
+ {
+ name: "multi word search (search service)",
+ text: "test search",
+ expectText: "test+search",
+ },
+ {
+ name: "?-prefixed search (search service)",
+ text: "? foo ",
+ expectText: "foo",
+ },
+];
+
+add_task(async function setup() {
+ let engine = await SearchTestUtils.promiseNewSearchEngine(
+ getRootDirectory(gTestPath) + "POSTSearchEngine.xml"
+ );
+ let oldDefaultEngine = await Services.search.getDefault();
+ await Services.search.setDefault(engine);
+ registerCleanupFunction(async function() {
+ await Services.search.setDefault(oldDefaultEngine);
+ });
+});
+
+add_task(async function() {
+ // Test both directly setting a value and pressing enter, or setting the
+ // value through input events, like the user would do.
+ const setValueFns = [
+ value => {
+ gURLBar.value = value;
+ },
+ value => {
+ return UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value,
+ });
+ },
+ ];
+
+ for (let test of gTests) {
+ info("Testing: " + test.name);
+ await BrowserTestUtils.withNewTab({ gBrowser }, async browser => {
+ for (let setValueFn of setValueFns) {
+ gURLBar.select();
+ await setValueFn(test.text);
+ EventUtils.synthesizeKey("KEY_Enter");
+
+ await BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ "http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/print_postdata.sjs"
+ );
+
+ let textContent = await SpecialPowers.spawn(browser, [], async () => {
+ return content.document.body.textContent;
+ });
+
+ Assert.ok(textContent, "search page loaded");
+ let needle = "searchterms=" + test.expectText;
+ Assert.equal(
+ textContent,
+ needle,
+ "The query POST data should be returned in the response"
+ );
+ }
+ });
+ }
+});
diff --git a/browser/components/urlbar/tests/browser/browser_keyword_override.js b/browser/components/urlbar/tests/browser/browser_keyword_override.js
new file mode 100644
index 0000000000..c53adaef73
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_keyword_override.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This tests that the display of keyword results are not changed when the user
+ * presses the override button.
+ */
+
+add_task(async function() {
+ let bm = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.com/?q=%s",
+ title: "test",
+ });
+ await PlacesUtils.keywords.insert({
+ keyword: "keyword",
+ url: "http://example.com/?q=%s",
+ });
+
+ registerCleanupFunction(async function() {
+ await PlacesUtils.bookmarks.remove(bm);
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "keyword search",
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+
+ info("Before override");
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.KEYWORD,
+ "Should have a keyword result"
+ );
+ Assert.equal(
+ result.displayed.title,
+ "example.com: search",
+ "Node should contain the name of the bookmark and query"
+ );
+ Assert.ok(!result.displayed.action, "Should have an empty action");
+
+ info("During override");
+ EventUtils.synthesizeKey("VK_SHIFT", { type: "keydown" });
+
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.KEYWORD,
+ "Should have a keyword result"
+ );
+ Assert.equal(
+ result.displayed.title,
+ "example.com: search",
+ "Node should contain the name of the bookmark and query"
+ );
+ Assert.ok(!result.displayed.action, "Should have an empty action");
+
+ EventUtils.synthesizeKey("VK_SHIFT", { type: "keyup" });
+});
diff --git a/browser/components/urlbar/tests/browser/browser_keyword_select_and_type.js b/browser/components/urlbar/tests/browser/browser_keyword_select_and_type.js
new file mode 100644
index 0000000000..dc907b5ee9
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_keyword_select_and_type.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This tests that changing away from a keyword result and back again, still
+ * operates correctly.
+ */
+
+add_task(async function() {
+ let bookmarks = [];
+ bookmarks.push(
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.com/?q=%s",
+ title: "test",
+ })
+ );
+ await PlacesUtils.keywords.insert({
+ keyword: "keyword",
+ url: "http://example.com/?q=%s",
+ });
+
+ // This item is only needed so we can select the keyword item, select something
+ // else, then select the keyword item again.
+ bookmarks.push(
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.com/keyword",
+ title: "keyword abc",
+ })
+ );
+
+ registerCleanupFunction(async function() {
+ for (let bm of bookmarks) {
+ await PlacesUtils.bookmarks.remove(bm);
+ }
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:mozilla"
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "keyword a",
+ });
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1);
+
+ // First item should already be selected
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 0,
+ "Should have the first item selected"
+ );
+
+ // Select next one (important!)
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 1,
+ "Should have the second item selected"
+ );
+
+ // Re-select keyword item
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 0,
+ "Should have the first item selected"
+ );
+
+ EventUtils.sendString("b");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ Assert.equal(
+ gURLBar.value,
+ "keyword ab",
+ "urlbar should have expected input"
+ );
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.KEYWORD,
+ "Should have a result of type keyword"
+ );
+ Assert.equal(
+ result.url,
+ "http://example.com/?q=ab",
+ "Should have the correct url"
+ );
+
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_loadRace.js b/browser/components/urlbar/tests/browser/browser_loadRace.js
new file mode 100644
index 0000000000..4e1e48e5d6
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_loadRace.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This test is for testing races of loading the Urlbar when loading shortcuts.
+// For example, ensuring that if a search query is entered, but something causes
+// a page load whilst we're getting the search url, then we don't handle the
+// original search query.
+
+add_task(async function setup() {
+ sandbox = sinon.createSandbox();
+
+ registerCleanupFunction(async () => {
+ sandbox.restore();
+ });
+});
+
+async function checkShortcutLoading(modifierKeys) {
+ let deferred = PromiseUtils.defer();
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "about:robots",
+ });
+
+ // We stub getHeuristicResultFor to guarentee it doesn't resolve until after
+ // we've loaded a new page.
+ let original = UrlbarUtils.getHeuristicResultFor;
+ sandbox
+ .stub(UrlbarUtils, "getHeuristicResultFor")
+ .callsFake(async searchString => {
+ await deferred.promise;
+ return original.call(this, searchString);
+ });
+
+ // This load will be blocked until the deferred is resolved.
+ // Use a string that will be interepreted as a local URL to avoid hitting the
+ // network.
+ gURLBar.focus();
+ gURLBar.value = "example.com";
+ gURLBar.userTypedValue = true;
+ EventUtils.synthesizeKey("KEY_Enter", modifierKeys);
+
+ Assert.ok(
+ UrlbarUtils.getHeuristicResultFor.calledOnce,
+ "should have called getHeuristicResultFor"
+ );
+
+ // Now load a different page.
+ BrowserTestUtils.loadURI(tab.linkedBrowser, "about:license");
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ Assert.equal(gBrowser.visibleTabs.length, 2, "Should have 2 tabs");
+
+ // Now that the new page has loaded, unblock the previous urlbar load.
+ deferred.resolve();
+ if (modifierKeys) {
+ let openedTab = await new Promise(resolve => {
+ window.addEventListener(
+ "TabOpen",
+ event => {
+ resolve(event.target);
+ },
+ { once: true }
+ );
+ });
+ await BrowserTestUtils.browserLoaded(openedTab.linkedBrowser);
+ Assert.ok(
+ openedTab.linkedBrowser.currentURI.spec.includes("example.com"),
+ "Should have attempted to open the shortcut page"
+ );
+ BrowserTestUtils.removeTab(openedTab);
+ }
+
+ Assert.equal(
+ tab.linkedBrowser.currentURI.spec,
+ "about:license",
+ "Tab url should not have changed"
+ );
+ Assert.equal(gBrowser.visibleTabs.length, 2, "Should still have 2 tabs");
+
+ BrowserTestUtils.removeTab(tab);
+ sandbox.restore();
+}
+
+add_task(async function test_location_change_stops_load() {
+ await checkShortcutLoading();
+});
+
+add_task(async function test_opening_different_tab_with_location_change() {
+ await checkShortcutLoading({ altKey: true });
+});
diff --git a/browser/components/urlbar/tests/browser/browser_locationBarCommand.js b/browser/components/urlbar/tests/browser/browser_locationBarCommand.js
new file mode 100644
index 0000000000..aeffac3045
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_locationBarCommand.js
@@ -0,0 +1,291 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This test is designed to ensure that the correct command/operation happens
+ * when pressing enter with various key combinations in the urlbar.
+ */
+
+const TEST_VALUE = "example.com";
+const START_VALUE = "example.org";
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.altClickSave", true],
+ ["browser.urlbar.autoFill", false],
+ ],
+ });
+});
+
+add_task(async function alt_left_click_test() {
+ info("Running test: Alt left click");
+
+ // Monkey patch saveURL() to avoid dealing with file save code paths.
+ let oldSaveURL = saveURL;
+ let saveURLPromise = new Promise(resolve => {
+ saveURL = () => {
+ // Restore old saveURL() value.
+ saveURL = oldSaveURL;
+ resolve();
+ };
+ });
+
+ await triggerCommand("click", { altKey: true });
+
+ await saveURLPromise;
+ ok(true, "SaveURL was called");
+ is(gURLBar.value, "", "Urlbar reverted to original value");
+});
+
+add_task(async function shift_left_click_test() {
+ info("Running test: Shift left click");
+
+ let destinationURL = "http://" + TEST_VALUE + "/";
+ let newWindowPromise = BrowserTestUtils.waitForNewWindow({
+ url: destinationURL,
+ });
+ await triggerCommand("click", { shiftKey: true });
+ let win = await newWindowPromise;
+
+ info("URL should be loaded in a new window");
+ is(gURLBar.value, "", "Urlbar reverted to original value");
+ await promiseCheckChildNoFocusedElement(gBrowser.selectedBrowser);
+ is(
+ document.activeElement,
+ gBrowser.selectedBrowser,
+ "Content window should be focused"
+ );
+ is(win.gURLBar.value, TEST_VALUE, "New URL is loaded in new window");
+
+ // Cleanup.
+ let ourWindowRefocusedPromise = Promise.all([
+ BrowserTestUtils.waitForEvent(window, "activate"),
+ BrowserTestUtils.waitForEvent(window, "focus", true),
+ ]);
+ await BrowserTestUtils.closeWindow(win);
+ await ourWindowRefocusedPromise;
+});
+
+add_task(async function right_click_test() {
+ info("Running test: Right click on go button");
+
+ // Add a new tab.
+ await promiseOpenNewTab();
+
+ await triggerCommand("click", { button: 2 });
+
+ // Right click should do nothing (context menu will be shown).
+ is(gURLBar.value, TEST_VALUE, "Urlbar still has the value we entered");
+
+ // Cleanup.
+ gBrowser.removeCurrentTab();
+});
+
+add_task(async function shift_accel_left_click_test() {
+ info("Running test: Shift+Ctrl/Cmd left click on go button");
+
+ // Add a new tab.
+ let tab = await promiseOpenNewTab();
+
+ let loadStartedPromise = promiseLoadStarted();
+ await triggerCommand("click", { accelKey: true, shiftKey: true });
+ await loadStartedPromise;
+
+ // Check the load occurred in a new background tab.
+ info("URL should be loaded in a new background tab");
+ is(gURLBar.value, "", "Urlbar reverted to original value");
+ ok(!gURLBar.focused, "Urlbar is no longer focused after urlbar command");
+ is(gBrowser.selectedTab, tab, "Focus did not change to the new tab");
+
+ // Select the new background tab
+ gBrowser.selectedTab = gBrowser.selectedTab.nextElementSibling;
+ is(gURLBar.value, TEST_VALUE, "New URL is loaded in new tab");
+
+ // Cleanup.
+ gBrowser.removeCurrentTab();
+ gBrowser.removeCurrentTab();
+});
+
+add_task(async function load_in_current_tab_test() {
+ let tests = [
+ {
+ desc: "Simple return keypress",
+ type: "keypress",
+ },
+ {
+ desc: "Left click on go button",
+ type: "click",
+ },
+ {
+ desc: "Ctrl/Cmd+Return keypress",
+ type: "keypress",
+ details: { accelKey: true },
+ },
+ {
+ desc: "Alt+Return keypress in a blank tab",
+ type: "keypress",
+ details: { altKey: true },
+ },
+ {
+ desc: "AltGr+Return keypress in a blank tab",
+ type: "keypress",
+ details: { altGraphKey: true },
+ },
+ ];
+
+ for (let { desc, type, details } of tests) {
+ info(`Running test: ${desc}`);
+
+ // Add a new tab.
+ let tab = await promiseOpenNewTab();
+
+ // Trigger a load and check it occurs in the current tab.
+ let loadStartedPromise = promiseLoadStarted();
+ await triggerCommand(type, details);
+ await loadStartedPromise;
+
+ info("URL should be loaded in the current tab");
+ is(gURLBar.value, TEST_VALUE, "Urlbar still has the value we entered");
+ await promiseCheckChildNoFocusedElement(gBrowser.selectedBrowser);
+ is(
+ document.activeElement,
+ gBrowser.selectedBrowser,
+ "Content window should be focused"
+ );
+ is(gBrowser.selectedTab, tab, "New URL was loaded in the current tab");
+
+ // Cleanup.
+ gBrowser.removeCurrentTab();
+ }
+});
+
+add_task(async function load_in_new_tab_test() {
+ let tests = [
+ {
+ desc: "Ctrl/Cmd left click on go button",
+ type: "click",
+ details: { accelKey: true },
+ url: "about:blank",
+ },
+ {
+ desc: "Alt+Return keypress in a dirty tab",
+ type: "keypress",
+ details: { altKey: true },
+ url: START_VALUE,
+ },
+ {
+ desc: "AltGr+Return keypress in a dirty tab",
+ type: "keypress",
+ details: { altGraphKey: true },
+ url: START_VALUE,
+ },
+ ];
+
+ for (let { desc, type, details, url } of tests) {
+ info(`Running test: ${desc}`);
+
+ // Add a new tab.
+ let tab = await promiseOpenNewTab(url);
+
+ // Trigger a load and check it occurs in a new tab.
+ let tabSwitchedPromise = promiseNewTabSwitched();
+ await triggerCommand(type, details);
+ await tabSwitchedPromise;
+
+ // Check the load occurred in a new tab.
+ info("URL should be loaded in a new focused tab");
+ is(gURLBar.value, TEST_VALUE, "Urlbar still has the value we entered");
+ await promiseCheckChildNoFocusedElement(gBrowser.selectedBrowser);
+ is(
+ document.activeElement,
+ gBrowser.selectedBrowser,
+ "Content window should be focused"
+ );
+ isnot(gBrowser.selectedTab, tab, "New URL was loaded in a new tab");
+
+ // Cleanup.
+ gBrowser.removeCurrentTab();
+ gBrowser.removeCurrentTab();
+ }
+});
+
+async function triggerCommand(type, details = {}) {
+ gURLBar.focus();
+ gURLBar.value = "";
+ EventUtils.sendString(TEST_VALUE);
+
+ Assert.equal(
+ await UrlbarTestUtils.promiseUserContextId(window),
+ gBrowser.selectedTab.getAttribute("usercontextid"),
+ "userContextId must be the same as the originating tab"
+ );
+
+ if (type == "click") {
+ ok(
+ gURLBar.hasAttribute("usertyping"),
+ "usertyping attribute must be set for the go button to be visible"
+ );
+ EventUtils.synthesizeMouseAtCenter(gURLBar.goButton, details);
+ } else if (type == "keypress") {
+ EventUtils.synthesizeKey("KEY_Enter", details);
+ } else {
+ throw new Error("Unsupported event type");
+ }
+}
+
+function promiseLoadStarted() {
+ return new Promise(resolve => {
+ gBrowser.addTabsProgressListener({
+ onStateChange(browser, webProgress, req, flags, status) {
+ if (flags & Ci.nsIWebProgressListener.STATE_START) {
+ gBrowser.removeTabsProgressListener(this);
+ resolve();
+ }
+ },
+ });
+ });
+}
+
+let gUserContextIdSerial = 1;
+async function promiseOpenNewTab(url = "about:blank") {
+ let tab = BrowserTestUtils.addTab(gBrowser, url, {
+ userContextId: gUserContextIdSerial++,
+ });
+ let tabSwitchPromise = promiseNewTabSwitched(tab);
+ gBrowser.selectedTab = tab;
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await tabSwitchPromise;
+ return tab;
+}
+
+function promiseNewTabSwitched() {
+ return new Promise(resolve => {
+ gBrowser.addEventListener(
+ "TabSwitchDone",
+ function() {
+ executeSoon(resolve);
+ },
+ { once: true }
+ );
+ });
+}
+
+function promiseCheckChildNoFocusedElement(browser) {
+ if (!gMultiProcessBrowser) {
+ Assert.equal(
+ Services.focus.focusedElement,
+ null,
+ "There should be no focused element"
+ );
+ return null;
+ }
+
+ return ContentTask.spawn(browser, null, async function() {
+ Assert.equal(
+ Services.focus.focusedElement,
+ null,
+ "There should be no focused element"
+ );
+ });
+}
diff --git a/browser/components/urlbar/tests/browser/browser_locationBarExternalLoad.js b/browser/components/urlbar/tests/browser/browser_locationBarExternalLoad.js
new file mode 100644
index 0000000000..3560179be4
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_locationBarExternalLoad.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.autoFill", false]],
+ });
+ const url = "data:text/html,<body>hi";
+ await testURL(url, urlEnter);
+ await testURL(url, urlClick);
+});
+
+function urlEnter(url) {
+ gURLBar.value = url;
+ gURLBar.focus();
+ EventUtils.synthesizeKey("KEY_Enter");
+}
+
+function urlClick(url) {
+ gURLBar.focus();
+ gURLBar.value = "";
+ EventUtils.sendString(url);
+ EventUtils.synthesizeMouseAtCenter(gURLBar.goButton, {});
+}
+
+function promiseNewTabSwitched() {
+ return new Promise(resolve => {
+ gBrowser.addEventListener(
+ "TabSwitchDone",
+ function() {
+ executeSoon(resolve);
+ },
+ { once: true }
+ );
+ });
+}
+
+function promiseLoaded(browser) {
+ return SpecialPowers.spawn(browser, [undefined], async () => {
+ if (!["interactive", "complete"].includes(content.document.readyState)) {
+ await new Promise(resolve =>
+ docShell.chromeEventHandler.addEventListener(
+ "DOMContentLoaded",
+ resolve,
+ {
+ once: true,
+ capture: true,
+ }
+ )
+ );
+ }
+ });
+}
+
+async function testURL(url, loadFunc, endFunc) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ let browser = tab.linkedBrowser;
+
+ let pagePrincipal = gBrowser.contentPrincipal;
+ // We need to ensure that we set the pageshow event listener before running
+ // loadFunc, otherwise there's a chance that the content process will finish
+ // loading the page and fire pageshow before the event listener gets set.
+ let pageShowPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "pageshow"
+ );
+ loadFunc(url);
+ await pageShowPromise;
+
+ await SpecialPowers.spawn(
+ browser,
+ [{ isRemote: gMultiProcessBrowser }],
+ async function(arg) {
+ Assert.equal(
+ Services.focus.focusedElement,
+ null,
+ "focusedElement not null"
+ );
+ }
+ );
+
+ is(document.activeElement, browser, "content window should be focused");
+
+ ok(
+ !gBrowser.contentPrincipal.equals(pagePrincipal),
+ "load of " +
+ url +
+ " by " +
+ loadFunc.name +
+ " should produce a page with a different principal"
+ );
+
+ await BrowserTestUtils.removeTab(tab);
+}
diff --git a/browser/components/urlbar/tests/browser/browser_locationchange_urlbar_edit_dos.js b/browser/components/urlbar/tests/browser/browser_locationchange_urlbar_edit_dos.js
new file mode 100644
index 0000000000..925f8b0ac5
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_locationchange_urlbar_edit_dos.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URL = `${TEST_BASE_URL}file_urlbar_edit_dos.html`;
+
+async function checkURLBarValueStays(browser) {
+ gURLBar.select();
+ EventUtils.sendString("a");
+ is(gURLBar.value, "a", "URL bar value should match after sending a key");
+ await new Promise(resolve => {
+ let listener = {
+ onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
+ ok(
+ aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT,
+ "Should only get a same document location change"
+ );
+ gBrowser.selectedBrowser.removeProgressListener(filter);
+ filter = null;
+ // Wait an extra tick before resolving. We want to make sure that other
+ // web progress listeners queued after this one are called before we
+ // continue the test, in case the remainder of the test depends on those
+ // listeners. That should happen anyway since promises are resolved on
+ // the next tick, but do this to be a little safer. In particular we
+ // want to avoid having the test pass when it should fail.
+ executeSoon(resolve);
+ },
+ };
+ let filter = Cc[
+ "@mozilla.org/appshell/component/browser-status-filter;1"
+ ].createInstance(Ci.nsIWebProgress);
+ filter.addProgressListener(listener, Ci.nsIWebProgress.NOTIFY_ALL);
+ gBrowser.selectedBrowser.addProgressListener(filter);
+ });
+ is(
+ gURLBar.value,
+ "a",
+ "URL bar should not have been changed by location changes."
+ );
+}
+
+add_task(async function() {
+ // Disable autofill so that when checkURLBarValueStays types "a", it's not
+ // autofilled to addons.mozilla.org (or anything else).
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.autoFill", false]],
+ });
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_URL,
+ },
+ async function(browser) {
+ let promise1 = checkURLBarValueStays(browser);
+ SpecialPowers.spawn(browser, [""], function() {
+ content.wrappedJSObject.dos_hash();
+ });
+ await promise1;
+ let promise2 = checkURLBarValueStays(browser);
+ SpecialPowers.spawn(browser, [""], function() {
+ content.wrappedJSObject.dos_pushState();
+ });
+ await promise2;
+ }
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_new_tab_urlbar_reset.js b/browser/components/urlbar/tests/browser/browser_new_tab_urlbar_reset.js
new file mode 100644
index 0000000000..fc1319b631
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_new_tab_urlbar_reset.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Verify that urlbar state is reset when opening a new tab, so searching for the
+ * same text will reopen the results popup.
+ */
+add_task(async function() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank",
+ false
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "m",
+ });
+ assertOpen();
+
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank",
+ false
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "m",
+ });
+ assertOpen();
+
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(tab2);
+});
+
+function assertOpen() {
+ Assert.equal(gURLBar.view.isOpen, true, "Should be showing the popup");
+}
diff --git a/browser/components/urlbar/tests/browser/browser_oneOffs.js b/browser/components/urlbar/tests/browser/browser_oneOffs.js
new file mode 100644
index 0000000000..16f2f51e14
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_oneOffs.js
@@ -0,0 +1,961 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the one-off search buttons in the urlbar.
+ */
+
+"use strict";
+
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+
+let gMaxResults;
+let engine;
+
+XPCOMUtils.defineLazyGetter(this, "oneOffSearchButtons", () => {
+ return UrlbarTestUtils.getOneOffSearchButtons(window);
+});
+
+add_task(async function init() {
+ gMaxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults");
+
+ // Add a search suggestion engine and move it to the front so that it appears
+ // as the first one-off.
+ engine = await SearchTestUtils.promiseNewSearchEngine(
+ getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME
+ );
+ await Services.search.moveEngine(engine, 0);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.separatePrivateDefault.ui.enabled", false]],
+ });
+
+ registerCleanupFunction(async function() {
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.formHistory.clear();
+ });
+
+ // Initialize history with enough visits to fill up the view.
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.formHistory.clear();
+ for (let i = 0; i < gMaxResults; i++) {
+ await PlacesTestUtils.addVisits(
+ "http://example.com/browser_urlbarOneOffs.js/?" + i
+ );
+ }
+
+ // Add some more visits to the last URL added above so that the top-sites view
+ // will be non-empty.
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits(
+ "http://example.com/browser_urlbarOneOffs.js/?" + (gMaxResults - 1)
+ );
+ }
+ await updateTopSites(sites => {
+ return sites && sites[0] && sites[0].url.startsWith("http://example.com/");
+ });
+
+ // Move the mouse away from the view so that a result or one-off isn't
+ // inadvertently highlighted. See bug 1659011.
+ EventUtils.synthesizeMouse(
+ gURLBar.inputField,
+ 0,
+ 0,
+ { type: "mousemove" },
+ window
+ );
+});
+
+// Opens the view without showing the one-offs. They should be hidden and arrow
+// key selection should work properly.
+add_task(async function noOneOffs() {
+ // Do a search for "@" since we hide the one-offs in that case.
+ let value = "@";
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value,
+ fireInputEvent: true,
+ });
+ await TestUtils.waitForCondition(
+ () => !oneOffSearchButtons._rebuilding,
+ "Waiting for one-offs to finish rebuilding"
+ );
+
+ Assert.equal(
+ UrlbarTestUtils.getOneOffSearchButtonsVisible(window),
+ false,
+ "One-offs should be hidden"
+ );
+ assertState(-1, -1, value);
+
+ // Get the result count. We don't particularly care what the results are,
+ // just what the count is so that we can key through them all.
+ let resultCount = UrlbarTestUtils.getResultCount(window);
+
+ // Key down through all results.
+ for (let i = 0; i < resultCount; i++) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ assertState(i, -1);
+ }
+
+ // Key down again. Nothing should be selected.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ assertState(-1, -1, value);
+
+ // Key down again. The first result should be selected.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ assertState(0, -1);
+
+ // Key up. Nothing should be selected.
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ assertState(-1, -1, value);
+
+ // Key up through all the results.
+ for (let i = resultCount - 1; i >= 0; i--) {
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ assertState(i, -1);
+ }
+
+ // Key up again. Nothing should be selected.
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ assertState(-1, -1, value);
+
+ await hidePopup();
+});
+
+// Opens the top-sites view. The one-offs should be shown.
+add_task(async function topSites() {
+ // Do a search that shows top sites.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ fireInputEvent: true,
+ });
+ await TestUtils.waitForCondition(
+ () => !oneOffSearchButtons._rebuilding,
+ "Waiting for one-offs to finish rebuilding"
+ );
+
+ // There's one top sites result, the page with a lot of visits from init.
+ let resultURL = "example.com/browser_urlbarOneOffs.js/?" + (gMaxResults - 1);
+ Assert.equal(UrlbarTestUtils.getResultCount(window), 1, "Result count");
+
+ Assert.equal(
+ UrlbarTestUtils.getOneOffSearchButtonsVisible(window),
+ true,
+ "One-offs are visible"
+ );
+
+ assertState(-1, -1, "");
+
+ // Key down into the result.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ assertState(0, -1, resultURL);
+
+ // Key down through each one-off.
+ let numButtons = oneOffSearchButtons.getSelectableButtons(true).length;
+ for (let i = 0; i < numButtons; i++) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ assertState(-1, i, "");
+ }
+
+ // Key down again. The selection should go away.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ assertState(-1, -1, "");
+
+ // Key down again. The result should be selected.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ assertState(0, -1, resultURL);
+
+ // Key back up. The selection should go away.
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ assertState(-1, -1, "");
+
+ // Key up again. The selection should wrap back around to the one-offs. Key
+ // up through all the one-offs.
+ for (let i = numButtons - 1; i >= 0; i--) {
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ assertState(-1, i, "");
+ }
+
+ // Key up. The result should be selected.
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ assertState(0, -1, resultURL);
+
+ // Key up again. The selection should go away.
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ assertState(-1, -1, "");
+
+ await hidePopup();
+});
+
+// Keys up and down through the non-top-sites view, i.e., the view that's shown
+// when the input has been edited.
+add_task(async function editedView() {
+ // Use a typed value that returns the visits added above but that doesn't
+ // trigger autofill since that would complicate the test.
+ let typedValue = "browser_urlbarOneOffs";
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: typedValue,
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, gMaxResults - 1);
+ let heuristicResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ assertState(0, -1, typedValue);
+
+ // Key down through each result. The first result is already selected, which
+ // is why gMaxResults - 1 is the correct number of times to do this.
+ for (let i = 0; i < gMaxResults - 1; i++) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ // i starts at zero so that the textValue passed to assertState is correct.
+ // But that means that i + 1 is the expected selected index, since initially
+ // (when this loop starts) the first result is selected.
+ assertState(
+ i + 1,
+ -1,
+ "example.com/browser_urlbarOneOffs.js/?" + (gMaxResults - i - 1)
+ );
+ Assert.ok(
+ !BrowserTestUtils.is_visible(heuristicResult.element.action),
+ "The heuristic action should not be visible"
+ );
+ }
+
+ // Key down through each one-off.
+ let numButtons = oneOffSearchButtons.getSelectableButtons(true).length;
+ for (let i = 0; i < numButtons; i++) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ assertState(-1, i, typedValue);
+ Assert.equal(
+ BrowserTestUtils.is_visible(heuristicResult.element.action),
+ !oneOffSearchButtons.selectedButton.classList.contains(
+ "search-setting-button-compact"
+ ),
+ "The heuristic action should be visible when a one-off button is selected"
+ );
+ }
+
+ // Key down once more. The selection should wrap around to the first result.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ assertState(0, -1, typedValue);
+ Assert.ok(
+ BrowserTestUtils.is_visible(heuristicResult.element.action),
+ "The heuristic action should be visible"
+ );
+
+ // Now key up. The selection should wrap back around to the one-offs. Key
+ // up through all the one-offs.
+ for (let i = numButtons - 1; i >= 0; i--) {
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ assertState(-1, i, typedValue);
+ Assert.equal(
+ BrowserTestUtils.is_visible(heuristicResult.element.action),
+ !oneOffSearchButtons.selectedButton.classList.contains(
+ "search-setting-button-compact"
+ ),
+ "The heuristic action should be visible when a one-off button is selected"
+ );
+ }
+
+ // Key up through each non-heuristic result.
+ for (let i = gMaxResults - 2; i >= 0; i--) {
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ assertState(
+ i + 1,
+ -1,
+ "example.com/browser_urlbarOneOffs.js/?" + (gMaxResults - i - 1)
+ );
+ Assert.ok(
+ !BrowserTestUtils.is_visible(heuristicResult.element.action),
+ "The heuristic action should not be visible"
+ );
+ }
+
+ // Key up once more. The heuristic result should be selected.
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ assertState(0, -1, typedValue);
+ Assert.ok(
+ BrowserTestUtils.is_visible(heuristicResult.element.action),
+ "The heuristic action should be visible"
+ );
+
+ await hidePopup();
+});
+
+// Checks that "Search with Current Search Engine" items are updated to "Search
+// with One-Off Engine" when a one-off is selected.
+add_task(async function searchWith() {
+ // Enable suggestions for this subtest so we can check non-heuristic results.
+ let oldDefaultEngine = await Services.search.getDefault();
+ await Services.search.setDefault(engine);
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.searches", true]],
+ });
+
+ let typedValue = "foo";
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: typedValue,
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ assertState(0, -1, typedValue);
+
+ Assert.equal(
+ result.displayed.action,
+ "Search with " + (await Services.search.getDefault()).name,
+ "Sanity check: first result's action text"
+ );
+
+ // Alt+Down to the second one-off. Now the first result and the second
+ // one-off should both be selected.
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true, repeat: 2 });
+ assertState(0, 1, typedValue);
+
+ let engineName = oneOffSearchButtons.selectedButton.engine.name;
+ Assert.notEqual(
+ engineName,
+ (await Services.search.getDefault()).name,
+ "Sanity check: Second one-off engine should not be the current engine"
+ );
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ result.displayed.action,
+ "Search with " + engineName,
+ "First result's action text should be updated"
+ );
+
+ // Check non-heuristic results.
+ await hidePopup();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: typedValue,
+ });
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ assertState(1, -1, typedValue + "foo");
+ Assert.equal(
+ result.displayed.action,
+ "Search with " + engine.name,
+ "Sanity check: second result's action text"
+ );
+ Assert.ok(!result.heuristic, "The second result is not heuristic.");
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true, repeat: 2 });
+ assertState(1, 1, typedValue + "foo");
+
+ engineName = oneOffSearchButtons.selectedButton.engine.name;
+ Assert.notEqual(
+ engineName,
+ (await Services.search.getDefault()).name,
+ "Sanity check: Second one-off engine should not be the current engine"
+ );
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+
+ Assert.equal(
+ result.displayed.action,
+ "Search with " + engineName,
+ "Second result's action text should be updated"
+ );
+
+ await SpecialPowers.popPrefEnv();
+ await Services.search.setDefault(oldDefaultEngine);
+ await hidePopup();
+});
+
+// Clicks a one-off with an engine.
+add_task(async function oneOffClick() {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+
+ // We are explicitly using something that looks like a url, to make the test
+ // stricter. Even if it looks like a url, we should search.
+ let typedValue = "foo.bar";
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: typedValue,
+ });
+ await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ assertState(0, -1, typedValue);
+ let oneOffs = oneOffSearchButtons.getSelectableButtons(true);
+
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeMouseAtCenter(oneOffs[0], {});
+ await searchPromise;
+ Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Urlbar view is still open.");
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: oneOffs[0].engine.name,
+ entry: "oneoff",
+ });
+ await UrlbarTestUtils.exitSearchMode(window, { backspace: true });
+
+ gBrowser.removeTab(gBrowser.selectedTab);
+ await UrlbarTestUtils.formHistory.clear();
+});
+
+// Presses the Return key when a one-off with an engine is selected.
+add_task(async function oneOffReturn() {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+
+ // We are explicitly using something that looks like a url, to make the test
+ // stricter. Even if it looks like a url, we should search.
+ let typedValue = "foo.bar";
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: typedValue,
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ assertState(0, -1, typedValue);
+ let oneOffs = oneOffSearchButtons.getSelectableButtons(true);
+
+ // Alt+Down to select the first one-off.
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ assertState(0, 0, typedValue);
+
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await searchPromise;
+ Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Urlbar view is still open.");
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: oneOffs[0].engine.name,
+ entry: "oneoff",
+ });
+ await UrlbarTestUtils.exitSearchMode(window, { backspace: true });
+
+ gBrowser.removeTab(gBrowser.selectedTab);
+ await UrlbarTestUtils.formHistory.clear();
+ await hidePopup();
+});
+
+// When all engines and local shortcuts are hidden except for the current
+// engine, the one-offs container should be hidden.
+add_task(async function allOneOffsHiddenExceptCurrentEngine() {
+ // Disable all the engines but the current one, check the oneoffs are
+ // hidden and that moving up selects the last match.
+ let defaultEngine = await Services.search.getDefault();
+ let engines = (await Services.search.getVisibleEngines()).filter(
+ e => e.name != defaultEngine.name
+ );
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.search.hiddenOneOffs", engines.map(e => e.name).join(",")],
+ ...UrlbarUtils.LOCAL_SEARCH_MODES.map(m => [
+ `browser.urlbar.${m.pref}`,
+ false,
+ ]),
+ ],
+ });
+
+ let typedValue = "foo";
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: typedValue,
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ assertState(0, -1);
+ Assert.equal(
+ UrlbarTestUtils.getOneOffSearchButtonsVisible(window),
+ false,
+ "The one-off buttons should be hidden"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ assertState(0, -1);
+ await hidePopup();
+ await SpecialPowers.popPrefEnv();
+});
+
+// The one-offs should be hidden when searching with an "@engine" search engine
+// alias.
+add_task(async function hiddenWhenUsingSearchAlias() {
+ let typedValue = "@example";
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: typedValue,
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ UrlbarTestUtils.getOneOffSearchButtonsVisible(window),
+ false,
+ "Should not be showing the one-off buttons"
+ );
+ await hidePopup();
+
+ typedValue = "not an engine alias";
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: typedValue,
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ UrlbarTestUtils.getOneOffSearchButtonsVisible(window),
+ true,
+ "Should be showing the one-off buttons"
+ );
+ await hidePopup();
+});
+
+// Makes sure the local shortcuts exist.
+add_task(async function localShortcuts() {
+ oneOffSearchButtons.invalidateCache();
+ await doLocalShortcutsShownTest();
+});
+
+// Clicks a local shortcut button.
+add_task(async function localShortcutClick() {
+ // We are explicitly using something that looks like a url, to make the test
+ // stricter. Even if it looks like a url, we should search.
+ let typedValue = "foo.bar";
+
+ oneOffSearchButtons.invalidateCache();
+ let rebuildPromise = BrowserTestUtils.waitForEvent(
+ oneOffSearchButtons,
+ "rebuild"
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: typedValue,
+ });
+ await rebuildPromise;
+
+ let buttons = oneOffSearchButtons.localButtons;
+ Assert.ok(buttons.length, "Sanity check: Local shortcuts exist");
+
+ for (let button of buttons) {
+ Assert.ok(button.source, "Sanity check: Button has a source");
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ await searchPromise;
+ Assert.ok(
+ UrlbarTestUtils.isPopupOpen(window),
+ "Urlbar view is still open."
+ );
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: button.source,
+ entry: "oneoff",
+ });
+ }
+
+ await UrlbarTestUtils.exitSearchMode(window, { backspace: true });
+ await hidePopup();
+});
+
+// Presses the Return key when a local shortcut is selected.
+add_task(async function localShortcutReturn() {
+ // We are explicitly using something that looks like a url, to make the test
+ // stricter. Even if it looks like a url, we should search.
+ let typedValue = "foo.bar";
+
+ oneOffSearchButtons.invalidateCache();
+ let rebuildPromise = BrowserTestUtils.waitForEvent(
+ oneOffSearchButtons,
+ "rebuild"
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: typedValue,
+ });
+ await rebuildPromise;
+
+ let buttons = oneOffSearchButtons.localButtons;
+ Assert.ok(buttons.length, "Sanity check: Local shortcuts exist");
+
+ let allButtons = oneOffSearchButtons.getSelectableButtons(false);
+ let firstLocalIndex = allButtons.length - buttons.length;
+
+ for (let i = 0; i < buttons.length; i++) {
+ let button = buttons[i];
+
+ // Alt+Down enough times to select the button.
+ let index = firstLocalIndex + i;
+ EventUtils.synthesizeKey("KEY_ArrowDown", {
+ altKey: true,
+ repeat: index + 1,
+ });
+ await TestUtils.waitForCondition(
+ () => oneOffSearchButtons.selectedButtonIndex == index,
+ "Waiting for local shortcut to become selected"
+ );
+
+ let expectedSelectedResultIndex = -1;
+ let count = UrlbarTestUtils.getResultCount(window);
+ if (count > 0) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ if (result.heuristic) {
+ expectedSelectedResultIndex = 0;
+ }
+ }
+ assertState(expectedSelectedResultIndex, index, typedValue);
+
+ Assert.ok(button.source, "Sanity check: Button has a source");
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await searchPromise;
+ Assert.ok(
+ UrlbarTestUtils.isPopupOpen(window),
+ "Urlbar view is still open."
+ );
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: button.source,
+ entry: "oneoff",
+ });
+ }
+
+ await UrlbarTestUtils.exitSearchMode(window, { backspace: true });
+ await hidePopup();
+});
+
+// With an empty search string, clicking a local shortcut should result in no
+// heuristic result.
+add_task(async function localShortcutEmptySearchString() {
+ oneOffSearchButtons.invalidateCache();
+ let rebuildPromise = BrowserTestUtils.waitForEvent(
+ oneOffSearchButtons,
+ "rebuild"
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ await rebuildPromise;
+
+ Assert.equal(
+ UrlbarTestUtils.getOneOffSearchButtonsVisible(window),
+ true,
+ "One-offs are visible"
+ );
+
+ let buttons = oneOffSearchButtons.localButtons;
+ Assert.ok(buttons.length, "Sanity check: Local shortcuts exist");
+
+ for (let button of buttons) {
+ Assert.ok(button.source, "Sanity check: Button has a source");
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ await searchPromise;
+ Assert.ok(
+ UrlbarTestUtils.isPopupOpen(window),
+ "Urlbar view is still open."
+ );
+ Assert.equal(
+ UrlbarTestUtils.getOneOffSearchButtonsVisible(window),
+ true,
+ "One-offs are visible"
+ );
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: button.source,
+ entry: "oneoff",
+ });
+
+ let resultCount = UrlbarTestUtils.getResultCount(window);
+ if (!resultCount) {
+ Assert.equal(
+ gURLBar.panel.getAttribute("noresults"),
+ "true",
+ "Panel has no results, therefore should have noresults attribute"
+ );
+ continue;
+ }
+ Assert.ok(
+ !gURLBar.panel.hasAttribute("noresults"),
+ "Panel has results, therefore should not have noresults attribute"
+ );
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(!result.heuristic, "The first result should not be heuristic");
+ }
+
+ await UrlbarTestUtils.exitSearchMode(window, { backspace: true });
+
+ await hidePopup();
+});
+
+// Trigger SearchOneOffs.willHide() outside of SearchOneOffs.__rebuild(). Ensure
+// that we always show the correct engines in the one-offs. This effectively
+// tests SearchOneOffs._engineInfo.domWasUpdated.
+add_task(async function avoidWillHideRace() {
+ // We set maxHistoricalSearchSuggestions to 0 since this test depends on
+ // UrlbarView calling SearchOneOffs.willHide(). That only happens when the
+ // Urlbar is in search mode after a query that returned no results.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.maxHistoricalSearchSuggestions", 0]],
+ });
+
+ oneOffSearchButtons.invalidateCache();
+
+ // Accel+K triggers SearchOneOffs.willHide() from UrlbarView instead of from
+ // SearchOneOffs.__rebuild.
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("k", { accelKey: true });
+ await searchPromise;
+ Assert.ok(
+ UrlbarTestUtils.getOneOffSearchButtonsVisible(window),
+ "One-offs should be visible"
+ );
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ info("Hide all engines but the test engine.");
+ let oldDefaultEngine = await Services.search.getDefault();
+ await Services.search.setDefault(engine);
+ let engines = (await Services.search.getVisibleEngines()).filter(
+ e => e.name != engine.name
+ );
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.search.hiddenOneOffs", engines.map(e => e.name).join(",")],
+ ...UrlbarUtils.LOCAL_SEARCH_MODES.map(m => [
+ `browser.urlbar.${m.pref}`,
+ false,
+ ]),
+ ],
+ });
+ Assert.ok(
+ !oneOffSearchButtons._engineInfo,
+ "_engineInfo should be nulled out."
+ );
+
+ // This call to SearchOneOffs.willHide() should repopulate _engineInfo but not
+ // rebuild the one-offs. _engineInfo.willHide will be true and thus UrlbarView
+ // will not open.
+ EventUtils.synthesizeKey("k", { accelKey: true });
+ // We can't wait for UrlbarTestUtils.promiseSearchComplete here since we
+ // expect the popup will not open. We wait for _engineInfo to be populated
+ // instead.
+ await BrowserTestUtils.waitForCondition(
+ () => !!oneOffSearchButtons._engineInfo,
+ "_engineInfo is set."
+ );
+ Assert.ok(!UrlbarTestUtils.isPopupOpen(window), "The UrlbarView is closed.");
+ Assert.equal(
+ oneOffSearchButtons._engineInfo.willHide,
+ true,
+ "_engineInfo should be repopulated and willHide should be true."
+ );
+ Assert.equal(
+ oneOffSearchButtons._engineInfo.domWasUpdated,
+ undefined,
+ "domWasUpdated should not be populated since we haven't yet tried to rebuild the one-offs."
+ );
+
+ // Now search. The view will open and the one-offs will rebuild, although
+ // the one-offs will not be shown since there is only one engine.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+ Assert.equal(
+ oneOffSearchButtons._engineInfo.domWasUpdated,
+ true,
+ "domWasUpdated should be true"
+ );
+ Assert.ok(
+ !UrlbarTestUtils.getOneOffSearchButtonsVisible(window),
+ "One-offs should be hidden since there is only one engine."
+ );
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ await SpecialPowers.popPrefEnv();
+ await Services.search.setDefault(oldDefaultEngine);
+ await SpecialPowers.popPrefEnv();
+});
+
+// Hides each of the local shortcuts one at a time. The search buttons should
+// automatically rebuild themselves.
+add_task(async function individualLocalShortcutsHidden() {
+ for (let { pref, source } of UrlbarUtils.LOCAL_SEARCH_MODES) {
+ await SpecialPowers.pushPrefEnv({
+ set: [[`browser.urlbar.${pref}`, false]],
+ });
+
+ let rebuildPromise = BrowserTestUtils.waitForEvent(
+ oneOffSearchButtons,
+ "rebuild"
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ await rebuildPromise;
+
+ Assert.equal(
+ UrlbarTestUtils.getOneOffSearchButtonsVisible(window),
+ true,
+ "One-offs are visible"
+ );
+
+ let buttons = oneOffSearchButtons.localButtons;
+ Assert.ok(buttons.length, "Sanity check: Local shortcuts exist");
+
+ let otherModes = UrlbarUtils.LOCAL_SEARCH_MODES.filter(
+ m => m.source != source
+ );
+ Assert.equal(
+ buttons.length,
+ otherModes.length,
+ "Expected number of enabled local shortcut buttons"
+ );
+
+ for (let i = 0; i < buttons.length; i++) {
+ Assert.equal(
+ buttons[i].source,
+ otherModes[i].source,
+ "Button has the expected source"
+ );
+ }
+
+ await hidePopup();
+ await SpecialPowers.popPrefEnv();
+ }
+});
+
+// Hides all the local shortcuts at once.
+add_task(async function allLocalShortcutsHidden() {
+ await SpecialPowers.pushPrefEnv({
+ set: UrlbarUtils.LOCAL_SEARCH_MODES.map(m => [
+ `browser.urlbar.${m.pref}`,
+ false,
+ ]),
+ });
+
+ let rebuildPromise = BrowserTestUtils.waitForEvent(
+ oneOffSearchButtons,
+ "rebuild"
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ await rebuildPromise;
+
+ Assert.equal(
+ UrlbarTestUtils.getOneOffSearchButtonsVisible(window),
+ true,
+ "One-offs are visible"
+ );
+
+ Assert.equal(
+ oneOffSearchButtons.localButtons.length,
+ 0,
+ "All local shortcuts should be hidden"
+ );
+
+ Assert.greater(
+ oneOffSearchButtons.getSelectableButtons(false).filter(b => b.engine)
+ .length,
+ 0,
+ "Engine one-offs should not be hidden"
+ );
+
+ await hidePopup();
+ await SpecialPowers.popPrefEnv();
+});
+
+// Hides all the engines but none of the local shortcuts.
+add_task(async function localShortcutsShownWhenEnginesHidden() {
+ let engines = await Services.search.getVisibleEngines();
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.hiddenOneOffs", engines.map(e => e.name).join(",")]],
+ });
+
+ let rebuildPromise = BrowserTestUtils.waitForEvent(
+ oneOffSearchButtons,
+ "rebuild"
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ await rebuildPromise;
+
+ Assert.equal(
+ UrlbarTestUtils.getOneOffSearchButtonsVisible(window),
+ true,
+ "One-offs are visible"
+ );
+
+ Assert.equal(
+ oneOffSearchButtons.localButtons.length,
+ UrlbarUtils.LOCAL_SEARCH_MODES.length,
+ "All local shortcuts are visible"
+ );
+
+ Assert.equal(
+ oneOffSearchButtons.getSelectableButtons(false).filter(b => b.engine)
+ .length,
+ 0,
+ "All engine one-offs are hidden"
+ );
+
+ await hidePopup();
+ await SpecialPowers.popPrefEnv();
+});
+
+/**
+ * Checks that the local shortcuts are shown correctly.
+ */
+async function doLocalShortcutsShownTest() {
+ let rebuildPromise = BrowserTestUtils.waitForEvent(
+ oneOffSearchButtons,
+ "rebuild"
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "doLocalShortcutsShownTest",
+ });
+ await rebuildPromise;
+
+ let buttons = oneOffSearchButtons.localButtons;
+ Assert.equal(buttons.length, 3, "Expected number of local shortcuts");
+
+ let expectedSource;
+ let seenIDs = new Set();
+ for (let button of buttons) {
+ Assert.ok(
+ !seenIDs.has(button.id),
+ "Should not have already seen button.id"
+ );
+ seenIDs.add(button.id);
+ switch (button.id) {
+ case "urlbar-engine-one-off-item-bookmarks":
+ expectedSource = UrlbarUtils.RESULT_SOURCE.BOOKMARKS;
+ break;
+ case "urlbar-engine-one-off-item-tabs":
+ expectedSource = UrlbarUtils.RESULT_SOURCE.TABS;
+ break;
+ case "urlbar-engine-one-off-item-history":
+ expectedSource = UrlbarUtils.RESULT_SOURCE.HISTORY;
+ break;
+ default:
+ Assert.ok(false, `Unexpected local shortcut ID: ${button.id}`);
+ break;
+ }
+ Assert.equal(button.source, expectedSource, "Expected button.source");
+ }
+
+ await hidePopup();
+}
+
+function assertState(result, oneOff, textValue = undefined) {
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ result,
+ "Expected result should be selected"
+ );
+ Assert.equal(
+ oneOffSearchButtons.selectedButtonIndex,
+ oneOff,
+ "Expected one-off should be selected"
+ );
+ if (textValue !== undefined) {
+ Assert.equal(gURLBar.value, textValue, "Expected value");
+ }
+}
+
+function hidePopup() {
+ return UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeKey("KEY_Escape");
+ });
+}
diff --git a/browser/components/urlbar/tests/browser/browser_oneOffs_contextMenu.js b/browser/components/urlbar/tests/browser/browser_oneOffs_contextMenu.js
new file mode 100644
index 0000000000..60d46608cd
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_oneOffs_contextMenu.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that the right-click menu works correctly for the one-off buttons.
+ */
+
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+
+let gMaxResults;
+
+XPCOMUtils.defineLazyGetter(this, "oneOffSearchButtons", () => {
+ return UrlbarTestUtils.getOneOffSearchButtons(window);
+});
+
+let originalEngine;
+let newEngine;
+
+// The one-off context menu should not be shown.
+add_task(async function contextMenu_not_shown() {
+ // Add a popupshown listener on the context menu that sets this
+ // popupshownFired boolean.
+ let popupshownFired = false;
+ let onPopupshown = () => {
+ popupshownFired = true;
+ };
+ let contextMenu = oneOffSearchButtons.querySelector(
+ ".search-one-offs-context-menu"
+ );
+ contextMenu.addEventListener("popupshown", onPopupshown);
+
+ // Do a search to open the view.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ });
+
+ // First, try to open the context menu on a remote engine.
+ let allOneOffs = oneOffSearchButtons.getSelectableButtons(true);
+ Assert.greater(allOneOffs.length, 0, "There should be at least one one-off");
+ Assert.ok(
+ allOneOffs[0].engine,
+ "The first one-off should be a remote one-off"
+ );
+ EventUtils.synthesizeMouseAtCenter(allOneOffs[0], {
+ type: "contextmenu",
+ button: 2,
+ });
+ let timeout = 500;
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, timeout));
+ Assert.ok(
+ !popupshownFired,
+ "popupshown should not be fired on a remote one-off"
+ );
+
+ // Now try to open the context menu on a local one-off.
+ let localOneOffs = oneOffSearchButtons.localButtons;
+ Assert.greater(
+ localOneOffs.length,
+ 0,
+ "There should be at least one local one-off"
+ );
+ EventUtils.synthesizeMouseAtCenter(localOneOffs[0], {
+ type: "contextmenu",
+ button: 2,
+ });
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, timeout));
+ Assert.ok(
+ !popupshownFired,
+ "popupshown should not be fired on a local one-off"
+ );
+
+ contextMenu.removeEventListener("popupshown", onPopupshown);
+ await UrlbarTestUtils.promisePopupClose(window);
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_oneOffs_heuristicRestyle.js b/browser/components/urlbar/tests/browser/browser_oneOffs_heuristicRestyle.js
new file mode 100644
index 0000000000..e0bf868ee9
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_oneOffs_heuristicRestyle.js
@@ -0,0 +1,506 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that heuristic results are updated/restyled to search results when a
+ * one-off is selected.
+ */
+
+"use strict";
+
+XPCOMUtils.defineLazyGetter(this, "oneOffSearchButtons", () => {
+ return UrlbarTestUtils.getOneOffSearchButtons(window);
+});
+
+const TEST_DEFAULT_ENGINE_NAME = "Test";
+
+const HISTORY_URL = "https://mozilla.org/";
+
+const KEYWORD = "kw";
+const KEYWORD_URL = "https://mozilla.org/search?q=%s";
+
+// Expected result data for our test results.
+const RESULT_DATA_BY_TYPE = {
+ [UrlbarUtils.RESULT_TYPE.URL]: {
+ icon: `page-icon:${HISTORY_URL}`,
+ actionL10n: {
+ id: "urlbar-result-action-visit",
+ },
+ },
+ [UrlbarUtils.RESULT_TYPE.SEARCH]: {
+ icon: "chrome://browser/skin/search-glass.svg",
+ actionL10n: {
+ id: "urlbar-result-action-search-w-engine",
+ args: { engine: TEST_DEFAULT_ENGINE_NAME },
+ },
+ },
+ [UrlbarUtils.RESULT_TYPE.KEYWORD]: {
+ icon: `page-icon:${KEYWORD_URL}`,
+ },
+};
+
+function getSourceIcon(source) {
+ switch (source) {
+ case UrlbarUtils.RESULT_SOURCE.BOOKMARKS:
+ return "chrome://browser/skin/bookmark.svg";
+ case UrlbarUtils.RESULT_SOURCE.HISTORY:
+ return "chrome://browser/skin/history.svg";
+ case UrlbarUtils.RESULT_SOURCE.TABS:
+ return "chrome://browser/skin/tab.svg";
+ default:
+ return null;
+ }
+}
+
+/**
+ * Asserts that the heuristic result is *not* restyled to look like a search
+ * result.
+ *
+ * @param {UrlbarUtils.RESULT_TYPE} expectedType
+ * The expected type of the heuristic.
+ * @param {object} resultDetails
+ * The return value of UrlbarTestUtils.getDetailsOfResultAt(window, 0).
+ */
+async function heuristicIsNotRestyled(expectedType, resultDetails) {
+ Assert.equal(
+ resultDetails.type,
+ expectedType,
+ "The restyled result is the expected type."
+ );
+
+ Assert.equal(
+ resultDetails.displayed.title,
+ resultDetails.title,
+ "The displayed title is equal to the payload title."
+ );
+
+ let data = RESULT_DATA_BY_TYPE[expectedType];
+ Assert.ok(data, "Sanity check: Expected type is recognized");
+
+ let [actionText] = data.actionL10n
+ ? await document.l10n.formatValues([data.actionL10n])
+ : [""];
+ Assert.equal(
+ resultDetails.displayed.action,
+ actionText,
+ "The result has the expected non-styled action text."
+ );
+
+ Assert.equal(
+ BrowserTestUtils.is_visible(resultDetails.element.separator),
+ !!actionText,
+ "The title separator is " + (actionText ? "visible" : "hidden")
+ );
+ Assert.equal(
+ BrowserTestUtils.is_visible(resultDetails.element.action),
+ !!actionText,
+ "The action text is " + (actionText ? "visible" : "hidden")
+ );
+
+ Assert.equal(
+ resultDetails.image,
+ data.icon,
+ "The result has the expected non-styled icon."
+ );
+}
+
+/**
+ * Asserts that the heuristic result is restyled to look like a search result.
+ *
+ * @param {UrlbarUtils.RESULT_TYPE} expectedType
+ * The expected type of the heuristic.
+ * @param {object} resultDetails
+ * The return value of UrlbarTestUtils.getDetailsOfResultAt(window, 0).
+ * @param {string} searchString
+ * The current search string. The restyled heuristic result's title is
+ * expected to be this string.
+ * @param {element} selectedOneOff
+ * The selected one-off button.
+ */
+async function heuristicIsRestyled(
+ expectedType,
+ resultDetails,
+ searchString,
+ selectedOneOff
+) {
+ let engine = selectedOneOff.engine;
+ let source = selectedOneOff.source;
+ if (!engine && !source) {
+ Assert.fail("An invalid one-off was passed to urlbarResultIsRestyled");
+ return;
+ }
+ Assert.equal(
+ resultDetails.type,
+ expectedType,
+ "The restyled result is still the expected type."
+ );
+
+ let actionText;
+ if (engine) {
+ [actionText] = await document.l10n.formatValues([
+ {
+ id: "urlbar-result-action-search-w-engine",
+ args: { engine: engine.name },
+ },
+ ]);
+ } else if (source) {
+ [actionText] = await document.l10n.formatValues([
+ {
+ id: `urlbar-result-action-search-${UrlbarUtils.getResultSourceName(
+ source
+ )}`,
+ },
+ ]);
+ }
+ Assert.equal(
+ resultDetails.displayed.action,
+ actionText,
+ "Restyled result's action text should be updated"
+ );
+
+ Assert.equal(
+ resultDetails.displayed.title,
+ searchString,
+ "The restyled result's title should be equal to the search string."
+ );
+
+ Assert.ok(
+ BrowserTestUtils.is_visible(resultDetails.element.separator),
+ "The restyled result's title separator should be visible"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_visible(resultDetails.element.action),
+ "The restyled result's action text should be visible"
+ );
+
+ if (engine) {
+ Assert.equal(
+ resultDetails.image,
+ engine.iconURI?.spec || UrlbarUtils.ICON.SEARCH_GLASS,
+ "The restyled result's icon should be the engine's icon."
+ );
+ } else if (source) {
+ Assert.equal(
+ resultDetails.image,
+ getSourceIcon(source),
+ "The restyled result's icon should be the local one-off's icon."
+ );
+ }
+}
+
+/**
+ * Asserts that the specified one-off (if any) is selected and that the
+ * heuristic result is either restyled or not restyled as appropriate. If
+ * there's a selected one-off, then the heuristic is expected to be restyled; if
+ * there's no selected one-off, then it's expected not to be restyled.
+ *
+ * @param {string} searchString
+ * The current search string. If a one-off is selected, then the restyled
+ * heuristic result's title is expected to be this string.
+ * @param {UrlbarUtils.RESULT_TYPE} expectedHeuristicType
+ * The expected type of the heuristic.
+ * @param {number} expectedSelectedOneOffIndex
+ * The index of the expected selected one-off button. If no one-off is
+ * expected to be selected, then pass -1.
+ */
+async function assertState(
+ searchString,
+ expectedHeuristicType,
+ expectedSelectedOneOffIndex
+) {
+ Assert.equal(
+ oneOffSearchButtons.selectedButtonIndex,
+ expectedSelectedOneOffIndex,
+ "Expected one-off should be selected"
+ );
+
+ let resultDetails = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ if (expectedSelectedOneOffIndex >= 0) {
+ await heuristicIsRestyled(
+ expectedHeuristicType,
+ resultDetails,
+ searchString,
+ oneOffSearchButtons.selectedButton
+ );
+ } else {
+ await heuristicIsNotRestyled(expectedHeuristicType, resultDetails);
+ }
+}
+
+add_task(async function init() {
+ let oldDefaultEngine = await Services.search.getDefault();
+ let engine = await Services.search.addEngineWithDetails(
+ TEST_DEFAULT_ENGINE_NAME,
+ {
+ template: `http://example.com/?search={searchTerms}`,
+ }
+ );
+ await Services.search.setDefault(engine);
+ await Services.search.moveEngine(engine, 0);
+
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits(HISTORY_URL);
+ }
+
+ await PlacesUtils.keywords.insert({
+ keyword: KEYWORD,
+ url: KEYWORD_URL,
+ });
+
+ registerCleanupFunction(async function() {
+ await Services.search.setDefault(oldDefaultEngine);
+ await Services.search.removeEngine(engine);
+ await PlacesUtils.history.clear();
+ await PlacesUtils.keywords.remove(KEYWORD);
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.searches", false]],
+ });
+
+ // Move the mouse away from the view so that a result or one-off isn't
+ // inadvertently highlighted. See bug 1659011.
+ EventUtils.synthesizeMouse(
+ gURLBar.inputField,
+ 0,
+ 0,
+ { type: "mousemove" },
+ window
+ );
+});
+
+add_task(async function arrow_engine_url() {
+ await doArrowTest("mozilla.or", UrlbarUtils.RESULT_TYPE.URL, false);
+});
+
+add_task(async function arrow_engine_search() {
+ await doArrowTest("test", UrlbarUtils.RESULT_TYPE.SEARCH, false);
+});
+
+add_task(async function arrow_engine_keyword() {
+ await doArrowTest(`${KEYWORD} test`, UrlbarUtils.RESULT_TYPE.KEYWORD, false);
+});
+
+add_task(async function arrow_local_url() {
+ await doArrowTest("mozilla.or", UrlbarUtils.RESULT_TYPE.URL, true);
+});
+
+add_task(async function arrow_local_search() {
+ await doArrowTest("test", UrlbarUtils.RESULT_TYPE.SEARCH, true);
+});
+
+add_task(async function arrow_local_keyword() {
+ await doArrowTest(`${KEYWORD} test`, UrlbarUtils.RESULT_TYPE.KEYWORD, true);
+});
+
+/**
+ * Arrows down to the one-offs, checks the heuristic, and clicks it.
+ *
+ * @param {string} searchString
+ * The search string to use.
+ * @param {UrlbarUtils.RESULT_TYPE} expectedHeuristicType
+ * The type of heuristic result that the search string is expected to trigger.
+ * @param {boolean} useLocal
+ * Whether to test a local one-off or an engine one-off. If true, test a
+ * local one-off. If false, test an engine one-off.
+ */
+async function doArrowTest(searchString, expectedHeuristicType, useLocal) {
+ await doTest(searchString, expectedHeuristicType, useLocal, async () => {
+ info(
+ "Arrow down to the one-offs, observe heuristic is restyled as a search result."
+ );
+ let resultCount = UrlbarTestUtils.getResultCount(window);
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: resultCount });
+ await searchPromise;
+ await assertState(searchString, expectedHeuristicType, 0);
+
+ let depth = 1;
+ if (useLocal) {
+ for (; !oneOffSearchButtons.selectedButton.source; depth++) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+ Assert.ok(
+ oneOffSearchButtons.selectedButton.source,
+ "Selected one-off is local"
+ );
+ await assertState(searchString, expectedHeuristicType, depth - 1);
+ }
+
+ info(
+ "Arrow up out of the one-offs, observe heuristic styling is restored."
+ );
+ EventUtils.synthesizeKey("KEY_ArrowUp", { repeat: depth });
+ await assertState(searchString, expectedHeuristicType, -1);
+
+ info(
+ "Arrow back down into the one-offs, observe heuristic is restyled as a search result."
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: depth });
+ await assertState(searchString, expectedHeuristicType, depth - 1);
+ });
+}
+
+add_task(async function altArrow_engine_url() {
+ await doAltArrowTest("mozilla.or", UrlbarUtils.RESULT_TYPE.URL, false);
+});
+
+add_task(async function altArrow_engine_search() {
+ await doAltArrowTest("test", UrlbarUtils.RESULT_TYPE.SEARCH, false);
+});
+
+add_task(async function altArrow_engine_keyword() {
+ await doAltArrowTest(
+ `${KEYWORD} test`,
+ UrlbarUtils.RESULT_TYPE.KEYWORD,
+ false
+ );
+});
+
+add_task(async function altArrow_local_url() {
+ await doAltArrowTest("mozilla.or", UrlbarUtils.RESULT_TYPE.URL, true);
+});
+
+add_task(async function altArrow_local_search() {
+ await doAltArrowTest("test", UrlbarUtils.RESULT_TYPE.SEARCH, true);
+});
+
+add_task(async function altArrow_local_keyword() {
+ await doAltArrowTest(
+ `${KEYWORD} test`,
+ UrlbarUtils.RESULT_TYPE.KEYWORD,
+ true
+ );
+});
+
+/**
+ * Alt-arrows down to the one-offs so that the heuristic remains selected,
+ * checks the heuristic, and clicks it.
+ *
+ * @param {string} searchString
+ * The search string to use.
+ * @param {UrlbarUtils.RESULT_TYPE} expectedHeuristicType
+ * The type of heuristic result that the search string is expected to trigger.
+ * @param {boolean} useLocal
+ * Whether to test a local one-off or an engine one-off. If true, test a
+ * local one-off. If false, test an engine one-off.
+ */
+async function doAltArrowTest(searchString, expectedHeuristicType, useLocal) {
+ await doTest(searchString, expectedHeuristicType, useLocal, async () => {
+ info(
+ "Alt+down into the one-offs, observe heuristic is restyled as a search result."
+ );
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ await searchPromise;
+ await assertState(searchString, expectedHeuristicType, 0);
+
+ let depth = 1;
+ if (useLocal) {
+ for (; !oneOffSearchButtons.selectedButton.source; depth++) {
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ }
+ Assert.ok(
+ oneOffSearchButtons.selectedButton.source,
+ "Selected one-off is local"
+ );
+ await assertState(searchString, expectedHeuristicType, depth - 1);
+ }
+
+ info(
+ "Arrow down and then up to re-select the heuristic, observe its styling is restored."
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ await assertState(searchString, expectedHeuristicType, -1);
+
+ info(
+ "Alt+down into the one-offs, observe the heuristic is restyled as a search result."
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true, repeat: depth });
+ await assertState(searchString, expectedHeuristicType, depth - 1);
+
+ info("Alt+up out of the one-offs, observe the heuristic is restored.");
+ EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true, repeat: depth });
+ await assertState(searchString, expectedHeuristicType, -1);
+
+ info(
+ "Alt+down into the one-offs, observe the heuristic is restyled as a search result."
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true, repeat: depth });
+ await assertState(searchString, expectedHeuristicType, depth - 1);
+ });
+}
+
+/**
+ * The main test function. Starts a search, asserts that the heuristic has the
+ * expected type, calls a callback to run more checks, and then finally clicks
+ * the restyled heuristic to make sure search mode is confirmed.
+ *
+ * @param {string} searchString
+ * The search string to use.
+ * @param {UrlbarUtils.RESULT_TYPE} expectedHeuristicType
+ * The type of heuristic result that the search string is expected to trigger.
+ * @param {boolean} useLocal
+ * Whether to test a local one-off or an engine one-off. If true, test a
+ * local one-off. If false, test an engine one-off.
+ * @param {function} callback
+ * This is called after the search completes. It should perform whatever
+ * checks are necessary for the test task. Important: When it returns, it
+ * should make sure that the first one-off is selected.
+ */
+async function doTest(searchString, expectedHeuristicType, useLocal, callback) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: searchString,
+ fireInputEvent: true,
+ });
+ await TestUtils.waitForCondition(
+ () => !oneOffSearchButtons._rebuilding,
+ "Waiting for one-offs to finish rebuilding"
+ );
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(result.heuristic, "First result is heuristic");
+ Assert.equal(
+ result.type,
+ expectedHeuristicType,
+ "Heuristic is expected type"
+ );
+ await assertState(searchString, expectedHeuristicType, -1);
+
+ await callback();
+
+ Assert.ok(
+ oneOffSearchButtons.selectedButton,
+ "The callback should leave a one-off selected so that the heuristic remains re-styled"
+ );
+
+ info("Click the heuristic result and observe it confirms search mode.");
+ let selectedButton = oneOffSearchButtons.selectedButton;
+ let expectedSearchMode = {
+ entry: "oneoff",
+ isPreview: true,
+ };
+ if (useLocal) {
+ expectedSearchMode.source = selectedButton.source;
+ } else {
+ expectedSearchMode.engineName = selectedButton.engine.name;
+ }
+
+ await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode);
+
+ let heuristicRow = await UrlbarTestUtils.waitForAutocompleteResultAt(
+ window,
+ 0
+ );
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeMouseAtCenter(heuristicRow, {});
+ await searchPromise;
+
+ expectedSearchMode.isPreview = false;
+ await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode);
+
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+}
diff --git a/browser/components/urlbar/tests/browser/browser_oneOffs_keyModifiers.js b/browser/components/urlbar/tests/browser/browser_oneOffs_keyModifiers.js
new file mode 100644
index 0000000000..786818b9be
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_oneOffs_keyModifiers.js
@@ -0,0 +1,389 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that one-offs behave differently with key modifiers.
+ */
+
+"use strict";
+
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+const SEARCH_STRING = "foo.bar";
+
+XPCOMUtils.defineLazyGetter(this, "oneOffSearchButtons", () => {
+ return UrlbarTestUtils.getOneOffSearchButtons(window);
+});
+
+let engine;
+
+async function searchAndOpenPopup(value) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value,
+ fireInputEvent: true,
+ });
+ await TestUtils.waitForCondition(
+ () => !oneOffSearchButtons._rebuilding,
+ "Waiting for one-offs to finish rebuilding"
+ );
+}
+
+add_task(async function init() {
+ // Add a search suggestion engine and move it to the front so that it appears
+ // as the first one-off.
+ engine = await SearchTestUtils.promiseNewSearchEngine(
+ getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME
+ );
+ await Services.search.moveEngine(engine, 0);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.separatePrivateDefault.ui.enabled", false]],
+ });
+
+ registerCleanupFunction(async function() {
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.formHistory.clear();
+ });
+
+ // Initialize history with enough visits to fill up the view.
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.formHistory.clear();
+ let maxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults");
+ for (let i = 0; i < maxResults; i++) {
+ await PlacesTestUtils.addVisits(
+ "http://mochi.test:8888/browser_urlbarOneOffs.js/?" + i
+ );
+ }
+
+ // Add some more visits to the last URL added above so that the top-sites view
+ // will be non-empty.
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits(
+ "http://mochi.test:8888/browser_urlbarOneOffs.js/?" + (maxResults - 1)
+ );
+ }
+ await updateTopSites(sites => {
+ return (
+ sites && sites[0] && sites[0].url.startsWith("http://mochi.test:8888/")
+ );
+ });
+
+ // Move the mouse away from the view so that a result or one-off isn't
+ // inadvertently highlighted. See bug 1659011.
+ EventUtils.synthesizeMouse(
+ gURLBar.inputField,
+ 0,
+ 0,
+ { type: "mousemove" },
+ window
+ );
+});
+
+// Shift clicking with no search string should open search mode, like an
+// unmodified click.
+add_task(async function shift_click_empty() {
+ await searchAndOpenPopup("");
+ let oneOffs = oneOffSearchButtons.getSelectableButtons(true);
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeMouseAtCenter(oneOffs[0], { shiftKey: true });
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: oneOffs[0].engine.name,
+ entry: "oneoff",
+ });
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+// Shift clicking with a search string should perform a search in the current
+// tab.
+add_task(async function shift_click_search() {
+ await searchAndOpenPopup(SEARCH_STRING);
+ let resultsPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ "http://mochi.test:8888/?terms=foo.bar"
+ );
+ let oneOffs = oneOffSearchButtons.getSelectableButtons(true);
+ EventUtils.synthesizeMouseAtCenter(oneOffs[0], { shiftKey: true });
+ await resultsPromise;
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+// Pressing Shift+Enter on a one-off with no search string should open search
+// mode, like an unmodified click.
+add_task(async function shift_enter_empty() {
+ await searchAndOpenPopup("");
+ // Alt+Down to select the first one-off.
+ let oneOffs = oneOffSearchButtons.getSelectableButtons(true);
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_Enter", { shiftKey: true });
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: oneOffs[0].engine.name,
+ entry: "oneoff",
+ });
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+// Pressing Shift+Enter on a one-off with a search string should perform a
+// search in the current tab.
+add_task(async function shift_enter_search() {
+ await searchAndOpenPopup(SEARCH_STRING);
+ let resultsPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ "http://mochi.test:8888/?terms=foo.bar"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ EventUtils.synthesizeKey("KEY_Enter", { shiftKey: true });
+ await resultsPromise;
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+// Pressing Alt+Enter on a one-off on an "empty" page (e.g. new tab) should open
+// search mode in the current tab.
+add_task(async function alt_enter_emptypage() {
+ await BrowserTestUtils.withNewTab("about:home", async function(browser) {
+ await searchAndOpenPopup(SEARCH_STRING);
+ let oneOffs = oneOffSearchButtons.getSelectableButtons(true);
+ // Alt+Down to select the first one-off.
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_Enter", { altKey: true });
+ await searchPromise;
+ Assert.equal(
+ browser,
+ gBrowser.selectedBrowser,
+ "The foreground tab hasn't changed."
+ );
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: oneOffs[0].engine.name,
+ entry: "oneoff",
+ });
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+ });
+});
+
+// Pressing Alt+Enter on a one-off with no search string and on a "non-empty"
+// page should open search mode in a new foreground tab.
+add_task(async function alt_enter_empty() {
+ await BrowserTestUtils.withNewTab("about:robots", async function(browser) {
+ await searchAndOpenPopup("");
+ let oneOffs = oneOffSearchButtons.getSelectableButtons(true);
+ // Alt+Down to select the first one-off.
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ let tabOpenPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabOpen"
+ );
+ EventUtils.synthesizeKey("KEY_Enter", { altKey: true });
+ await tabOpenPromise;
+ Assert.notEqual(
+ browser,
+ gBrowser.selectedBrowser,
+ "The current foreground tab is new."
+ );
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: oneOffs[0].engine.name,
+ entry: "oneoff",
+ });
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ Assert.equal(
+ browser,
+ gBrowser.selectedBrowser,
+ "We're back in the original tab."
+ );
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ await UrlbarTestUtils.promisePopupClose(window);
+ });
+});
+
+// Pressing Alt+Enter on a remote one-off with a search string and on a
+// "non-empty" page should perform a search in a new foreground tab.
+add_task(async function alt_enter_search_remote() {
+ await BrowserTestUtils.withNewTab("about:robots", async function(browser) {
+ await searchAndOpenPopup(SEARCH_STRING);
+ // Alt+Down to select the first one-off.
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ let tabOpenPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "http://mochi.test:8888/?terms=foo.bar",
+ true
+ );
+ EventUtils.synthesizeKey("KEY_Enter", { altKey: true });
+ // This implictly checks the correct page is loaded.
+ let newTab = await tabOpenPromise;
+ Assert.equal(
+ newTab,
+ gBrowser.selectedTab,
+ "The current foreground tab is new."
+ );
+ // Check search mode is not activated in the new tab.
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ BrowserTestUtils.removeTab(newTab);
+ Assert.equal(
+ browser,
+ gBrowser.selectedBrowser,
+ "We're back in the original tab."
+ );
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ await UrlbarTestUtils.promisePopupClose(window);
+ });
+});
+
+// Pressing Alt+Enter on a local one-off with a search string and on a
+// "non-empty" page should open search mode in a new foreground tab with the
+// search string already populated.
+add_task(async function alt_enter_search_local() {
+ await BrowserTestUtils.withNewTab("about:robots", async function(browser) {
+ await searchAndOpenPopup(SEARCH_STRING);
+ // Alt+Down to select the first local one-off.
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ while (
+ oneOffSearchButtons.selectedButton.id !=
+ "urlbar-engine-one-off-item-bookmarks"
+ ) {
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ }
+ let tabOpenPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabOpen"
+ );
+ EventUtils.synthesizeKey("KEY_Enter", { altKey: true });
+ await tabOpenPromise;
+ Assert.notEqual(
+ browser,
+ gBrowser.selectedBrowser,
+ "The current foreground tab is new."
+ );
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ entry: "oneoff",
+ });
+ Assert.equal(
+ gURLBar.value,
+ SEARCH_STRING,
+ "The search term was duplicated to the new tab."
+ );
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ Assert.equal(
+ browser,
+ gBrowser.selectedBrowser,
+ "We're back in the original tab."
+ );
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ await UrlbarTestUtils.promisePopupClose(window);
+ });
+});
+
+// Accel+Clicking a one-off with an empty search string should open search mode
+// in a new background tab.
+add_task(async function accel_click_empty() {
+ await searchAndOpenPopup("");
+ let oneOffs = oneOffSearchButtons.getSelectableButtons(true);
+
+ // We have to listen for the new tab using this brute force method.
+ // about:newtab is preloaded in the background. When about:newtab is opened,
+ // the cached version is shown. Since the page is already loaded,
+ // waitForNewTab does not detect it. It also doesn't fire the TabOpen event.
+ let tabCount = gBrowser.tabs.length;
+ let tabOpenPromise = TestUtils.waitForCondition(
+ () =>
+ gBrowser.tabs.length == tabCount + 1
+ ? gBrowser.tabs[gBrowser.tabs.length - 1]
+ : false,
+ "Waiting for background about:newtab to open."
+ );
+ EventUtils.synthesizeMouseAtCenter(oneOffs[0], { accelKey: true });
+ let newTab = await tabOpenPromise;
+ Assert.notEqual(
+ newTab.linkedBrowser,
+ gBrowser.selectedBrowser,
+ "The foreground tab hasn't changed."
+ );
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ BrowserTestUtils.switchTab(gBrowser, newTab);
+ // Check the new background tab is already in search mode.
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: oneOffs[0].engine.name,
+ entry: "oneoff",
+ });
+ BrowserTestUtils.removeTab(newTab);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+// Accel+Clicking a remote one-off with a search string should execute a search
+// in a new background tab.
+add_task(async function accel_click_search_remote() {
+ await searchAndOpenPopup(SEARCH_STRING);
+ let oneOffs = oneOffSearchButtons.getSelectableButtons(true);
+ let tabOpenPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "http://mochi.test:8888/?terms=foo.bar",
+ true
+ );
+ EventUtils.synthesizeMouseAtCenter(oneOffs[0], { accelKey: true });
+ // This implictly checks the correct page is loaded.
+ let newTab = await tabOpenPromise;
+ Assert.notEqual(
+ gBrowser.selectedTab,
+ newTab,
+ "The foreground tab hasn't changed."
+ );
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ // Switch to the background tab, which is the last tab in gBrowser.tabs.
+ BrowserTestUtils.switchTab(gBrowser, newTab);
+ // Check the new background tab is not search mode.
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ BrowserTestUtils.removeTab(newTab);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+// Accel+Clicking a local one-off with a search string should open search mode
+// in a new background tab with the search string already populated.
+add_task(async function accel_click_search_local() {
+ await searchAndOpenPopup(SEARCH_STRING);
+ let oneOffs = oneOffSearchButtons.getSelectableButtons(true);
+ let oneOff;
+ for (oneOff of oneOffs) {
+ if (oneOff.id == "urlbar-engine-one-off-item-bookmarks") {
+ break;
+ }
+ }
+ let tabCount = gBrowser.tabs.length;
+ let tabOpenPromise = TestUtils.waitForCondition(
+ () =>
+ gBrowser.tabs.length == tabCount + 1
+ ? gBrowser.tabs[gBrowser.tabs.length - 1]
+ : false,
+ "Waiting for background about:newtab to open."
+ );
+ EventUtils.synthesizeMouseAtCenter(oneOff, { accelKey: true });
+ let newTab = await tabOpenPromise;
+ Assert.notEqual(
+ newTab.linkedBrowser,
+ gBrowser.selectedBrowser,
+ "The foreground tab hasn't changed."
+ );
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ BrowserTestUtils.switchTab(gBrowser, newTab);
+ // Check the new background tab is already in search mode.
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ entry: "oneoff",
+ });
+ // Check the search string is already populated.
+ Assert.equal(
+ gURLBar.value,
+ SEARCH_STRING,
+ "The search term was duplicated to the new tab."
+ );
+ BrowserTestUtils.removeTab(newTab);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_oneOffs_searchSuggestions.js b/browser/components/urlbar/tests/browser/browser_oneOffs_searchSuggestions.js
new file mode 100644
index 0000000000..183e6275df
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_oneOffs_searchSuggestions.js
@@ -0,0 +1,354 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests various actions relating to search suggestions and the one-off buttons.
+ */
+
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+const TEST_ENGINE2_BASENAME = "searchSuggestionEngine2.xml";
+
+const serverInfo = {
+ scheme: "http",
+ host: "localhost",
+ port: 20709, // Must be identical to what is in searchSuggestionEngine2.xml
+};
+
+var gEngine;
+var gEngine2;
+
+add_task(async function init() {
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.formHistory.clear();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.searches", true],
+ ["browser.urlbar.maxHistoricalSearchSuggestions", 2],
+ ],
+ });
+ gEngine = await SearchTestUtils.promiseNewSearchEngine(
+ getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME
+ );
+ gEngine2 = await SearchTestUtils.promiseNewSearchEngine(
+ getRootDirectory(gTestPath) + TEST_ENGINE2_BASENAME
+ );
+ let oldDefaultEngine = await Services.search.getDefault();
+ await Services.search.moveEngine(gEngine2, 0);
+ await Services.search.moveEngine(gEngine, 0);
+ await Services.search.setDefault(gEngine);
+ registerCleanupFunction(async function() {
+ await Services.search.setDefault(oldDefaultEngine);
+
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.formHistory.clear();
+ });
+});
+
+async function withSuggestions(testFn) {
+ // First run with remote suggestions, and then run with form history.
+ await withSuggestionOnce(false, testFn);
+ await withSuggestionOnce(true, testFn);
+}
+
+async function withSuggestionOnce(useFormHistory, testFn) {
+ if (useFormHistory) {
+ // Add foofoo twice so it's more frecent so it appears first so that the
+ // order of form history results matches the order of remote suggestion
+ // results.
+ await UrlbarTestUtils.formHistory.add(["foofoo", "foofoo", "foobar"]);
+ }
+ await BrowserTestUtils.withNewTab(gBrowser, async () => {
+ let value = "foo";
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value,
+ fireInputEvent: true,
+ });
+ let index = await UrlbarTestUtils.promiseSuggestionsPresent(window);
+ await assertState({
+ inputValue: value,
+ resultIndex: 0,
+ });
+ await withHttpServer(serverInfo, () => {
+ return testFn(index, useFormHistory);
+ });
+ });
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.formHistory.clear();
+}
+
+async function selectSecondSuggestion(index, isFormHistory) {
+ // Down to select the first search suggestion.
+ for (let i = index; i > 0; --i) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+ await assertState({
+ inputValue: "foofoo",
+ resultIndex: index,
+ suggestion: {
+ isFormHistory,
+ },
+ });
+
+ // Down to select the next search suggestion.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await assertState({
+ inputValue: "foobar",
+ resultIndex: index + 1,
+ suggestion: {
+ isFormHistory,
+ },
+ });
+}
+
+// Presses the Return key when a one-off is selected after selecting a search
+// suggestion.
+add_task(async function test_returnAfterSuggestion() {
+ await withSuggestions(async (index, usingFormHistory) => {
+ await selectSecondSuggestion(index, usingFormHistory);
+
+ // Alt+Down to select the first one-off.
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ await assertState({
+ inputValue: "foobar",
+ resultIndex: index + 1,
+ oneOffIndex: 0,
+ suggestion: {
+ isFormHistory: usingFormHistory,
+ },
+ });
+
+ let heuristicResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(
+ !BrowserTestUtils.is_visible(heuristicResult.element.action),
+ "The heuristic action should not be visible"
+ );
+
+ let resultsPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await resultsPromise;
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: gEngine.name,
+ entry: "oneoff",
+ });
+ await UrlbarTestUtils.exitSearchMode(window, { backspace: true });
+ });
+});
+
+// Presses the Return key when a non-default one-off is selected after selecting
+// a search suggestion.
+add_task(async function test_returnAfterSuggestion_nonDefault() {
+ await withSuggestions(async (index, usingFormHistory) => {
+ await selectSecondSuggestion(index, usingFormHistory);
+
+ // Alt+Down twice to select the second one-off.
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ await assertState({
+ inputValue: "foobar",
+ resultIndex: index + 1,
+ oneOffIndex: 1,
+ suggestion: {
+ isFormHistory: usingFormHistory,
+ },
+ });
+
+ let resultsPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await resultsPromise;
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: gEngine2.name,
+ entry: "oneoff",
+ });
+ await UrlbarTestUtils.exitSearchMode(window, { backspace: true });
+ });
+});
+
+// Clicks a one-off engine after selecting a search suggestion.
+add_task(async function test_clickAfterSuggestion() {
+ await withSuggestions(async (index, usingFormHistory) => {
+ await selectSecondSuggestion(index, usingFormHistory);
+
+ let oneOffs = UrlbarTestUtils.getOneOffSearchButtons(
+ window
+ ).getSelectableButtons(true);
+ let resultsPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeMouseAtCenter(oneOffs[1], {});
+ await resultsPromise;
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: gEngine2.name,
+ entry: "oneoff",
+ });
+ await UrlbarTestUtils.exitSearchMode(window, { backspace: true });
+ });
+});
+
+// Clicks a non-default one-off engine after selecting a search suggestion.
+add_task(async function test_clickAfterSuggestion_nonDefault() {
+ await withSuggestions(async (index, usingFormHistory) => {
+ await selectSecondSuggestion(index, usingFormHistory);
+
+ let oneOffs = UrlbarTestUtils.getOneOffSearchButtons(
+ window
+ ).getSelectableButtons(true);
+ let resultsPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeMouseAtCenter(oneOffs[1], {});
+ await resultsPromise;
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: gEngine2.name,
+ entry: "oneoff",
+ });
+ await UrlbarTestUtils.exitSearchMode(window, { backspace: true });
+ });
+});
+
+// Selects a non-default one-off engine and then clicks a search suggestion.
+add_task(async function test_selectOneOffThenSuggestion() {
+ await withSuggestions(async (index, usingFormHistory) => {
+ // Select a non-default one-off engine.
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ await assertState({
+ inputValue: "foo",
+ resultIndex: 0,
+ oneOffIndex: 1,
+ });
+
+ let heuristicResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(
+ BrowserTestUtils.is_visible(heuristicResult.element.action),
+ "The heuristic action should be visible because the result is selected"
+ );
+
+ // Now click the second suggestion.
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index + 1);
+ // Note search history results don't change their engine when the selected
+ // one-off button changes!
+ let resultsPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ usingFormHistory
+ ? `http://mochi.test:8888/?terms=foobar`
+ : `http://localhost:20709/?terms=foobar`
+ );
+ EventUtils.synthesizeMouseAtCenter(result.element.row, {});
+ await resultsPromise;
+ });
+});
+
+add_task(async function overridden_engine_not_reused() {
+ info(
+ "An overridden search suggestion item should not be reused by a search with another engine"
+ );
+ await BrowserTestUtils.withNewTab(gBrowser, async () => {
+ let typedValue = "foo";
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: typedValue,
+ fireInputEvent: true,
+ });
+ let index = await UrlbarTestUtils.promiseSuggestionsPresent(window);
+ // Down to select the first search suggestion.
+ for (let i = index; i > 0; --i) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+ await assertState({
+ inputValue: "foofoo",
+ resultIndex: index,
+ suggestion: {
+ isFormHistory: false,
+ },
+ });
+
+ // ALT+Down to select the second search engine.
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ await assertState({
+ inputValue: "foofoo",
+ resultIndex: index,
+ oneOffIndex: 1,
+ suggestion: {
+ isFormHistory: false,
+ },
+ });
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ let label = result.displayed.action;
+ // Run again the query, check the label has been replaced.
+ await UrlbarTestUtils.promisePopupClose(window);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: typedValue,
+ fireInputEvent: true,
+ });
+ index = await UrlbarTestUtils.promiseSuggestionsPresent(window);
+ await assertState({
+ inputValue: "foo",
+ resultIndex: 0,
+ });
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, index);
+ Assert.notEqual(
+ result.displayed.action,
+ label,
+ "The label should have been updated"
+ );
+ });
+});
+
+async function assertState({
+ resultIndex,
+ inputValue,
+ oneOffIndex = -1,
+ suggestion = null,
+}) {
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ resultIndex,
+ "Expected result should be selected"
+ );
+ Assert.equal(
+ UrlbarTestUtils.getOneOffSearchButtons(window).selectedButtonIndex,
+ oneOffIndex,
+ "Expected one-off should be selected"
+ );
+ if (inputValue !== undefined) {
+ Assert.equal(gURLBar.value, inputValue, "Expected input value");
+ }
+
+ if (suggestion) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ resultIndex
+ );
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "Result type should be SEARCH"
+ );
+ if (suggestion.isFormHistory) {
+ Assert.equal(
+ result.source,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ "Result source should be HISTORY"
+ );
+ } else {
+ Assert.equal(
+ result.source,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ "Result source should be SEARCH"
+ );
+ }
+ Assert.equal(
+ typeof result.searchParams.suggestion,
+ "string",
+ "Result should have a suggestion"
+ );
+ Assert.equal(
+ result.searchParams.suggestion,
+ suggestion.value || inputValue,
+ "Result should have the expected suggestion"
+ );
+ }
+}
diff --git a/browser/components/urlbar/tests/browser/browser_oneOffs_settings.js b/browser/components/urlbar/tests/browser/browser_oneOffs_settings.js
new file mode 100644
index 0000000000..e34284a4f7
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_oneOffs_settings.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This tests that the settings button in the one-off buttons display correctly
+ * loads the search preferences.
+ */
+
+let gMaxResults;
+
+add_task(async function init() {
+ gMaxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults");
+
+ registerCleanupFunction(async function() {
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.formHistory.clear();
+ });
+
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.formHistory.clear();
+
+ let visits = [];
+ for (let i = 0; i < gMaxResults; i++) {
+ visits.push({
+ uri: makeURI("http://example.com/browser_urlbarOneOffs.js/?" + i),
+ // TYPED so that the visit shows up when the urlbar's drop-down arrow is
+ // pressed.
+ transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ });
+ }
+ await PlacesTestUtils.addVisits(visits);
+});
+
+async function selectSettings(activateFn) {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async browser => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "example.com",
+ });
+ await UrlbarTestUtils.waitForAutocompleteResultAt(
+ window,
+ gMaxResults - 1
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window, async () => {
+ let prefPaneLoaded = TestUtils.topicObserved(
+ "sync-pane-loaded",
+ () => true
+ );
+
+ activateFn();
+
+ await prefPaneLoaded;
+ });
+
+ Assert.equal(
+ gBrowser.contentWindow.history.state,
+ "paneSearch",
+ "Should have opened the search preferences pane"
+ );
+ }
+ );
+}
+
+add_task(async function test_open_settings_with_enter() {
+ await selectSettings(() => {
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+
+ Assert.ok(
+ UrlbarTestUtils.getOneOffSearchButtons(
+ window
+ ).selectedButton.classList.contains("search-setting-button-compact"),
+ "Should have selected the settings button"
+ );
+
+ EventUtils.synthesizeKey("KEY_Enter");
+ });
+});
+
+add_task(async function test_open_settings_with_click() {
+ await selectSettings(() => {
+ UrlbarTestUtils.getOneOffSearchButtons(window).settingsButton.click();
+ });
+});
diff --git a/browser/components/urlbar/tests/browser/browser_pasteAndGo.js b/browser/components/urlbar/tests/browser/browser_pasteAndGo.js
new file mode 100644
index 0000000000..980f84139e
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_pasteAndGo.js
@@ -0,0 +1,92 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests for the paste and go functionality of the urlbar.
+ */
+
+add_task(async function() {
+ const kURLs = [
+ "http://example.com/1",
+ "http://example.org/2\n",
+ "http://\nexample.com/3\n",
+ ];
+ for (let url of kURLs) {
+ await BrowserTestUtils.withNewTab("about:blank", async function(browser) {
+ gURLBar.focus();
+
+ await SimpleTest.promiseClipboardChange(url, () => {
+ clipboardHelper.copyString(url);
+ });
+ let menuitem = await promiseContextualMenuitem("paste-and-go");
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ url.replace(/\n/g, "")
+ );
+ EventUtils.synthesizeMouseAtCenter(menuitem, {});
+ // Using toSource in order to get the newlines escaped:
+ info("Paste and go, loading " + url.toSource());
+ await browserLoadedPromise;
+ ok(true, "Successfully loaded " + url);
+ });
+ }
+});
+
+add_task(async function test_invisible_char() {
+ const url = "http://example.com/4\u2028";
+ await BrowserTestUtils.withNewTab("about:blank", async function(browser) {
+ gURLBar.focus();
+ await SimpleTest.promiseClipboardChange(url, () => {
+ clipboardHelper.copyString(url);
+ });
+ let menuitem = await promiseContextualMenuitem("paste-and-go");
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ url.replace(/\u2028/g, "")
+ );
+ EventUtils.synthesizeMouseAtCenter(menuitem, {});
+ // Using toSource in order to get the newlines escaped:
+ info("Paste and go, loading " + url.toSource());
+ await browserLoadedPromise;
+ ok(true, "Successfully loaded " + url);
+ });
+});
+
+add_task(async function test_with_input_and_results() {
+ // Test paste and go When there's some input and the results pane is open.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ });
+ const url = "http://example.com/";
+ await SimpleTest.promiseClipboardChange(url, () => {
+ clipboardHelper.copyString(url);
+ });
+ let menuitem = await promiseContextualMenuitem("paste-and-go");
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ url
+ );
+ EventUtils.synthesizeMouseAtCenter(menuitem, {});
+ // Using toSource in order to get the newlines escaped:
+ info("Paste and go, loading " + url.toSource());
+ await browserLoadedPromise;
+ ok(true, "Successfully loaded " + url);
+});
+
+async function promiseContextualMenuitem(anonid) {
+ let textBox = gURLBar.querySelector("moz-input-box");
+ let cxmenu = textBox.menupopup;
+ let cxmenuPromise = BrowserTestUtils.waitForEvent(cxmenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {
+ type: "contextmenu",
+ button: 2,
+ });
+ await cxmenuPromise;
+ return textBox.getMenuItem(anonid);
+}
diff --git a/browser/components/urlbar/tests/browser/browser_percent_encoded.js b/browser/components/urlbar/tests/browser/browser_percent_encoded.js
new file mode 100644
index 0000000000..10b581b342
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_percent_encoded.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that searching history works for both encoded or decoded strings.
+
+add_task(async function test() {
+ const decoded = "日本";
+ const TEST_URL = TEST_BASE_URL + "?" + decoded;
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ });
+
+ // Visit url in a new tab, going through normal urlbar workflow.
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+ let promise = PlacesTestUtils.waitForNotification(
+ "page-visited",
+ visits => {
+ Assert.equal(
+ visits.length,
+ 1,
+ "Was notified for the right number of visits."
+ );
+ let { url, transitionType } = visits[0];
+ return (
+ url == encodeURI(TEST_URL) &&
+ transitionType == PlacesUtils.history.TRANSITIONS.TYPED
+ );
+ },
+ "places"
+ );
+ gURLBar.focus();
+ gURLBar.value = TEST_URL;
+ info("Visiting url");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await promise;
+ gBrowser.removeCurrentTab({ skipPermitUnload: true });
+
+ info("Search for the decoded string.");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: decoded,
+ });
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 2,
+ "Check number of results"
+ );
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.url, encodeURI(TEST_URL), "Check result url");
+
+ info("Search for the encoded string.");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: encodeURIComponent(decoded),
+ });
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 2,
+ "Check number of results"
+ );
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.url, encodeURI(TEST_URL), "Check result url");
+});
diff --git a/browser/components/urlbar/tests/browser/browser_placeholder.js b/browser/components/urlbar/tests/browser/browser_placeholder.js
new file mode 100644
index 0000000000..240392527f
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_placeholder.js
@@ -0,0 +1,295 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This test ensures the placeholder is set correctly for different search
+ * engines.
+ */
+
+"use strict";
+
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+const TEST_PRIVATE_ENGINE_BASENAME = "searchSuggestionEngine2.xml";
+
+var originalEngine, extraEngine, extraPrivateEngine, expectedString;
+var tabs = [];
+
+var noEngineString;
+
+add_task(async function setup() {
+ originalEngine = await Services.search.getDefault();
+ [noEngineString, expectedString] = (
+ await document.l10n.formatMessages([
+ { id: "urlbar-placeholder" },
+ {
+ id: "urlbar-placeholder-with-name",
+ args: { name: originalEngine.name },
+ },
+ ])
+ ).map(msg => msg.attributes[0].value);
+
+ let rootDir = getRootDirectory(gTestPath);
+ extraEngine = await SearchTestUtils.promiseNewSearchEngine(
+ rootDir + TEST_ENGINE_BASENAME
+ );
+ extraPrivateEngine = await SearchTestUtils.promiseNewSearchEngine(
+ rootDir + TEST_PRIVATE_ENGINE_BASENAME
+ );
+
+ // Force display of a tab with a URL bar, to clear out any possible placeholder
+ // initialization listeners that happen on startup.
+ let urlTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:mozilla"
+ );
+ BrowserTestUtils.removeTab(urlTab);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.search.separatePrivateDefault.ui.enabled", true],
+ ["browser.search.separatePrivateDefault", false],
+ ],
+ });
+
+ registerCleanupFunction(async () => {
+ await Services.search.setDefault(originalEngine);
+ for (let tab of tabs) {
+ BrowserTestUtils.removeTab(tab);
+ }
+ });
+});
+
+add_task(async function test_change_default_engine_updates_placeholder() {
+ tabs.push(await BrowserTestUtils.openNewForegroundTab(gBrowser));
+
+ await Services.search.setDefault(extraEngine);
+
+ await TestUtils.waitForCondition(
+ () => gURLBar.placeholder == noEngineString,
+ "The placeholder should match the default placeholder for non-built-in engines."
+ );
+ Assert.equal(gURLBar.placeholder, noEngineString);
+
+ await Services.search.setDefault(originalEngine);
+
+ await TestUtils.waitForCondition(
+ () => gURLBar.placeholder == expectedString,
+ "The placeholder should include the engine name for built-in engines."
+ );
+ Assert.equal(gURLBar.placeholder, expectedString);
+});
+
+add_task(async function test_delayed_update_placeholder() {
+ // We remove the change of engine listener here as that is set so that
+ // if the engine is changed by the user then the placeholder is always updated
+ // straight away. As we want to test the delay update here, we remove the
+ // listener and call the placeholder update manually with the delay flag.
+ Services.obs.removeObserver(BrowserSearch, "browser-search-engine-modified");
+
+ // Since we can't easily test for startup changes, we'll at least test the delay
+ // of update for the placeholder works.
+ let urlTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:mozilla"
+ );
+ tabs.push(urlTab);
+
+ // Open a tab with a blank URL bar.
+ let blankTab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ tabs.push(blankTab);
+
+ await Services.search.setDefault(extraEngine);
+ // Pretend we've "initialized".
+ BrowserSearch._updateURLBarPlaceholder(extraEngine.name, false, true);
+
+ Assert.equal(
+ gURLBar.placeholder,
+ expectedString,
+ "Placeholder should be unchanged."
+ );
+
+ // Now switch to a tab with something in the URL Bar.
+ await BrowserTestUtils.switchTab(gBrowser, urlTab);
+
+ await TestUtils.waitForCondition(
+ () => gURLBar.placeholder == noEngineString,
+ "The placeholder should have updated in the background."
+ );
+
+ // Do it the other way to check both named engine and fallback code paths.
+ await BrowserTestUtils.switchTab(gBrowser, blankTab);
+
+ await Services.search.setDefault(originalEngine);
+ BrowserSearch._updateURLBarPlaceholder(originalEngine.name, false, true);
+
+ Assert.equal(
+ gURLBar.placeholder,
+ noEngineString,
+ "Placeholder should be unchanged."
+ );
+ Assert.deepEqual(
+ document.l10n.getAttributes(gURLBar.inputField),
+ { id: "urlbar-placeholder", args: null },
+ "Placeholder data should be unchanged."
+ );
+
+ await BrowserTestUtils.switchTab(gBrowser, urlTab);
+
+ await TestUtils.waitForCondition(
+ () => gURLBar.placeholder == expectedString,
+ "The placeholder should include the engine name for built-in engines."
+ );
+
+ // Now check when we have a URL displayed, the placeholder is updated straight away.
+ BrowserSearch._updateURLBarPlaceholder(extraEngine.name, false);
+
+ await TestUtils.waitForCondition(
+ () => gURLBar.placeholder == noEngineString,
+ "The placeholder should go back to the default"
+ );
+ Assert.equal(
+ gURLBar.placeholder,
+ noEngineString,
+ "Placeholder should be the default."
+ );
+
+ Services.obs.addObserver(BrowserSearch, "browser-search-engine-modified");
+});
+
+add_task(async function test_private_window_no_separate_engine() {
+ const win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+
+ await Services.search.setDefault(extraEngine);
+
+ await TestUtils.waitForCondition(
+ () => win.gURLBar.placeholder == noEngineString,
+ "The placeholder should match the default placeholder for non-built-in engines."
+ );
+ Assert.equal(win.gURLBar.placeholder, noEngineString);
+
+ await Services.search.setDefault(originalEngine);
+
+ await TestUtils.waitForCondition(
+ () => win.gURLBar.placeholder == expectedString,
+ "The placeholder should include the engine name for built-in engines."
+ );
+ Assert.equal(win.gURLBar.placeholder, expectedString);
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_private_window_separate_engine() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.separatePrivateDefault", true]],
+ });
+ const originalPrivateEngine = await Services.search.getDefaultPrivate();
+ registerCleanupFunction(async () => {
+ await Services.search.setDefaultPrivate(originalPrivateEngine);
+ });
+
+ const win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+
+ // Keep the normal default as a different string to the private, so that we
+ // can be sure we're testing the right thing.
+ await Services.search.setDefault(originalEngine);
+ await Services.search.setDefaultPrivate(extraPrivateEngine);
+
+ await TestUtils.waitForCondition(
+ () => win.gURLBar.placeholder == noEngineString,
+ "The placeholder should match the default placeholder for non-built-in engines."
+ );
+ Assert.equal(win.gURLBar.placeholder, noEngineString);
+
+ await Services.search.setDefault(extraEngine);
+ await Services.search.setDefaultPrivate(originalEngine);
+
+ await TestUtils.waitForCondition(
+ () => win.gURLBar.placeholder == expectedString,
+ "The placeholder should include the engine name for built-in engines."
+ );
+ Assert.equal(win.gURLBar.placeholder, expectedString);
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_search_mode_engine_web() {
+ // Add our test engine to WEB_ENGINE_NAMES so that it's recognized as a web
+ // engine.
+ UrlbarUtils.WEB_ENGINE_NAMES.add(extraEngine.name);
+
+ await doSearchModeTest(
+ {
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ engineName: extraEngine.name,
+ },
+ {
+ id: "urlbar-placeholder-search-mode-web-2",
+ args: { name: extraEngine.name },
+ }
+ );
+
+ UrlbarUtils.WEB_ENGINE_NAMES.delete(extraEngine.name);
+});
+
+add_task(async function test_search_mode_engine_other() {
+ await doSearchModeTest(
+ { engineName: extraEngine.name },
+ {
+ id: "urlbar-placeholder-search-mode-other-engine",
+ args: { name: extraEngine.name },
+ }
+ );
+});
+
+add_task(async function test_search_mode_bookmarks() {
+ await doSearchModeTest(
+ { source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS },
+ { id: "urlbar-placeholder-search-mode-other-bookmarks", args: null }
+ );
+});
+
+add_task(async function test_search_mode_tabs() {
+ await doSearchModeTest(
+ { source: UrlbarUtils.RESULT_SOURCE.TABS },
+ { id: "urlbar-placeholder-search-mode-other-tabs", args: null }
+ );
+});
+
+add_task(async function test_search_mode_history() {
+ await doSearchModeTest(
+ { source: UrlbarUtils.RESULT_SOURCE.HISTORY },
+ { id: "urlbar-placeholder-search-mode-other-history", args: null }
+ );
+});
+
+/**
+ * Opens the view, clicks a one-off button to enter search mode, and asserts
+ * that the placeholder is corrrect.
+ *
+ * @param {object} expectedSearchMode
+ * The expected search mode object for the one-off.
+ * @param {object} expectedPlaceholderL10n
+ * The expected l10n object for the one-off.
+ */
+async function doSearchModeTest(expectedSearchMode, expectedPlaceholderL10n) {
+ // Click the urlbar to open the top-sites view.
+ if (gURLBar.getAttribute("pageproxystate") == "invalid") {
+ gURLBar.handleRevert();
+ }
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+
+ // Enter search mode.
+ await UrlbarTestUtils.enterSearchMode(window, expectedSearchMode);
+
+ // Check the placeholder.
+ Assert.deepEqual(
+ document.l10n.getAttributes(gURLBar.inputField),
+ expectedPlaceholderL10n,
+ "Placeholder has expected l10n"
+ );
+
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+ await UrlbarTestUtils.promisePopupClose(window);
+}
diff --git a/browser/components/urlbar/tests/browser/browser_populateAfterPushState.js b/browser/components/urlbar/tests/browser/browser_populateAfterPushState.js
new file mode 100644
index 0000000000..f799f56c3c
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_populateAfterPushState.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* When a user clears the URL bar, and then the page pushes state, we should
+ * re-fill the URL bar so it doesn't remain empty indefinitely. See bug 1441039.
+ * For normal loads, this happens automatically because a non-same-document state
+ * change takes place.
+ */
+add_task(async function() {
+ await BrowserTestUtils.withNewTab(
+ TEST_BASE_URL + "dummy_page.html",
+ async function(browser) {
+ gURLBar.value = "";
+
+ let locationChangePromise = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ TEST_BASE_URL + "dummy_page2.html"
+ );
+ await SpecialPowers.spawn(browser, [], function() {
+ content.history.pushState({}, "Page 2", "dummy_page2.html");
+ });
+ await locationChangePromise;
+ is(
+ gURLBar.value,
+ TEST_BASE_URL + "dummy_page2.html",
+ "Should have updated the URL bar."
+ );
+ }
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_primary_selection_safe_on_new_tab.js b/browser/components/urlbar/tests/browser/browser_primary_selection_safe_on_new_tab.js
new file mode 100644
index 0000000000..6a2e8c9e00
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_primary_selection_safe_on_new_tab.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Verify that the primary selection is unaffected by opening a new tab.
+ *
+ * The steps here follow STR for regression
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=1457355.
+ */
+
+"use strict";
+
+let tabs = [];
+let supportsPrimary = Services.clipboard.supportsSelectionClipboard();
+const NON_EMPTY_URL = "data:text/html,Hello";
+const TEXT_FOR_PRIMARY = "Text for PRIMARY selection";
+
+add_task(async function() {
+ tabs.push(
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, NON_EMPTY_URL)
+ );
+
+ // Bug 1457355 reproduced only when the url had a non-empty selection.
+ gURLBar.select();
+ Assert.equal(gURLBar.inputField.selectionStart, 0);
+ Assert.equal(
+ gURLBar.inputField.selectionEnd,
+ gURLBar.inputField.value.length
+ );
+
+ if (supportsPrimary) {
+ clipboardHelper.copyStringToClipboard(
+ TEXT_FOR_PRIMARY,
+ Services.clipboard.kSelectionClipboard
+ );
+ }
+
+ tabs.push(
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: () => {
+ // Simulate tab open from user input such as keyboard shortcut or new
+ // tab button.
+ let userInput = window.windowUtils.setHandlingUserInput(true);
+ try {
+ BrowserOpenTab();
+ } finally {
+ userInput.destruct();
+ }
+ },
+ waitForLoad: false,
+ })
+ );
+
+ if (!supportsPrimary) {
+ info("Primary selection not supported. Skipping assertion.");
+ return;
+ }
+
+ let primaryAsText = SpecialPowers.getClipboardData(
+ "text/unicode",
+ SpecialPowers.Ci.nsIClipboard.kSelectionClipboard
+ );
+ Assert.equal(primaryAsText, TEXT_FOR_PRIMARY);
+});
+
+registerCleanupFunction(() => {
+ for (let tab of tabs) {
+ BrowserTestUtils.removeTab(tab);
+ }
+});
diff --git a/browser/components/urlbar/tests/browser/browser_privateBrowsingWindowChange.js b/browser/components/urlbar/tests/browser/browser_privateBrowsingWindowChange.js
new file mode 100644
index 0000000000..50b97a77d2
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_privateBrowsingWindowChange.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test that when opening a private browsing window and typing in it before
+ * about:privatebrowsing loads, we don't clear the URL bar.
+ */
+add_task(async function() {
+ let urlbarTestValue = "Mary had a little lamb";
+ let win = OpenBrowserWindow({ private: true });
+ registerCleanupFunction(() => BrowserTestUtils.closeWindow(win));
+ await BrowserTestUtils.waitForEvent(win, "load");
+ let promise = new Promise(resolve => {
+ let wpl = {
+ onLocationChange(aWebProgress, aRequest, aLocation) {
+ if (aLocation && aLocation.spec == "about:privatebrowsing") {
+ win.gBrowser.removeProgressListener(wpl);
+ resolve();
+ }
+ },
+ };
+ win.gBrowser.addProgressListener(wpl);
+ });
+ Assert.notEqual(
+ win.gBrowser.selectedBrowser.currentURI.spec,
+ "about:privatebrowsing",
+ "Check privatebrowsing page has not been loaded yet"
+ );
+ info("Search in urlbar");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: urlbarTestValue,
+ fireInputEvent: true,
+ });
+ info("waiting for about:privatebrowsing load");
+ await promise;
+
+ let urlbar = win.gURLBar;
+ is(
+ urlbar.value,
+ urlbarTestValue,
+ "URL bar value should be the same once about:privatebrowsing has loaded"
+ );
+ is(
+ win.gBrowser.selectedBrowser.userTypedValue,
+ urlbarTestValue,
+ "User typed value should be the same once about:privatebrowsing has loaded"
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_raceWithTabs.js b/browser/components/urlbar/tests/browser/browser_raceWithTabs.js
new file mode 100644
index 0000000000..852ffd11fc
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_raceWithTabs.js
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URL = `${TEST_BASE_URL}dummy_page.html`;
+
+async function addBookmark(bookmark) {
+ info("Creating bookmark and keyword");
+ let bm = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: bookmark.url,
+ title: bookmark.title,
+ });
+ if (bookmark.keyword) {
+ await PlacesUtils.keywords.insert({
+ keyword: bookmark.keyword,
+ url: bookmark.url,
+ });
+ }
+
+ registerCleanupFunction(async function() {
+ if (bookmark.keyword) {
+ await PlacesUtils.keywords.remove(bookmark.keyword);
+ }
+ await PlacesUtils.bookmarks.remove(bm);
+ });
+}
+
+/**
+ * Check that if the user hits enter and ctrl-t at the same time, we open the
+ * URL in the right tab.
+ */
+add_task(async function hitEnterLoadInRightTab() {
+ await addBookmark({
+ title: "Test for keyword bookmark and URL",
+ url: TEST_URL,
+ keyword: "urlbarkeyword",
+ });
+
+ info("Opening a tab");
+ let oldTabOpenPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabOpen"
+ );
+ BrowserOpenTab();
+ let oldTab = (await oldTabOpenPromise).target;
+ let oldTabLoadedPromise = BrowserTestUtils.browserLoaded(
+ oldTab.linkedBrowser,
+ false,
+ TEST_URL
+ ).then(() => info("Old tab loaded"));
+
+ info("Filling URL bar, sending <return> and opening a tab");
+ let tabOpenPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabOpen"
+ );
+ gURLBar.value = "urlbarkeyword";
+ gURLBar.focus();
+ gURLBar.select();
+ EventUtils.sendKey("return");
+
+ info("Immediately open a second tab");
+ BrowserOpenTab();
+ let newTab = (await tabOpenPromise).target;
+
+ info("Created new tab; waiting for tabs to load");
+ let newTabLoadedPromise = BrowserTestUtils.browserLoaded(
+ newTab.linkedBrowser,
+ false,
+ "about:newtab"
+ ).then(() => info("New tab loaded"));
+ // If one of the tabs loads the wrong page, this will timeout, and that
+ // indicates we regressed this bug fix.
+ await Promise.all([newTabLoadedPromise, oldTabLoadedPromise]);
+ // These are not particularly useful, but the test must contain some checks.
+ is(
+ newTab.linkedBrowser.currentURI.spec,
+ "about:newtab",
+ "New tab loaded about:newtab"
+ );
+ is(oldTab.linkedBrowser.currentURI.spec, TEST_URL, "Old tab loaded URL");
+
+ info("Closing tabs");
+ BrowserTestUtils.removeTab(newTab);
+ BrowserTestUtils.removeTab(oldTab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_redirect_error.js b/browser/components/urlbar/tests/browser/browser_redirect_error.js
new file mode 100644
index 0000000000..c7d72425b9
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_redirect_error.js
@@ -0,0 +1,134 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const REDIRECT_FROM = `${TEST_BASE_URL}redirect_error.sjs`;
+
+const REDIRECT_TO = "https://www.bank1.com/"; // Bad-cert host.
+
+function isRedirectedURISpec(aURISpec) {
+ return isRedirectedURI(Services.io.newURI(aURISpec));
+}
+
+function isRedirectedURI(aURI) {
+ // Compare only their before-hash portion.
+ return Services.io.newURI(REDIRECT_TO).equalsExceptRef(aURI);
+}
+
+/*
+ Test.
+
+1. Load redirect_bug623155.sjs#BG in a background tab.
+
+2. The redirected URI is <https://www.bank1.com/#BG>, which displayes a cert
+ error page.
+
+3. Switch the tab to foreground.
+
+4. Check the URLbar's value, expecting <https://www.bank1.com/#BG>
+
+5. Load redirect_bug623155.sjs#FG in the foreground tab.
+
+6. The redirected URI is <https://www.bank1.com/#FG>. And this is also
+ a cert-error page.
+
+7. Check the URLbar's value, expecting <https://www.bank1.com/#FG>
+
+8. End.
+
+ */
+
+var gNewTab;
+
+function test() {
+ waitForExplicitFinish();
+
+ // Load a URI in the background.
+ gNewTab = BrowserTestUtils.addTab(gBrowser, REDIRECT_FROM + "#BG");
+ gBrowser
+ .getBrowserForTab(gNewTab)
+ .webProgress.addProgressListener(
+ gWebProgressListener,
+ Ci.nsIWebProgress.NOTIFY_LOCATION
+ );
+}
+
+var gWebProgressListener = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+
+ // ---------------------------------------------------------------------------
+ // NOTIFY_LOCATION mode should work fine without these methods.
+ //
+ // onStateChange: function() {},
+ // onStatusChange: function() {},
+ // onProgressChange: function() {},
+ // onSecurityChange: function() {},
+ // ----------------------------------------------------------------------------
+
+ onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
+ if (!aRequest) {
+ // This is bug 673752, or maybe initial "about:blank".
+ return;
+ }
+
+ ok(gNewTab, "There is a new tab.");
+ ok(
+ isRedirectedURI(aLocation),
+ "onLocationChange catches only redirected URI."
+ );
+
+ if (aLocation.ref == "BG") {
+ // This is background tab's request.
+ isnot(gNewTab, gBrowser.selectedTab, "This is a background tab.");
+ } else if (aLocation.ref == "FG") {
+ // This is foreground tab's request.
+ is(gNewTab, gBrowser.selectedTab, "This is a foreground tab.");
+ } else {
+ // We shonuld not reach here.
+ ok(false, "This URI hash is not expected:" + aLocation.ref);
+ }
+
+ let isSelectedTab = gNewTab.selected;
+ setTimeout(delayed, 0, isSelectedTab);
+ },
+};
+
+function delayed(aIsSelectedTab) {
+ // Switch tab and confirm URL bar.
+ if (!aIsSelectedTab) {
+ gBrowser.selectedTab = gNewTab;
+ }
+
+ let currentURI = gBrowser.selectedBrowser.currentURI.spec;
+ ok(
+ isRedirectedURISpec(currentURI),
+ "The content area is redirected. aIsSelectedTab:" + aIsSelectedTab
+ );
+ is(
+ gURLBar.value,
+ currentURI,
+ "The URL bar shows the content URI. aIsSelectedTab:" + aIsSelectedTab
+ );
+
+ if (!aIsSelectedTab) {
+ // If this was a background request, go on a foreground request.
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, REDIRECT_FROM + "#FG");
+ } else {
+ // Othrewise, nothing to do remains.
+ finish();
+ }
+}
+
+/* Cleanup */
+registerCleanupFunction(function() {
+ if (gNewTab) {
+ gBrowser
+ .getBrowserForTab(gNewTab)
+ .webProgress.removeProgressListener(gWebProgressListener);
+
+ gBrowser.removeTab(gNewTab);
+ }
+ gNewTab = null;
+});
diff --git a/browser/components/urlbar/tests/browser/browser_remoteness_switch.js b/browser/components/urlbar/tests/browser/browser_remoteness_switch.js
new file mode 100644
index 0000000000..c81e9e232a
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_remoteness_switch.js
@@ -0,0 +1,51 @@
+"use strict";
+
+/**
+ * Verify that when loading and going back/forward through history between URLs
+ * loaded in the content process, and URLs loaded in the parent process, we
+ * don't set the URL for the tab to about:blank inbetween the loads.
+ */
+add_task(async function() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.navigation.requireUserInteraction", false]],
+ });
+ let url = "http://www.example.com/foo.html";
+ await BrowserTestUtils.withNewTab({ gBrowser, url }, async function(browser) {
+ let wpl = {
+ onLocationChange(unused, unused2, location) {
+ if (location.schemeIs("about")) {
+ is(
+ location.spec,
+ "about:config",
+ "Only about: location change should be for about:preferences"
+ );
+ } else {
+ is(
+ location.spec,
+ url,
+ "Only non-about: location change should be for the http URL we're dealing with."
+ );
+ }
+ },
+ };
+ gBrowser.addProgressListener(wpl);
+
+ let didLoad = BrowserTestUtils.browserLoaded(browser, null, function(
+ loadedURL
+ ) {
+ return loadedURL == "about:config";
+ });
+ BrowserTestUtils.loadURI(browser, "about:config");
+ await didLoad;
+
+ gBrowser.goBack();
+ await BrowserTestUtils.browserLoaded(browser, null, function(loadedURL) {
+ return url == loadedURL;
+ });
+ gBrowser.goForward();
+ await BrowserTestUtils.browserLoaded(browser, null, function(loadedURL) {
+ return loadedURL == "about:config";
+ });
+ gBrowser.removeProgressListener(wpl);
+ });
+});
diff --git a/browser/components/urlbar/tests/browser/browser_remotetab.js b/browser/components/urlbar/tests/browser/browser_remotetab.js
new file mode 100644
index 0000000000..51bb79eac2
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_remotetab.js
@@ -0,0 +1,111 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This tests checks that the remote tab result is displayed and can be
+ * selected.
+ */
+
+"use strict";
+
+const { SyncedTabs } = ChromeUtils.import(
+ "resource://services-sync/SyncedTabs.jsm"
+);
+
+const TEST_URL = `${TEST_BASE_URL}dummy_page.html`;
+
+const REMOTE_TAB = {
+ id: "7cqCr77ptzX3",
+ type: "client",
+ lastModified: 1492201200,
+ name: "zcarter's Nightly on MacBook-Pro-25",
+ clientType: "desktop",
+ tabs: [
+ {
+ type: "tab",
+ title: "Test Remote",
+ url: TEST_URL,
+ icon: UrlbarUtils.ICON.DEFAULT,
+ client: "7cqCr77ptzX3",
+ lastUsed: 1452124677,
+ },
+ ],
+};
+
+add_task(async function setup() {
+ sandbox = sinon.createSandbox();
+
+ let originalSyncedTabsInternal = SyncedTabs._internal;
+ SyncedTabs._internal = {
+ isConfiguredToSyncTabs: true,
+ hasSyncedThisSession: true,
+ getTabClients() {
+ return Promise.resolve([]);
+ },
+ syncTabs() {
+ return Promise.resolve();
+ },
+ };
+
+ // Tell the Sync XPCOM service it is initialized.
+ let weaveXPCService = Cc["@mozilla.org/weave/service;1"].getService(
+ Ci.nsISupports
+ ).wrappedJSObject;
+ let oldWeaveServiceReady = weaveXPCService.ready;
+ weaveXPCService.ready = true;
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.autoFill", false],
+ ["services.sync.username", "fake"],
+ ["services.sync.syncedTabs.showRemoteTabs", true],
+ ],
+ });
+
+ sandbox
+ .stub(SyncedTabs._internal, "getTabClients")
+ .callsFake(() => Promise.resolve(Cu.cloneInto([REMOTE_TAB], {})));
+
+ registerCleanupFunction(async () => {
+ sandbox.restore();
+ weaveXPCService.ready = oldWeaveServiceReady;
+ SyncedTabs._internal = originalSyncedTabsInternal;
+ });
+});
+
+add_task(async function test_remotetab_opens() {
+ await BrowserTestUtils.withNewTab(
+ { url: "about:robots", gBrowser },
+ async function() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "Test Remote",
+ });
+
+ // There should be two items in the pop-up, the first is the default search
+ // suggestion, the second is the remote tab.
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.REMOTE_TAB,
+ "Should be the remote tab entry"
+ );
+
+ // The URL is going to open in the current tab as it is currently about:blank
+ let promiseTabLoaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await promiseTabLoaded;
+
+ Assert.equal(
+ gBrowser.selectedTab.linkedBrowser.currentURI.spec,
+ TEST_URL,
+ "correct URL loaded"
+ );
+ }
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_removeUnsafeProtocolsFromURLBarPaste.js b/browser/components/urlbar/tests/browser/browser_removeUnsafeProtocolsFromURLBarPaste.js
new file mode 100644
index 0000000000..3793b69bd0
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_removeUnsafeProtocolsFromURLBarPaste.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Ensures that pasting unsafe protocols in the urlbar have the protocol
+ * correctly stripped.
+ */
+
+var pairs = [
+ ["javascript:", ""],
+ ["javascript:1+1", "1+1"],
+ ["javascript:document.domain", "document.domain"],
+ [
+ " \u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\u0009javascript:document.domain",
+ "document.domain",
+ ],
+ ["java\nscript:foo", "foo"],
+ ["java\tscript:foo", "foo"],
+ ["http://\nexample.com", "http://example.com"],
+ ["http://\nexample.com\n", "http://example.com"],
+ ["data:text/html,<body>hi</body>", "data:text/html,<body>hi</body>"],
+ ["javaScript:foopy", "foopy"],
+ ["javaScript:javaScript:alert('hi')", "alert('hi')"],
+ // Nested things get confusing because some things don't parse as URIs:
+ ["javascript:javascript:alert('hi!')", "alert('hi!')"],
+ [
+ "data:data:text/html,<body>hi</body>",
+ "data:data:text/html,<body>hi</body>",
+ ],
+ ["javascript:data:javascript:alert('hi!')", "data:javascript:alert('hi!')"],
+ [
+ "javascript:data:text/html,javascript:alert('hi!')",
+ "data:text/html,javascript:alert('hi!')",
+ ],
+ [
+ "data:data:text/html,javascript:alert('hi!')",
+ "data:data:text/html,javascript:alert('hi!')",
+ ],
+];
+
+let supportsNullBytes = AppConstants.platform == "macosx";
+// Note that \u000d (\r) is missing here; we test it separately because it
+// makes the test sad on Windows.
+let gobbledygook =
+ "\u000a\u000b\u000c\u000e\u000f\u0010\u0011\u0012\u0013\u0014javascript:foo";
+if (supportsNullBytes) {
+ gobbledygook = "\u0000" + gobbledygook;
+}
+pairs.push([gobbledygook, "foo"]);
+
+let supportsReturnWithoutNewline =
+ AppConstants.platform != "win" && AppConstants.platform != "linux";
+if (supportsReturnWithoutNewline) {
+ pairs.push(["java\rscript:foo", "foo"]);
+}
+
+async function paste(input) {
+ try {
+ await SimpleTest.promiseClipboardChange(
+ aData => {
+ // This test checks how "\r" is treated. Therefore, we cannot specify
+ // string here and instead, we need to compare strictly with this
+ // function.
+ return aData === input;
+ },
+ () => {
+ clipboardHelper.copyString(input);
+ }
+ );
+ } catch (ex) {
+ Assert.ok(false, "Failed to copy string '" + input + "' to clipboard");
+ }
+
+ document.commandDispatcher
+ .getControllerForCommand("cmd_paste")
+ .doCommand("cmd_paste");
+}
+
+add_task(async function test_stripUnsafeProtocolPaste() {
+ for (let [inputValue, expectedURL] of pairs) {
+ gURLBar.value = "";
+ gURLBar.focus();
+ await paste(inputValue);
+
+ Assert.equal(
+ gURLBar.value,
+ expectedURL,
+ `entering ${inputValue} strips relevant bits.`
+ );
+
+ await new Promise(resolve => setTimeout(resolve, 0));
+ }
+});
diff --git a/browser/components/urlbar/tests/browser/browser_remove_match.js b/browser/components/urlbar/tests/browser/browser_remove_match.js
new file mode 100644
index 0000000000..1b227c9ea8
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_remove_match.js
@@ -0,0 +1,227 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ FormHistory: "resource://gre/modules/FormHistory.jsm",
+});
+
+add_task(async function test_remove_history() {
+ const TEST_URL = "http://remove.me/from_urlbar/";
+ await PlacesTestUtils.addVisits(TEST_URL);
+
+ registerCleanupFunction(async function() {
+ await PlacesUtils.history.clear();
+ });
+
+ let promiseVisitRemoved = PlacesTestUtils.waitForNotification(
+ "onDeleteURI",
+ uri => uri.spec == TEST_URL,
+ "history"
+ );
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "from_urlbar",
+ });
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.url, TEST_URL, "Found the expected result");
+
+ let expectedResultCount = UrlbarTestUtils.getResultCount(window) - 1;
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), 1);
+ EventUtils.synthesizeKey("KEY_Delete", { shiftKey: true });
+ await promiseVisitRemoved;
+ await TestUtils.waitForCondition(
+ () => UrlbarTestUtils.getResultCount(window) == expectedResultCount,
+ "Waiting for the result to disappear"
+ );
+
+ for (let i = 0; i < expectedResultCount; i++) {
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ Assert.notEqual(
+ details.url,
+ TEST_URL,
+ "Should not find the test URL in the remaining results"
+ );
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+add_task(async function test_remove_form_history() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.searches", true],
+ ["browser.urlbar.maxHistoricalSearchSuggestions", 1],
+ ],
+ });
+
+ await Services.search.addEngineWithDetails("test", {
+ method: "GET",
+ template: "http://example.com/?q={searchTerms}",
+ });
+ let engine = Services.search.getEngineByName("test");
+ let originalEngine = await Services.search.getDefault();
+ await Services.search.setDefault(engine);
+
+ let formHistoryValue = "foobar";
+ await UrlbarTestUtils.formHistory.add([formHistoryValue]);
+
+ let formHistory = (
+ await UrlbarTestUtils.formHistory.search({
+ value: formHistoryValue,
+ })
+ ).map(entry => entry.value);
+ Assert.deepEqual(
+ formHistory,
+ [formHistoryValue],
+ "Should find form history after adding it"
+ );
+
+ let promiseRemoved = UrlbarTestUtils.formHistory.promiseChanged("remove");
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ });
+
+ let index = 1;
+ let count = UrlbarTestUtils.getResultCount(window);
+ for (; index < count; index++) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index);
+ if (
+ result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
+ result.source == UrlbarUtils.RESULT_SOURCE.HISTORY
+ ) {
+ break;
+ }
+ }
+ Assert.ok(index < count, "Result found");
+
+ EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: index });
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), index);
+ EventUtils.synthesizeKey("KEY_Delete", { shiftKey: true });
+ await promiseRemoved;
+
+ await TestUtils.waitForCondition(
+ () => UrlbarTestUtils.getResultCount(window) == count - 1,
+ "Waiting for the result to disappear"
+ );
+
+ for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ Assert.ok(
+ result.type != UrlbarUtils.RESULT_TYPE.SEARCH ||
+ result.source != UrlbarUtils.RESULT_SOURCE.HISTORY,
+ "Should not find the form history result in the remaining results"
+ );
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ formHistory = (
+ await UrlbarTestUtils.formHistory.search({
+ value: formHistoryValue,
+ })
+ ).map(entry => entry.value);
+ Assert.deepEqual(
+ formHistory,
+ [],
+ "Should not find form history after removing it"
+ );
+
+ await SpecialPowers.popPrefEnv();
+ await Services.search.setDefault(originalEngine);
+ await Services.search.removeEngine(engine);
+});
+
+// We shouldn't be able to remove a bookmark item.
+add_task(async function test_remove_bookmark_doesnt() {
+ const TEST_URL = "http://dont.remove.me/from_urlbar/";
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "test",
+ url: TEST_URL,
+ });
+
+ registerCleanupFunction(async function() {
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "from_urlbar",
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.url, TEST_URL, "Found the expected result");
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), 1);
+ EventUtils.synthesizeKey("KEY_Delete", { shiftKey: true });
+
+ // We don't have an easy way of determining if the event was process or not,
+ // so let any event queues clear before testing.
+ await new Promise(resolve => setTimeout(resolve, 0));
+ await PlacesTestUtils.promiseAsyncUpdates();
+
+ Assert.ok(
+ await PlacesUtils.bookmarks.fetch({ url: TEST_URL }),
+ "Should still have the URL bookmarked."
+ );
+});
+
+add_task(async function test_searchMode_removeRestyledHistory() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.searches", true],
+ ["browser.urlbar.maxHistoricalSearchSuggestions", 1],
+ ],
+ });
+
+ let engine = await Services.search.addEngineWithDetails("test", {
+ method: "GET",
+ template: "http://example.com/",
+ searchGetParams: "q={searchTerms}",
+ });
+ let originalEngine = await Services.search.getDefault();
+ await Services.search.setDefault(engine);
+ await Services.search.moveEngine(engine, 0);
+
+ let query = "ciao";
+ let url = `http://example.com/?q=${query}bar`;
+ await PlacesTestUtils.addVisits(url);
+
+ await BrowserTestUtils.withNewTab("about:robots", async function(browser) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: query,
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.SEARCH);
+ Assert.equal(result.source, UrlbarUtils.RESULT_SOURCE.HISTORY);
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), 1);
+ EventUtils.synthesizeKey("KEY_Delete", { shiftKey: true });
+ await TestUtils.waitForCondition(
+ async () => !(await PlacesTestUtils.isPageInDB(url)),
+ "Wait for url to be removed from history"
+ );
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 1,
+ "Urlbar result should be removed"
+ );
+
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+ await UrlbarTestUtils.promisePopupClose(window);
+ });
+ await PlacesUtils.history.clear();
+ await SpecialPowers.popPrefEnv();
+ await Services.search.setDefault(originalEngine);
+ await Services.search.removeEngine(engine);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_restoreEmptyInput.js b/browser/components/urlbar/tests/browser/browser_restoreEmptyInput.js
new file mode 100644
index 0000000000..50ac4044e6
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_restoreEmptyInput.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// When the input is empty and the view is opened, keying down through the
+// results and then out of the results should restore the empty input.
+
+"use strict";
+
+add_task(async function test() {
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits("http://example.com/");
+ }
+ // Update Top Sites to make sure the last Top Site is a URL. Otherwise, it
+ // would be a search shortcut and thus would not fill the Urlbar when
+ // selected.
+ await updateTopSites(sites => {
+ return (
+ sites &&
+ sites[sites.length - 1] &&
+ sites[sites.length - 1].url == "http://example.com/"
+ );
+ });
+
+ registerCleanupFunction(async function() {
+ await PlacesUtils.history.clear();
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ fireInputEvent: true,
+ });
+
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ -1,
+ "Nothing selected"
+ );
+
+ let resultCount = UrlbarTestUtils.getResultCount(window);
+ Assert.greater(resultCount, 0, "At least one result");
+
+ for (let i = 0; i < resultCount; i++) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ resultCount - 1,
+ "Last result selected"
+ );
+ Assert.notEqual(gURLBar.value, "", "Input should not be empty");
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ -1,
+ "Nothing selected"
+ );
+ Assert.equal(gURLBar.value, "", "Input should be empty");
+});
diff --git a/browser/components/urlbar/tests/browser/browser_resultSpan.js b/browser/components/urlbar/tests/browser/browser_resultSpan.js
new file mode 100644
index 0000000000..29f8a50d44
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_resultSpan.js
@@ -0,0 +1,264 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that displaying results with resultSpan > 1 limits other results in
+// the view.
+
+const TEST_RESULTS = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/1" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TIP,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ text: "This is a test tip.",
+ buttonText: "Done",
+ type: "test",
+ helpUrl: "about:about",
+ }
+ ),
+];
+
+const MAX_RESULTS = UrlbarPrefs.get("maxRichResults");
+const TIP_SPAN = UrlbarUtils.getSpanForResult({
+ type: UrlbarUtils.RESULT_TYPE.TIP,
+});
+
+add_task(async function init() {
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+// A restricting provider with one tip result and many history results.
+add_task(async function oneTip() {
+ let results = Array.from(TEST_RESULTS);
+ for (let i = TEST_RESULTS.length; i < MAX_RESULTS; i++) {
+ results.push(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: `http://mozilla.org/${i}` }
+ )
+ );
+ }
+
+ let expectedResults = Array.from(results).slice(
+ 0,
+ MAX_RESULTS - TIP_SPAN + 1
+ );
+
+ let provider = new UrlbarTestUtils.TestProvider({ results, priority: 1 });
+ UrlbarProvidersManager.registerProvider(provider);
+
+ let context = await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ value: "test",
+ window,
+ });
+
+ checkResults(context.results, expectedResults);
+
+ UrlbarProvidersManager.unregisterProvider(provider);
+ gURLBar.view.close();
+});
+
+// A restricting provider with three tip results and many history results.
+add_task(async function threeTips() {
+ let results = Array.from(TEST_RESULTS);
+ for (let i = 1; i < 3; i++) {
+ results.push(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TIP,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ text: "This is a test tip.",
+ buttonText: "Done",
+ type: "test",
+ helpUrl: `about:about#${i}`,
+ }
+ )
+ );
+ }
+ for (let i = 2; i < 15; i++) {
+ results.push(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: `http://mozilla.org/${i}` }
+ )
+ );
+ }
+
+ let expectedResults = Array.from(results).slice(
+ 0,
+ MAX_RESULTS - 3 * (TIP_SPAN - 1)
+ );
+
+ let provider = new UrlbarTestUtils.TestProvider({ results, priority: 1 });
+ UrlbarProvidersManager.registerProvider(provider);
+
+ let context = await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ value: "test",
+ window,
+ });
+
+ checkResults(context.results, expectedResults);
+
+ UrlbarProvidersManager.unregisterProvider(provider);
+ gURLBar.view.close();
+});
+
+// A non-restricting provider with one tip result and many history results.
+add_task(async function oneTip_nonRestricting() {
+ let results = Array.from(TEST_RESULTS);
+ for (let i = 2; i < 15; i++) {
+ results.push(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: `http://mozilla.org/${i}` }
+ )
+ );
+ }
+
+ let expectedResults = Array.from(results);
+
+ // UnifiedComplete's heuristic search result
+ expectedResults.unshift({
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ payload: {
+ engine: Services.search.defaultEngine.name,
+ query: "test",
+ },
+ });
+
+ expectedResults = expectedResults.slice(0, MAX_RESULTS - TIP_SPAN + 1);
+
+ let provider = new UrlbarTestUtils.TestProvider({ results });
+ UrlbarProvidersManager.registerProvider(provider);
+
+ let context = await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ value: "test",
+ window,
+ });
+
+ checkResults(context.results, expectedResults);
+
+ UrlbarProvidersManager.unregisterProvider(provider);
+ gURLBar.view.close();
+});
+
+// A non-restricting provider with three tip results and many history results.
+add_task(async function threeTips_nonRestricting() {
+ let results = Array.from(TEST_RESULTS);
+ for (let i = 1; i < 3; i++) {
+ results.push(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TIP,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ text: "This is a test tip.",
+ buttonText: "Done",
+ type: "test",
+ helpUrl: `about:about#${i}`,
+ }
+ )
+ );
+ }
+ for (let i = 2; i < 15; i++) {
+ results.push(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: `http://mozilla.org/${i}` }
+ )
+ );
+ }
+
+ let expectedResults = Array.from(results);
+
+ // UnifiedComplete's heuristic search result
+ expectedResults.unshift({
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ payload: {
+ engine: Services.search.defaultEngine.name,
+ query: "test",
+ },
+ });
+
+ expectedResults = expectedResults.slice(0, MAX_RESULTS - 3 * (TIP_SPAN - 1));
+
+ let provider = new UrlbarTestUtils.TestProvider({ results });
+ UrlbarProvidersManager.registerProvider(provider);
+
+ let context = await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ value: "test",
+ window,
+ });
+
+ checkResults(context.results, expectedResults);
+
+ UrlbarProvidersManager.unregisterProvider(provider);
+ gURLBar.view.close();
+});
+
+add_task(async function customValue() {
+ let results = [];
+ for (let i = 0; i < 15; i++) {
+ results.push(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: `http://mozilla.org/${i}` }
+ )
+ );
+ }
+
+ results[1].resultSpan = 5;
+
+ let expectedResults = Array.from(results);
+ expectedResults = expectedResults.slice(0, 6);
+
+ let provider = new UrlbarTestUtils.TestProvider({ results });
+ UrlbarProvidersManager.registerProvider(provider);
+
+ let context = await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ value: "test",
+ window,
+ });
+
+ checkResults(context.results, expectedResults);
+
+ UrlbarProvidersManager.unregisterProvider(provider);
+ gURLBar.view.close();
+});
+
+function checkResults(actual, expected) {
+ Assert.equal(actual.length, expected.length, "Number of results");
+ for (let i = 0; i < expected.length; i++) {
+ info(`Checking results at index ${i}`);
+ let actualResult = collectExpectedProperties(actual[i], expected[i]);
+ Assert.deepEqual(actualResult, expected[i], "Actual vs. expected result");
+ }
+}
+
+function collectExpectedProperties(actualObj, expectedObj) {
+ let newActualObj = {};
+ for (let name in expectedObj) {
+ if (typeof expectedObj[name] == "object") {
+ newActualObj[name] = collectExpectedProperties(
+ actualObj[name],
+ expectedObj[name]
+ );
+ } else {
+ newActualObj[name] = expectedObj[name];
+ }
+ }
+ return newActualObj;
+}
diff --git a/browser/components/urlbar/tests/browser/browser_result_onSelection.js b/browser/components/urlbar/tests/browser/browser_result_onSelection.js
new file mode 100644
index 0000000000..329091ccf8
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_result_onSelection.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test() {
+ let results = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/1" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/2" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TIP,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ text: "This is a test tip.",
+ buttonText: "Done",
+ type: "test",
+ helpUrl: "about:about",
+ }
+ ),
+ ];
+
+ results[0].heuristic = true;
+
+ let selectionCount = 0;
+ let provider = new UrlbarTestUtils.TestProvider({
+ results,
+ priority: 1,
+ onSelection: (result, element) => {
+ selectionCount++;
+ },
+ });
+ UrlbarProvidersManager.registerProvider(provider);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+
+ let oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window);
+
+ while (!oneOffs.selectedButton) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+
+ Assert.equal(selectionCount, 4, "We selected the four elements in the view.");
+ UrlbarProvidersManager.unregisterProvider(provider);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_retainedResultsOnFocus.js b/browser/components/urlbar/tests/browser/browser_retainedResultsOnFocus.js
new file mode 100644
index 0000000000..911f398614
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_retainedResultsOnFocus.js
@@ -0,0 +1,435 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests retained results.
+// When there is a pending search (user typed a search string and blurred
+// without picking a result), on focus we should the search results again.
+
+async function checkPanelStatePersists(win, isOpen) {
+ // Check for popup events, we should not see any of them because the urlbar
+ // popup state should not change. This also ensures we don't cause flickering
+ // open/close actions.
+ function handler(event) {
+ Assert.ok(false, `Received unexpected event ${event.type}`);
+ }
+ win.gURLBar.addEventListener("popupshowing", handler);
+ win.gURLBar.addEventListener("popuphiding", handler);
+ // Because the panel opening may not be immediate, we must wait a bit.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 300));
+ win.gURLBar.removeEventListener("popupshowing", handler);
+ win.gURLBar.removeEventListener("popuphiding", handler);
+ Assert.equal(
+ isOpen,
+ win.gURLBar.view.isOpen,
+ `check urlbar remains ${isOpen ? "open" : "closed"}`
+ );
+}
+
+async function checkOpensOnFocus(win, state) {
+ Assert.ok(!win.gURLBar.view.isOpen, "check urlbar panel is not open");
+ win.gURLBar.blur();
+
+ info("Check the keyboard shortcut.");
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ win.document.getElementById("Browser:OpenLocation").doCommand();
+ });
+
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ Assert.equal(state.selectionStart, win.gURLBar.selectionStart);
+ Assert.equal(state.selectionEnd, win.gURLBar.selectionEnd);
+
+ await UrlbarTestUtils.promisePopupClose(win, () => {
+ win.gURLBar.blur();
+ });
+ info("Focus with the mouse.");
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ EventUtils.synthesizeMouseAtCenter(win.gURLBar.inputField, {}, win);
+ });
+
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ Assert.equal(state.selectionStart, win.gURLBar.selectionStart);
+ Assert.equal(state.selectionEnd, win.gURLBar.selectionEnd);
+}
+
+async function checkDoesNotOpenOnFocus(win) {
+ Assert.ok(!win.gURLBar.view.isOpen, "check urlbar panel is not open");
+ win.gURLBar.blur();
+
+ info("Check the keyboard shortcut.");
+ let promiseState = checkPanelStatePersists(win, false);
+ win.document.getElementById("Browser:OpenLocation").doCommand();
+ await promiseState;
+ win.gURLBar.blur();
+ info("Focus with the mouse.");
+ promiseState = checkPanelStatePersists(win, false);
+ EventUtils.synthesizeMouseAtCenter(win.gURLBar.inputField, {}, win);
+ await promiseState;
+}
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.autoFill", true]],
+ });
+ // Add some history for the empty panel and autofill.
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com/",
+ transition: PlacesUtils.history.TRANSITIONS.TYPED,
+ },
+ {
+ uri: "http://example.com/foo/",
+ transition: PlacesUtils.history.TRANSITIONS.TYPED,
+ },
+ ]);
+ registerCleanupFunction(async function() {
+ await PlacesUtils.history.clear();
+ });
+});
+
+async function test_window(win) {
+ for (let url of ["about:newtab", "about:home", "http://example.com/"]) {
+ // withNewTab may hang on preloaded pages, thus instead of waiting for load
+ // we just wait for the expected currentURI value.
+ await BrowserTestUtils.withNewTab(
+ { gBrowser: win.gBrowser, url, waitForLoad: false },
+ async browser => {
+ await TestUtils.waitForCondition(
+ () => win.gBrowser.currentURI.spec == url,
+ "Ensure we're on the expected page"
+ );
+
+ // In one case use a value that triggers autofill.
+ let autofill = url == "http://example.com/";
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: autofill ? "ex" : "foo",
+ fireInputEvent: true,
+ });
+ let { value, selectionStart, selectionEnd } = win.gURLBar;
+ if (!autofill) {
+ selectionStart = 0;
+ }
+ info("expected " + value + " " + selectionStart + " " + selectionEnd);
+ await UrlbarTestUtils.promisePopupClose(win, () => {
+ win.gURLBar.blur();
+ });
+
+ info("The panel should open when there's a search string");
+ await checkOpensOnFocus(win, { value, selectionStart, selectionEnd });
+ await UrlbarTestUtils.promisePopupClose(win, () => {
+ win.gURLBar.blur();
+ });
+ }
+ );
+ }
+}
+
+add_task(async function test_normalWindow() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await test_window(win);
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_privateWindow() {
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ await test_window(privateWin);
+ await BrowserTestUtils.closeWindow(privateWin);
+});
+
+add_task(async function test_tabSwitch() {
+ info("Check that switching tabs reopens the view.");
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "ex",
+ fireInputEvent: true,
+ });
+ let { value, selectionStart, selectionEnd } = win.gURLBar;
+ Assert.equal(value, "example.com/", "Check autofill value");
+ Assert.ok(
+ selectionStart > 0 && selectionEnd > selectionStart,
+ "Check autofill selection"
+ );
+
+ Assert.ok(win.gURLBar.focused, "The urlbar should be focused");
+ let tab1 = win.gBrowser.selectedTab;
+
+ async function check_autofill() {
+ // The urlbar code waits for both TabSelect and the focus change, thus
+ // we can't just wait for search completion here, we have to poll for a
+ // value.
+ await TestUtils.waitForCondition(
+ () => win.gURLBar.value == "example.com/",
+ "wait for autofill value"
+ );
+ // Ensure stable results.
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ Assert.equal(selectionStart, win.gURLBar.selectionStart);
+ Assert.equal(selectionEnd, win.gURLBar.selectionEnd);
+ }
+
+ info("Open a new tab with the same search");
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(win.gBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "ex",
+ fireInputEvent: true,
+ });
+
+ info("Switch across tabs");
+ for (let tab of win.gBrowser.tabs) {
+ await UrlbarTestUtils.promisePopupOpen(win, async () => {
+ await BrowserTestUtils.switchTab(win.gBrowser, tab);
+ });
+ await check_autofill();
+ }
+
+ info("Close tab and check the view is open.");
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ BrowserTestUtils.removeTab(tab2);
+ });
+ await check_autofill();
+
+ info("Open a new tab with a different search");
+ tab2 = await BrowserTestUtils.openNewForegroundTab(win.gBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "xam",
+ fireInputEvent: true,
+ });
+
+ info("Switch to the first tab and check the panel remains open");
+ let promiseState = checkPanelStatePersists(win, true);
+ await BrowserTestUtils.switchTab(win.gBrowser, tab1);
+ await promiseState;
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ await check_autofill();
+
+ info("Switch to the second tab and check the panel remains open");
+ promiseState = checkPanelStatePersists(win, true);
+ await BrowserTestUtils.switchTab(win.gBrowser, tab2);
+ await promiseState;
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ Assert.equal(win.gURLBar.value, "xam", "check value");
+ Assert.equal(win.gURLBar.selectionStart, 3);
+ Assert.equal(win.gURLBar.selectionEnd, 3);
+
+ info("autofill in tab2, switch to tab1, then back to tab2 with the mouse");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "e",
+ fireInputEvent: true,
+ });
+ // Adjust selection start, we are using a different search string.
+ await BrowserTestUtils.switchTab(win.gBrowser, tab1);
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ await check_autofill();
+ tab2.click();
+ selectionStart = 1;
+ await check_autofill();
+
+ info("Check we don't rerun a search if the shortcut is used on an open view");
+ EventUtils.synthesizeKey("KEY_Backspace", {}, win);
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ Assert.ok(win.gURLBar.view.isOpen, "The view should be open");
+ Assert.equal(win.gURLBar.value, "e", "The value should be the typed one");
+ win.document.getElementById("Browser:OpenLocation").doCommand();
+ // A search should not run here, so there's nothing to wait for.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 300));
+ Assert.ok(win.gURLBar.view.isOpen, "The view should be open");
+ Assert.equal(win.gURLBar.value, "e", "The value should not change");
+
+ info(
+ "Tab switch from an empty search tab with unfocused urlbar to a tab with a search string and a focused urlbar"
+ );
+ win.gURLBar.value = "";
+ win.gURLBar.blur();
+ await UrlbarTestUtils.promisePopupOpen(win, async () => {
+ await BrowserTestUtils.switchTab(win.gBrowser, tab1);
+ });
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_tabSwitch_pageproxystate() {
+ info("Switching tabs on valid pageproxystate doesn't reopen.");
+
+ info("Adding some visits for the empty panel");
+ await PlacesTestUtils.addVisits([
+ "http://example.com/",
+ "http://mochi.test:8888/",
+ ]);
+ registerCleanupFunction(PlacesUtils.history.clear);
+
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ BrowserTestUtils.loadURI(win.gBrowser.selectedBrowser, "about:robots");
+ let tab1 = win.gBrowser.selectedTab;
+
+ info("Open a new tab and the empty search");
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(win.gBrowser);
+ await UrlbarTestUtils.promisePopupOpen(win, async () => {
+ win.gURLBar.focus();
+ // On Linux and Mac down moves caret to the end of the text unless it's
+ // there already.
+ win.gURLBar.selectionStart = win.gURLBar.selectionEnd =
+ win.gURLBar.value.length;
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ });
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(win, 0);
+ Assert.notEqual(result.url, "about:robots");
+
+ info("Switch to the first tab and start searching with DOWN");
+ await UrlbarTestUtils.promisePopupClose(win, async () => {
+ await BrowserTestUtils.switchTab(win.gBrowser, tab1);
+ });
+ await checkPanelStatePersists(win, false);
+ await UrlbarTestUtils.promisePopupOpen(win, async () => {
+ win.gURLBar.focus();
+ // On Linux and Mac down moves caret to the end of the text unless it's
+ // there already.
+ win.gURLBar.selectionStart = win.gURLBar.selectionEnd =
+ win.gURLBar.value.length;
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ });
+ await UrlbarTestUtils.promiseSearchComplete(win);
+
+ info("Switcihng to the second tab should not reopen the search");
+ await UrlbarTestUtils.promisePopupClose(win, async () => {
+ await BrowserTestUtils.switchTab(win.gBrowser, tab2);
+ });
+ await checkPanelStatePersists(win, false);
+
+ info("Switching to the first tab should not reopen the search");
+ let promiseState = await checkPanelStatePersists(win, false);
+ await BrowserTestUtils.switchTab(win.gBrowser, tab1);
+ await promiseState;
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_tabSwitch_emptySearch() {
+ info("Switching between empty-search tabs should not reopen the view.");
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+
+ info("Open the empty search");
+ let tab1 = win.gBrowser.selectedTab;
+ await UrlbarTestUtils.promisePopupOpen(win, async () => {
+ win.gURLBar.focus();
+ // On Linux and Mac down moves caret to the end of the text unless it's
+ // there already.
+ win.gURLBar.selectionStart = win.gURLBar.selectionEnd =
+ win.gURLBar.value.length;
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ });
+ await UrlbarTestUtils.promiseSearchComplete(win);
+
+ info("Open a new tab and the empty search");
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(win.gBrowser);
+ await UrlbarTestUtils.promisePopupOpen(win, async () => {
+ win.gURLBar.focus();
+ // On Linux and Mac down moves caret to the end of the text unless it's
+ // there already.
+ win.gURLBar.selectionStart = win.gURLBar.selectionEnd =
+ win.gURLBar.value.length;
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ });
+ await UrlbarTestUtils.promiseSearchComplete(win);
+
+ info("Switching to the first tab should not reopen the view");
+ await UrlbarTestUtils.promisePopupClose(win, async () => {
+ await BrowserTestUtils.switchTab(win.gBrowser, tab1);
+ });
+ await checkPanelStatePersists(win, false);
+
+ info("Switching to the second tab should not reopen the view");
+ let promiseState = await checkPanelStatePersists(win, false);
+ await BrowserTestUtils.switchTab(win.gBrowser, tab2);
+ await promiseState;
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_pageproxystate_valid() {
+ info("Focusing on valid pageproxystate should not reopen the view.");
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+
+ info("Search for a full url and confirm it with Enter");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "about:robots",
+ fireInputEvent: true,
+ });
+ let loadedPromise = BrowserTestUtils.browserLoaded(
+ win.gBrowser.selectedBrowser
+ );
+ EventUtils.synthesizeKey("KEY_Enter", {}, win);
+ await loadedPromise;
+
+ Assert.ok(!win.gURLBar.focused, "The urlbar should not be focused");
+ info("Focus the urlbar");
+ win.document.getElementById("Browser:OpenLocation").doCommand();
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_allowAutofill() {
+ info("Check we respect allowAutofill from the last search");
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+
+ await UrlbarTestUtils.promisePopupOpen(win, async () => {
+ await selectAndPaste("e", win);
+ });
+ Assert.equal(win.gURLBar.value, "e", "Should not autofill");
+ let context = await win.gURLBar.lastQueryContextPromise;
+ Assert.equal(context.allowAutofill, false, "Check initial allowAutofill");
+ await UrlbarTestUtils.promisePopupClose(win);
+
+ await UrlbarTestUtils.promisePopupOpen(win, async () => {
+ win.document.getElementById("Browser:OpenLocation").doCommand();
+ });
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ Assert.equal(win.gURLBar.value, "e", "Should not autofill");
+ context = await win.gURLBar.lastQueryContextPromise;
+ Assert.equal(context.allowAutofill, false, "Check reopened allowAutofill");
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_clicks_after_autofill() {
+ info(
+ "Check that clickin on an autofilled input field doesn't requery, causing loss of the caret position"
+ );
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ info("autofill in tab2, switch to tab1, then back to tab2 with the mouse");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "e",
+ fireInputEvent: true,
+ });
+ Assert.equal(win.gURLBar.value, "example.com/", "Should have autofilled");
+
+ // Check single click.
+ let input = win.gURLBar.inputField;
+ EventUtils.synthesizeMouse(input, 30, 10, {}, win);
+ // Wait a bit, in case of a mistake this would run a query, otherwise not.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 300));
+ Assert.ok(win.gURLBar.selectionStart < win.gURLBar.value.length);
+ Assert.equal(win.gURLBar.selectionStart, win.gURLBar.selectionEnd);
+
+ // Check double click.
+ EventUtils.synthesizeMouse(input, 30, 10, { clickCount: 2 }, win);
+ // Wait a bit, in case of a mistake this would run a query, otherwise not.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 300));
+ Assert.ok(win.gURLBar.selectionStart < win.gURLBar.value.length);
+ Assert.ok(win.gURLBar.selectionEnd > win.gURLBar.selectionStart);
+
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_revert.js b/browser/components/urlbar/tests/browser/browser_revert.js
new file mode 100644
index 0000000000..1b47189145
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_revert.js
@@ -0,0 +1,33 @@
+// Test reverting the urlbar value with ESC after a tab switch.
+
+add_task(async function() {
+ registerCleanupFunction(PlacesUtils.history.clear);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "http://example.com",
+ },
+ async function(browser) {
+ let originalValue = gURLBar.value;
+ let tab = gBrowser.selectedTab;
+ info("Put a typed value.");
+ gBrowser.userTypedValue = "foobar";
+ info("Switch tabs.");
+ gBrowser.selectedTab = gBrowser.tabs[0];
+ gBrowser.selectedTab = tab;
+ Assert.equal(
+ gURLBar.value,
+ "foobar",
+ "location bar displays typed value"
+ );
+
+ gURLBar.focus();
+ EventUtils.synthesizeKey("KEY_Escape");
+ Assert.equal(
+ gURLBar.value,
+ originalValue,
+ "ESC reverted the location bar value"
+ );
+ }
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_searchFunction.js b/browser/components/urlbar/tests/browser/browser_searchFunction.js
new file mode 100644
index 0000000000..f0ccd59f3e
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchFunction.js
@@ -0,0 +1,256 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test checks the urlbar.search() function.
+
+"use strict";
+
+const ALIAS = "@enginealias";
+let aliasEngine;
+
+add_task(async function init() {
+ // Run this in a new tab, to ensure all the locationchange notifications have
+ // fired.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ aliasEngine = await Services.search.addEngineWithDetails("Test", {
+ alias: ALIAS,
+ template: "http://example.com/?search={searchTerms}",
+ });
+ registerCleanupFunction(async function() {
+ await Services.search.removeEngine(aliasEngine);
+ BrowserTestUtils.removeTab(tab);
+ gURLBar.handleRevert();
+ });
+});
+
+// Calls search() with a normal, non-"@engine" search-string argument.
+add_task(async function basic() {
+ gURLBar.blur();
+ gURLBar.search("basic");
+ ok(gURLBar.hasAttribute("focused"), "url bar is focused");
+ await assertUrlbarValue("basic");
+
+ assertOneOffButtonsVisible(true);
+
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+});
+
+// Calls search() with an invalid "@engine" search engine alias so that the
+// one-off search buttons are disabled.
+add_task(async function searchEngineAlias() {
+ gURLBar.blur();
+ await UrlbarTestUtils.promisePopupOpen(window, () =>
+ gURLBar.search("@example")
+ );
+ ok(gURLBar.hasAttribute("focused"), "url bar is focused");
+ UrlbarTestUtils.assertSearchMode(window, null);
+ await assertUrlbarValue("@example");
+
+ assertOneOffButtonsVisible(false);
+
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+
+ // Open the popup again (by doing another search) to make sure the one-off
+ // buttons are shown -- i.e., that we didn't accidentally break them.
+ await UrlbarTestUtils.promisePopupOpen(window, () =>
+ gURLBar.search("not an engine alias")
+ );
+ await assertUrlbarValue("not an engine alias");
+ assertOneOffButtonsVisible(true);
+
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+});
+
+add_task(async function searchRestriction() {
+ gURLBar.blur();
+ await UrlbarTestUtils.promisePopupOpen(window, () =>
+ gURLBar.search(UrlbarTokenizer.RESTRICT.SEARCH)
+ );
+ ok(gURLBar.hasAttribute("focused"), "url bar is focused");
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: UrlbarSearchUtils.getDefaultEngine().name,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ // Entry is "other" because we didn't pass searchModeEntry to search().
+ entry: "other",
+ });
+ assertOneOffButtonsVisible(true);
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+add_task(async function historyRestriction() {
+ gURLBar.blur();
+ await UrlbarTestUtils.promisePopupOpen(window, () =>
+ gURLBar.search(UrlbarTokenizer.RESTRICT.HISTORY)
+ );
+ ok(gURLBar.hasAttribute("focused"), "url bar is focused");
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ entry: "other",
+ });
+ assertOneOffButtonsVisible(true);
+ Assert.ok(!gURLBar.value, "The Urlbar has no value.");
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+add_task(async function historyRestrictionWithString() {
+ gURLBar.blur();
+ // The leading and trailing spaces are intentional to verify that search()
+ // preserves them.
+ let searchString = " foo bar ";
+ await UrlbarTestUtils.promisePopupOpen(window, () =>
+ gURLBar.search(`${UrlbarTokenizer.RESTRICT.HISTORY} ${searchString}`)
+ );
+ ok(gURLBar.hasAttribute("focused"), "url bar is focused");
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ entry: "other",
+ });
+ // We don't use assertUrlbarValue here since we expect to open a local search
+ // mode. In those modes, we don't show a heuristic search result, which
+ // assertUrlbarValue checks for.
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ Assert.equal(
+ gURLBar.value,
+ searchString,
+ "The Urlbar value should be the search string."
+ );
+ assertOneOffButtonsVisible(true);
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+add_task(async function tagRestriction() {
+ gURLBar.blur();
+ await UrlbarTestUtils.promisePopupOpen(window, () =>
+ gURLBar.search(UrlbarTokenizer.RESTRICT.TAG)
+ );
+ ok(gURLBar.hasAttribute("focused"), "url bar is focused");
+ // Since tags are not a supported search mode, we should just insert the tag
+ // restriction token and not enter search mode.
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ await assertUrlbarValue(`${UrlbarTokenizer.RESTRICT.TAG} `);
+ assertOneOffButtonsVisible(true);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+// Calls search() twice with the same value. The popup should reopen.
+add_task(async function searchTwice() {
+ gURLBar.blur();
+ await UrlbarTestUtils.promisePopupOpen(window, () => gURLBar.search("test"));
+ ok(gURLBar.hasAttribute("focused"), "url bar is focused");
+ await assertUrlbarValue("test");
+ assertOneOffButtonsVisible(true);
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ await UrlbarTestUtils.promisePopupOpen(window, () => gURLBar.search("test"));
+ ok(gURLBar.hasAttribute("focused"), "url bar is focused");
+ await assertUrlbarValue("test");
+ assertOneOffButtonsVisible(true);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+// Calls search() during an IME composition.
+add_task(async function searchIME() {
+ // First run a search.
+ gURLBar.blur();
+ await UrlbarTestUtils.promisePopupOpen(window, () => gURLBar.search("test"));
+ ok(gURLBar.hasAttribute("focused"), "url bar is focused");
+ await assertUrlbarValue("test");
+ // Start composition.
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeComposition({ type: "compositionstart" })
+ );
+
+ gURLBar.search("test");
+ // Unfortunately there's no other way to check we don't open the view than to
+ // wait for it.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ ok(!UrlbarTestUtils.isPopupOpen(window), "The panel should still be closed");
+
+ await UrlbarTestUtils.promisePopupOpen(window, () =>
+ EventUtils.synthesizeComposition({ type: "compositioncommitasis" })
+ );
+
+ assertOneOffButtonsVisible(true);
+
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+// Calls search() with an engine alias.
+add_task(async function searchWithAlias() {
+ await UrlbarTestUtils.promisePopupOpen(window, async () =>
+ gURLBar.search(`${ALIAS} test`, {
+ searchEngine: aliasEngine,
+ searchModeEntry: "handoff",
+ })
+ );
+ Assert.ok(gURLBar.hasAttribute("focused"), "Urlbar is focused");
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: aliasEngine.name,
+ entry: "handoff",
+ });
+ await assertUrlbarValue("test");
+ assertOneOffButtonsVisible(true);
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+/**
+ * Asserts that the one-off search buttons are or aren't visible.
+ *
+ * @param {boolean} visible
+ * True if they should be visible, false if not.
+ */
+function assertOneOffButtonsVisible(visible) {
+ Assert.equal(
+ UrlbarTestUtils.getOneOffSearchButtonsVisible(window),
+ visible,
+ "Should show or not the one-off search buttons"
+ );
+}
+
+/**
+ * Asserts that the urlbar's input value is the given value. Also asserts that
+ * the first (heuristic) result in the popup is a search suggestion whose search
+ * query is the given value.
+ *
+ * @param {string} value
+ * The urlbar's expected value.
+ */
+async function assertUrlbarValue(value) {
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
+
+ Assert.equal(gURLBar.value, value);
+ Assert.greater(
+ UrlbarTestUtils.getResultCount(window),
+ 0,
+ "Should have at least one result"
+ );
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "Should have type search for the first result"
+ );
+ // Strip search restriction token from value.
+ if (value[0] == UrlbarTokenizer.RESTRICT.SEARCH) {
+ value = value.substring(1).trim();
+ }
+ Assert.equal(
+ result.searchParams.query,
+ value,
+ "Should have the correct query for the first result"
+ );
+}
diff --git a/browser/components/urlbar/tests/browser/browser_searchHistoryLimit.js b/browser/components/urlbar/tests/browser/browser_searchHistoryLimit.js
new file mode 100644
index 0000000000..9410a8a604
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchHistoryLimit.js
@@ -0,0 +1,92 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This test checks that search values longer than
+ * SearchSuggestionController.SEARCH_HISTORY_MAX_VALUE_LENGTH are not added to
+ * search history.
+ */
+
+"use strict";
+
+const { SearchSuggestionController } = ChromeUtils.import(
+ "resource://gre/modules/SearchSuggestionController.jsm"
+);
+
+let gEngine;
+
+add_task(async function setup() {
+ gEngine = await Services.search.addEngineWithDetails("TestLimit", {
+ template: "http://example.com/?search={searchTerms}",
+ });
+ let oldDefaultEngine = await Services.search.getDefault();
+ await Services.search.setDefault(gEngine);
+ await UrlbarTestUtils.formHistory.clear();
+
+ registerCleanupFunction(async function() {
+ await Services.search.setDefault(oldDefaultEngine);
+ await Services.search.removeEngine(gEngine);
+ await UrlbarTestUtils.formHistory.clear();
+ });
+});
+
+add_task(async function sanityCheckShortString() {
+ const shortString = new Array(
+ SearchSuggestionController.SEARCH_HISTORY_MAX_VALUE_LENGTH
+ )
+ .fill("a")
+ .join("");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: shortString,
+ });
+ let url = gEngine.getSubmission(shortString).uri.spec;
+ let loadPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ url
+ );
+ let addPromise = UrlbarTestUtils.formHistory.promiseChanged("add");
+ EventUtils.synthesizeKey("VK_RETURN");
+ await Promise.all([loadPromise, addPromise]);
+
+ let formHistory = (
+ await UrlbarTestUtils.formHistory.search({ source: gEngine.name })
+ ).map(entry => entry.value);
+ Assert.deepEqual(
+ formHistory,
+ [shortString],
+ "Should find form history after adding it"
+ );
+
+ await UrlbarTestUtils.formHistory.clear();
+});
+
+add_task(async function urlbar_checkLongString() {
+ const longString = new Array(
+ SearchSuggestionController.SEARCH_HISTORY_MAX_VALUE_LENGTH + 1
+ )
+ .fill("a")
+ .join("");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: longString,
+ });
+ let url = gEngine.getSubmission(longString).uri.spec;
+ let loadPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ url
+ );
+ EventUtils.synthesizeKey("VK_RETURN");
+ await loadPromise;
+ // There's nothing we can wait for, since addition should not be happening.
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ await new Promise(resolve => setTimeout(resolve, 500));
+ let formHistory = (
+ await UrlbarTestUtils.formHistory.search({ source: gEngine.name })
+ ).map(entry => entry.value);
+ Assert.deepEqual(formHistory, [], "Should not find form history");
+
+ await UrlbarTestUtils.formHistory.clear();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_alias_replacement.js b/browser/components/urlbar/tests/browser/browser_searchMode_alias_replacement.js
new file mode 100644
index 0000000000..0190c57add
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchMode_alias_replacement.js
@@ -0,0 +1,280 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that user-defined aliases are replaced by the search mode indicator.
+ */
+
+const ALIAS = "testalias";
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+
+// We make sure that aliases and search terms are correctly recognized when they
+// are separated by each of these different types of spaces and combinations of
+// spaces. U+3000 is the ideographic space in CJK and is commonly used by CJK
+// speakers.
+const TEST_SPACES = [" ", "\u3000", " \u3000", "\u3000 "];
+
+let defaultEngine, aliasEngine;
+
+add_task(async function setup() {
+ defaultEngine = await SearchTestUtils.promiseNewSearchEngine(
+ getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME
+ );
+ defaultEngine.alias = "@default";
+ let oldDefaultEngine = await Services.search.getDefault();
+ Services.search.setDefault(defaultEngine);
+ aliasEngine = await Services.search.addEngineWithDetails("Test", {
+ alias: ALIAS,
+ template: "http://example.com/?search={searchTerms}",
+ });
+
+ registerCleanupFunction(async function() {
+ await Services.search.removeEngine(aliasEngine);
+ Services.search.setDefault(oldDefaultEngine);
+ });
+});
+
+// An incomplete alias should not be replaced.
+add_task(async function incompleteAlias() {
+ // Check that a non-fully typed alias is not replaced.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: ALIAS.slice(0, -1),
+ });
+ await UrlbarTestUtils.assertSearchMode(window, null);
+
+ // Type a space just to make sure it's not replaced.
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey(" ");
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ Assert.equal(
+ gURLBar.value,
+ ALIAS.slice(0, -1) + " ",
+ "The typed value should be unchanged except for the space."
+ );
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+});
+
+// A complete alias without a trailing space should not be replaced.
+add_task(async function noTrailingSpace() {
+ let value = ALIAS;
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value,
+ });
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+});
+
+// A complete typed alias without a trailing space should not be replaced.
+add_task(async function noTrailingSpace_typed() {
+ // Start by searching for the alias minus its last char.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: ALIAS.slice(0, -1),
+ });
+ await UrlbarTestUtils.assertSearchMode(window, null);
+
+ // Now type the last char.
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey(ALIAS.slice(-1));
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ Assert.equal(
+ gURLBar.value,
+ ALIAS,
+ "The typed value should be the full alias."
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+});
+
+// A complete alias with a trailing space should be replaced.
+add_task(async function trailingSpace() {
+ for (let spaces of TEST_SPACES) {
+ info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) }));
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: ALIAS + spaces,
+ });
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: aliasEngine.name,
+ entry: "typed",
+ });
+ Assert.ok(!gURLBar.value, "The urlbar value should be cleared.");
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+ }
+});
+
+// A complete alias should be replaced after typing a space.
+add_task(async function trailingSpace_typed() {
+ for (let spaces of TEST_SPACES) {
+ if (spaces.length != 1) {
+ continue;
+ }
+ info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) }));
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: ALIAS,
+ });
+ await UrlbarTestUtils.assertSearchMode(window, null);
+
+ // We need to wait for two searches: The first enters search mode, the second
+ // does the search in search mode.
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey(spaces);
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: aliasEngine.name,
+ entry: "typed",
+ });
+ Assert.ok(!gURLBar.value, "The urlbar value should be cleared.");
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+ }
+});
+
+// A complete alias with a trailing space should be replaced, and the query
+// after the trailing space should be the new value of the input.
+add_task(async function trailingSpace_query() {
+ for (let spaces of TEST_SPACES) {
+ info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) }));
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: ALIAS + spaces + "query",
+ });
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: aliasEngine.name,
+ entry: "typed",
+ });
+ Assert.equal(
+ gURLBar.value,
+ "query",
+ "The urlbar value should be the query."
+ );
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+ }
+});
+
+add_task(async function() {
+ info("Test search mode when typing an alias after selecting one-off button");
+
+ info("Open the result popup");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+
+ info("Select one of one-off button");
+ const oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window);
+ await TestUtils.waitForCondition(
+ () => !oneOffs._rebuilding,
+ "Waiting for one-offs to finish rebuilding"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ ok(oneOffs.selectedButton, "There is a selected one-off button");
+ const selectedEngine = oneOffs.selectedButton.engine;
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: selectedEngine.name,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ entry: "oneoff",
+ isPreview: true,
+ });
+
+ info("Type a search engine alias and query");
+ const inputString = "@default query";
+ inputString.split("").forEach(c => EventUtils.synthesizeKey(c));
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ Assert.equal(
+ gURLBar.value,
+ inputString,
+ "Alias and query is inputed correctly to the urlbar"
+ );
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: selectedEngine.name,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ entry: "oneoff",
+ });
+
+ // When starting typing, as the search mode is confirmed, the one-off
+ // selection is removed.
+ ok(!oneOffs.selectedButton, "There is no any selected one-off button");
+
+ // Clean up
+ gURLBar.value = "";
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+add_task(async function() {
+ info(
+ "Test search mode after removing current search mode when multiple aliases are written"
+ );
+
+ info("Open the result popup with multiple aliases");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@default testalias @default",
+ });
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: defaultEngine.name,
+ entry: "typed",
+ });
+ Assert.equal(
+ gURLBar.value,
+ "testalias @default",
+ "The value on the urlbar is correct"
+ );
+
+ info("Exit search mode by clicking");
+ const indicator = gURLBar.querySelector("#urlbar-search-mode-indicator");
+ EventUtils.synthesizeMouseAtCenter(indicator, { type: "mouseover" }, window);
+ const closeButton = gURLBar.querySelector(
+ "#urlbar-search-mode-indicator-close"
+ );
+ const searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeMouseAtCenter(closeButton, {}, window);
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: aliasEngine.name,
+ entry: "typed",
+ });
+ Assert.equal(gURLBar.value, "@default", "The value on the urlbar is correct");
+
+ // Clean up
+ gURLBar.value = "";
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+/**
+ * Returns an array of code points in the given string. Each code point is
+ * returned as a hexidecimal string.
+ *
+ * @param {string} str
+ * The code points of this string will be returned.
+ * @returns {array}
+ * Array of code points in the string, where each is a hexidecimal string.
+ */
+function codePoints(str) {
+ return str.split("").map(s => s.charCodeAt(0).toString(16));
+}
diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_autofill.js b/browser/components/urlbar/tests/browser/browser_searchMode_autofill.js
new file mode 100644
index 0000000000..4ddb11bff2
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchMode_autofill.js
@@ -0,0 +1,137 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that autofill is cleared if a remote search mode is entered but still
+ * works for local search modes.
+ */
+
+"use strict";
+
+const DEFAULT_ENGINE_NAME = "Test";
+
+add_task(async function setup() {
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits([{ uri: "http://example.com/" }]);
+ }
+
+ let oldDefaultEngine = await Services.search.getDefault();
+ let defaultEngine = await Services.search.addEngineWithDetails(
+ DEFAULT_ENGINE_NAME,
+ {
+ template: "http://example.com/?search={searchTerms}",
+ }
+ );
+ await Services.search.setDefault(defaultEngine);
+ await Services.search.moveEngine(defaultEngine, 0);
+
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ await Services.search.setDefault(oldDefaultEngine);
+ await Services.search.removeEngine(defaultEngine);
+ });
+});
+
+// Tests that autofill is cleared when entering a remote search mode and that
+// autofill doesn't happen when in that mode.
+add_task(async function remote() {
+ info("Sanity check: we autofill in a normal search.");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "ex",
+ fireInputEvent: true,
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill, "We're autofilling.");
+ Assert.equal(
+ gURLBar.value,
+ "example.com/",
+ "Urlbar contains the autofilled URL."
+ );
+ info("Enter remote search mode and check autofill is cleared.");
+ await UrlbarTestUtils.enterSearchMode(window);
+ Assert.equal(gURLBar.value, "ex", "Urlbar contains the typed string.");
+
+ info("Continue typing and check that we're not autofilling.");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exa",
+ fireInputEvent: true,
+ });
+
+ details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(!details.autofill, "We're not autofilling.");
+ Assert.equal(gURLBar.value, "exa", "Urlbar contains the typed string.");
+
+ info("Exit remote search mode and check that we now autofill.");
+ await UrlbarTestUtils.exitSearchMode(window, { backspace: true });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ fireInputEvent: true,
+ });
+ details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill, "We're autofilling.");
+ Assert.equal(
+ gURLBar.value,
+ "example.com/",
+ "Urlbar contains the typed string."
+ );
+});
+
+// Tests that autofill works as normal when entering and when in a local search
+// mode.
+add_task(async function local() {
+ info("Sanity check: we autofill in a normal search.");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "ex",
+ fireInputEvent: true,
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill, "We're autofilling.");
+ Assert.equal(
+ gURLBar.value,
+ "example.com/",
+ "Urlbar contains the autofilled URL."
+ );
+ info("Enter local search mode and check autofill is preserved.");
+ await UrlbarTestUtils.enterSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ });
+ Assert.equal(
+ gURLBar.value,
+ "example.com/",
+ "Urlbar contains the autofilled URL."
+ );
+
+ info("Continue typing and check that we're autofilling.");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exa",
+ fireInputEvent: true,
+ });
+
+ details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill, "We're autofilling.");
+ Assert.equal(
+ gURLBar.value,
+ "example.com/",
+ "Urlbar contains the autofilled URL."
+ );
+
+ info("Exit local search mode and check that nothing has changed.");
+ await UrlbarTestUtils.exitSearchMode(window, { backspace: true });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ fireInputEvent: true,
+ });
+ details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill, "We're autofilling.");
+ Assert.equal(
+ gURLBar.value,
+ "example.com/",
+ "Urlbar contains the typed string."
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_clickLink.js b/browser/components/urlbar/tests/browser/browser_searchMode_clickLink.js
new file mode 100644
index 0000000000..d037c77bbb
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchMode_clickLink.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that search mode is exited after clicking a link and loading a page in
+ * the current tab.
+ */
+
+"use strict";
+
+const LINK_PAGE_URL =
+ "http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/dummy_page.html";
+
+// Opens a new tab containing a link, enters search mode, and clicks the link.
+// Uses a variety of search strings and link hrefs in order to hit different
+// branches in setURI. Search mode should be exited in all cases, and the href
+// in the link should be opened.
+add_task(async function clickLink() {
+ for (let test of [
+ // searchString, href to use in the link
+ [LINK_PAGE_URL, LINK_PAGE_URL],
+ [LINK_PAGE_URL, "http://www.example.com/"],
+ ["test", LINK_PAGE_URL],
+ ["test", "http://www.example.com/"],
+ [null, LINK_PAGE_URL],
+ [null, "http://www.example.com/"],
+ ]) {
+ await doClickLinkTest(...test);
+ }
+});
+
+async function doClickLinkTest(searchString, href) {
+ info(
+ "doClickLinkTest with args: " +
+ JSON.stringify({
+ searchString,
+ href,
+ })
+ );
+
+ await BrowserTestUtils.withNewTab(LINK_PAGE_URL, async () => {
+ if (searchString) {
+ // Do a search with the search string.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: searchString,
+ fireInputEvent: true,
+ });
+ Assert.ok(
+ gBrowser.selectedBrowser.userTypedValue,
+ "userTypedValue should be defined"
+ );
+ } else {
+ // Open top sites.
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ document.getElementById("Browser:OpenLocation").doCommand();
+ });
+ Assert.strictEqual(
+ gBrowser.selectedBrowser.userTypedValue,
+ null,
+ "userTypedValue should be null"
+ );
+ }
+
+ // Enter search mode and then close the popup so we can click the link in
+ // the page.
+ await UrlbarTestUtils.enterSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ entry: "oneoff",
+ });
+
+ // Add a link to the page and click it.
+ let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await ContentTask.spawn(gBrowser.selectedBrowser, href, async cHref => {
+ let link = this.content.document.createElement("a");
+ link.textContent = "Click me";
+ link.href = cHref;
+ this.content.document.body.append(link);
+ link.click();
+ });
+ await loadPromise;
+ Assert.equal(
+ gBrowser.currentURI.spec,
+ href,
+ "Should have loaded the href URL"
+ );
+
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ });
+}
diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_engineRemoval.js b/browser/components/urlbar/tests/browser/browser_searchMode_engineRemoval.js
new file mode 100644
index 0000000000..ef2fc15d17
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchMode_engineRemoval.js
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that we exit search mode when the search mode engine is removed.
+ */
+
+"use strict";
+
+// Tests that we exit search mode in the active tab when the search mode engine
+// is removed.
+add_task(async function activeTab() {
+ let engine = await Services.search.addEngineWithDetails("Test", {
+ template: "http://example.com/?search={searchTerms}",
+ });
+ await Services.search.moveEngine(engine, 0);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "ex",
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ // Sanity check: we are in the correct search mode.
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: engine.name,
+ entry: "oneoff",
+ });
+ await Services.search.removeEngine(engine);
+ // Check that we are no longer in search mode.
+ await UrlbarTestUtils.assertSearchMode(window, null);
+});
+
+// Tests that we exit search mode in a background tab when the search mode
+// engine is removed.
+add_task(async function backgroundTab() {
+ let engine = await Services.search.addEngineWithDetails("Test", {
+ template: "http://example.com/?search={searchTerms}",
+ });
+ await Services.search.moveEngine(engine, 0);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "ex",
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ let tab1 = gBrowser.selectedTab;
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ // Sanity check: tab1 is still in search mode.
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: engine.name,
+ entry: "oneoff",
+ });
+
+ // Switch back to tab2 so tab1 is in the background when the engine is
+ // removed.
+ await BrowserTestUtils.switchTab(gBrowser, tab2);
+ // tab2 shouldn't be in search mode.
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ await Services.search.removeEngine(engine);
+
+ // tab1 should have exited search mode.
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ BrowserTestUtils.removeTab(tab2);
+});
+
+// Tests that we exit search mode in a background window when the search mode
+// engine is removed.
+add_task(async function backgroundWindow() {
+ let engine = await Services.search.addEngineWithDetails("Test", {
+ template: "http://example.com/?search={searchTerms}",
+ });
+ await Services.search.moveEngine(engine, 0);
+
+ let win1 = window;
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win1,
+ value: "ex",
+ });
+ await UrlbarTestUtils.enterSearchMode(win1);
+ let win2 = await BrowserTestUtils.openNewBrowserWindow();
+
+ // Sanity check: win1 is still in search mode.
+ win1.focus();
+ await UrlbarTestUtils.assertSearchMode(win1, {
+ engineName: engine.name,
+ entry: "oneoff",
+ });
+
+ // Switch back to win2 so win1 is in the background when the engine is
+ // removed.
+ win2.focus();
+ // win2 shouldn't be in search mode.
+ await UrlbarTestUtils.assertSearchMode(win2, null);
+ await Services.search.removeEngine(engine);
+
+ // win1 should not be in search mode.
+ win1.focus();
+ await UrlbarTestUtils.assertSearchMode(win1, null);
+ await BrowserTestUtils.closeWindow(win2);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_excludeResults.js b/browser/components/urlbar/tests/browser/browser_searchMode_excludeResults.js
new file mode 100644
index 0000000000..b8deaea9ec
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchMode_excludeResults.js
@@ -0,0 +1,210 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that results with hostnames other than the search mode engine are not
+ * shown.
+ */
+
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ SyncedTabs: "resource://services-sync/SyncedTabs.jsm",
+});
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.searches", false],
+ ["browser.urlbar.autoFill", false],
+ // Special prefs for remote tabs.
+ ["services.sync.username", "fake"],
+ ["services.sync.syncedTabs.showRemoteTabs", true],
+ ],
+ });
+
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ let oldDefaultEngine = await Services.search.getDefault();
+ // Note that the result domain is subdomain.example.ca. We still expect to
+ // match with example.com results because we ignore subdomains and the public
+ // suffix in this check.
+ let engine = await Services.search.addEngineWithDetails("Test", {
+ template: `http://subdomain.example.ca/?search={searchTerms}`,
+ });
+ await Services.search.setDefault(engine);
+ await Services.search.moveEngine(engine, 0);
+
+ const REMOTE_TAB = {
+ id: "7cqCr77ptzX3",
+ type: "client",
+ lastModified: 1492201200,
+ name: "Nightly on MacBook-Pro",
+ clientType: "desktop",
+ tabs: [
+ {
+ type: "tab",
+ title: "Test Remote",
+ url: "http://example.com",
+ icon: UrlbarUtils.ICON.DEFAULT,
+ client: "7cqCr77ptzX3",
+ lastUsed: 1452124677,
+ },
+ {
+ type: "tab",
+ title: "Test Remote 2",
+ url: "http://example-2.com",
+ icon: UrlbarUtils.ICON.DEFAULT,
+ client: "7cqCr77ptzX3",
+ lastUsed: 1452124677,
+ },
+ ],
+ };
+
+ const sandbox = sinon.createSandbox();
+
+ let originalSyncedTabsInternal = SyncedTabs._internal;
+ SyncedTabs._internal = {
+ isConfiguredToSyncTabs: true,
+ hasSyncedThisSession: true,
+ getTabClients() {
+ return Promise.resolve([]);
+ },
+ syncTabs() {
+ return Promise.resolve();
+ },
+ };
+
+ // Tell the Sync XPCOM service it is initialized.
+ let weaveXPCService = Cc["@mozilla.org/weave/service;1"].getService(
+ Ci.nsISupports
+ ).wrappedJSObject;
+ let oldWeaveServiceReady = weaveXPCService.ready;
+ weaveXPCService.ready = true;
+
+ sandbox
+ .stub(SyncedTabs._internal, "getTabClients")
+ .callsFake(() => Promise.resolve(Cu.cloneInto([REMOTE_TAB], {})));
+
+ // Reset internal cache in PlacesRemoteTabsAutocompleteProvider.
+ Services.obs.notifyObservers(null, "weave:engine:sync:finish", "tabs");
+
+ registerCleanupFunction(async function() {
+ sandbox.restore();
+ weaveXPCService.ready = oldWeaveServiceReady;
+ SyncedTabs._internal = originalSyncedTabsInternal;
+ await Services.search.setDefault(oldDefaultEngine);
+ await Services.search.removeEngine(engine);
+ await PlacesUtils.history.clear();
+ });
+});
+
+add_task(async function basic() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "example",
+ });
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 3,
+ "We have three results"
+ );
+ let firstResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ firstResult.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "The first result is the heuristic search result."
+ );
+ let secondResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(
+ secondResult.type,
+ UrlbarUtils.RESULT_TYPE.REMOTE_TAB,
+ "The second result is a remote tab."
+ );
+ let thirdResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(
+ thirdResult.type,
+ UrlbarUtils.RESULT_TYPE.REMOTE_TAB,
+ "The third result is a remote tab."
+ );
+
+ await UrlbarTestUtils.enterSearchMode(window);
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 2,
+ "We have two results. The second remote tab result is excluded despite matching the search string."
+ );
+ firstResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ firstResult.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "The first result is the heuristic search result."
+ );
+ secondResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(
+ secondResult.type,
+ UrlbarUtils.RESULT_TYPE.REMOTE_TAB,
+ "The second result is a remote tab."
+ );
+
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+// For engines with an invalid TLD, we filter on the entire domain.
+add_task(async function malformedEngine() {
+ let badEngine = await Services.search.addEngineWithDetails("TestMalformed", {
+ template: `http://example.foobar/?search={searchTerms}`,
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "example",
+ });
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 3,
+ "We have three results"
+ );
+ let firstResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ firstResult.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "The first result is the heuristic search result."
+ );
+ let secondResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(
+ secondResult.type,
+ UrlbarUtils.RESULT_TYPE.REMOTE_TAB,
+ "The second result is a remote tab."
+ );
+ let thirdResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(
+ thirdResult.type,
+ UrlbarUtils.RESULT_TYPE.REMOTE_TAB,
+ "The third result is a remote tab."
+ );
+
+ await UrlbarTestUtils.enterSearchMode(window, {
+ engineName: badEngine.name,
+ });
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 1,
+ "We only have one result."
+ );
+ firstResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(firstResult.heuristic, "The first result is heuristic.");
+ Assert.equal(
+ firstResult.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "The first result is the heuristic search result."
+ );
+
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+ await Services.search.removeEngine(badEngine);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_heuristic.js b/browser/components/urlbar/tests/browser/browser_searchMode_heuristic.js
new file mode 100644
index 0000000000..0505448d63
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchMode_heuristic.js
@@ -0,0 +1,226 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests heuristic results in search mode.
+ */
+
+"use strict";
+
+add_task(async function setup() {
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ // Add a new mock default engine so we don't hit the network.
+ let oldDefaultEngine = await Services.search.getDefault();
+ let engine = await Services.search.addEngineWithDetails("Test", {
+ template: "http://example.com/?search={searchTerms}",
+ });
+ await Services.search.setDefault(engine);
+ registerCleanupFunction(async () => {
+ await Services.search.setDefault(oldDefaultEngine);
+ await Services.search.removeEngine(engine);
+ });
+
+ // Add one bookmark we'll use below.
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.com/bookmark",
+ });
+ registerCleanupFunction(async () => {
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+});
+
+// Enters search mode with no results.
+add_task(async function noResults() {
+ // Do a search that doesn't match our bookmark and enter bookmark search mode.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "doesn't match anything",
+ });
+ await UrlbarTestUtils.enterSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ });
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 0,
+ "Zero results since no bookmark matches"
+ );
+
+ // Press enter. Nothing should happen.
+ let loadPromise = waitForLoadOrTimeout();
+ EventUtils.synthesizeKey("KEY_Enter");
+ let loadEvent = await loadPromise;
+ Assert.ok(!loadEvent, "Nothing should have loaded");
+
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+// Enters a local search mode (bookmarks) with a matching result. No heuristic
+// should be present.
+add_task(async function localNoHeuristic() {
+ // Do a search that matches our bookmark and enter bookmarks search mode.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "bookmark",
+ });
+ await UrlbarTestUtils.enterSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ });
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 1,
+ "There should be one result"
+ );
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ result.source,
+ UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ "Result source should be BOOKMARKS"
+ );
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.URL,
+ "Result type should be URL"
+ );
+ Assert.equal(
+ result.url,
+ "http://example.com/bookmark",
+ "Result URL is our bookmark URL"
+ );
+ Assert.ok(!result.heuristic, "Result should not be heuristic");
+
+ // Press enter. Nothing should happen.
+ let loadPromise = waitForLoadOrTimeout();
+ EventUtils.synthesizeKey("KEY_Enter");
+ let loadEvent = await loadPromise;
+ Assert.ok(!loadEvent, "Nothing should have loaded");
+
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+// Enters a local search mode (bookmarks) with a matching autofill result. The
+// result should be the heuristic.
+add_task(async function localAutofill() {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ // Do a search that autofills our bookmark's origin and enter bookmarks
+ // search mode.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "example",
+ });
+ await UrlbarTestUtils.enterSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ });
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 2,
+ "There should be two results"
+ );
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ result.source,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ "Result source should be HISTORY"
+ );
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.URL,
+ "Result type should be URL"
+ );
+ Assert.equal(
+ result.url,
+ "http://example.com/",
+ "Result URL is our bookmark's origin"
+ );
+ Assert.ok(result.heuristic, "Result should be heuristic");
+ Assert.ok(result.autofill, "Result should be autofill");
+
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(
+ result.source,
+ UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ "Result source should be BOOKMARKS"
+ );
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.URL,
+ "Result type should be URL"
+ );
+ Assert.equal(
+ result.url,
+ "http://example.com/bookmark",
+ "Result URL is our bookmark URL"
+ );
+
+ // Press enter. Our bookmark's origin should be loaded.
+ let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+ Assert.equal(
+ gBrowser.currentURI.spec,
+ "http://example.com/",
+ "Bookmark's origin should have loaded"
+ );
+ });
+});
+
+// Enters a remote engine search mode. There should be a heuristic.
+add_task(async function remote() {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ // Do a search and enter search mode with our test engine.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "remote",
+ });
+ await UrlbarTestUtils.enterSearchMode(window, {
+ engineName: "Test",
+ });
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 1,
+ "There should be one result"
+ );
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ result.source,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ "Result source should be SEARCH"
+ );
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "Result type should be SEARCH"
+ );
+ Assert.ok(result.searchParams, "searchParams should be present");
+ Assert.equal(
+ result.searchParams.engine,
+ "Test",
+ "searchParams.engine should be our test engine"
+ );
+ Assert.equal(
+ result.searchParams.query,
+ "remote",
+ "searchParams.query should be our query"
+ );
+ Assert.ok(result.heuristic, "Result should be heuristic");
+
+ // Press enter. The engine's SERP should load.
+ let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+ Assert.equal(
+ gBrowser.currentURI.spec,
+ "http://example.com/?search=remote",
+ "Engine's SERP should have loaded"
+ );
+ });
+});
diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_indicator.js b/browser/components/urlbar/tests/browser/browser_searchMode_indicator.js
new file mode 100644
index 0000000000..6412021a71
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchMode_indicator.js
@@ -0,0 +1,388 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests interactions with the search mode indicator. See browser_oneOffs.js for
+ * more coverage.
+ */
+
+const TEST_QUERY = "test string";
+const DEFAULT_ENGINE_NAME = "Test";
+const SUGGESTIONS_ENGINE_NAME = "searchSuggestionEngine.xml";
+
+// These need to have different domains because otherwise new tab and/or
+// activity stream collapses them.
+const TOP_SITES_URLS = [
+ "http://top-site-0.com/",
+ "http://top-site-1.com/",
+ "http://top-site-2.com/",
+];
+
+let suggestionsEngine;
+let defaultEngine;
+
+add_task(async function setup() {
+ suggestionsEngine = await SearchTestUtils.promiseNewSearchEngine(
+ getRootDirectory(gTestPath) + SUGGESTIONS_ENGINE_NAME
+ );
+
+ let oldDefaultEngine = await Services.search.getDefault();
+ defaultEngine = await Services.search.addEngineWithDetails(
+ DEFAULT_ENGINE_NAME,
+ {
+ template: "http://example.com/?search={searchTerms}",
+ }
+ );
+ await Services.search.setDefault(defaultEngine);
+ await Services.search.moveEngine(suggestionsEngine, 0);
+
+ registerCleanupFunction(async () => {
+ await Services.search.setDefault(oldDefaultEngine);
+ await Services.search.removeEngine(defaultEngine);
+ });
+
+ // Set our top sites.
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "browser.newtabpage.activity-stream.default.sites",
+ TOP_SITES_URLS.join(","),
+ ],
+ ],
+ });
+ await updateTopSites(sites =>
+ ObjectUtils.deepEqual(
+ sites.map(s => s.url),
+ TOP_SITES_URLS
+ )
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.separatePrivateDefault.ui.enabled", false]],
+ });
+});
+
+async function verifySearchModeResultsAdded(window) {
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 3,
+ "There should be three results."
+ );
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ result.searchParams.engine,
+ suggestionsEngine.name,
+ "The first result should be a search result for our suggestion engine."
+ );
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(
+ result.searchParams.suggestion,
+ `${TEST_QUERY}foo`,
+ "The second result should be a suggestion result."
+ );
+ Assert.equal(
+ result.searchParams.engine,
+ suggestionsEngine.name,
+ "The second result should be a search result for our suggestion engine."
+ );
+}
+
+async function verifySearchModeResultsRemoved(window) {
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 1,
+ "There should only be one result."
+ );
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ result.searchParams.engine,
+ defaultEngine.name,
+ "The first result should be a search result for our default engine."
+ );
+}
+
+async function verifyTopSitesResultsAdded(window) {
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ TOP_SITES_URLS.length,
+ "Expected number of top sites results"
+ );
+ for (let i = 0; i < TOP_SITES_URLS; i++) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ Assert.equal(
+ result.url,
+ TOP_SITES_URLS[i],
+ `Expected top sites result URL at index ${i}`
+ );
+ }
+}
+
+// Tests that the indicator is removed when backspacing at the beginning of
+// the search string.
+add_task(async function backspace() {
+ // View open, with string.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_QUERY,
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ await verifySearchModeResultsAdded(window);
+ await UrlbarTestUtils.exitSearchMode(window, { backspace: true });
+ await verifySearchModeResultsRemoved(window);
+ Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Urlbar view is open.");
+
+ // View open, no string (i.e., top sites).
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ if (gURLBar.getAttribute("pageproxystate") == "invalid") {
+ gURLBar.handleRevert();
+ }
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ await UrlbarTestUtils.exitSearchMode(window, { backspace: true });
+ Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Urlbar view is open.");
+ await verifyTopSitesResultsAdded(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ // View closed, with string.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_QUERY,
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ await verifySearchModeResultsAdded(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+ await UrlbarTestUtils.exitSearchMode(window, { backspace: true });
+ await verifySearchModeResultsRemoved(window);
+ Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Urlbar view is now open.");
+
+ // View closed, no string (i.e., top sites).
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ if (gURLBar.getAttribute("pageproxystate") == "invalid") {
+ gURLBar.handleRevert();
+ }
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+ await UrlbarTestUtils.exitSearchMode(window, { backspace: true });
+ Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Urlbar view is open.");
+ await verifyTopSitesResultsAdded(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+add_task(async function escapeOnInitialPage() {
+ info("Tests the indicator's interaction with the ESC key");
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_QUERY,
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ await verifySearchModeResultsAdded(window);
+
+ EventUtils.synthesizeKey("KEY_Escape");
+ Assert.ok(!UrlbarTestUtils.isPopupOpen(window, "UrlbarView is closed."));
+ Assert.equal(gURLBar.value, TEST_QUERY, "Urlbar value hasn't changed.");
+
+ let oneOffs = UrlbarTestUtils.getOneOffSearchButtons(
+ window
+ ).getSelectableButtons(true);
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: oneOffs[0].engine.name,
+ entry: "oneoff",
+ });
+
+ EventUtils.synthesizeKey("KEY_Escape");
+ Assert.ok(!UrlbarTestUtils.isPopupOpen(window, "UrlbarView is closed."));
+ Assert.ok(!gURLBar.value, "Urlbar value is empty.");
+ await UrlbarTestUtils.assertSearchMode(window, null);
+});
+
+add_task(async function escapeOnBrowsingPage() {
+ info("Tests the indicator's interaction with the ESC key on browsing page");
+
+ await BrowserTestUtils.withNewTab("http://example.com", async browser => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_QUERY,
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ await verifySearchModeResultsAdded(window);
+
+ EventUtils.synthesizeKey("KEY_Escape");
+ Assert.ok(!UrlbarTestUtils.isPopupOpen(window, "UrlbarView is closed."));
+ Assert.equal(gURLBar.value, TEST_QUERY, "Urlbar value hasn't changed.");
+
+ const oneOffs = UrlbarTestUtils.getOneOffSearchButtons(
+ window
+ ).getSelectableButtons(true);
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: oneOffs[0].engine.name,
+ entry: "oneoff",
+ });
+
+ EventUtils.synthesizeKey("KEY_Escape");
+ Assert.ok(!UrlbarTestUtils.isPopupOpen(window, "UrlbarView is closed."));
+ Assert.equal(
+ gURLBar.value,
+ "example.com",
+ "Urlbar value indicates the browsing page."
+ );
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ });
+});
+
+// Tests that the indicator is removed when its close button is clicked.
+add_task(async function click_close() {
+ // Clicking close with the view open, with string.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_QUERY,
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ await verifySearchModeResultsAdded(window);
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+ await verifySearchModeResultsRemoved(window);
+ Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Urlbar view is open.");
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ // Clicking close with the view open, no string (i.e., top sites).
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ if (gURLBar.getAttribute("pageproxystate") == "invalid") {
+ gURLBar.handleRevert();
+ }
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+ Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Urlbar view is open.");
+ await verifyTopSitesResultsAdded(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ // Clicking close with the view closed, with string.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_QUERY,
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ await verifySearchModeResultsAdded(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+ await UrlbarTestUtils.exitSearchMode(window, {
+ clickClose: true,
+ waitForSearch: false,
+ });
+ Assert.ok(!UrlbarTestUtils.isPopupOpen(window), "Urlbar view is closed.");
+
+ // Clicking close with the view closed, no string (i.e., top sites).
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ if (gURLBar.getAttribute("pageproxystate") == "invalid") {
+ gURLBar.handleRevert();
+ }
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+ await UrlbarTestUtils.exitSearchMode(window, {
+ clickClose: true,
+ waitForSearch: false,
+ });
+ Assert.ok(!UrlbarTestUtils.isPopupOpen(window), "Urlbar view is closed.");
+});
+
+// Tests that Accel+K enters search mode with the default engine. Also tests
+// that Accel+K highlights the typed search string.
+add_task(async function keyboard_shortcut() {
+ const query = "test query";
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: query,
+ });
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ Assert.equal(
+ gURLBar.selectionStart,
+ gURLBar.selectionEnd,
+ "The search string is not highlighted."
+ );
+ EventUtils.synthesizeKey("k", { accelKey: true });
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ engineName: defaultEngine.name,
+ entry: "shortcut",
+ });
+ Assert.equal(gURLBar.value, query, "The search string was not cleared.");
+ Assert.equal(gURLBar.selectionStart, 0);
+ Assert.equal(
+ gURLBar.selectionEnd,
+ query.length,
+ "The search string is highlighted."
+ );
+ await UrlbarTestUtils.exitSearchMode(window, {
+ clickClose: true,
+ waitForSearch: false,
+ });
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+});
+
+// Tests that the Tools:Search menu item enters search mode with the default
+// engine. Also tests that Tools:Search highlights the typed search string.
+add_task(async function menubar_item() {
+ const query = "test query 2";
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: query,
+ });
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ Assert.equal(
+ gURLBar.selectionStart,
+ gURLBar.selectionEnd,
+ "The search string is not highlighted."
+ );
+ let command = window.document.getElementById("Tools:Search");
+ command.doCommand();
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ engineName: defaultEngine.name,
+ entry: "shortcut",
+ });
+ Assert.equal(gURLBar.value, query, "The search string was not cleared.");
+ Assert.equal(gURLBar.selectionStart, 0);
+ Assert.equal(
+ gURLBar.selectionEnd,
+ query.length,
+ "The search string is highlighted."
+ );
+ await UrlbarTestUtils.exitSearchMode(window, {
+ clickClose: true,
+ waitForSearch: false,
+ });
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+});
+
+// Tests that entering search mode invalidates pageproxystate and that
+// pageproxystate remains invalid after exiting search mode.
+add_task(async function invalidate_pageproxystate() {
+ await BrowserTestUtils.withNewTab("about:robots", async function(browser) {
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ Assert.equal(gURLBar.getAttribute("pageproxystate"), "valid");
+ await UrlbarTestUtils.enterSearchMode(window);
+ Assert.equal(
+ gURLBar.getAttribute("pageproxystate"),
+ "invalid",
+ "Entering search mode should clear pageproxystate."
+ );
+ Assert.equal(gURLBar.value, "", "Urlbar value should be cleared.");
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+ Assert.equal(
+ gURLBar.getAttribute("pageproxystate"),
+ "invalid",
+ "Pageproxystate should still be invalid after exiting search mode."
+ );
+ });
+});
diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_localOneOffs_actionText.js b/browser/components/urlbar/tests/browser/browser_searchMode_localOneOffs_actionText.js
new file mode 100644
index 0000000000..1c426a3a8e
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchMode_localOneOffs_actionText.js
@@ -0,0 +1,462 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests action text shown on heuristic and search suggestions when keyboard
+ * navigating local one-off buttons.
+ */
+
+"use strict";
+
+const DEFAULT_ENGINE_NAME = "Test";
+const SUGGESTIONS_ENGINE_NAME = "searchSuggestionEngine.xml";
+
+let engine;
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.searches", true]],
+ });
+ engine = await SearchTestUtils.promiseNewSearchEngine(
+ getRootDirectory(gTestPath) + SUGGESTIONS_ENGINE_NAME
+ );
+ let oldDefaultEngine = await Services.search.getDefault();
+ await Services.search.setDefault(engine);
+ await Services.search.moveEngine(engine, 0);
+
+ await PlacesUtils.history.clear();
+
+ registerCleanupFunction(async () => {
+ await Services.search.setDefault(oldDefaultEngine);
+ await PlacesUtils.history.clear();
+ });
+});
+
+add_task(async function localOneOff() {
+ info("Type some text, select a local one-off, check heuristic action");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "query",
+ });
+ Assert.ok(UrlbarTestUtils.getResultCount(window) > 1, "Sanity check results");
+
+ info("Alt UP to select the last local one-off.");
+ EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true });
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 0,
+ "the heuristic result should be selected"
+ );
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ let oneOffButtons = UrlbarTestUtils.getOneOffSearchButtons(window);
+ Assert.equal(
+ oneOffButtons.selectedButton.source,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ "A local one-off button should be selected"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_visible(result.element.action),
+ "The heuristic action should be visible"
+ );
+ let [actionHistory, actionBookmarks] = await document.l10n.formatValues([
+ { id: "urlbar-result-action-search-history" },
+ { id: "urlbar-result-action-search-bookmarks" },
+ ]);
+ Assert.equal(
+ result.displayed.action,
+ actionHistory,
+ "Check the heuristic action"
+ );
+ Assert.equal(
+ result.image,
+ "chrome://browser/skin/history.svg",
+ "Check the heuristic icon"
+ );
+
+ info("Move to an engine one-off and check heuristic action");
+ EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true });
+ EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true });
+ EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true });
+ Assert.ok(
+ oneOffButtons.selectedButton.engine,
+ "A one-off search button should be selected"
+ );
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(
+ BrowserTestUtils.is_visible(result.element.action),
+ "The heuristic action should be visible"
+ );
+ Assert.ok(
+ result.displayed.action.includes(oneOffButtons.selectedButton.engine.name),
+ "Check the heuristic action"
+ );
+ Assert.equal(
+ result.image,
+ oneOffButtons.selectedButton.engine.iconURI.spec,
+ "Check the heuristic icon"
+ );
+
+ info("Move again to a local one-off, deselect and reselect the heuristic");
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ oneOffButtons.selectedButton.source,
+ UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ "A local one-off button should be selected"
+ );
+ Assert.equal(
+ result.displayed.action,
+ actionBookmarks,
+ "Check the heuristic action"
+ );
+ Assert.equal(
+ result.image,
+ "chrome://browser/skin/bookmark.svg",
+ "Check the heuristic icon"
+ );
+
+ info(
+ "Select the next result, then reselect the heuristic, check it's a search with the default engine"
+ );
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 0,
+ "the heuristic result should be selected"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown", {});
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 1,
+ "the heuristic result should not be selected"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowUp", {});
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 0,
+ "the heuristic result should be selected"
+ );
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.SEARCH);
+ Assert.equal(result.searchParams.engine, engine.name);
+ Assert.ok(
+ result.displayed.action.includes(engine.name),
+ "Check the heuristic action"
+ );
+ Assert.equal(
+ result.image,
+ "chrome://browser/skin/search-glass.svg",
+ "Check the heuristic icon"
+ );
+});
+
+add_task(async function localOneOff_withVisit() {
+ info("Type a url, select a local one-off, check heuristic action");
+ await PlacesUtils.history.clear();
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits("https://mozilla.org/");
+ await PlacesTestUtils.addVisits("https://other.mozilla.org/");
+ }
+ const searchString = "mozilla.org";
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: searchString,
+ });
+ Assert.ok(UrlbarTestUtils.getResultCount(window) > 1, "Sanity check results");
+ let oneOffButtons = UrlbarTestUtils.getOneOffSearchButtons(window);
+
+ let [
+ actionHistory,
+ actionTabs,
+ actionBookmarks,
+ actionVisit,
+ ] = await document.l10n.formatValues([
+ { id: "urlbar-result-action-search-history" },
+ { id: "urlbar-result-action-search-tabs" },
+ { id: "urlbar-result-action-search-bookmarks" },
+ { id: "urlbar-result-action-visit" },
+ ]);
+
+ info("Alt UP to select the history one-off.");
+ EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true });
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 0,
+ "the heuristic result should be selected"
+ );
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ oneOffButtons.selectedButton.source,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ "The history one-off button should be selected"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_visible(result.element.action),
+ "The heuristic action should be visible"
+ );
+ Assert.equal(
+ result.displayed.action,
+ actionHistory,
+ "Check the heuristic action"
+ );
+ Assert.equal(
+ result.image,
+ "chrome://browser/skin/history.svg",
+ "Check the heuristic icon"
+ );
+ let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
+ Assert.equal(
+ row.querySelector(".urlbarView-title").textContent,
+ searchString,
+ "Check that the result title has been replaced with the search string."
+ );
+
+ info("Alt UP to select the tabs one-off.");
+ EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true });
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ oneOffButtons.selectedButton.source,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ "The tabs one-off button should be selected"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_visible(result.element.action),
+ "The heuristic action should be visible"
+ );
+ Assert.equal(
+ result.displayed.action,
+ actionTabs,
+ "Check the heuristic action"
+ );
+ Assert.equal(
+ result.image,
+ "chrome://browser/skin/tab.svg",
+ "Check the heuristic icon"
+ );
+
+ info("Alt UP to select the bookmarks one-off.");
+ EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true });
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ oneOffButtons.selectedButton.source,
+ UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ "The bookmarks one-off button should be selected"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_visible(result.element.action),
+ "The heuristic action should be visible"
+ );
+ Assert.equal(
+ result.displayed.action,
+ actionBookmarks,
+ "Check the heuristic action"
+ );
+ Assert.equal(
+ result.image,
+ "chrome://browser/skin/bookmark.svg",
+ "Check the heuristic icon"
+ );
+
+ info(
+ "Select the next result, then reselect the heuristic, check it's a visit"
+ );
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 0,
+ "the heuristic result should be selected"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown", {});
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 1,
+ "the heuristic result should not be selected"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowUp", {});
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 0,
+ "the heuristic result should be selected"
+ );
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ oneOffButtons.selectedButton,
+ null,
+ "No one-off button should be selected"
+ );
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.URL);
+ Assert.equal(
+ result.displayed.action,
+ actionVisit,
+ "Check the heuristic action"
+ );
+ Assert.notEqual(
+ result.image,
+ "chrome://browser/skin/history.svg",
+ "Check the heuristic icon"
+ );
+ row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
+ Assert.equal(
+ row.querySelector(".urlbarView-title").textContent,
+ `https://${searchString}`,
+ "Check that the result title has been restored to the fixed-up URI."
+ );
+
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function localOneOff_suggestion() {
+ info("Type some text, select the first suggestion, then a local one-off");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "query",
+ });
+ let count = UrlbarTestUtils.getResultCount(window);
+ Assert.ok(count > 1, "Sanity check results");
+ let result = null;
+ let suggestionIndex = -1;
+ for (let i = 1; i < count; ++i) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ let index = await UrlbarTestUtils.getSelectedRowIndex(window);
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, index);
+ if (
+ result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
+ result.searchParams.suggestion
+ ) {
+ suggestionIndex = i;
+ break;
+ }
+ }
+ Assert.ok(
+ result.searchParams.suggestion,
+ "Should have selected a search suggestion"
+ );
+ Assert.ok(
+ result.displayed.action.includes(result.searchParams.engine),
+ "Check the search suggestion action"
+ );
+
+ info("Alt UP to select the last local one-off.");
+ EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true });
+ Assert.equal(
+ await UrlbarTestUtils.getSelectedRowIndex(window),
+ suggestionIndex,
+ "the suggestion should still be selected"
+ );
+
+ let [actionHistory] = await document.l10n.formatValues([
+ { id: "urlbar-result-action-search-history" },
+ ]);
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, suggestionIndex);
+ Assert.equal(
+ result.displayed.action,
+ actionHistory,
+ "Check the search suggestion action changed to local one-off"
+ );
+ // Like in the normal engine one-offs case, we don't replace the favicon.
+ Assert.equal(
+ result.image,
+ "chrome://browser/skin/search-glass.svg",
+ "Check the suggestion icon"
+ );
+
+ info("DOWN to select the next suggestion");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ result = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ suggestionIndex + 1
+ );
+ Assert.ok(
+ result.searchParams.suggestion,
+ "Should have selected a search suggestion"
+ );
+ Assert.ok(
+ result.displayed.action.includes(result.searchParams.engine),
+ "Check the search suggestion action"
+ );
+ Assert.equal(
+ result.image,
+ "chrome://browser/skin/search-glass.svg",
+ "Check the suggestion icon"
+ );
+
+ info("UP back to the previous suggestion");
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, suggestionIndex);
+ Assert.ok(
+ result.displayed.action.includes(result.searchParams.engine),
+ "Check the search suggestion action"
+ );
+ Assert.equal(
+ result.image,
+ "chrome://browser/skin/search-glass.svg",
+ "Check the suggestion icon"
+ );
+});
+
+add_task(async function localOneOff_shortcut() {
+ info("Select a search shortcut, then a local one-off");
+
+ await PlacesUtils.history.clear();
+ // Enough vists to get this site into Top Sites.
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits("http://example.com/");
+ }
+
+ await updateTopSites(
+ sites => sites && sites[0] && sites[0].searchTopSite,
+ true
+ );
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ let count = UrlbarTestUtils.getResultCount(window);
+ Assert.ok(count > 1, "Sanity check results");
+ let result = null;
+ let shortcutIndex = -1;
+ for (let i = 0; i < count; ++i) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ let index = await UrlbarTestUtils.getSelectedRowIndex(window);
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, index);
+ if (
+ result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
+ result.searchParams.keyword
+ ) {
+ shortcutIndex = i;
+ break;
+ }
+ }
+ Assert.ok(result.searchParams.keyword, "Should have selected a shortcut");
+ let shortcutEngine = result.searchParams.engine;
+
+ info("Alt UP to select the last local one-off.");
+ EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true });
+ Assert.equal(
+ await UrlbarTestUtils.getSelectedRowIndex(window),
+ shortcutIndex,
+ "the shortcut should still be selected"
+ );
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, shortcutIndex);
+ Assert.equal(
+ result.displayed.action,
+ "",
+ "Check the shortcut action is empty"
+ );
+ Assert.equal(
+ result.searchParams.engine,
+ shortcutEngine,
+ "Check the shortcut engine"
+ );
+ Assert.ok(
+ result.displayed.title.includes(shortcutEngine),
+ "Check the shortcut title"
+ );
+ Assert.notEqual(
+ result.image,
+ "chrome://browser/skin/search-glass.svg",
+ "Check the icon was not replaced"
+ );
+
+ await UrlbarTestUtils.exitSearchMode(window);
+ await PlacesUtils.history.clear();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_no_results.js b/browser/components/urlbar/tests/browser/browser_searchMode_no_results.js
new file mode 100644
index 0000000000..96da1d9721
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchMode_no_results.js
@@ -0,0 +1,290 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests entering search mode and there are no results in the view.
+ */
+
+"use strict";
+
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+
+add_task(async function setup() {
+ // In order to open the view without any results, we need to be in search mode
+ // with an empty search string so that no heuristic result is shown, and the
+ // empty search must yield zero additional results. We'll enter search mode
+ // using the bookmarks one-off, and first we'll delete all bookmarks so that
+ // there are no results.
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ // Also clear history so that using the alias of our test engine doesn't
+ // inadvertently return any history results due to bug 1658646.
+ await PlacesUtils.history.clear();
+
+ // Add a top site so we're guaranteed the view has at least one result to
+ // show initially with an empty search. Otherwise the view won't even open.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "browser.newtabpage.activity-stream.default.sites",
+ "http://example.com/",
+ ],
+ ],
+ });
+ await updateTopSites(sites => sites.length);
+});
+
+// Basic test for entering search mode with no results.
+add_task(async function basic() {
+ await withNewWindow(async win => {
+ // Do an empty search.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "",
+ });
+
+ // Initially there should be at least the top site we added above.
+ Assert.greater(
+ UrlbarTestUtils.getResultCount(win),
+ 0,
+ "Top sites should be present initially"
+ );
+ Assert.ok(
+ !win.gURLBar.panel.hasAttribute("noresults"),
+ "Panel has results, therefore should not have noresults attribute"
+ );
+
+ // Enter search mode by clicking the bookmarks one-off.
+ await UrlbarTestUtils.enterSearchMode(win, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ });
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(win),
+ 0,
+ "Zero results since no bookmarks exist"
+ );
+ Assert.equal(
+ win.gURLBar.panel.getAttribute("noresults"),
+ "true",
+ "Panel has no results, therefore should have noresults attribute"
+ );
+
+ // Exit search mode by backspacing. The top sites should be shown again.
+ await UrlbarTestUtils.exitSearchMode(win, { backspace: true });
+ Assert.greater(
+ UrlbarTestUtils.getResultCount(win),
+ 0,
+ "Top sites should be present again"
+ );
+ Assert.ok(
+ !win.gURLBar.panel.hasAttribute("noresults"),
+ "noresults attribute should be absent again"
+ );
+
+ await UrlbarTestUtils.promisePopupClose(win);
+ });
+});
+
+// When the urlbar is in search mode, has no results, and is not focused,
+// focusing it should auto-open the view.
+add_task(async function autoOpen() {
+ await withNewWindow(async win => {
+ // Do an empty search.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "",
+ });
+
+ // Initially there should be at least the top site we added above.
+ Assert.greater(
+ UrlbarTestUtils.getResultCount(win),
+ 0,
+ "Top sites should be present initially"
+ );
+ Assert.ok(
+ !win.gURLBar.panel.hasAttribute("noresults"),
+ "Panel has results, therefore should not have noresults attribute"
+ );
+
+ // Enter search mode by clicking the bookmarks one-off.
+ await UrlbarTestUtils.enterSearchMode(win, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ });
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(win),
+ 0,
+ "Zero results since no bookmarks exist"
+ );
+ Assert.equal(
+ win.gURLBar.panel.getAttribute("noresults"),
+ "true",
+ "Panel has no results, therefore should have noresults attribute"
+ );
+
+ // Blur the urlbar.
+ win.gURLBar.blur();
+ await UrlbarTestUtils.assertSearchMode(win, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ entry: "oneoff",
+ });
+
+ // Click the urlbar.
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ EventUtils.synthesizeMouseAtCenter(win.gURLBar.inputField, {}, win);
+ });
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(win),
+ 0,
+ "Still zero results since no bookmarks exist"
+ );
+ Assert.equal(
+ win.gURLBar.panel.getAttribute("noresults"),
+ "true",
+ "Panel still has no results, therefore should have noresults attribute"
+ );
+
+ // Exit search mode by backspacing. The top sites should be shown again.
+ await UrlbarTestUtils.exitSearchMode(win, { backspace: true });
+ Assert.greater(
+ UrlbarTestUtils.getResultCount(win),
+ 0,
+ "Top sites should be present again"
+ );
+ Assert.ok(
+ !win.gURLBar.panel.hasAttribute("noresults"),
+ "noresults attribute should be absent again"
+ );
+
+ await UrlbarTestUtils.promisePopupClose(win);
+ });
+});
+
+// When the urlbar is in search mode, the user backspaces over the final char
+// (but remains in search mode), and there are no results, the view should
+// remain open.
+add_task(async function backspaceRemainOpen() {
+ await withNewWindow(async win => {
+ // Do a one-char search.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "x",
+ });
+ Assert.greater(
+ UrlbarTestUtils.getResultCount(win),
+ 0,
+ "At least the heuristic result should be shown"
+ );
+ Assert.ok(
+ !win.gURLBar.panel.hasAttribute("noresults"),
+ "Panel has results, therefore should not have noresults attribute"
+ );
+
+ // Enter search mode by clicking the bookmarks one-off.
+ await UrlbarTestUtils.enterSearchMode(win, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ });
+
+ // The heursitic should not be shown since we don't show it in local search
+ // modes.
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(win),
+ 0,
+ "No results should be present"
+ );
+ Assert.ok(
+ win.gURLBar.panel.hasAttribute("noresults"),
+ "Panel has no results, therefore should have noresults attribute"
+ );
+
+ // Backspace. The search string will now be empty.
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(win);
+ EventUtils.synthesizeKey("KEY_Backspace", {}, win);
+ await searchPromise;
+ Assert.ok(UrlbarTestUtils.isPopupOpen(win), "View remains open");
+ await UrlbarTestUtils.assertSearchMode(win, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ entry: "oneoff",
+ });
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(win),
+ 0,
+ "Zero results since no bookmarks exist"
+ );
+ Assert.equal(
+ win.gURLBar.panel.getAttribute("noresults"),
+ "true",
+ "Panel has no results, therefore should have noresults attribute"
+ );
+
+ // Exit search mode by backspacing. The top sites should be shown.
+ await UrlbarTestUtils.exitSearchMode(win, { backspace: true });
+ Assert.greater(
+ UrlbarTestUtils.getResultCount(win),
+ 0,
+ "Top sites should be present again"
+ );
+ Assert.ok(
+ !win.gURLBar.panel.hasAttribute("noresults"),
+ "noresults attribute should be absent again"
+ );
+
+ await UrlbarTestUtils.promisePopupClose(win);
+ });
+});
+
+// Types a search alias and then a space to enter search mode, with no results.
+// The one-offs should be shown.
+add_task(async function spaceToEnterSearchMode() {
+ let engine = await SearchTestUtils.promiseNewSearchEngine(
+ getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME
+ );
+ engine.alias = "@test";
+
+ await withNewWindow(async win => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: engine.alias,
+ });
+
+ // We need to wait for two searches: The first enters search mode, the
+ // second does the search in search mode.
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(win);
+ EventUtils.synthesizeKey(" ", {}, win);
+ await searchPromise;
+
+ Assert.equal(UrlbarTestUtils.getResultCount(win), 0, "Zero results");
+ Assert.equal(
+ win.gURLBar.panel.getAttribute("noresults"),
+ "true",
+ "Panel has no results, therefore should have noresults attribute"
+ );
+ await UrlbarTestUtils.assertSearchMode(win, {
+ engineName: engine.name,
+ entry: "typed",
+ });
+ this.Assert.equal(
+ UrlbarTestUtils.getOneOffSearchButtonsVisible(window),
+ true,
+ "One-offs are visible"
+ );
+
+ await UrlbarTestUtils.exitSearchMode(win, { backspace: true });
+ await UrlbarTestUtils.promisePopupClose(win);
+ });
+});
+
+/**
+ * Opens a new window, waits for it to load, calls a callback, and closes the
+ * window. We use a new window in each task so that the view starts with a
+ * blank slate each time.
+ *
+ * @param {function} callback
+ * Will be called as: callback(newWindow)
+ */
+async function withNewWindow(callback) {
+ // Start in a new window so we have a blank slate.
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await callback(win);
+ await BrowserTestUtils.closeWindow(win);
+}
diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_oneOffButton.js b/browser/components/urlbar/tests/browser/browser_searchMode_oneOffButton.js
new file mode 100644
index 0000000000..8ebf4152a0
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchMode_oneOffButton.js
@@ -0,0 +1,118 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests one-off search button behavior with search mode.
+ */
+
+const TEST_ENGINE = {
+ name: "test engine",
+ details: {
+ alias: "@test",
+ template: "http://example.com/?search={searchTerms}",
+ },
+};
+
+add_task(async function setup() {
+ const engine = await Services.search.addEngineWithDetails(
+ TEST_ENGINE.name,
+ TEST_ENGINE.details
+ );
+
+ registerCleanupFunction(async () => {
+ await Services.search.removeEngine(engine);
+ });
+});
+
+add_task(async function test() {
+ info("Test no one-off buttons are selected when entering search mode");
+
+ info("Open the result popup");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+
+ info("Select one of one-off button");
+ const oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window);
+ await TestUtils.waitForCondition(
+ () => !oneOffs._rebuilding,
+ "Waiting for one-offs to finish rebuilding"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ ok(oneOffs.selectedButton, "There is a selected one-off button");
+
+ info("Enter search mode");
+ await UrlbarTestUtils.enterSearchMode(window, {
+ engineName: TEST_ENGINE.name,
+ });
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ENGINE.name,
+ entry: "oneoff",
+ });
+ ok(!oneOffs.selectedButton, "There is no selected one-off button");
+});
+
+add_task(async function() {
+ info(
+ "Test the status of the selected one-off button when exiting search mode with backspace"
+ );
+
+ info("Open the result popup");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+
+ info("Select one of one-off button");
+ const oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window);
+ await TestUtils.waitForCondition(
+ () => !oneOffs._rebuilding,
+ "Waiting for one-offs to finish rebuilding"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ ok(oneOffs.selectedButton, "There is a selected one-off button");
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: oneOffs.selectedButton.engine.name,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ entry: "oneoff",
+ isPreview: true,
+ });
+
+ info("Exit from search mode");
+ await UrlbarTestUtils.exitSearchMode(window);
+ ok(!oneOffs.selectedButton, "There is no any selected one-off button");
+});
+
+add_task(async function() {
+ info(
+ "Test the status of the selected one-off button when exiting search mode with clicking close button"
+ );
+
+ info("Open the result popup");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+
+ info("Select one of one-off button");
+ const oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window);
+ await TestUtils.waitForCondition(
+ () => !oneOffs._rebuilding,
+ "Waiting for one-offs to finish rebuilding"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ ok(oneOffs.selectedButton, "There is a selected one-off button");
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: oneOffs.selectedButton.engine.name,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ entry: "oneoff",
+ isPreview: true,
+ });
+
+ info("Exit from search mode");
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+ ok(!oneOffs.selectedButton, "There is no any selected one-off button");
+});
diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_pickResult.js b/browser/components/urlbar/tests/browser/browser_searchMode_pickResult.js
new file mode 100644
index 0000000000..98416ee106
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchMode_pickResult.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that search mode is exited after picking a result.
+ */
+
+"use strict";
+
+const BOOKMARK_URL = "http://www.example.com/browser_searchMode_pickResult.js";
+
+add_task(async function setup() {
+ // Add a bookmark so we can enter bookmarks search mode and pick it.
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: BOOKMARK_URL,
+ });
+ registerCleanupFunction(async () => {
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+});
+
+// Opens a new tab, enters search mode, does a search for our test bookmark, and
+// picks it. Uses a variety of initial URLs and search strings in order to hit
+// different branches in setURI. Search mode should be exited in all cases.
+add_task(async function pickResult() {
+ for (let test of [
+ // initialURL, searchString
+ ["about:blank", BOOKMARK_URL],
+ ["about:blank", new URL(BOOKMARK_URL).origin],
+ ["about:blank", new URL(BOOKMARK_URL).pathname],
+ [BOOKMARK_URL, BOOKMARK_URL],
+ [BOOKMARK_URL, new URL(BOOKMARK_URL).origin],
+ [BOOKMARK_URL, new URL(BOOKMARK_URL).pathname],
+ ]) {
+ await doPickResultTest(...test);
+ }
+});
+
+async function doPickResultTest(initialURL, searchString) {
+ info(
+ "doPickResultTest with args: " +
+ JSON.stringify({
+ initialURL,
+ searchString,
+ })
+ );
+
+ await BrowserTestUtils.withNewTab(initialURL, async () => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: searchString,
+ fireInputEvent: true,
+ });
+
+ await UrlbarTestUtils.enterSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ });
+
+ // Arrow down to the bookmark result.
+ let firstResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ if (!firstResult.heuristic) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+ let foundResult = false;
+ for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ if (result.url == BOOKMARK_URL) {
+ foundResult = true;
+ break;
+ }
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+ Assert.ok(foundResult, "The bookmark result should have been found");
+
+ // Press enter.
+ let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+ Assert.equal(
+ gBrowser.currentURI.spec,
+ BOOKMARK_URL,
+ "Should have loaded the bookmarked URL"
+ );
+
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ });
+}
diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_preview.js b/browser/components/urlbar/tests/browser/browser_searchMode_preview.js
new file mode 100644
index 0000000000..d7531f6e7b
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchMode_preview.js
@@ -0,0 +1,494 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests search mode preview.
+ */
+
+"use strict";
+
+const TEST_ENGINE_NAME = "Test";
+const TEST_ENGINE_DOMAIN = "example.com";
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // TODO (Bug 1675558) - This should not be a requirement for the whole test.
+ ["browser.urlbar.update2.emptySearchBehavior", 2],
+ ],
+ });
+ let testEngine = await Services.search.addEngineWithDetails(
+ TEST_ENGINE_NAME,
+ {
+ alias: "@test",
+ template: `http://${TEST_ENGINE_DOMAIN}/?search={searchTerms}`,
+ }
+ );
+
+ registerCleanupFunction(async () => {
+ await Services.search.removeEngine(testEngine);
+ await PlacesUtils.history.clear();
+ });
+});
+
+/**
+ * @param {Node} button
+ * A one-off button.
+ * @param {boolean} [isPreview]
+ * Whether the expected search mode should be a preview. Defaults to true.
+ * @returns {object}
+ * The search mode object expected when that one-off is selected.
+ */
+function getExpectedSearchMode(button, isPreview = true) {
+ let expectedSearchMode = {
+ entry: "oneoff",
+ isPreview,
+ };
+ if (button.engine) {
+ expectedSearchMode.engineName = button.engine.name;
+ if (UrlbarUtils.WEB_ENGINE_NAMES.has(button.engine.name)) {
+ expectedSearchMode.source = UrlbarUtils.RESULT_SOURCE.SEARCH;
+ }
+ } else {
+ expectedSearchMode.source = button.source;
+ }
+
+ return expectedSearchMode;
+}
+
+// Tests that cycling through token alias engines enters search mode preview.
+add_task(async function tokenAlias() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@",
+ });
+
+ let result;
+ while (gURLBar.searchMode?.engineName != TEST_ENGINE_NAME) {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
+ let index = UrlbarTestUtils.getSelectedRowIndex(window);
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, index);
+ let expectedSearchMode = {
+ engineName: result.searchParams.engine,
+ isPreview: true,
+ entry: "keywordoffer",
+ };
+ if (UrlbarUtils.WEB_ENGINE_NAMES.has(result.searchParams.engine)) {
+ expectedSearchMode.source = UrlbarUtils.RESULT_SOURCE.SEARCH;
+ }
+ await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode);
+ }
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await searchPromise;
+ // Test that we are in confirmed search mode.
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: result.searchParams.engine,
+ entry: "keywordoffer",
+ });
+ await UrlbarTestUtils.exitSearchMode(window);
+});
+
+// Tests that starting to type a query exits search mode preview in favour of
+// full search mode.
+add_task(async function startTyping() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@",
+ });
+ while (gURLBar.searchMode?.engineName != TEST_ENGINE_NAME) {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
+ }
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ENGINE_NAME,
+ isPreview: true,
+ entry: "keywordoffer",
+ });
+
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("M");
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ENGINE_NAME,
+ entry: "keywordoffer",
+ });
+ await UrlbarTestUtils.exitSearchMode(window);
+});
+
+// Tests that highlighting a search shortcut Top Site enters search mode
+// preview.
+add_task(async function topSites() {
+ // Enable search shortcut Top Sites.
+ await PlacesUtils.history.clear();
+ await updateTopSites(
+ sites => sites && sites[0] && sites[0].searchTopSite,
+ true
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ fireInputEvent: true,
+ });
+
+ // We previously verified that the first Top Site is a search shortcut.
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
+ let searchTopSite = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: searchTopSite.searchParams.engine,
+ isPreview: true,
+ entry: "topsites_urlbar",
+ });
+ await UrlbarTestUtils.exitSearchMode(window);
+});
+
+// Tests that search mode preview is exited when the view is closed.
+add_task(async function closeView() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@",
+ });
+
+ while (gURLBar.searchMode?.engineName != TEST_ENGINE_NAME) {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
+ }
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ENGINE_NAME,
+ isPreview: true,
+ entry: "keywordoffer",
+ });
+
+ // We should close search mode when closing the view.
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+ await UrlbarTestUtils.assertSearchMode(window, null);
+
+ // Check search mode isn't re-entered when re-opening the view.
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ if (gURLBar.getAttribute("pageproxystate") == "invalid") {
+ gURLBar.handleRevert();
+ }
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+// Tests that search more preview is exited when the user switches tabs.
+add_task(async function tabSwitch() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@",
+ });
+
+ while (gURLBar.searchMode?.engineName != TEST_ENGINE_NAME) {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
+ }
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ENGINE_NAME,
+ isPreview: true,
+ entry: "keywordoffer",
+ });
+
+ // Open a new tab then switch back to the original tab.
+ let tab1 = gBrowser.selectedTab;
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ BrowserTestUtils.removeTab(tab2);
+});
+
+// Tests that search mode is previewed when the user down arrows through the
+// one-offs.
+add_task(async function oneOff_downArrow() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ let oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window);
+ await TestUtils.waitForCondition(
+ () => !oneOffs._rebuilding,
+ "Waiting for one-offs to finish rebuilding"
+ );
+ let resultCount = UrlbarTestUtils.getResultCount(window);
+
+ // Key down through all results.
+ for (let i = 0; i < resultCount; i++) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+
+ // Key down again. The first one-off should be selected.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+
+ // Check for the one-off's search mode previews.
+ while (oneOffs.selectedButton != oneOffs.settingsButtonCompact) {
+ await UrlbarTestUtils.assertSearchMode(
+ window,
+ getExpectedSearchMode(oneOffs.selectedButton)
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+
+ // Check that selecting the search settings button leaves search mode preview.
+ Assert.equal(
+ oneOffs.selectedButton,
+ oneOffs.settingsButtonCompact,
+ "The settings button is selected."
+ );
+ await UrlbarTestUtils.assertSearchMode(window, null);
+
+ // Closing the view should also exit search mode preview.
+ await UrlbarTestUtils.promisePopupClose(window);
+ await UrlbarTestUtils.assertSearchMode(window, null);
+});
+
+// Tests that search mode is previewed when the user Alt+down arrows through the
+// one-offs. This subtest also highlights a keywordoffer result (the first Top
+// Site) before Alt+Arrowing to the one-offs. This checks that the search mode
+// previews from keywordoffer results are overwritten by selected one-offs.
+add_task(async function oneOff_alt_downArrow() {
+ // Add some visits to a URL so we have multiple Top Sites.
+ await PlacesUtils.history.clear();
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits("http://example.com/");
+ }
+ await updateTopSites(
+ sites =>
+ sites &&
+ sites[0]?.searchTopSite &&
+ sites[1]?.url == "http://example.com/",
+ true
+ );
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ let oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window);
+ await TestUtils.waitForCondition(
+ () => !oneOffs._rebuilding,
+ "Waiting for one-offs to finish rebuilding"
+ );
+
+ // Key down to the first result and check that it enters search mode preview.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ let searchTopSite = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: searchTopSite.searchParams.engine,
+ isPreview: true,
+ entry: "topsites_urlbar",
+ });
+
+ // Alt+down. The first one-off should be selected.
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ // Check for the one-offs' search mode previews.
+ while (oneOffs.selectedButton) {
+ await UrlbarTestUtils.assertSearchMode(
+ window,
+ getExpectedSearchMode(oneOffs.selectedButton)
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ }
+
+ // Now key down without a modifier. We should move to the second result and
+ // have no search mode preview.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 1,
+ "The second result is selected."
+ );
+ await UrlbarTestUtils.assertSearchMode(window, null);
+
+ // Arrow back up to the keywordoffer result and check for search mode preview.
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: searchTopSite.searchParams.engine,
+ isPreview: true,
+ entry: "topsites_urlbar",
+ });
+
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.promisePopupClose(window);
+ await UrlbarTestUtils.assertSearchMode(window, null);
+});
+
+// Tests that search mode is previewed when the user is in full search mode
+// and down arrows through the one-offs.
+add_task(async function fullSearchMode_oneOff_downArrow() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ let oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window);
+ await TestUtils.waitForCondition(
+ () => !oneOffs._rebuilding,
+ "Waiting for one-offs to finish rebuilding"
+ );
+ let oneOffButtons = oneOffs.getSelectableButtons(true);
+
+ await UrlbarTestUtils.enterSearchMode(window);
+ let expectedSearchMode = getExpectedSearchMode(oneOffButtons[0], false);
+ // Sanity check: we are in the correct search mode.
+ await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode);
+
+ // Key down through all results.
+ let resultCount = UrlbarTestUtils.getResultCount(window);
+ for (let i = 0; i < resultCount; i++) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ // If the result is a shortcut, it will enter preview mode.
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ await UrlbarTestUtils.assertSearchMode(
+ window,
+ Object.assign(expectedSearchMode, {
+ isPreview: !!result.searchParams.keyword,
+ })
+ );
+ }
+
+ // Key down again. The first one-off should be selected.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ // Check that we show the correct preview as we cycle through the one-offs.
+ while (oneOffs.selectedButton != oneOffs.settingsButtonCompact) {
+ await UrlbarTestUtils.assertSearchMode(
+ window,
+ getExpectedSearchMode(oneOffs.selectedButton, true)
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+
+ // We should still be in the same search mode after cycling through all the
+ // one-offs.
+ await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode);
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+// Tests that search mode is previewed when the user is in full search mode
+// and alt+down arrows through the one-offs. This subtest also checks that
+// exiting full search mode while alt+arrowing through the one-offs enters
+// search mode preview for subsequent one-offs.
+add_task(async function fullSearchMode_oneOff_alt_downArrow() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ let oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window);
+ let oneOffButtons = oneOffs.getSelectableButtons(true);
+ await TestUtils.waitForCondition(
+ () => !oneOffs._rebuilding,
+ "Waiting for one-offs to finish rebuilding"
+ );
+
+ await UrlbarTestUtils.enterSearchMode(window);
+ let expectedSearchMode = getExpectedSearchMode(oneOffButtons[0], false);
+ await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode);
+
+ // Key down to the first result.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+
+ // Alt+down. The first one-off should be selected.
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ // Cycle through the first half of the one-offs and verify that search mode
+ // preview is entered.
+ Assert.greater(
+ oneOffButtons.length,
+ 1,
+ "Sanity check: We should have at least two one-offs."
+ );
+ for (let i = 1; i < oneOffButtons.length / 2; i++) {
+ await UrlbarTestUtils.assertSearchMode(
+ window,
+ getExpectedSearchMode(oneOffs.selectedButton, true)
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ }
+ // Now click out of search mode.
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+ // Now check for the remaining one-offs' search mode previews.
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ while (oneOffs.selectedButton) {
+ await UrlbarTestUtils.assertSearchMode(
+ window,
+ getExpectedSearchMode(oneOffs.selectedButton, true)
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ await UrlbarTestUtils.assertSearchMode(window, null);
+});
+
+// Tests that the original search mode is preserved when going through some
+// one-off buttons and then back down in the results list.
+add_task(async function fullSearchMode_oneOff_restore_on_down() {
+ info("Add a few visits to top sites");
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits([
+ "http://1.example.com/",
+ "http://2.example.com/",
+ "http://3.example.com/",
+ ]);
+ }
+ await updateTopSites(sites => sites?.length > 2, false);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ let oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window);
+ let oneOffButtons = oneOffs.getSelectableButtons(true);
+ await TestUtils.waitForCondition(
+ () => !oneOffs._rebuilding,
+ "Waiting for one-offs to finish rebuilding"
+ );
+
+ await UrlbarTestUtils.enterSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ });
+ let expectedSearchMode = getExpectedSearchMode(
+ oneOffButtons.find(b => b.source == UrlbarUtils.RESULT_SOURCE.HISTORY),
+ false
+ );
+ await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode);
+ info("Down to the first result");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode);
+ info("Alt+down to the first one-off.");
+ Assert.greater(
+ oneOffButtons.length,
+ 1,
+ "Sanity check: We should have at least two one-offs."
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ await UrlbarTestUtils.assertSearchMode(
+ window,
+ getExpectedSearchMode(oneOffs.selectedButton, true)
+ );
+ info("Go again down through the list of results");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode);
+
+ // Now do a similar test without initial search mode.
+ info("Exit search mode.");
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ info("Down to the first result");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ info("select a one-off to start preview");
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ await UrlbarTestUtils.assertSearchMode(
+ window,
+ getExpectedSearchMode(oneOffs.selectedButton, true)
+ );
+ info("Go again through the list of results");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await UrlbarTestUtils.assertSearchMode(window, null);
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ await PlacesUtils.history.clear();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_sessionStore.js b/browser/components/urlbar/tests/browser/browser_searchMode_sessionStore.js
new file mode 100644
index 0000000000..fb132eff36
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchMode_sessionStore.js
@@ -0,0 +1,313 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests search mode and session store. Also tests that search mode is
+ * duplicated when duplicating tabs, since tab duplication is handled by session
+ * store.
+ */
+
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ SessionStore: "resource:///modules/sessionstore/SessionStore.jsm",
+ TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.jsm",
+});
+
+// This test takes a long time on the OS X 10.14 machines, so request a longer
+// timeout. See bug 1671045. This may also fix a different failure on Linux in
+// bug 1671087, but it's not clear. Regardless, a longer timeout won't hurt.
+requestLongerTimeout(5);
+
+const SEARCH_STRING = "test browser_sessionStore.js";
+const URL = "http://example.com/";
+
+// A URL in gInitialPages. We test this separately since SessionStore sometimes
+// takes different paths for these URLs.
+const INITIAL_URL = "about:newtab";
+
+// The following tasks make sure non-null search mode is restored.
+
+add_task(async function initialPageOnRestore() {
+ await doTest({
+ urls: [INITIAL_URL],
+ searchModeTabIndex: 0,
+ exitSearchMode: false,
+ switchTabsAfterEnteringSearchMode: false,
+ });
+});
+
+add_task(async function switchToInitialPage() {
+ await doTest({
+ urls: [URL, INITIAL_URL],
+ searchModeTabIndex: 1,
+ exitSearchMode: false,
+ switchTabsAfterEnteringSearchMode: true,
+ });
+});
+
+add_task(async function nonInitialPageOnRestore() {
+ await doTest({
+ urls: [URL],
+ searchModeTabIndex: 0,
+ exitSearchMode: false,
+ switchTabsAfterEnteringSearchMode: false,
+ });
+});
+
+add_task(async function switchToNonInitialPage() {
+ await doTest({
+ urls: [INITIAL_URL, URL],
+ searchModeTabIndex: 1,
+ exitSearchMode: false,
+ switchTabsAfterEnteringSearchMode: true,
+ });
+});
+
+// The following tasks enter and then exit search mode to make sure that no
+// search mode is restored.
+
+add_task(async function initialPageOnRestore_exit() {
+ await doTest({
+ urls: [INITIAL_URL],
+ searchModeTabIndex: 0,
+ exitSearchMode: true,
+ switchTabsAfterEnteringSearchMode: false,
+ });
+});
+
+add_task(async function switchToInitialPage_exit() {
+ await doTest({
+ urls: [URL, INITIAL_URL],
+ searchModeTabIndex: 1,
+ exitSearchMode: true,
+ switchTabsAfterEnteringSearchMode: true,
+ });
+});
+
+add_task(async function nonInitialPageOnRestore_exit() {
+ await doTest({
+ urls: [URL],
+ searchModeTabIndex: 0,
+ exitSearchMode: true,
+ switchTabsAfterEnteringSearchMode: false,
+ });
+});
+
+add_task(async function switchToNonInitialPage_exit() {
+ await doTest({
+ urls: [INITIAL_URL, URL],
+ searchModeTabIndex: 1,
+ exitSearchMode: true,
+ switchTabsAfterEnteringSearchMode: true,
+ });
+});
+
+/**
+ * The main test function. Opens some URLs in a new window, enters search mode
+ * in one of the tabs, closes the window, restores it, and makes sure that
+ * search mode is restored properly.
+ *
+ * @param {array} urls
+ * Array of string URLs to open.
+ * @param {number} searchModeTabIndex
+ * The index of the tab in which to enter search mode.
+ * @param {boolean} exitSearchMode
+ * If true, search mode will be immediately exited after entering it. Use
+ * this to make sure search mode is *not* restored after it's exited.
+ * @param {boolean} switchTabsAfterEnteringSearchMode
+ * If true, we'll switch to a tab other than the one that search mode was
+ * entered in before closing the window. `urls` should contain more than one
+ * URL in this case.
+ */
+async function doTest({
+ urls,
+ searchModeTabIndex,
+ exitSearchMode,
+ switchTabsAfterEnteringSearchMode,
+}) {
+ let searchModeURL = urls[searchModeTabIndex];
+ let otherTabIndex = (searchModeTabIndex + 1) % urls.length;
+ let otherURL = urls[otherTabIndex];
+
+ await withNewWindow(urls, async win => {
+ if (win.gBrowser.selectedTab != win.gBrowser.tabs[searchModeTabIndex]) {
+ await BrowserTestUtils.switchTab(
+ win.gBrowser,
+ win.gBrowser.tabs[searchModeTabIndex]
+ );
+ }
+
+ Assert.equal(
+ win.gBrowser.currentURI.spec,
+ searchModeURL,
+ `Sanity check: Tab at index ${searchModeTabIndex} is correct`
+ );
+ Assert.equal(
+ searchModeURL == INITIAL_URL,
+ win.gInitialPages.includes(win.gBrowser.currentURI.spec),
+ `Sanity check: ${searchModeURL} is or is not in gInitialPages as expected`
+ );
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: SEARCH_STRING,
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.enterSearchMode(win, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ });
+
+ if (exitSearchMode) {
+ await UrlbarTestUtils.exitSearchMode(win);
+ }
+
+ // Make sure session store is updated.
+ await TabStateFlusher.flush(win.gBrowser.selectedBrowser);
+
+ if (switchTabsAfterEnteringSearchMode) {
+ await BrowserTestUtils.switchTab(
+ win.gBrowser,
+ win.gBrowser.tabs[otherTabIndex]
+ );
+ }
+ });
+
+ let restoredURL = switchTabsAfterEnteringSearchMode
+ ? otherURL
+ : searchModeURL;
+
+ let win = await restoreWindow(restoredURL);
+
+ Assert.equal(
+ win.gBrowser.currentURI.spec,
+ restoredURL,
+ "Sanity check: Initially selected tab in restored window is correct"
+ );
+
+ if (switchTabsAfterEnteringSearchMode) {
+ // Switch back to the tab with search mode.
+ await BrowserTestUtils.switchTab(
+ win.gBrowser,
+ win.gBrowser.tabs[searchModeTabIndex]
+ );
+ }
+
+ if (exitSearchMode) {
+ // If we exited search mode, it should be null.
+ await new Promise(r => win.setTimeout(r, 500));
+ await UrlbarTestUtils.assertSearchMode(win, null);
+ } else {
+ // If we didn't exit search mode, it should be restored.
+ await TestUtils.waitForCondition(
+ () => win.gURLBar.searchMode,
+ "Waiting for search mode to be restored"
+ );
+ await UrlbarTestUtils.assertSearchMode(win, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ entry: "oneoff",
+ });
+ Assert.equal(
+ win.gURLBar.value,
+ SEARCH_STRING,
+ "Search string should be restored"
+ );
+ }
+
+ await BrowserTestUtils.closeWindow(win);
+}
+
+// Tests that search mode is duplicated when duplicating tabs. Note that tab
+// duplication is handled by session store.
+add_task(async function duplicateTabs() {
+ // Enter search mode with a search string in the current tab.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: SEARCH_STRING,
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.enterSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ });
+
+ // Now duplicate the current tab using the context menu item. First we need
+ // to set TabContextMenu.contextTab because that's how the menu item's command
+ // determines which tab to duplicate.
+ window.TabContextMenu.contextTab = gBrowser.selectedTab;
+ let tabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ gBrowser.currentURI.spec
+ );
+ let menuitem = document.getElementById("context_duplicateTab");
+ menuitem.click();
+ let newTab = await tabPromise;
+ Assert.equal(
+ gBrowser.selectedTab,
+ newTab,
+ "Sanity check: The duplicated tab is now the selected tab"
+ );
+
+ // Wait for search mode, then check it and the input value.
+ await TestUtils.waitForCondition(
+ () => gURLBar.searchMode,
+ "Waiting for search mode to be duplicated/restored"
+ );
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ entry: "oneoff",
+ });
+ Assert.equal(
+ gURLBar.value,
+ SEARCH_STRING,
+ "Search string should be duplicated/restored"
+ );
+
+ BrowserTestUtils.removeTab(newTab);
+ gURLBar.handleRevert();
+});
+
+/**
+ * Opens a new browser window with the given URLs, calls a callback, and then
+ * closes the window.
+ *
+ * @param {array} urls
+ * Array of string URLs to open.
+ * @param {function} callback
+ * The callback.
+ */
+async function withNewWindow(urls, callback) {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ for (let url of urls) {
+ await BrowserTestUtils.openNewForegroundTab({
+ url,
+ gBrowser: win.gBrowser,
+ waitForLoad: url != "about:newtab",
+ });
+ if (url == "about:newtab") {
+ await TestUtils.waitForCondition(
+ () => win.gBrowser.currentURI.spec == "about:newtab",
+ "Waiting for about:newtab"
+ );
+ }
+ }
+ BrowserTestUtils.removeTab(win.gBrowser.tabs[0]);
+ await callback(win);
+ await BrowserTestUtils.closeWindow(win);
+}
+
+/**
+ * Uses SessionStore to reopen the last closed window.
+ *
+ * @param {string} expectedRestoredURL
+ * The URL you expect will be restored in the selected browser.
+ */
+async function restoreWindow(expectedRestoredURL) {
+ let winPromise = BrowserTestUtils.waitForNewWindow();
+ let win = SessionStore.undoCloseWindow(0);
+ await winPromise;
+ await TestUtils.waitForCondition(
+ () => win.gBrowser.currentURI.spec == expectedRestoredURL,
+ "Waiting for restored selected browser to have expected URI"
+ );
+ return win;
+}
diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_setURI.js b/browser/components/urlbar/tests/browser/browser_searchMode_setURI.js
new file mode 100644
index 0000000000..46f0a84256
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchMode_setURI.js
@@ -0,0 +1,119 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that search mode remains active or is exited when setURI is called,
+ * depending on the situation.
+ */
+
+"use strict";
+
+// Opens a new tab, does a search, enters search mode, and then manually calls
+// setURI. Uses a variety of initial URLs, search strings, and setURI arguments
+// in order to hit different branches in setURI. Search mode should remain
+// active or be exited as appropriate.
+add_task(async function setURI() {
+ for (let test of [
+ // initialURL, searchString, url, expectSearchMode
+
+ ["about:blank", "", null, true],
+ ["about:blank", "", "about:blank", true],
+ ["about:blank", "", "http://www.example.com/", true],
+
+ ["about:blank", "about:blank", null, false],
+ ["about:blank", "about:blank", "about:blank", false],
+ ["about:blank", "about:blank", "http://www.example.com/", false],
+
+ ["about:blank", "http://www.example.com/", null, true],
+ ["about:blank", "http://www.example.com/", "about:blank", true],
+ ["about:blank", "http://www.example.com/", "http://www.example.com/", true],
+
+ ["about:blank", "not a URL", null, true],
+ ["about:blank", "not a URL", "about:blank", true],
+ ["about:blank", "not a URL", "http://www.example.com/", true],
+
+ ["http://www.example.com/", "", null, true],
+ ["http://www.example.com/", "", "about:blank", true],
+ ["http://www.example.com/", "", "http://www.example.com/", true],
+
+ ["http://www.example.com/", "about:blank", null, false],
+ ["http://www.example.com/", "about:blank", "about:blank", false],
+ [
+ "http://www.example.com/",
+ "about:blank",
+ "http://www.example.com/",
+ false,
+ ],
+
+ ["http://www.example.com/", "http://www.example.com/", null, true],
+ ["http://www.example.com/", "http://www.example.com/", "about:blank", true],
+ [
+ "http://www.example.com/",
+ "http://www.example.com/",
+ "http://www.example.com/",
+ true,
+ ],
+
+ ["http://www.example.com/", "not a URL", null, true],
+ ["http://www.example.com/", "not a URL", "about:blank", true],
+ ["http://www.example.com/", "not a URL", "http://www.example.com/", true],
+ ]) {
+ await doSetURITest(...test);
+ }
+});
+
+async function doSetURITest(initialURL, searchString, url, expectSearchMode) {
+ info(
+ "doSetURITest with args: " +
+ JSON.stringify({
+ initialURL,
+ searchString,
+ url,
+ expectSearchMode,
+ })
+ );
+
+ await BrowserTestUtils.withNewTab(initialURL, async () => {
+ if (searchString) {
+ // Do a search with the search string.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: searchString,
+ fireInputEvent: true,
+ });
+ } else {
+ // Open top sites.
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ document.getElementById("Browser:OpenLocation").doCommand();
+ });
+ }
+
+ // Enter search mode and close the view.
+ await UrlbarTestUtils.enterSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+ Assert.strictEqual(
+ gBrowser.selectedBrowser.userTypedValue,
+ searchString,
+ `userTypedValue should be ${searchString}`
+ );
+
+ // Call setURI.
+ let uri = url ? Services.io.newURI(url) : null;
+ gURLBar.setURI(uri);
+
+ await UrlbarTestUtils.assertSearchMode(
+ window,
+ !expectSearchMode
+ ? null
+ : {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ entry: "oneoff",
+ }
+ );
+
+ gURLBar.handleRevert();
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ });
+}
diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_suggestions.js b/browser/components/urlbar/tests/browser/browser_searchMode_suggestions.js
new file mode 100644
index 0000000000..aef08b7c91
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchMode_suggestions.js
@@ -0,0 +1,585 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests search suggestions in search mode.
+ */
+
+const DEFAULT_ENGINE_NAME = "Test";
+const SUGGESTIONS_ENGINE_NAME = "searchSuggestionEngine.xml";
+const MANY_SUGGESTIONS_ENGINE_NAME = "searchSuggestionEngineMany.xml";
+const MAX_HISTORICAL_SEARCH_SUGGESTIONS = UrlbarPrefs.get(
+ "maxHistoricalSearchSuggestions"
+);
+
+let suggestionsEngine;
+let defaultEngine;
+let expectedFormHistoryResults = [];
+
+add_task(async function setup() {
+ suggestionsEngine = await SearchTestUtils.promiseNewSearchEngine(
+ getRootDirectory(gTestPath) + SUGGESTIONS_ENGINE_NAME
+ );
+
+ let oldDefaultEngine = await Services.search.getDefault();
+ defaultEngine = await Services.search.addEngineWithDetails(
+ DEFAULT_ENGINE_NAME,
+ {
+ template: "http://example.com/?search={searchTerms}",
+ }
+ );
+ await Services.search.setDefault(defaultEngine);
+ await Services.search.moveEngine(suggestionsEngine, 0);
+
+ async function cleanup() {
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ }
+ await cleanup();
+ registerCleanupFunction(cleanup);
+
+ // Add some form history for our test engine. Add more than
+ // maxHistoricalSearchSuggestions so we can verify that excess form history is
+ // added after remote suggestions.
+ for (let i = 0; i < MAX_HISTORICAL_SEARCH_SUGGESTIONS + 1; i++) {
+ let value = `hello formHistory ${i}`;
+ await UrlbarTestUtils.formHistory.add([
+ { value, source: suggestionsEngine.name },
+ ]);
+ expectedFormHistoryResults.push({
+ heuristic: false,
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ searchParams: {
+ suggestion: value,
+ engine: suggestionsEngine.name,
+ },
+ });
+ }
+
+ // Add other form history.
+ await UrlbarTestUtils.formHistory.add([
+ { value: "hello formHistory global" },
+ { value: "hello formHistory other", source: "other engine" },
+ ]);
+
+ registerCleanupFunction(async () => {
+ await Services.search.setDefault(oldDefaultEngine);
+ await Services.search.removeEngine(defaultEngine);
+ await UrlbarTestUtils.formHistory.clear();
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.separatePrivateDefault.ui.enabled", false]],
+ });
+});
+
+add_task(async function emptySearch() {
+ await BrowserTestUtils.withNewTab("about:robots", async function(browser) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.update2.emptySearchBehavior", 2]],
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ Assert.equal(gURLBar.value, "", "Urlbar value should be cleared.");
+ // For the empty search case, we expect to get the form history relative to
+ // the picked engine and no heuristic.
+ await checkResults(expectedFormHistoryResults);
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+ await UrlbarTestUtils.promisePopupClose(window);
+ await SpecialPowers.popPrefEnv();
+ });
+});
+
+add_task(async function emptySearch_withRestyledHistory() {
+ // URLs with the same host as the search engine.
+ await PlacesTestUtils.addVisits([
+ `http://mochi.test/`,
+ `http://mochi.test/redirect`,
+ // Should not be returned because it's a redirect target.
+ {
+ uri: `http://mochi.test/target`,
+ transition: PlacesUtils.history.TRANSITIONS.REDIRECT_TEMPORARY,
+ referrer: `http://mochi.test/redirect`,
+ },
+ // Can be restyled and dupes form history.
+ "http://mochi.test:8888/?terms=hello+formHistory+0",
+ // Can be restyled but does not dupe form history.
+ "http://mochi.test:8888/?terms=ciao",
+ ]);
+ await BrowserTestUtils.withNewTab("about:robots", async function(browser) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.update2.emptySearchBehavior", 2]],
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ Assert.equal(gURLBar.value, "", "Urlbar value should be cleared.");
+ // For the empty search case, we expect to get the form history relative to
+ // the picked engine, history without redirects, and no heuristic.
+ await checkResults([
+ {
+ heuristic: false,
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ searchParams: {
+ suggestion: "ciao",
+ engine: suggestionsEngine.name,
+ },
+ },
+ ...expectedFormHistoryResults,
+ {
+ heuristic: false,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ url: `http://mochi.test/redirect`,
+ },
+ {
+ heuristic: false,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ url: `http://mochi.test/`,
+ },
+ ]);
+
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+ await UrlbarTestUtils.promisePopupClose(window);
+ await SpecialPowers.popPrefEnv();
+ });
+
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function emptySearch_withRestyledHistory_noSearchHistory() {
+ // URLs with the same host as the search engine.
+ await PlacesTestUtils.addVisits([
+ `http://mochi.test/`,
+ `http://mochi.test/redirect`,
+ // Can be restyled but does not dupe form history.
+ "http://mochi.test:8888/?terms=ciao",
+ ]);
+ await BrowserTestUtils.withNewTab("about:robots", async function(browser) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.update2.emptySearchBehavior", 2],
+ ["browser.urlbar.maxHistoricalSearchSuggestions", 0],
+ ],
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ Assert.equal(gURLBar.value, "", "Urlbar value should be cleared.");
+ // For the empty search case, we expect to get the form history relative to
+ // the picked engine, history without redirects, and no heuristic.
+ await checkResults([
+ {
+ heuristic: false,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ url: `http://mochi.test/redirect`,
+ },
+ {
+ heuristic: false,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ url: `http://mochi.test/`,
+ },
+ ]);
+
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+ await UrlbarTestUtils.promisePopupClose(window);
+ await SpecialPowers.popPrefEnv();
+ });
+
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function emptySearch_behavior() {
+ // URLs with the same host as the search engine.
+ await PlacesTestUtils.addVisits([`http://mochi.test/`]);
+
+ await BrowserTestUtils.withNewTab("about:robots", async function(browser) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.update2.emptySearchBehavior", 0]],
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ Assert.equal(gURLBar.value, "", "Urlbar value should be cleared.");
+ // For the empty search case, we expect to get the form history relative to
+ // the picked engine, history without redirects, and no heuristic.
+ await checkResults([]);
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+
+ // We should still show history for empty searches when not in search mode.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: " ",
+ });
+ await checkResults([
+ {
+ heuristic: true,
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ searchParams: {
+ query: " ",
+ engine: defaultEngine.name,
+ },
+ },
+ {
+ heuristic: false,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ url: `http://mochi.test/`,
+ },
+ ]);
+ await UrlbarTestUtils.promisePopupClose(window);
+ await SpecialPowers.popPrefEnv();
+ });
+
+ await BrowserTestUtils.withNewTab("about:robots", async function(browser) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.update2.emptySearchBehavior", 1]],
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ Assert.equal(gURLBar.value, "", "Urlbar value should be cleared.");
+ // For the empty search case, we expect to get the form history relative to
+ // the picked engine, history without redirects, and no heuristic.
+ await checkResults([...expectedFormHistoryResults]);
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+ await UrlbarTestUtils.promisePopupClose(window);
+ await SpecialPowers.popPrefEnv();
+ });
+
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function emptySearch_local() {
+ await PlacesTestUtils.addVisits([`http://mochi.test/`]);
+
+ await BrowserTestUtils.withNewTab("about:robots", async function(browser) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.update2.emptySearchBehavior", 0]],
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ await UrlbarTestUtils.enterSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ });
+ Assert.equal(gURLBar.value, "", "Urlbar value should be cleared.");
+ // Even when emptySearchBehavior is 0, we still show the user's most frecent
+ // history for an empty search.
+ await checkResults([
+ {
+ heuristic: false,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ url: `http://mochi.test/`,
+ },
+ ]);
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+ await UrlbarTestUtils.promisePopupClose(window);
+ await SpecialPowers.popPrefEnv();
+ });
+
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function nonEmptySearch() {
+ await BrowserTestUtils.withNewTab("about:robots", async function(browser) {
+ let query = "hello";
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: query,
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ Assert.equal(gURLBar.value, query, "Urlbar value should be set.");
+ // We expect to get the heuristic and all the suggestions.
+ await checkResults([
+ {
+ heuristic: true,
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ searchParams: {
+ query,
+ engine: suggestionsEngine.name,
+ },
+ },
+ ...expectedFormHistoryResults.slice(0, 2),
+ {
+ heuristic: false,
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ searchParams: {
+ query,
+ suggestion: `${query}foo`,
+ engine: suggestionsEngine.name,
+ },
+ },
+ {
+ heuristic: false,
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ searchParams: {
+ query,
+ suggestion: `${query}bar`,
+ engine: suggestionsEngine.name,
+ },
+ },
+ ...expectedFormHistoryResults.slice(2, 4),
+ ]);
+
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+ await UrlbarTestUtils.promisePopupClose(window);
+ });
+});
+
+add_task(async function nonEmptySearch_nonMatching() {
+ await BrowserTestUtils.withNewTab("about:robots", async function(browser) {
+ let query = "ciao";
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: query,
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ Assert.equal(gURLBar.value, query, "Urlbar value should be set.");
+ // We expect to get the heuristic and the remote suggestions since the local
+ // ones don't match.
+ await checkResults([
+ {
+ heuristic: true,
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ searchParams: {
+ query,
+ engine: suggestionsEngine.name,
+ },
+ },
+ {
+ heuristic: false,
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ searchParams: {
+ query,
+ suggestion: `${query}foo`,
+ engine: suggestionsEngine.name,
+ },
+ },
+ {
+ heuristic: false,
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ searchParams: {
+ query,
+ suggestion: `${query}bar`,
+ engine: suggestionsEngine.name,
+ },
+ },
+ ]);
+
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+ await UrlbarTestUtils.promisePopupClose(window);
+ });
+});
+
+add_task(async function nonEmptySearch_withHistory() {
+ let manySuggestionsEngine = await SearchTestUtils.promiseNewSearchEngine(
+ getRootDirectory(gTestPath) + MANY_SUGGESTIONS_ENGINE_NAME
+ );
+ // URLs with the same host as the search engine.
+ let query = "ciao";
+ await PlacesTestUtils.addVisits([
+ `http://mochi.test/${query}`,
+ `http://mochi.test/${query}1`,
+ // Should not be returned because it has a different host, even if it
+ // matches the host in the path.
+ `http://example.com/mochi.test/${query}`,
+ ]);
+
+ function makeSuggestionResult(suffix) {
+ return {
+ heuristic: false,
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ searchParams: {
+ query,
+ suggestion: `${query}${suffix}`,
+ engine: manySuggestionsEngine.name,
+ },
+ };
+ }
+
+ await BrowserTestUtils.withNewTab("about:robots", async function(browser) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: query,
+ });
+ await UrlbarTestUtils.enterSearchMode(window, {
+ engineName: manySuggestionsEngine.name,
+ });
+ Assert.equal(gURLBar.value, query, "Urlbar value should be set.");
+
+ await checkResults([
+ {
+ heuristic: true,
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ searchParams: {
+ query,
+ engine: manySuggestionsEngine.name,
+ },
+ },
+ makeSuggestionResult("foo"),
+ makeSuggestionResult("bar"),
+ makeSuggestionResult("1"),
+ makeSuggestionResult("2"),
+ {
+ heuristic: false,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ url: `http://mochi.test/${query}1`,
+ },
+ {
+ heuristic: false,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ url: `http://mochi.test/${query}`,
+ },
+ makeSuggestionResult("3"),
+ makeSuggestionResult("4"),
+ makeSuggestionResult("5"),
+ ]);
+
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ info("Test again with history before suggestions");
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.matchBuckets", "general:5,suggestion:Infinity"]],
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: query,
+ });
+ await UrlbarTestUtils.enterSearchMode(window, {
+ engineName: manySuggestionsEngine.name,
+ });
+ Assert.equal(gURLBar.value, query, "Urlbar value should be set.");
+
+ await checkResults([
+ {
+ heuristic: true,
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ searchParams: {
+ query,
+ engine: manySuggestionsEngine.name,
+ },
+ },
+ makeSuggestionResult("foo"),
+ makeSuggestionResult("bar"),
+ makeSuggestionResult("1"),
+ makeSuggestionResult("2"),
+ makeSuggestionResult("3"),
+ makeSuggestionResult("4"),
+ makeSuggestionResult("5"),
+ {
+ heuristic: false,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ url: `http://mochi.test/${query}1`,
+ },
+ {
+ heuristic: false,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ url: `http://mochi.test/${query}`,
+ },
+ ]);
+
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+ await UrlbarTestUtils.promisePopupClose(window);
+ await SpecialPowers.popPrefEnv();
+ });
+
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function nonEmptySearch_url() {
+ await BrowserTestUtils.withNewTab("about:robots", async function(browser) {
+ let query = "http://www.example.com/";
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: query,
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+
+ // The heuristic result for a search that's a valid URL should be a search
+ // result, not a URL result.
+ await checkResults([
+ {
+ heuristic: true,
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ searchParams: {
+ query,
+ engine: suggestionsEngine.name,
+ },
+ },
+ ]);
+
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+ await UrlbarTestUtils.promisePopupClose(window);
+ });
+});
+
+async function checkResults(expectedResults) {
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ expectedResults.length,
+ "Check results count."
+ );
+ for (let i = 0; i < expectedResults.length; ++i) {
+ info(`Checking result at index ${i}`);
+ let expected = expectedResults[i];
+ let actual = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+
+ // Check each property defined in the expected result against the property
+ // in the actual result.
+ for (let key of Object.keys(expected)) {
+ // For searchParams, remove undefined properties in the actual result so
+ // that the expected result doesn't need to include them.
+ if (key == "searchParams") {
+ let actualSearchParams = actual.searchParams;
+ for (let spKey of Object.keys(actualSearchParams)) {
+ if (actualSearchParams[spKey] === undefined) {
+ delete actualSearchParams[spKey];
+ }
+ }
+ }
+ Assert.deepEqual(
+ actual[key],
+ expected[key],
+ `${key} should match at result index ${i}.`
+ );
+ }
+ }
+}
diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_switchTabs.js b/browser/components/urlbar/tests/browser/browser_searchMode_switchTabs.js
new file mode 100644
index 0000000000..7a32bbc10c
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchMode_switchTabs.js
@@ -0,0 +1,305 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that search mode is stored per tab and restored when switching tabs.
+ */
+
+"use strict";
+
+// Enters search mode using the one-off buttons.
+add_task(async function switchTabs() {
+ // Open three tabs. We'll enter search mode in tabs 0 and 2.
+ let tabs = [];
+ for (let i = 0; i < 3; i++) {
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "http://example.com/" + i,
+ });
+ tabs.push(tab);
+ }
+
+ // Switch to tab 0.
+ await BrowserTestUtils.switchTab(gBrowser, tabs[0]);
+
+ // Do a search and enter search mode. Pass fireInputEvent so that
+ // userTypedValue is set and restored when we switch back to this tab. This
+ // isn't really necessary but it simulates the user's typing, and it also
+ // means that we'll start a search when we switch back to this tab.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.enterSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ });
+
+ // Switch to tab 1. Search mode should be exited.
+ await BrowserTestUtils.switchTab(gBrowser, tabs[1]);
+ await UrlbarTestUtils.assertSearchMode(window, null);
+
+ // Switch back to tab 0. We should do a search (for "test") and re-enter
+ // search mode.
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ await BrowserTestUtils.switchTab(gBrowser, tabs[0]);
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ entry: "oneoff",
+ });
+ Assert.equal(
+ gURLBar.value,
+ "test",
+ "Value should remain the search string after switching back"
+ );
+
+ // Switch to tab 2. Search mode should be exited.
+ await BrowserTestUtils.switchTab(gBrowser, tabs[2]);
+ await UrlbarTestUtils.assertSearchMode(window, null);
+
+ // Do another search (in tab 2) and enter search mode. Use a different source
+ // from tab 0 just to use something different.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test tab 2",
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.enterSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.TABS,
+ });
+
+ // Switch back to tab 0. We should do a search and still be in search mode.
+ searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ await BrowserTestUtils.switchTab(gBrowser, tabs[0]);
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ entry: "oneoff",
+ });
+ Assert.equal(
+ gURLBar.value,
+ "test",
+ "Value should remain the search string after switching back"
+ );
+
+ // Switch to tab 1. Search mode should be exited.
+ await BrowserTestUtils.switchTab(gBrowser, tabs[1]);
+ await UrlbarTestUtils.assertSearchMode(window, null);
+
+ // Switch back to tab 2. We should do a search and re-enter search mode.
+ searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ await BrowserTestUtils.switchTab(gBrowser, tabs[2]);
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.TABS,
+ entry: "oneoff",
+ });
+ Assert.equal(
+ gURLBar.value,
+ "test tab 2",
+ "Value should remain the search string after switching back"
+ );
+
+ // Exit search mode.
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+
+ // Switch to tab 0. We should do a search and re-enter search mode.
+ searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ await BrowserTestUtils.switchTab(gBrowser, tabs[0]);
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ entry: "oneoff",
+ });
+ Assert.equal(
+ gURLBar.value,
+ "test",
+ "Value should remain the search string after switching back"
+ );
+
+ // Switch back to tab 2. We should do a search but search mode should be
+ // inactive.
+ searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ await BrowserTestUtils.switchTab(gBrowser, tabs[2]);
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ Assert.equal(
+ gURLBar.value,
+ "test tab 2",
+ "Value should remain the search string after switching back"
+ );
+
+ // Switch back to tab 0. We should do a search and re-enter search mode.
+ searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ await BrowserTestUtils.switchTab(gBrowser, tabs[0]);
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ entry: "oneoff",
+ });
+ Assert.equal(
+ gURLBar.value,
+ "test",
+ "Value should remain the search string after switching back"
+ );
+
+ // Exit search mode.
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+
+ // Switch back to tab 2. We should do a search but search mode should be
+ // inactive.
+ searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ await BrowserTestUtils.switchTab(gBrowser, tabs[2]);
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ Assert.equal(
+ gURLBar.value,
+ "test tab 2",
+ "Value should remain the search string after switching back"
+ );
+
+ // Switch back to tab 0. We should do a search but search mode should be
+ // inactive.
+ searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ await BrowserTestUtils.switchTab(gBrowser, tabs[0]);
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ Assert.equal(
+ gURLBar.value,
+ "test",
+ "Value should remain the search string after switching back"
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ for (let tab of tabs) {
+ BrowserTestUtils.removeTab(tab);
+ }
+});
+
+// Start loading a SERP from search mode then immediately switch to a new tab so
+// the SERP finishes loading in the background. Switch back to the SERP tab and
+// observe that we don't re-enter search mode despite having an entry for that
+// tab in UrlbarInput._searchModesByBrowser. See bug 1675926.
+//
+// This subtest intermittently does not test bug 1675926 (NB: this does not mean
+// it is an intermittent failure). The false-positive occurs if the SERP page
+// finishes loading before we switch tabs. We include this subtest so we have
+// one covering real-world behaviour. A subtest that is guaranteed to test this
+// behaviour that does not simulate real world behaviour is included below.
+add_task(async function slow_load() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.searches", false]],
+ });
+ const engineName = "Test";
+ let engine = await Services.search.addEngineWithDetails(engineName, {
+ template: "http://example.com/?search={searchTerms}",
+ });
+ const originalTab = gBrowser.selectedTab;
+ const newTab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.enterSearchMode(window, { engineName });
+
+ const loadPromise = BrowserTestUtils.browserLoaded(newTab.linkedBrowser);
+ // Select the search mode heuristic to load the example.com SERP.
+ EventUtils.synthesizeKey("KEY_Enter");
+ // Switch away from the tab before we let it load.
+ await BrowserTestUtils.switchTab(gBrowser, originalTab);
+ await loadPromise;
+
+ // Switch back to the search mode tab and confirm we don't restore search
+ // mode.
+ await BrowserTestUtils.switchTab(gBrowser, newTab);
+ await UrlbarTestUtils.assertSearchMode(window, null);
+
+ BrowserTestUtils.removeTab(newTab);
+ await Services.search.removeEngine(engine);
+ await SpecialPowers.popPrefEnv();
+});
+
+// Tests the same behaviour as slow_load, but in a more reliable way using
+// non-real-world behaviour.
+add_task(async function slow_load_guaranteed() {
+ const engineName = "Test";
+ let engine = await Services.search.addEngineWithDetails(engineName, {
+ template: "http://example.com/?search={searchTerms}",
+ });
+ const backgroundTab = BrowserTestUtils.addTab(gBrowser);
+
+ // Simulate a tab that was in search mode, loaded a SERP, then was switched
+ // away from before setURI was called.
+ backgroundTab.ownerGlobal.gURLBar.searchMode = { engineName };
+ let loadPromise = BrowserTestUtils.browserLoaded(backgroundTab.linkedBrowser);
+ BrowserTestUtils.loadURI(
+ backgroundTab.linkedBrowser,
+ "http://example.com/?search=test"
+ );
+ await loadPromise;
+
+ // Switch to the background mode tab and confirm we don't restore search mode.
+ await BrowserTestUtils.switchTab(gBrowser, backgroundTab);
+ await UrlbarTestUtils.assertSearchMode(window, null);
+
+ BrowserTestUtils.removeTab(backgroundTab);
+ await Services.search.removeEngine(engine);
+});
+
+// Enters search mode by typing a restriction char with no search string.
+// Search mode and the search string should be restored after switching back to
+// the tab.
+add_task(async function userTypedValue_empty() {
+ await doUserTypedValueTest("");
+});
+
+// Enters search mode by typing a restriction char followed by a search string.
+// Search mode and the search string should be restored after switching back to
+// the tab.
+add_task(async function userTypedValue_nonEmpty() {
+ await doUserTypedValueTest("foo bar");
+});
+
+/**
+ * Enters search mode by typing a restriction char followed by a search string,
+ * opens a new tab and immediately closes it so we switch back to the search
+ * mode tab, and checks the search mode state and input value.
+ *
+ * @param {string} searchString
+ * The search string to enter search mode with.
+ */
+async function doUserTypedValueTest(searchString) {
+ let value = `${UrlbarTokenizer.RESTRICT.BOOKMARK} ${searchString}`;
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value,
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ entry: "typed",
+ });
+ Assert.equal(
+ gURLBar.value,
+ searchString,
+ "Sanity check: Value is the search string"
+ );
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({ gBrowser });
+ BrowserTestUtils.removeTab(tab);
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ entry: "typed",
+ });
+ Assert.equal(
+ gURLBar.value,
+ searchString,
+ "Value should remain the search string after switching back"
+ );
+
+ gURLBar.handleRevert();
+}
diff --git a/browser/components/urlbar/tests/browser/browser_searchSettings.js b/browser/components/urlbar/tests/browser/browser_searchSettings.js
new file mode 100644
index 0000000000..ff89a868df
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchSettings.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async function() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "a",
+ });
+
+ // Since the current tab is blank the preferences pane will load there
+ let loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ let button = document.getElementById(
+ "urlbar-anon-search-settings-compact"
+ );
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ });
+ await loaded;
+
+ is(
+ gBrowser.selectedBrowser.currentURI.spec,
+ "about:preferences#search",
+ "Should have loaded the right page"
+ );
+ }
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_searchSingleWordNotification.js b/browser/components/urlbar/tests/browser/browser_searchSingleWordNotification.js
new file mode 100644
index 0000000000..3218d2d30d
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchSingleWordNotification.js
@@ -0,0 +1,356 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+"use strict";
+
+let gDNSResolved = false;
+let gRealDNSService = gDNSService;
+add_task(async function setup() {
+ gDNSService = {
+ asyncResolve() {
+ gDNSResolved = true;
+ return gRealDNSService.asyncResolve(...arguments);
+ },
+ };
+ registerCleanupFunction(function() {
+ gDNSService = gRealDNSService;
+ Services.prefs.clearUserPref("browser.fixup.domainwhitelist.localhost");
+ });
+});
+
+function promiseNotification(aBrowser, value, expected, input) {
+ return new Promise(resolve => {
+ let notificationBox = aBrowser.getNotificationBox(aBrowser.selectedBrowser);
+ if (expected) {
+ info("Waiting for " + value + " notification");
+ resolve(
+ BrowserTestUtils.waitForNotificationInNotificationBox(
+ notificationBox,
+ value
+ )
+ );
+ } else {
+ setTimeout(() => {
+ is(
+ notificationBox.getNotificationWithValue(value),
+ null,
+ `We are expecting to not get a notification for ${input}`
+ );
+ resolve();
+ }, 1000);
+ }
+ });
+}
+
+async function runURLBarSearchTest({
+ valueToOpen,
+ enterSearchMode,
+ expectSearch,
+ expectNotification,
+ expectDNSResolve,
+ aWindow = window,
+}) {
+ gDNSResolved = false;
+ // Test both directly setting a value and pressing enter, or setting the
+ // value through input events, like the user would do.
+ const setValueFns = [
+ value => {
+ aWindow.gURLBar.value = value;
+ if (enterSearchMode) {
+ // Ensure to open the panel.
+ UrlbarTestUtils.fireInputEvent(aWindow);
+ }
+ },
+ value => {
+ return UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: aWindow,
+ value,
+ });
+ },
+ ];
+
+ for (let i = 0; i < setValueFns.length; ++i) {
+ await setValueFns[i](valueToOpen);
+ if (enterSearchMode) {
+ if (!expectSearch) {
+ throw new Error("Must execute a search in search mode");
+ }
+ await UrlbarTestUtils.enterSearchMode(aWindow);
+ }
+
+ let expectedURI;
+ if (!expectSearch) {
+ expectedURI = "http://" + valueToOpen + "/";
+ } else {
+ expectedURI = (await Services.search.getDefault()).getSubmission(
+ valueToOpen,
+ null,
+ "keyword"
+ ).uri.spec;
+ }
+ aWindow.gURLBar.focus();
+ let docLoadPromise = BrowserTestUtils.waitForDocLoadAndStopIt(
+ expectedURI,
+ aWindow.gBrowser.selectedBrowser
+ );
+ EventUtils.synthesizeKey("VK_RETURN", {}, aWindow);
+
+ if (!enterSearchMode) {
+ await promiseNotification(
+ aWindow.gBrowser,
+ "keyword-uri-fixup",
+ expectNotification,
+ valueToOpen
+ );
+ }
+ await docLoadPromise;
+
+ if (expectNotification) {
+ let notificationBox = aWindow.gBrowser.getNotificationBox(
+ aWindow.gBrowser.selectedBrowser
+ );
+ let notification = notificationBox.getNotificationWithValue(
+ "keyword-uri-fixup"
+ );
+ // Confirm the notification only on the last loop.
+ if (i == setValueFns.length - 1) {
+ docLoadPromise = BrowserTestUtils.waitForDocLoadAndStopIt(
+ "http://" + valueToOpen + "/",
+ aWindow.gBrowser.selectedBrowser
+ );
+ notification.querySelector("button").click();
+ await docLoadPromise;
+ } else {
+ notificationBox.currentNotification.close();
+ }
+ }
+ Assert.equal(
+ gDNSResolved,
+ expectDNSResolve,
+ `Should${expectDNSResolve ? "" : " not"} DNS resolve "${valueToOpen}"`
+ );
+ }
+}
+
+add_task(async function test_navigate_full_domain() {
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "about:blank"
+ ));
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await runURLBarSearchTest({
+ valueToOpen: "www.singlewordtest.org",
+ expectSearch: false,
+ expectNotification: false,
+ expectDNSResolve: false,
+ });
+ gBrowser.removeTab(tab);
+});
+
+add_task(async function test_navigate_decimal_ip() {
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "about:blank"
+ ));
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await runURLBarSearchTest({
+ valueToOpen: "1234",
+ expectSearch: true,
+ expectNotification: false,
+ expectDNSResolve: false, // Possible IP in numeric format.
+ });
+ gBrowser.removeTab(tab);
+});
+
+add_task(async function test_navigate_decimal_ip_with_path() {
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "about:blank"
+ ));
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await runURLBarSearchTest({
+ valueToOpen: "1234/12",
+ expectSearch: true,
+ expectNotification: false,
+ expectDNSResolve: false,
+ });
+ gBrowser.removeTab(tab);
+});
+
+add_task(async function test_navigate_large_number() {
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "about:blank"
+ ));
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await runURLBarSearchTest({
+ valueToOpen: "123456789012345",
+ expectSearch: true,
+ expectNotification: false,
+ expectDNSResolve: false, // Possible IP in numeric format.
+ });
+ gBrowser.removeTab(tab);
+});
+
+add_task(async function test_navigate_small_hex_number() {
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "about:blank"
+ ));
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await runURLBarSearchTest({
+ valueToOpen: "0x1f00ffff",
+ expectSearch: true,
+ expectNotification: false,
+ expectDNSResolve: false, // Possible IP in numeric format.
+ });
+ gBrowser.removeTab(tab);
+});
+
+add_task(async function test_navigate_large_hex_number() {
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "about:blank"
+ ));
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await runURLBarSearchTest({
+ valueToOpen: "0x7f0000017f000001",
+ expectSearch: true,
+ expectNotification: false,
+ expectDNSResolve: false, // Possible IP in numeric format.
+ });
+ gBrowser.removeTab(tab);
+});
+
+function get_test_function_for_localhost_with_hostname(
+ hostName,
+ isPrivate = false
+) {
+ return async function test_navigate_single_host() {
+ info(`Test ${hostName}${isPrivate ? " in Private Browsing mode" : ""}`);
+ const pref = "browser.fixup.domainwhitelist.localhost";
+ let win;
+ if (isPrivate) {
+ let promiseWin = BrowserTestUtils.waitForNewWindow();
+ win = OpenBrowserWindow({ private: true });
+ await promiseWin;
+ await SimpleTest.promiseFocus(win);
+ // We can do this since the window will be gone shortly.
+ delete win.gDNSService;
+ win.gDNSService = {
+ asyncResolve() {
+ gDNSResolved = true;
+ return gRealDNSService.asyncResolve(...arguments);
+ },
+ };
+ } else {
+ win = window;
+ }
+
+ // Remove the domain from the whitelist, the notification sould appear,
+ // unless we are in private browsing mode.
+ Services.prefs.setBoolPref(pref, false);
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser: win.gBrowser,
+ url: "about:blank",
+ },
+ browser =>
+ runURLBarSearchTest({
+ valueToOpen: hostName,
+ expectSearch: true,
+ expectNotification: true,
+ expectDNSResolve: true,
+ aWindow: win,
+ })
+ );
+
+ // check pref value
+ let prefValue = Services.prefs.getBoolPref(pref);
+ is(prefValue, !isPrivate, "Pref should have the correct state.");
+
+ // Now try again with the pref set.
+ // In a private window, the notification should appear again.
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser: win.gBrowser,
+ url: "about:blank",
+ },
+ browser =>
+ runURLBarSearchTest({
+ valueToOpen: hostName,
+ expectSearch: isPrivate,
+ expectNotification: isPrivate,
+ expectDNSResolve: isPrivate,
+ aWindow: win,
+ })
+ );
+
+ if (isPrivate) {
+ info("Waiting for private window to close");
+ await BrowserTestUtils.closeWindow(win);
+ await SimpleTest.promiseFocus(window);
+ }
+ };
+}
+
+add_task(get_test_function_for_localhost_with_hostname("localhost"));
+add_task(get_test_function_for_localhost_with_hostname("localhost."));
+add_task(get_test_function_for_localhost_with_hostname("localhost", true));
+
+add_task(async function test_dnsResolveSingleWordsAfterSearch() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.dnsResolveSingleWordsAfterSearch", 0],
+ ["browser.fixup.domainwhitelist.localhost", false],
+ ],
+ });
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:blank",
+ },
+ browser =>
+ runURLBarSearchTest({
+ valueToOpen: "localhost",
+ expectSearch: true,
+ expectNotification: false,
+ expectDNSResolve: false,
+ })
+ );
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_navigate_invalid_url() {
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "about:blank"
+ ));
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await runURLBarSearchTest({
+ valueToOpen: "mozilla is awesome",
+ expectSearch: true,
+ expectNotification: false,
+ expectDNSResolve: false,
+ });
+ gBrowser.removeTab(tab);
+});
+
+add_task(async function test_search_mode() {
+ info("When in search mode we should never query the DNS");
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.suggest.enabled", false]],
+ });
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "about:blank"
+ ));
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await runURLBarSearchTest({
+ enterSearchMode: true,
+ valueToOpen: "mozilla",
+ expectSearch: true,
+ expectNotification: false,
+ expectDNSResolve: false,
+ });
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_searchSuggestions.js b/browser/components/urlbar/tests/browser/browser_searchSuggestions.js
new file mode 100644
index 0000000000..d325129788
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchSuggestions.js
@@ -0,0 +1,343 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This tests checks that search suggestions can be acted upon correctly
+ * e.g. selection with modifiers, copying text.
+ */
+
+"use strict";
+
+const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches";
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+
+const MAX_CHARS_PREF = "browser.urlbar.maxCharsForSearchSuggestions";
+
+// Must run first.
+add_task(async function prepare() {
+ let suggestionsEnabled = Services.prefs.getBoolPref(SUGGEST_URLBAR_PREF);
+ Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, true);
+ let engine = await SearchTestUtils.promiseNewSearchEngine(
+ getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME
+ );
+ let oldDefaultEngine = await Services.search.getDefault();
+ await Services.search.setDefault(engine);
+ await UrlbarTestUtils.formHistory.clear();
+ registerCleanupFunction(async function() {
+ Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, suggestionsEnabled);
+ await Services.search.setDefault(oldDefaultEngine);
+
+ // Clicking suggestions causes visits to search results pages, so clear that
+ // history now.
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.formHistory.clear();
+ });
+});
+
+add_task(async function clickSuggestion() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ gURLBar.focus();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ });
+ let [idx, suggestion, engineName] = await getFirstSuggestion();
+ Assert.equal(
+ engineName,
+ "browser_searchSuggestionEngine searchSuggestionEngine.xml",
+ "Expected suggestion engine"
+ );
+
+ let uri = (await Services.search.getDefault()).getSubmission(suggestion).uri;
+ let loadPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ uri.spec
+ );
+ let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, idx);
+ EventUtils.synthesizeMouseAtCenter(element, {}, window);
+ await loadPromise;
+
+ let formHistory = (
+ await UrlbarTestUtils.formHistory.search({ source: engineName })
+ ).map(entry => entry.value);
+ Assert.deepEqual(
+ formHistory,
+ ["foofoo"],
+ "Should find form history after adding it"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ await UrlbarTestUtils.formHistory.clear();
+});
+
+async function testPressEnterOnSuggestion(
+ expectedUrl = null,
+ keyModifiers = {}
+) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ gURLBar.focus();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ });
+ let [idx, suggestion, engineName] = await getFirstSuggestion();
+ Assert.equal(
+ engineName,
+ "browser_searchSuggestionEngine searchSuggestionEngine.xml",
+ "Expected suggestion engine"
+ );
+
+ let hasExpectedUrl = !!expectedUrl;
+ if (!expectedUrl) {
+ expectedUrl = (await Services.search.getDefault()).getSubmission(suggestion)
+ .uri.spec;
+ }
+
+ let promiseLoad = BrowserTestUtils.waitForDocLoadAndStopIt(
+ expectedUrl,
+ gBrowser.selectedBrowser
+ );
+
+ let promiseFormHistory;
+ if (!hasExpectedUrl) {
+ promiseFormHistory = UrlbarTestUtils.formHistory.promiseChanged("add");
+ }
+
+ for (let i = 0; i < idx; ++i) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+ EventUtils.synthesizeKey("KEY_Enter", keyModifiers);
+
+ await promiseLoad;
+
+ if (!hasExpectedUrl) {
+ await promiseFormHistory;
+ let formHistory = (
+ await UrlbarTestUtils.formHistory.search({ source: engineName })
+ ).map(entry => entry.value);
+ Assert.deepEqual(
+ formHistory,
+ ["foofoo"],
+ "Should find form history after adding it"
+ );
+ }
+
+ BrowserTestUtils.removeTab(tab);
+ await UrlbarTestUtils.formHistory.clear();
+}
+
+add_task(async function plainEnterOnSuggestion() {
+ await testPressEnterOnSuggestion();
+});
+
+add_task(async function ctrlEnterOnSuggestion() {
+ await testPressEnterOnSuggestion("https://www.foofoo.com/", {
+ ctrlKey: true,
+ });
+});
+
+add_task(async function copySuggestionText() {
+ gURLBar.focus();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ });
+ let [idx, suggestion] = await getFirstSuggestion();
+ for (let i = 0; i < idx; ++i) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+ gURLBar.select();
+ await SimpleTest.promiseClipboardChange(suggestion, () => {
+ goDoCommand("cmd_copy");
+ });
+});
+
+add_task(async function typeMaxChars() {
+ gURLBar.focus();
+
+ let maxChars = 10;
+ await SpecialPowers.pushPrefEnv({
+ set: [[MAX_CHARS_PREF, maxChars]],
+ });
+
+ // Make a string as long as maxChars and type it.
+ let value = "";
+ for (let i = 0; i < maxChars; i++) {
+ value += String.fromCharCode("a".charCodeAt(0) + i);
+ }
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value,
+ });
+
+ // Suggestions should be fetched since we allow them when typing, and the
+ // value so far isn't longer than maxChars anyway.
+ await assertSuggestions([value + "foo", value + "bar"]);
+
+ // Now type some additional chars. Suggestions should still be fetched since
+ // we allow them when typing.
+ for (let i = 0; i < 3; i++) {
+ let char = String.fromCharCode("z".charCodeAt(0) - i);
+ value += char;
+ EventUtils.synthesizeKey(char);
+ await assertSuggestions([value + "foo", value + "bar"]);
+ }
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function pasteMaxChars() {
+ gURLBar.focus();
+
+ let maxChars = 10;
+ await SpecialPowers.pushPrefEnv({
+ set: [[MAX_CHARS_PREF, maxChars]],
+ });
+
+ // Make a string as long as maxChars and paste it.
+ let value = "";
+ for (let i = 0; i < maxChars; i++) {
+ value += String.fromCharCode("a".charCodeAt(0) + i);
+ }
+ await selectAndPaste(value);
+
+ // Suggestions should be fetched since the pasted string is not longer than
+ // maxChars.
+ await assertSuggestions([value + "foo", value + "bar"]);
+
+ // Now type some additional chars. Suggestions should still be fetched since
+ // we allow them when typing.
+ for (let i = 0; i < 3; i++) {
+ let char = String.fromCharCode("z".charCodeAt(0) - i);
+ value += char;
+ EventUtils.synthesizeKey(char);
+ await assertSuggestions([value + "foo", value + "bar"]);
+ }
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function pasteMoreThanMaxChars() {
+ gURLBar.focus();
+
+ let maxChars = 10;
+ await SpecialPowers.pushPrefEnv({
+ set: [[MAX_CHARS_PREF, maxChars]],
+ });
+
+ // Make a string longer than maxChars and paste it.
+ let value = "";
+ for (let i = 0; i < 2 * maxChars; i++) {
+ value += String.fromCharCode("a".charCodeAt(0) + i);
+ }
+ await selectAndPaste(value);
+
+ // Suggestions should not be fetched since the value was pasted and it was
+ // longer than maxChars.
+ await assertSuggestions([]);
+
+ // Now type some additional chars. Suggestions should now be fetched since we
+ // allow them when typing.
+ for (let i = 0; i < 3; i++) {
+ let char = String.fromCharCode("z".charCodeAt(0) - i);
+ value += char;
+ EventUtils.synthesizeKey(char);
+ await assertSuggestions([value + "foo", value + "bar"]);
+ }
+
+ // Paste again. The string is longer than maxChars, so suggestions should not
+ // be fetched.
+ await selectAndPaste(value);
+ await assertSuggestions([]);
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function heuristicAddsFormHistory() {
+ await UrlbarTestUtils.formHistory.clear();
+ let formHistory = (await UrlbarTestUtils.formHistory.search()).map(
+ entry => entry.value
+ );
+ Assert.deepEqual(formHistory, [], "Form history should be empty initially");
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ gURLBar.focus();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ });
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(result.heuristic);
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.SEARCH);
+ Assert.equal(result.searchParams.query, "foo");
+
+ let uri = (await Services.search.getDefault()).getSubmission("foo").uri;
+ let loadPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ uri.spec
+ );
+ let formHistoryPromise = UrlbarTestUtils.formHistory.promiseChanged("add");
+ let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
+ EventUtils.synthesizeMouseAtCenter(element, {}, window);
+ await loadPromise;
+
+ await formHistoryPromise;
+ formHistory = (
+ await UrlbarTestUtils.formHistory.search({
+ source: result.searchParams.engine,
+ })
+ ).map(entry => entry.value);
+ Assert.deepEqual(
+ formHistory,
+ ["foo"],
+ "Should find form history after adding it"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ await UrlbarTestUtils.formHistory.clear();
+});
+
+async function getFirstSuggestion() {
+ let results = await getSuggestionResults();
+ if (!results.length) {
+ return [-1, null, null];
+ }
+ let result = results[0];
+ return [
+ result.index,
+ result.searchParams.suggestion,
+ result.searchParams.engine,
+ ];
+}
+
+async function getSuggestionResults() {
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ let results = [];
+ let matchCount = UrlbarTestUtils.getResultCount(window);
+ for (let i = 0; i < matchCount; i++) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ if (
+ result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
+ result.searchParams.suggestion
+ ) {
+ result.index = i;
+ results.push(result);
+ }
+ }
+ return results;
+}
+
+async function assertSuggestions(expectedSuggestions) {
+ let results = await getSuggestionResults();
+ let actualSuggestions = results.map(r => r.searchParams.suggestion);
+ Assert.deepEqual(
+ actualSuggestions,
+ expectedSuggestions,
+ "Expected suggestions"
+ );
+}
diff --git a/browser/components/urlbar/tests/browser/browser_searchTelemetry.js b/browser/components/urlbar/tests/browser/browser_searchTelemetry.js
new file mode 100644
index 0000000000..219a7610c3
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchTelemetry.js
@@ -0,0 +1,225 @@
+"use strict";
+
+const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches";
+const MAX_FORM_HISTORY_PREF = "browser.urlbar.maxHistoricalSearchSuggestions";
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+
+// Must run first.
+add_task(async function prepare() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [SUGGEST_URLBAR_PREF, true],
+ [MAX_FORM_HISTORY_PREF, 2],
+ ],
+ });
+
+ let engine = await SearchTestUtils.promiseNewSearchEngine(
+ getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME
+ );
+ let oldDefaultEngine = await Services.search.getDefault();
+ await Services.search.setDefault(engine);
+
+ registerCleanupFunction(async function() {
+ await Services.search.setDefault(oldDefaultEngine);
+
+ // Clicking urlbar results causes visits to their associated pages, so clear
+ // that history now.
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.formHistory.clear();
+ });
+
+ // Move the mouse away from the urlbar one-offs so that a one-off engine is
+ // not inadvertently selected.
+ await new Promise(resolve => {
+ EventUtils.synthesizeNativeMouseMove(
+ window.document.documentElement,
+ 0,
+ 0,
+ resolve
+ );
+ });
+});
+
+add_task(async function heuristicResultMouse() {
+ await compareCounts(async function() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ gURLBar.focus();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "heuristicResult",
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "Should be of type search"
+ );
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
+ EventUtils.synthesizeMouseAtCenter(element, {});
+ await loadPromise;
+ BrowserTestUtils.removeTab(tab);
+ await UrlbarTestUtils.formHistory.clear();
+ });
+});
+
+add_task(async function heuristicResultKeyboard() {
+ await compareCounts(async function() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ gURLBar.focus();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "heuristicResult",
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "Should be of type search"
+ );
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ EventUtils.sendKey("return");
+ await loadPromise;
+ BrowserTestUtils.removeTab(tab);
+ await UrlbarTestUtils.formHistory.clear();
+ });
+});
+
+add_task(async function searchSuggestionMouse() {
+ await compareCounts(async function() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ gURLBar.focus();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "searchSuggestion",
+ });
+ let idx = await getFirstSuggestionIndex();
+ Assert.greaterOrEqual(idx, 0, "there should be a first suggestion");
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ let element = await UrlbarTestUtils.waitForAutocompleteResultAt(
+ window,
+ idx
+ );
+ EventUtils.synthesizeMouseAtCenter(element, {});
+ await loadPromise;
+ BrowserTestUtils.removeTab(tab);
+ await UrlbarTestUtils.formHistory.clear();
+ });
+});
+
+add_task(async function searchSuggestionKeyboard() {
+ await compareCounts(async function() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ gURLBar.focus();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "searchSuggestion",
+ });
+ let idx = await getFirstSuggestionIndex();
+ Assert.greaterOrEqual(idx, 0, "there should be a first suggestion");
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ while (idx--) {
+ EventUtils.sendKey("down");
+ }
+ EventUtils.sendKey("return");
+ await loadPromise;
+ BrowserTestUtils.removeTab(tab);
+ await UrlbarTestUtils.formHistory.clear();
+ });
+});
+
+add_task(async function formHistoryMouse() {
+ await compareCounts(async function() {
+ await UrlbarTestUtils.formHistory.add(["foofoo", "foobar"]);
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ gURLBar.focus();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ });
+ let index = await getFirstSuggestionIndex();
+ Assert.greaterOrEqual(index, 0, "there should be a first suggestion");
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index);
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.SEARCH);
+ Assert.equal(result.source, UrlbarUtils.RESULT_SOURCE.HISTORY);
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ let element = await UrlbarTestUtils.waitForAutocompleteResultAt(
+ window,
+ index
+ );
+ EventUtils.synthesizeMouseAtCenter(element, {});
+ await loadPromise;
+ BrowserTestUtils.removeTab(tab);
+ await UrlbarTestUtils.formHistory.clear();
+ });
+});
+
+add_task(async function formHistoryKeyboard() {
+ await compareCounts(async function() {
+ await UrlbarTestUtils.formHistory.add(["foofoo", "foobar"]);
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ gURLBar.focus();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ });
+ let index = await getFirstSuggestionIndex();
+ Assert.greaterOrEqual(index, 0, "there should be a first suggestion");
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index);
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.SEARCH);
+ Assert.equal(result.source, UrlbarUtils.RESULT_SOURCE.HISTORY);
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ while (index--) {
+ EventUtils.sendKey("down");
+ }
+ EventUtils.sendKey("return");
+ await loadPromise;
+ BrowserTestUtils.removeTab(tab);
+ await UrlbarTestUtils.formHistory.clear();
+ });
+});
+
+/**
+ * This does three things: gets current telemetry/FHR counts, calls
+ * clickCallback, gets telemetry/FHR counts again to compare them to the old
+ * counts.
+ *
+ * @param {function} clickCallback Use this to open the urlbar popup and choose
+ * and click a result.
+ */
+async function compareCounts(clickCallback) {
+ // Search events triggered by clicks (not the Return key in the urlbar) are
+ // recorded in three places:
+ // * Telemetry histogram named "SEARCH_COUNTS"
+ // * FHR
+
+ let engine = await Services.search.getDefault();
+
+ let histogramKey = `other-${engine.name}.urlbar`;
+ let histogram = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS");
+ histogram.clear();
+
+ gURLBar.focus();
+ await clickCallback();
+
+ TelemetryTestUtils.assertKeyedHistogramSum(histogram, histogramKey, 1);
+}
+
+/**
+ * Returns the index of the first search suggestion in the urlbar results.
+ *
+ * @returns {number} An index, or -1 if there are no search suggestions.
+ */
+async function getFirstSuggestionIndex() {
+ const matchCount = UrlbarTestUtils.getResultCount(window);
+ for (let i = 0; i < matchCount; i++) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ if (
+ result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
+ result.searchParams.suggestion
+ ) {
+ return i;
+ }
+ }
+ return -1;
+}
diff --git a/browser/components/urlbar/tests/browser/browser_selectStaleResults.js b/browser/components/urlbar/tests/browser/browser_selectStaleResults.js
new file mode 100644
index 0000000000..8732a6d356
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_selectStaleResults.js
@@ -0,0 +1,301 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This test makes sure that arrowing down and up through the view's results
+// works correctly with regard to stale results.
+
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ UrlbarView: "resource:///modules/UrlbarView.jsm",
+});
+
+add_task(async function init() {
+ // Increase the timeout of the remove-stale-rows timer so that it doesn't
+ // interfere with the tests.
+ let originalRemoveStaleRowsTimeout = UrlbarView.removeStaleRowsTimeout;
+ UrlbarView.removeStaleRowsTimeout = 1000;
+ registerCleanupFunction(() => {
+ UrlbarView.removeStaleRowsTimeout = originalRemoveStaleRowsTimeout;
+ });
+});
+
+// This tests the case where queryContext.results.length < the number of rows in
+// the view, i.e., the view contains stale rows.
+add_task(async function viewContainsStaleRows() {
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ let maxResults = UrlbarPrefs.get("maxRichResults");
+ let halfResults = Math.floor(maxResults / 2);
+
+ // Add enough visits to pages with "xx" in the title to fill up half the view.
+ for (let i = 0; i < halfResults; i++) {
+ await PlacesTestUtils.addVisits({
+ uri: "http://mochi.test:8888/" + i,
+ title: "xx" + i,
+ });
+ }
+
+ // Add enough visits to pages with "x" in the title to fill up the entire
+ // view.
+ for (let i = 0; i < maxResults; i++) {
+ await PlacesTestUtils.addVisits({
+ uri: "http://example.com/" + i,
+ title: "x" + i,
+ });
+ }
+
+ gURLBar.focus();
+
+ // Search for "x" and wait for the search to finish. All the "x" results
+ // added above should be in the view. (Actually one fewer will be in the
+ // view due to the heuristic result, but that's not important.)
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "x",
+ fireInputEvent: true,
+ });
+
+ // Below we'll do a search for "xx". Get the row that will show the last
+ // result in that search.
+ let row = gURLBar.view._rows.children[halfResults];
+
+ // Add a mutation listener on that row. Wait for its "stale" attribute to be
+ // removed.
+ let mutationPromise = new Promise(resolve => {
+ let observer = new MutationObserver(mutations => {
+ for (let mut of mutations) {
+ if (mut.attributeName == "stale" && !row.hasAttribute("stale")) {
+ observer.disconnect();
+ resolve();
+ break;
+ }
+ }
+ });
+ observer.observe(row, { attributes: true });
+ });
+
+ // Type another "x" so that we search for "xx", but don't wait for the search
+ // to finish. Instead, wait for the row's stale attribute to be removed.
+ EventUtils.synthesizeKey("x");
+ info("Waiting for 'stale' attribute to be removed... ");
+ await mutationPromise;
+
+ // Now arrow down. The search, which is still ongoing, will now stop and the
+ // view won't be updated anymore.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+
+ // Wait for the search to stop.
+ info("Waiting for the search to stop... ");
+ await gURLBar.lastQueryContextPromise;
+
+ // The query context for the last search ("xx") should contain only
+ // halfResults + 1 results (+ 1 for the heuristic).
+ Assert.ok(gURLBar.controller._lastQueryContextWrapper);
+ let { queryContext } = gURLBar.controller._lastQueryContextWrapper;
+ Assert.ok(queryContext);
+ Assert.equal(queryContext.results.length, halfResults + 1);
+
+ // But there should be maxResults visible rows in the view.
+ let items = Array.from(gURLBar.view._rows.children).filter(r =>
+ gURLBar.view._isElementVisible(r)
+ );
+ Assert.equal(items.length, maxResults);
+
+ // Arrow down through all the results. After arrowing down from the last "xx"
+ // result, the stale "x" results should be selected. We should *not* enter
+ // the one-off search buttons at that point.
+ for (let i = 1; i < maxResults; i++) {
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), i);
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ Assert.equal(result.element.row.result.rowIndex, i);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+
+ // Now the first one-off should be selected.
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), -1);
+ Assert.equal(gURLBar.view.oneOffSearchButtons.selectedButtonIndex, 0);
+
+ // Arrow back up through all the results.
+ for (let i = maxResults - 1; i >= 0; i--) {
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), i);
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+});
+
+// This tests the case where, before the search finishes, stale results have
+// been removed and replaced with non-stale results.
+add_task(async function staleReplacedWithFresh() {
+ // For this test, we need one set of results that's added quickly and another
+ // set that's added after a delay. We do an initial search and wait for both
+ // sets to be added. Then we do another search, but this time only wait for
+ // the fast results to be added, and then we arrow down to stop the search
+ // before the delayed results are added. The order in which things should
+ // happen after the second search goes like this:
+ //
+ // (1) second search
+ // (2) fast results are added
+ // (3) remove-stale-rows timer fires and removes stale rows (the rows from
+ // the delayed set of results from the first search)
+ // (4) we arrow down to stop the search
+ //
+ // We use history for the fast results and a slow search engine for the
+ // delayed results.
+ //
+ // NB: If this test ends up failing, it may be because the remove-stale-rows
+ // timer fires before the history results are added. i.e., steps 2 and 3
+ // above happen out of order. If that happens, try increasing
+ // UrlbarView.removeStaleRowsTimeout above.
+
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ // Enable search suggestions, and add an engine that returns suggestions on a
+ // delay.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.searches", true]],
+ });
+ let engine = await SearchTestUtils.promiseNewSearchEngine(
+ getRootDirectory(gTestPath) + "searchSuggestionEngineSlow.xml"
+ );
+ let oldDefaultEngine = await Services.search.getDefault();
+ await Services.search.moveEngine(engine, 0);
+ await Services.search.setDefault(engine);
+
+ let maxResults = UrlbarPrefs.get("maxRichResults");
+
+ // Add enough visits to pages with "test" in the title to fill up the entire
+ // view.
+ for (let i = 0; i < maxResults; i++) {
+ await PlacesTestUtils.addVisits({
+ uri: "http://example.com/" + i,
+ title: "test" + i,
+ });
+ }
+
+ gURLBar.focus();
+
+ // Search for "tes" and wait for the search to finish.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "tes",
+ fireInputEvent: true,
+ });
+
+ // Sanity check the results. They should be:
+ //
+ // tes -- Search with searchSuggestionEngineSlow [heuristic]
+ // tesfoo [search suggestion]
+ // tesbar [search suggestion]
+ // test9 [history]
+ // test8 [history]
+ // test7 [history]
+ // test6 [history]
+ // test5 [history]
+ // test4 [history]
+ // test3 [history]
+ let count = UrlbarTestUtils.getResultCount(window);
+ Assert.equal(count, maxResults);
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(result.heuristic);
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.ok(result.searchParams);
+ Assert.equal(result.searchParams.suggestion, "tesfoo");
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2);
+ Assert.ok(result.searchParams);
+ Assert.equal(result.searchParams.suggestion, "tesbar");
+ for (let i = 3; i < maxResults; i++) {
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.URL);
+ Assert.equal(result.title, "test" + (maxResults - i + 2));
+ }
+
+ // Below we'll do a search for "test" *but* not wait for the two search
+ // suggestion results to be added. We'll only wait for the history results to
+ // be added. To determine when the history results are added, use a mutation
+ // listener on the node containing the rows, and wait until the title of the
+ // next-to-last row is "test2". At that point, the results should be:
+ //
+ // test -- Search with searchSuggestionEngineSlow
+ // test9
+ // test8
+ // test7
+ // test6
+ // test5
+ // test4
+ // test3
+ // test2
+ // test1
+ let mutationPromise = new Promise(resolve => {
+ let observer = new MutationObserver(mutations => {
+ let row = gURLBar.view._rows.children[maxResults - 2];
+ if (row && row._elements.get("title").textContent == "test2") {
+ observer.disconnect();
+ resolve();
+ }
+ });
+ observer.observe(gURLBar.view._rows, {
+ subtree: true,
+ characterData: true,
+ childList: true,
+ attributes: true,
+ });
+ });
+
+ // Now type a "t" so that we search for "test", but only wait for history
+ // results to be added, as described above.
+ EventUtils.synthesizeKey("t");
+ info("Waiting for the 'test2' row... ");
+ await mutationPromise;
+
+ // Now arrow down. The search, which is still ongoing, will now stop and the
+ // view won't be updated anymore.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+
+ // Wait for the search to stop.
+ info("Waiting for the search to stop... ");
+ await gURLBar.lastQueryContextPromise;
+
+ // Sanity check the results. They should be as described above.
+ count = UrlbarTestUtils.getResultCount(window);
+ Assert.equal(count, maxResults);
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(result.heuristic);
+ Assert.equal(result.element.row.result.rowIndex, 0);
+ for (let i = 1; i < maxResults; i++) {
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.URL);
+ Assert.equal(result.title, "test" + (maxResults - i));
+ Assert.equal(result.element.row.result.rowIndex, i);
+ }
+
+ // Arrow down through all the results. After arrowing down from "test3", we
+ // should continue on to "test2". We should *not* enter the one-off search
+ // buttons at that point.
+ for (let i = 1; i < maxResults; i++) {
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), i);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+
+ // Now the first one-off should be selected.
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), -1);
+ Assert.equal(gURLBar.view.oneOffSearchButtons.selectedButtonIndex, 0);
+
+ // Arrow back up through all the results.
+ for (let i = maxResults - 1; i >= 0; i--) {
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), i);
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+ await SpecialPowers.popPrefEnv();
+ await Services.search.setDefault(oldDefaultEngine);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_selectionKeyNavigation.js b/browser/components/urlbar/tests/browser/browser_selectionKeyNavigation.js
new file mode 100644
index 0000000000..99f0a41d7d
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_selectionKeyNavigation.js
@@ -0,0 +1,158 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test makes sure that the up/down and page-up/down properly adjust the
+// selection. See also browser_caret_navigation.js and
+// browser_urlbar_tabKeyBehavior.js.
+
+"use strict";
+
+const MAX_RESULTS = UrlbarPrefs.get("maxRichResults");
+
+add_task(async function init() {
+ for (let i = 0; i < MAX_RESULTS; i++) {
+ await PlacesTestUtils.addVisits("http://example.com/" + i);
+ }
+ registerCleanupFunction(async function() {
+ await PlacesUtils.history.clear();
+ });
+});
+
+add_task(async function downKey() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ fireInputEvent: true,
+ });
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 0,
+ "The heuristic autofill result should be selected initially"
+ );
+ for (let i = 1; i < MAX_RESULTS; i++) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), i);
+ }
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ let oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window);
+ Assert.ok(oneOffs.selectedButton, "A one-off should now be selected");
+ while (oneOffs.selectedButton) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 0,
+ "The heuristic autofill result should be selected again"
+ );
+});
+
+add_task(async function upKey() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ fireInputEvent: true,
+ });
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 0,
+ "The heuristic autofill result should be selected initially"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ let oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window);
+ Assert.ok(oneOffs.selectedButton, "A one-off should now be selected");
+ while (oneOffs.selectedButton) {
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ }
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ MAX_RESULTS - 1,
+ "The last result should be selected"
+ );
+ for (let i = 1; i < MAX_RESULTS; i++) {
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ MAX_RESULTS - i - 1
+ );
+ }
+});
+
+add_task(async function pageDownKey() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ fireInputEvent: true,
+ });
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 0,
+ "The heuristic autofill result should be selected initially"
+ );
+ let pageCount = Math.ceil((MAX_RESULTS - 1) / UrlbarUtils.PAGE_UP_DOWN_DELTA);
+ for (let i = 0; i < pageCount; i++) {
+ EventUtils.synthesizeKey("KEY_PageDown");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ Math.min((i + 1) * UrlbarUtils.PAGE_UP_DOWN_DELTA, MAX_RESULTS - 1)
+ );
+ }
+ EventUtils.synthesizeKey("KEY_PageDown");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 0,
+ "Page down at end should wrap around to first result"
+ );
+});
+
+add_task(async function pageUpKey() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ fireInputEvent: true,
+ });
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 0,
+ "The heuristic autofill result should be selected initially"
+ );
+ EventUtils.synthesizeKey("KEY_PageUp");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ MAX_RESULTS - 1,
+ "Page up at start should wrap around to last result"
+ );
+ let pageCount = Math.ceil((MAX_RESULTS - 1) / UrlbarUtils.PAGE_UP_DOWN_DELTA);
+ for (let i = 0; i < pageCount; i++) {
+ EventUtils.synthesizeKey("KEY_PageUp");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ Math.max(MAX_RESULTS - 1 - (i + 1) * UrlbarUtils.PAGE_UP_DOWN_DELTA, 0)
+ );
+ }
+});
+
+add_task(async function pageDownKeyShowsView() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+ EventUtils.synthesizeKey("KEY_PageDown");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ Assert.ok(UrlbarTestUtils.isPopupOpen(window));
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), 0);
+});
+
+add_task(async function pageUpKeyShowsView() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+ EventUtils.synthesizeKey("KEY_PageUp");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ Assert.ok(UrlbarTestUtils.isPopupOpen(window));
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), 0);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_separatePrivateDefault.js b/browser/components/urlbar/tests/browser/browser_separatePrivateDefault.js
new file mode 100644
index 0000000000..9d997f1663
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_separatePrivateDefault.js
@@ -0,0 +1,215 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the 'Search in a Private Window' result of the address bar.
+// Tests here don't have a different private engine, for that see
+// browser_separatePrivateDefault_differentPrivateEngine.js
+
+const serverInfo = {
+ scheme: "http",
+ host: "localhost",
+ port: 20709, // Must be identical to what is in searchSuggestionEngine2.xml
+};
+
+let aliasEngine;
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.search.separatePrivateDefault.ui.enabled", true],
+ ["browser.search.separatePrivateDefault", true],
+ ["browser.urlbar.suggest.searches", true],
+ ],
+ });
+
+ // Add some history for the empty panel.
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com/",
+ transition: PlacesUtils.history.TRANSITIONS.TYPED,
+ },
+ ]);
+
+ // Add a search suggestion engine and move it to the front so that it appears
+ // as the first one-off.
+ let oldDefaultEngine = await Services.search.getDefault();
+ let oldDefaultPrivateEngine = await Services.search.getDefaultPrivate();
+ let engine = await SearchTestUtils.promiseNewSearchEngine(
+ getRootDirectory(gTestPath) + "searchSuggestionEngine.xml"
+ );
+ await Services.search.setDefault(engine);
+ await Services.search.setDefaultPrivate(engine);
+
+ // Add another engine in the first one-off position.
+ let engine2 = await SearchTestUtils.promiseNewSearchEngine(
+ getRootDirectory(gTestPath) + "POSTSearchEngine.xml"
+ );
+ await Services.search.moveEngine(engine2, 0);
+
+ // Add an engine with an alias.
+ aliasEngine = await Services.search.addEngineWithDetails("MozSearch", {
+ alias: "alias",
+ method: "GET",
+ template: "http://example.com/?q={searchTerms}",
+ });
+
+ registerCleanupFunction(async () => {
+ await Services.search.setDefault(oldDefaultEngine);
+ await Services.search.setDefaultPrivate(oldDefaultPrivateEngine);
+ await Services.search.removeEngine(aliasEngine);
+ await PlacesUtils.history.clear();
+ });
+});
+
+async function AssertNoPrivateResult(win) {
+ let count = await UrlbarTestUtils.getResultCount(win);
+ Assert.ok(count > 0, "Sanity check result count");
+ for (let i = 0; i < count; ++i) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(win, i);
+ Assert.ok(
+ result.type != UrlbarUtils.RESULT_TYPE.SEARCH ||
+ !result.searchParams.inPrivateWindow,
+ "Check this result is not a 'Search in a Private Window' one"
+ );
+ }
+}
+
+async function AssertPrivateResult(win, engine, isPrivateEngine) {
+ let count = await UrlbarTestUtils.getResultCount(win);
+ Assert.ok(count > 1, "Sanity check result count");
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "Check result type"
+ );
+ Assert.ok(result.searchParams.inPrivateWindow, "Check inPrivateWindow");
+ Assert.equal(
+ result.searchParams.isPrivateEngine,
+ isPrivateEngine,
+ "Check isPrivateEngine"
+ );
+ Assert.equal(
+ result.searchParams.engine,
+ engine.name,
+ "Check the search engine"
+ );
+ return result;
+}
+
+add_task(async function test_nonsearch() {
+ info(
+ "Test that 'Search in a Private Window' does not appear with non-search results"
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exa",
+ });
+ await AssertNoPrivateResult(window);
+});
+
+add_task(async function test_search() {
+ info(
+ "Test that 'Search in a Private Window' appears with only search results"
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "unique198273982173",
+ });
+ await AssertPrivateResult(window, await Services.search.getDefault(), false);
+});
+
+add_task(async function test_search_disabled_suggestions() {
+ info(
+ "Test that 'Search in a Private Window' appears if suggestions are disabled"
+ );
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.searches", false]],
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "unique198273982173",
+ });
+ await AssertPrivateResult(window, await Services.search.getDefault(), false);
+ await SpecialPowers.popPrefEnv();
+});
+
+// TODO: (Bug 1658620) Write a new subtest for this behaviour with the update2
+// pref on.
+// add_task(async function test_oneoff_selected_keyboard() {
+// info(
+// "Test that 'Search in a Private Window' with keyboard opens the selected one-off engine if there's no private engine"
+// );
+// await SpecialPowers.pushPrefEnv({
+// set: [
+// ["browser.urlbar.update2", false],
+// ["browser.urlbar.update2.oneOffsRefresh", false],
+// ],
+// });
+// await UrlbarTestUtils.promiseAutocompleteResultPopup({
+// window,
+// value: "unique198273982173",
+// });
+// await AssertPrivateResult(window, await Services.search.getDefault(), false);
+// // Select the 'Search in a Private Window' result, alt down to select the
+// // first one-off button, Enter. It should open a pb window, but using the
+// // selected one-off engine.
+// let promiseWindow = BrowserTestUtils.waitForNewWindow({
+// url:
+// "http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/print_postdata.sjs",
+// });
+// // Select the private result.
+// EventUtils.synthesizeKey("KEY_ArrowDown");
+// // Select the first one-off button.
+// EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+// EventUtils.synthesizeKey("VK_RETURN");
+// let win = await promiseWindow;
+// Assert.ok(
+// PrivateBrowsingUtils.isWindowPrivate(win),
+// "Should open a private window"
+// );
+// await BrowserTestUtils.closeWindow(win);
+// await SpecialPowers.popPrefEnv();
+// });
+
+// TODO: (Bug 1658620) Write a new subtest for this behaviour with the update2
+// pref on.
+// add_task(async function test_oneoff_selected_mouse() {
+// info(
+// "Test that 'Search in a Private Window' with mouse opens the selected one-off engine if there's no private engine"
+// );
+// await SpecialPowers.pushPrefEnv({
+// set: [
+// ["browser.urlbar.update2", false],
+// ["browser.urlbar.update2.oneOffsRefresh", false],
+// ],
+// });
+// await UrlbarTestUtils.promiseAutocompleteResultPopup({
+// window,
+// value: "unique198273982173",
+// });
+// await AssertPrivateResult(window, await Services.search.getDefault(), false);
+// // Select the 'Search in a Private Window' result, alt down to select the
+// // first one-off button, Enter. It should open a pb window, but using the
+// // selected one-off engine.
+// let promiseWindow = BrowserTestUtils.waitForNewWindow({
+// url:
+// "http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/print_postdata.sjs",
+// });
+// // Select the private result.
+// EventUtils.synthesizeKey("KEY_ArrowDown");
+// // Select the first one-off button.
+// EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+// // Click on the result.
+// let element = UrlbarTestUtils.getSelectedRow(window);
+// EventUtils.synthesizeMouseAtCenter(element, {});
+// let win = await promiseWindow;
+// Assert.ok(
+// PrivateBrowsingUtils.isWindowPrivate(win),
+// "Should open a private window"
+// );
+// await BrowserTestUtils.closeWindow(win);
+// await SpecialPowers.popPrefEnv();
+// });
diff --git a/browser/components/urlbar/tests/browser/browser_separatePrivateDefault_differentEngine.js b/browser/components/urlbar/tests/browser/browser_separatePrivateDefault_differentEngine.js
new file mode 100644
index 0000000000..23d7712de1
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_separatePrivateDefault_differentEngine.js
@@ -0,0 +1,358 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the 'Search in a Private Window' result of the address bar.
+
+const serverInfo = {
+ scheme: "http",
+ host: "localhost",
+ port: 20709, // Must be identical to what is in searchSuggestionEngine2.xml
+};
+
+let gAliasEngine;
+let gPrivateEngine;
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.search.separatePrivateDefault.ui.enabled", true],
+ ["browser.search.separatePrivateDefault", true],
+ ["browser.urlbar.suggest.searches", true],
+ ],
+ });
+
+ // Add some history for the empty panel.
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com/",
+ transition: PlacesUtils.history.TRANSITIONS.TYPED,
+ },
+ ]);
+
+ // Add a search suggestion engine and move it to the front so that it appears
+ // as the first one-off.
+ let oldDefaultEngine = await Services.search.getDefault();
+ let oldDefaultPrivateEngine = await Services.search.getDefaultPrivate();
+ let engine = await SearchTestUtils.promiseNewSearchEngine(
+ getRootDirectory(gTestPath) + "searchSuggestionEngine.xml"
+ );
+ await Services.search.setDefault(engine);
+ gPrivateEngine = await SearchTestUtils.promiseNewSearchEngine(
+ getRootDirectory(gTestPath) + "searchSuggestionEngine2.xml"
+ );
+ await Services.search.setDefaultPrivate(gPrivateEngine);
+
+ // Add another engine in the first one-off position.
+ let engine2 = await SearchTestUtils.promiseNewSearchEngine(
+ getRootDirectory(gTestPath) + "POSTSearchEngine.xml"
+ );
+ await Services.search.moveEngine(engine2, 0);
+
+ // Add an engine with an alias.
+ gAliasEngine = await Services.search.addEngineWithDetails("MozSearch", {
+ alias: "alias",
+ method: "GET",
+ template: "http://example.com/?q={searchTerms}",
+ });
+
+ registerCleanupFunction(async () => {
+ await Services.search.setDefault(oldDefaultEngine);
+ await Services.search.setDefaultPrivate(oldDefaultPrivateEngine);
+ await Services.search.removeEngine(gAliasEngine);
+ await PlacesUtils.history.clear();
+ });
+});
+
+async function AssertNoPrivateResult(win) {
+ let count = await UrlbarTestUtils.getResultCount(win);
+ for (let i = 0; i < count; ++i) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(win, i);
+ Assert.ok(
+ result.type != UrlbarUtils.RESULT_TYPE.SEARCH ||
+ !result.searchParams.inPrivateWindow,
+ "Check this result is not a 'Search in a Private Window' one"
+ );
+ }
+}
+
+async function AssertPrivateResult(win, engine, isPrivateEngine) {
+ let count = await UrlbarTestUtils.getResultCount(win);
+ Assert.ok(count > 1, "Sanity check result count");
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "Check result type"
+ );
+ Assert.ok(result.searchParams.inPrivateWindow, "Check inPrivateWindow");
+ Assert.equal(
+ result.searchParams.isPrivateEngine,
+ isPrivateEngine,
+ "Check isPrivateEngine"
+ );
+ Assert.equal(
+ result.searchParams.engine,
+ engine.name,
+ "Check the search engine"
+ );
+ return result;
+}
+
+// Tests from here on have a different default private engine.
+
+add_task(async function test_search_private_engine() {
+ info(
+ "Test that 'Search in a Private Window' reports a separate private engine"
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "unique198273982173",
+ });
+ await AssertPrivateResult(window, gPrivateEngine, true);
+});
+
+add_task(async function test_privateWindow() {
+ info(
+ "Test that 'Search in a Private Window' does not appear in a private window"
+ );
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: privateWin,
+ value: "unique198273982173",
+ });
+ await AssertNoPrivateResult(privateWin);
+ await BrowserTestUtils.closeWindow(privateWin);
+});
+
+add_task(async function test_permanentPB() {
+ info(
+ "Test that 'Search in a Private Window' does not appear in Permanent Private Browsing"
+ );
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.privatebrowsing.autostart", true]],
+ });
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "unique198273982173",
+ });
+ await AssertNoPrivateResult(win);
+ await BrowserTestUtils.closeWindow(win);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_openPBWindow() {
+ info(
+ "Test that 'Search in a Private Window' opens the search in a new Private Window"
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "unique198273982173",
+ });
+ await AssertPrivateResult(
+ window,
+ await Services.search.getDefaultPrivate(),
+ true
+ );
+
+ await withHttpServer(serverInfo, async () => {
+ let promiseWindow = BrowserTestUtils.waitForNewWindow({
+ url: "http://localhost:20709/?terms=unique198273982173",
+ maybeErrorPage: true,
+ });
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("VK_RETURN");
+ let win = await promiseWindow;
+ Assert.ok(
+ PrivateBrowsingUtils.isWindowPrivate(win),
+ "Should open a private window"
+ );
+ await BrowserTestUtils.closeWindow(win);
+ });
+});
+
+// TODO: (Bug 1658620) Write a new subtest for this behaviour with the update2
+// pref on.
+// add_task(async function test_oneoff_selected_with_private_engine_mouse() {
+// info(
+// "Test that 'Search in a Private Window' opens the private engine even if a one-off is selected"
+// );
+// await SpecialPowers.pushPrefEnv({
+// set: [
+// ["browser.urlbar.update2", false],
+// ["browser.urlbar.update2.oneOffsRefresh", false],
+// ],
+// });
+// await UrlbarTestUtils.promiseAutocompleteResultPopup({
+// window,
+// value: "unique198273982173",
+// });
+// await AssertPrivateResult(
+// window,
+// await Services.search.getDefaultPrivate(),
+// true
+// );
+
+// await withHttpServer(serverInfo, async () => {
+// // Select the 'Search in a Private Window' result, alt down to select the
+// // first one-off button, Click on the result. It should open a pb window using
+// // the private search engine, because it has been set.
+// let promiseWindow = BrowserTestUtils.waitForNewWindow({
+// url: "http://localhost:20709/?terms=unique198273982173",
+// maybeErrorPage: true,
+// });
+// // Select the private result.
+// EventUtils.synthesizeKey("KEY_ArrowDown");
+// // Select the first one-off button.
+// EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+// // Click on the result.
+// let element = UrlbarTestUtils.getSelectedRow(window);
+// EventUtils.synthesizeMouseAtCenter(element, {});
+// let win = await promiseWindow;
+// Assert.ok(
+// PrivateBrowsingUtils.isWindowPrivate(win),
+// "Should open a private window"
+// );
+// await BrowserTestUtils.closeWindow(win);
+// });
+// await SpecialPowers.popPrefEnv();
+// });
+
+// TODO: (Bug 1658620) Write a new subtest for this behaviour with the update2
+// pref on.
+// add_task(async function test_oneoff_selected_with_private_engine_keyboard() {
+// info(
+// "Test that 'Search in a Private Window' opens the private engine even if a one-off is selected"
+// );
+// await SpecialPowers.pushPrefEnv({
+// set: [
+// ["browser.urlbar.update2", false],
+// ["browser.urlbar.update2.oneOffsRefresh", false],
+// ],
+// });
+// await UrlbarTestUtils.promiseAutocompleteResultPopup({
+// window,
+// value: "unique198273982173",
+// });
+// await AssertPrivateResult(
+// window,
+// await Services.search.getDefaultPrivate(),
+// true
+// );
+
+// await withHttpServer(serverInfo, async () => {
+// // Select the 'Search in a Private Window' result, alt down to select the
+// // first one-off button, Enter. It should open a pb window, but using the
+// // selected one-off engine.
+// let promiseWindow = BrowserTestUtils.waitForNewWindow({
+// url: "http://localhost:20709/?terms=unique198273982173",
+// maybeErrorPage: true,
+// });
+// // Select the private result.
+// EventUtils.synthesizeKey("KEY_ArrowDown");
+// // Select the first one-off button.
+// EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+// EventUtils.synthesizeKey("VK_RETURN");
+// let win = await promiseWindow;
+// Assert.ok(
+// PrivateBrowsingUtils.isWindowPrivate(win),
+// "Should open a private window"
+// );
+// await BrowserTestUtils.closeWindow(win);
+// });
+// await SpecialPowers.popPrefEnv();
+// });
+
+add_task(async function test_alias_no_query() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.update2.emptySearchBehavior", 2]],
+ });
+ info(
+ "Test that 'Search in a Private Window' doesn't appear if an alias is typed with no query"
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "alias ",
+ });
+ // Wait for the second new search that starts when search mode is entered.
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: gAliasEngine.name,
+ entry: "typed",
+ });
+ await AssertNoPrivateResult(window);
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_alias_query() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.update2.emptySearchBehavior", 2]],
+ });
+ info(
+ "Test that 'Search in a Private Window' appears when an alias is typed with a query"
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "alias something",
+ });
+ // Wait for the second new search that starts when search mode is entered.
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: "MozSearch",
+ entry: "typed",
+ });
+ await AssertPrivateResult(window, gAliasEngine, true);
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_restrict() {
+ info(
+ "Test that 'Search in a Private Window' doesn's appear for just the restriction token"
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: UrlbarTokenizer.RESTRICT.SEARCH,
+ });
+ await AssertNoPrivateResult(window);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: UrlbarTokenizer.RESTRICT.SEARCH + " ",
+ });
+ await AssertNoPrivateResult(window);
+ await UrlbarTestUtils.exitSearchMode(window);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: " " + UrlbarTokenizer.RESTRICT.SEARCH,
+ });
+ await AssertNoPrivateResult(window);
+});
+
+add_task(async function test_restrict_search() {
+ info(
+ "Test that 'Search in a Private Window' has the right string with the restriction token"
+ );
+ let engine = await Services.search.getDefaultPrivate();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: UrlbarTokenizer.RESTRICT.SEARCH + "test",
+ });
+ let result = await AssertPrivateResult(window, engine, true);
+ Assert.equal(result.searchParams.query, "test");
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test" + UrlbarTokenizer.RESTRICT.SEARCH,
+ });
+ result = await AssertPrivateResult(window, engine, true);
+ Assert.equal(result.searchParams.query, "test");
+});
diff --git a/browser/components/urlbar/tests/browser/browser_speculative_connect.js b/browser/components/urlbar/tests/browser/browser_speculative_connect.js
new file mode 100644
index 0000000000..2b4d132812
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_speculative_connect.js
@@ -0,0 +1,202 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// This test ensures that we setup a speculative network connection to
+// the site in various cases:
+// 1. search engine if it's the first result
+// 2. mousedown event before the http request happens(in mouseup).
+
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine2.xml";
+
+const serverInfo = {
+ scheme: "http",
+ host: "localhost",
+ port: 20709, // Must be identical to what is in searchSuggestionEngine2.xml
+};
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.autoFill", true],
+ ["browser.urlbar.suggest.searches", true],
+ ["browser.urlbar.speculativeConnect.enabled", true],
+ // In mochitest this number is 0 by default but we have to turn it on.
+ ["network.http.speculative-parallel-limit", 6],
+ // The http server is using IPv4, so it's better to disable IPv6 to avoid
+ // weird networking problem.
+ ["network.dns.disableIPv6", true],
+ ],
+ });
+
+ // Ensure we start from a clean situation.
+ await PlacesUtils.history.clear();
+
+ await PlacesTestUtils.addVisits([
+ {
+ uri: `${serverInfo.scheme}://${serverInfo.host}:${serverInfo.port}`,
+ title: "test visit for speculative connection",
+ transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ },
+ ]);
+
+ let engine = await SearchTestUtils.promiseNewSearchEngine(
+ getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME
+ );
+ let oldCurrentEngine = Services.search.defaultEngine;
+ Services.search.defaultEngine = engine;
+
+ registerCleanupFunction(async function() {
+ await PlacesUtils.history.clear();
+ Services.search.defaultEngine = oldCurrentEngine;
+ });
+});
+
+add_task(async function search_test() {
+ // We speculative connect to the search engine only if suggestions are enabled.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.suggest.enabled", true]],
+ });
+ await withHttpServer(serverInfo, async server => {
+ let connectionNumber = server.connectionNumber;
+ info("Searching for 'foo'");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ fireInputEvent: true,
+ });
+ // Check if the first result is with type "searchengine"
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ details.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "The first result is a search"
+ );
+ await UrlbarTestUtils.promiseSpeculativeConnections(
+ server,
+ connectionNumber + 1
+ );
+ });
+});
+
+add_task(async function popup_mousedown_test() {
+ // Disable search suggestions and autofill, to avoid other speculative
+ // connections.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.search.suggest.enabled", false],
+ ["browser.urlbar.autoFill", false],
+ ],
+ });
+ await withHttpServer(serverInfo, async server => {
+ let connectionNumber = server.connectionNumber;
+ let searchString = "ocal";
+ let completeValue = `${serverInfo.scheme}://${serverInfo.host}:${serverInfo.port}/`;
+ info(`Searching for '${searchString}'`);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: searchString,
+ fireInputEvent: true,
+ });
+ let listitem = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1);
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(
+ details.url,
+ completeValue,
+ "The second item has the url we visited."
+ );
+
+ info("Clicking on the second result");
+ EventUtils.synthesizeMouseAtCenter(listitem, { type: "mousedown" }, window);
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRow(window),
+ listitem,
+ "The second item is selected"
+ );
+ await UrlbarTestUtils.promiseSpeculativeConnections(
+ server,
+ connectionNumber + 1
+ );
+ });
+});
+
+add_task(async function test_autofill() {
+ // Disable search suggestions but enable autofill.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.search.suggest.enabled", false],
+ ["browser.urlbar.autoFill", true],
+ ],
+ });
+ await withHttpServer(serverInfo, async server => {
+ let connectionNumber = server.connectionNumber;
+ let searchString = serverInfo.host;
+ info(`Searching for '${searchString}'`);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: searchString,
+ fireInputEvent: true,
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ let completeValue = `${serverInfo.scheme}://${serverInfo.host}:${serverInfo.port}/`;
+ Assert.equal(details.url, completeValue, `Autofilled value is as expected`);
+ await UrlbarTestUtils.promiseSpeculativeConnections(
+ server,
+ connectionNumber + 1
+ );
+ });
+});
+
+add_task(async function test_autofill_privateContext() {
+ info("Autofill in private context.");
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ registerCleanupFunction(async () => {
+ let promisePBExit = TestUtils.topicObserved("last-pb-context-exited");
+ await BrowserTestUtils.closeWindow(privateWin);
+ await promisePBExit;
+ });
+ await withHttpServer(serverInfo, async server => {
+ let connectionNumber = server.connectionNumber;
+ let searchString = serverInfo.host;
+ info(`Searching for '${searchString}'`);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: privateWin,
+ value: searchString,
+ fireInputEvent: true,
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(privateWin, 0);
+ let completeValue = `${serverInfo.scheme}://${serverInfo.host}:${serverInfo.port}/`;
+ Assert.equal(details.url, completeValue, `Autofilled value is as expected`);
+ await UrlbarTestUtils.promiseSpeculativeConnections(
+ server,
+ connectionNumber
+ );
+ });
+});
+
+add_task(async function test_no_heuristic_result() {
+ info("Don't speculative connect on results addition if there's no heuristic");
+ await withHttpServer(serverInfo, async server => {
+ let connectionNumber = server.connectionNumber;
+ info(`Searching for the empty string`);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ fireInputEvent: true,
+ });
+ ok(UrlbarTestUtils.getResultCount(window) > 0, "Has results");
+ let result = await UrlbarTestUtils.getSelectedRow(window);
+ Assert.strictEqual(result, null, `Should have no selection`);
+ await UrlbarTestUtils.promiseSpeculativeConnections(
+ server,
+ connectionNumber
+ );
+ });
+});
diff --git a/browser/components/urlbar/tests/browser/browser_speculative_connect_not_with_client_cert.js b/browser/components/urlbar/tests/browser/browser_speculative_connect_not_with_client_cert.js
new file mode 100644
index 0000000000..958a239ec5
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_speculative_connect_not_with_client_cert.js
@@ -0,0 +1,219 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Tests that we don't speculatively connect when user certificates are installed
+
+const { MockRegistrar } = ChromeUtils.import(
+ "resource://testing-common/MockRegistrar.jsm"
+);
+
+const certService = Cc["@mozilla.org/security/local-cert-service;1"].getService(
+ Ci.nsILocalCertService
+);
+const certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+].getService(Ci.nsICertOverrideService);
+
+const host = "localhost";
+let uri;
+let handshakeDone = false;
+let expectingChooseCertificate = false;
+let chooseCertificateCalled = false;
+
+const clientAuthDialogs = {
+ chooseCertificate(
+ hostname,
+ port,
+ organization,
+ issuerOrg,
+ certList,
+ selectedIndex,
+ rememberClientAuthCertificate
+ ) {
+ ok(
+ expectingChooseCertificate,
+ `${
+ expectingChooseCertificate ? "" : "not "
+ }expecting chooseCertificate to be called`
+ );
+ is(certList.length, 1, "should have only one client certificate available");
+ selectedIndex.value = 0;
+ rememberClientAuthCertificate.value = false;
+ chooseCertificateCalled = true;
+ return true;
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIClientAuthDialogs"]),
+};
+
+function startServer(cert) {
+ let tlsServer = Cc["@mozilla.org/network/tls-server-socket;1"].createInstance(
+ Ci.nsITLSServerSocket
+ );
+ tlsServer.init(-1, true, -1);
+ tlsServer.serverCert = cert;
+
+ let input, output;
+
+ let listener = {
+ onSocketAccepted(socket, transport) {
+ info("Accepted TLS client connection");
+ let connectionInfo = transport.securityInfo.QueryInterface(
+ Ci.nsITLSServerConnectionInfo
+ );
+ connectionInfo.setSecurityObserver(listener);
+ input = transport.openInputStream(0, 0, 0);
+ output = transport.openOutputStream(0, 0, 0);
+ },
+
+ onHandshakeDone(socket, status) {
+ info("TLS handshake done");
+ handshakeDone = true;
+
+ input.asyncWait(
+ {
+ onInputStreamReady(readyInput) {
+ try {
+ let request = NetUtil.readInputStreamToString(
+ readyInput,
+ readyInput.available()
+ );
+ ok(
+ request.startsWith("GET /") && request.includes("HTTP/1.1"),
+ "expecting an HTTP/1.1 GET request"
+ );
+ let response =
+ "HTTP/1.1 200 OK\r\nContent-Type:text/plain\r\n" +
+ "Connection:Close\r\nContent-Length:2\r\n\r\nOK";
+ output.write(response, response.length);
+ } catch (e) {
+ // This will fail when we close the speculative connection.
+ }
+ },
+ },
+ 0,
+ 0,
+ Services.tm.currentThread
+ );
+ },
+
+ onStopListening() {
+ info("onStopListening");
+ input.close();
+ output.close();
+ },
+ };
+
+ tlsServer.setSessionTickets(false);
+ tlsServer.setRequestClientCertificate(Ci.nsITLSServerSocket.REQUEST_ALWAYS);
+
+ tlsServer.asyncListen(listener);
+
+ return tlsServer;
+}
+
+let server;
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.autoFill", true],
+ // Turn off search suggestion so we won't speculative connect to the search engine.
+ ["browser.search.suggest.enabled", false],
+ ["browser.urlbar.speculativeConnect.enabled", true],
+ // In mochitest this number is 0 by default but we have to turn it on.
+ ["network.http.speculative-parallel-limit", 6],
+ // The http server is using IPv4, so it's better to disable IPv6 to avoid weird
+ // networking problem.
+ ["network.dns.disableIPv6", true],
+ ["security.default_personal_cert", "Ask Every Time"],
+ ],
+ });
+
+ let clientAuthDialogsCID = MockRegistrar.register(
+ "@mozilla.org/nsClientAuthDialogs;1",
+ clientAuthDialogs
+ );
+
+ let cert = await new Promise((resolve, reject) => {
+ certService.getOrCreateCert("speculative-connect", {
+ handleCert(c, rv) {
+ if (!Components.isSuccessCode(rv)) {
+ reject(rv);
+ return;
+ }
+ resolve(c);
+ },
+ });
+ });
+ server = startServer(cert);
+ uri = `https://${host}:${server.port}/`;
+ info(`running tls server at ${uri}`);
+ await PlacesTestUtils.addVisits([
+ {
+ uri,
+ title: "test visit for speculative connection",
+ transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ },
+ ]);
+
+ let overrideBits =
+ Ci.nsICertOverrideService.ERROR_UNTRUSTED |
+ Ci.nsICertOverrideService.ERROR_MISMATCH;
+ certOverrideService.rememberValidityOverride(
+ "localhost",
+ server.port,
+ cert,
+ overrideBits,
+ true
+ );
+
+ registerCleanupFunction(async function() {
+ await PlacesUtils.history.clear();
+ MockRegistrar.unregister(clientAuthDialogsCID);
+ certOverrideService.clearValidityOverride("localhost", server.port);
+ });
+});
+
+add_task(
+ async function popup_mousedown_no_client_cert_dialog_until_navigate_test() {
+ // To not trigger autofill, search keyword starts from the second character.
+ let searchString = host.substr(1, 4);
+ let completeValue = uri;
+ info(`Searching for '${searchString}'`);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: searchString,
+ fireInputEvent: true,
+ });
+ let listitem = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1);
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ info(`The url of the second item is ${details.url}`);
+ is(details.url, completeValue, "The second item has the url we visited.");
+
+ expectingChooseCertificate = false;
+ EventUtils.synthesizeMouseAtCenter(listitem, { type: "mousedown" }, window);
+ is(
+ UrlbarTestUtils.getSelectedRow(window),
+ listitem,
+ "The second item is selected"
+ );
+
+ // We shouldn't have triggered a speculative connection, because a client
+ // certificate is installed.
+ SimpleTest.requestFlakyTimeout("Wait for UI");
+ await new Promise(resolve => setTimeout(resolve, 200));
+
+ // Now mouseup, expect that we choose a client certificate, and expect that
+ // we successfully load a page.
+ expectingChooseCertificate = true;
+ EventUtils.synthesizeMouseAtCenter(listitem, { type: "mouseup" }, window);
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ ok(chooseCertificateCalled, "chooseCertificate must have been called");
+ server.close();
+ }
+);
diff --git a/browser/components/urlbar/tests/browser/browser_stop.js b/browser/components/urlbar/tests/browser/browser_stop.js
new file mode 100644
index 0000000000..1bc011a31c
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_stop.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This tests ensures the urlbar reflects the correct value if a page load is
+ * stopped immediately after loading.
+ */
+
+"use strict";
+
+const goodURL = "http://mochi.test:8888/";
+const badURL = "http://mochi.test:8888/whatever.html";
+
+add_task(async function() {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, goodURL);
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ is(
+ gURLBar.value,
+ BrowserUtils.trimURL(goodURL),
+ "location bar reflects loaded page"
+ );
+
+ await typeAndSubmitAndStop(badURL);
+ is(
+ gURLBar.value,
+ BrowserUtils.trimURL(goodURL),
+ "location bar reflects loaded page after stop()"
+ );
+ gBrowser.removeCurrentTab();
+
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank");
+ is(gURLBar.value, "", "location bar is empty");
+
+ await typeAndSubmitAndStop(badURL);
+ is(
+ gURLBar.value,
+ BrowserUtils.trimURL(badURL),
+ "location bar reflects stopped page in an empty tab"
+ );
+ gBrowser.removeCurrentTab();
+});
+
+async function typeAndSubmitAndStop(url) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: url,
+ fireInputEvent: true,
+ });
+
+ let docLoadPromise = BrowserTestUtils.waitForDocLoadAndStopIt(
+ url,
+ gBrowser.selectedBrowser
+ );
+
+ // When the load is stopped, tabbrowser calls gURLBar.setURI and then calls
+ // onStateChange on its progress listeners. So to properly wait until the
+ // urlbar value has been updated, add our own progress listener here.
+ let progressPromise = new Promise(resolve => {
+ let listener = {
+ onStateChange(browser, webProgress, request, stateFlags, status) {
+ if (
+ webProgress.isTopLevel &&
+ stateFlags & Ci.nsIWebProgressListener.STATE_STOP
+ ) {
+ gBrowser.removeTabsProgressListener(listener);
+ resolve();
+ }
+ },
+ };
+ gBrowser.addTabsProgressListener(listener);
+ });
+
+ gURLBar.handleCommand();
+ await Promise.all([docLoadPromise, progressPromise]);
+}
diff --git a/browser/components/urlbar/tests/browser/browser_stopSearchOnSelection.js b/browser/components/urlbar/tests/browser/browser_stopSearchOnSelection.js
new file mode 100644
index 0000000000..3fc05a196f
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_stopSearchOnSelection.js
@@ -0,0 +1,115 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This tests that when a search is stopped due to the user selecting a result,
+ * the view doesn't update after that.
+ */
+
+"use strict";
+
+const TEST_ENGINE_BASENAME = "searchSuggestionEngineSlow.xml";
+
+// This should match the `timeout` query param used in the suggestions URL in
+// the test engine.
+const TEST_ENGINE_SUGGESTIONS_TIMEOUT = 3000;
+
+// The number of suggestions returned by the test engine.
+const TEST_ENGINE_NUM_EXPECTED_RESULTS = 2;
+
+add_task(async function init() {
+ await PlacesUtils.history.clear();
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.searches", true]],
+ });
+ // Add a test search engine that returns suggestions on a delay.
+ let engine = await SearchTestUtils.promiseNewSearchEngine(
+ getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME
+ );
+ let oldDefaultEngine = await Services.search.getDefault();
+ await Services.search.moveEngine(engine, 0);
+ await Services.search.setDefault(engine);
+ registerCleanupFunction(async () => {
+ await Services.search.setDefault(oldDefaultEngine);
+ await PlacesUtils.history.clear();
+ });
+});
+
+add_task(async function mainTest() {
+ // Open a tab that will match the search string below so that we're guaranteed
+ // to have more than one result (the heuristic result) so that we can change
+ // the selected result. We open a tab instead of adding a page in history
+ // because open tabs are kept in a memory SQLite table, so open-tab results
+ // are more likely than history results to be fetched before our slow search
+ // suggestions. This is important when the test runs on slow debug builds on
+ // slow machines.
+ await BrowserTestUtils.withNewTab("http://example.com/", async () => {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ // Do an initial search. There should be 4 results: heuristic, open tab,
+ // and the two suggestions.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "amp",
+ });
+ await TestUtils.waitForCondition(() => {
+ return (
+ UrlbarTestUtils.getResultCount(window) ==
+ 2 + TEST_ENGINE_NUM_EXPECTED_RESULTS
+ );
+ });
+
+ // Type a character to start a new search. The new search should still
+ // match the open tab so that the open-tab result appears again.
+ EventUtils.synthesizeKey("l");
+
+ // There should be 2 results immediately: heuristic and open tab.
+ await TestUtils.waitForCondition(() => {
+ return UrlbarTestUtils.getResultCount(window) == 2;
+ });
+
+ // Before the search completes, change the selected result. Pressing only
+ // the down arrow key ends up selecting the first one-off on Linux debug
+ // builds on the infrastructure for some reason, so arrow back up to
+ // select the heuristic result again. The important thing is to change
+ // the selection. It doesn't matter which result ends up selected.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+
+ // Wait for the new search to complete. It should be canceled due to the
+ // selection change, but it should still complete.
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ // To make absolutely sure the suggestions don't appear after the search
+ // completes, wait a bit.
+ await new Promise(r =>
+ setTimeout(r, 1 + TEST_ENGINE_SUGGESTIONS_TIMEOUT)
+ );
+
+ // The heuristic result should reflect the new search, "ampl".
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "Should have the correct result type"
+ );
+ Assert.equal(
+ result.searchParams.query,
+ "ampl",
+ "Should have the correct query"
+ );
+
+ // None of the other results should be "ampl" suggestions, i.e., amplfoo
+ // and amplbar should not be in the results.
+ let count = UrlbarTestUtils.getResultCount(window);
+ for (let i = 1; i < count; i++) {
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ Assert.ok(
+ result.type != UrlbarUtils.RESULT_TYPE.SEARCH ||
+ !["amplfoo", "amplbar"].includes(result.searchParams.suggestion),
+ "Suggestions should not contain the typed l char"
+ );
+ }
+ });
+ });
+});
diff --git a/browser/components/urlbar/tests/browser/browser_stop_pending.js b/browser/components/urlbar/tests/browser/browser_stop_pending.js
new file mode 100644
index 0000000000..3b9c509f95
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_stop_pending.js
@@ -0,0 +1,220 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+"use strict";
+
+const SLOW_PAGE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://www.example.com"
+ ) + "slow-page.sjs";
+const SLOW_PAGE2 =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://mochi.test:8888"
+ ) + "slow-page.sjs?faster";
+
+/**
+ * Check that if we:
+ * 1) have a loaded page
+ * 2) load a separate URL
+ * 3) before the URL for step 2 has finished loading, load a third URL
+ * we don't revert to the URL from (1).
+ */
+add_task(async function() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com",
+ true,
+ true
+ );
+
+ let initialValue = gURLBar.untrimmedValue;
+ let expectedURLBarChange = SLOW_PAGE;
+ let sawChange = false;
+ let handler = () => {
+ isnot(
+ gURLBar.untrimmedValue,
+ initialValue,
+ "Should not revert URL bar value!"
+ );
+ if (gURLBar.getAttribute("pageproxystate") == "valid") {
+ sawChange = true;
+ is(
+ gURLBar.untrimmedValue,
+ expectedURLBarChange,
+ "Should set expected URL bar value!"
+ );
+ }
+ };
+
+ let obs = new MutationObserver(handler);
+
+ obs.observe(gURLBar.textbox, { attributes: true });
+ gURLBar.value = SLOW_PAGE;
+ gURLBar.handleCommand();
+
+ // If this ever starts going intermittent, we've broken this.
+ await new Promise(resolve => setTimeout(resolve, 200));
+ expectedURLBarChange = SLOW_PAGE2;
+ let pageLoadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ gURLBar.value = expectedURLBarChange;
+ gURLBar.handleCommand();
+ is(
+ gURLBar.untrimmedValue,
+ expectedURLBarChange,
+ "Should not have changed URL bar value synchronously."
+ );
+ await pageLoadPromise;
+ ok(
+ sawChange,
+ "The URL bar change handler should have been called by the time the page was loaded"
+ );
+ obs.disconnect();
+ obs = null;
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * Check that if we:
+ * 1) middle-click a link to a separate page whose server doesn't respond
+ * 2) we switch to that tab and stop the request
+ *
+ * The URL bar continues to contain the URL of the page we wanted to visit.
+ */
+add_task(async function() {
+ let socket = Cc["@mozilla.org/network/server-socket;1"].createInstance(
+ Ci.nsIServerSocket
+ );
+ socket.init(-1, true, -1);
+ const PORT = socket.port;
+ registerCleanupFunction(() => {
+ socket.close();
+ });
+
+ const BASE_PAGE = TEST_BASE_URL + "dummy_page.html";
+ const SLOW_HOST = `https://localhost:${PORT}/`;
+ info("Using URLs: " + SLOW_HOST);
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_PAGE);
+ info("opened tab");
+ await SpecialPowers.spawn(tab.linkedBrowser, [SLOW_HOST], URL => {
+ let link = content.document.createElement("a");
+ link.href = URL;
+ link.textContent = "click me to open a slow page";
+ link.id = "clickme";
+ content.document.body.appendChild(link);
+ });
+ info("added link");
+ let newTabPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabOpen"
+ );
+ // Middle click the link:
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#clickme",
+ { button: 1 },
+ tab.linkedBrowser
+ );
+ // get new tab, switch to it
+ let newTab = (await newTabPromise).target;
+ await BrowserTestUtils.switchTab(gBrowser, newTab);
+ is(gURLBar.untrimmedValue, SLOW_HOST, "Should have slow page in URL bar");
+ let browserStoppedPromise = BrowserTestUtils.browserStopped(
+ newTab.linkedBrowser,
+ null,
+ true
+ );
+ BrowserStop();
+ await browserStoppedPromise;
+
+ is(
+ gURLBar.untrimmedValue,
+ SLOW_HOST,
+ "Should still have slow page in URL bar after stop"
+ );
+ BrowserTestUtils.removeTab(newTab);
+ BrowserTestUtils.removeTab(tab);
+});
+/**
+ * Check that if we:
+ * 1) middle-click a link to a separate page whose server doesn't respond
+ * 2) we alter the URL on that page to some other server that doesn't respond
+ * 3) we stop the request
+ *
+ * The URL bar continues to contain the second URL.
+ */
+add_task(async function() {
+ let socket = Cc["@mozilla.org/network/server-socket;1"].createInstance(
+ Ci.nsIServerSocket
+ );
+ socket.init(-1, true, -1);
+ const PORT1 = socket.port;
+ let socket2 = Cc["@mozilla.org/network/server-socket;1"].createInstance(
+ Ci.nsIServerSocket
+ );
+ socket2.init(-1, true, -1);
+ const PORT2 = socket2.port;
+ registerCleanupFunction(() => {
+ socket.close();
+ socket2.close();
+ });
+
+ const BASE_PAGE = TEST_BASE_URL + "dummy_page.html";
+ const SLOW_HOST1 = `https://localhost:${PORT1}/`;
+ const SLOW_HOST2 = `https://localhost:${PORT2}/`;
+ info("Using URLs: " + SLOW_HOST1 + " and " + SLOW_HOST2);
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_PAGE);
+ info("opened tab");
+ await SpecialPowers.spawn(tab.linkedBrowser, [SLOW_HOST1], URL => {
+ let link = content.document.createElement("a");
+ link.href = URL;
+ link.textContent = "click me to open a slow page";
+ link.id = "clickme";
+ content.document.body.appendChild(link);
+ });
+ info("added link");
+ let newTabPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabOpen"
+ );
+ // Middle click the link:
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#clickme",
+ { button: 1 },
+ tab.linkedBrowser
+ );
+ // get new tab, switch to it
+ let newTab = (await newTabPromise).target;
+ await BrowserTestUtils.switchTab(gBrowser, newTab);
+ is(gURLBar.untrimmedValue, SLOW_HOST1, "Should have slow page in URL bar");
+ let browserStoppedPromise = BrowserTestUtils.browserStopped(
+ newTab.linkedBrowser,
+ null,
+ true
+ );
+ gURLBar.value = SLOW_HOST2;
+ gURLBar.handleCommand();
+ await browserStoppedPromise;
+
+ is(
+ gURLBar.untrimmedValue,
+ SLOW_HOST2,
+ "Should have second slow page in URL bar"
+ );
+ browserStoppedPromise = BrowserTestUtils.browserStopped(
+ newTab.linkedBrowser,
+ null,
+ true
+ );
+ BrowserStop();
+ await browserStoppedPromise;
+
+ is(
+ gURLBar.untrimmedValue,
+ SLOW_HOST2,
+ "Should still have second slow page in URL bar after stop"
+ );
+ BrowserTestUtils.removeTab(newTab);
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_suggestedIndex.js b/browser/components/urlbar/tests/browser/browser_suggestedIndex.js
new file mode 100644
index 0000000000..563202036a
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_suggestedIndex.js
@@ -0,0 +1,120 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that results with a suggestedIndex property end up in the expected
+// position.
+
+add_task(async function suggestedIndex() {
+ let result1 = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/1" }
+ );
+ result1.suggestedIndex = 2;
+ let result2 = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/2" }
+ );
+ result2.suggestedIndex = 6;
+
+ let provider = new UrlbarTestUtils.TestProvider({
+ results: [result1, result2],
+ });
+ UrlbarProvidersManager.registerProvider(provider);
+ async function clean() {
+ UrlbarProvidersManager.unregisterProvider(provider);
+ await PlacesUtils.history.clear();
+ }
+ registerCleanupFunction(clean);
+
+ let urls = [];
+ let maxResults = UrlbarPrefs.get("maxRichResults");
+ // Add more results, so that the sum of these results plus the above ones,
+ // will be greater than maxResults.
+ for (let i = 0; i < maxResults; ++i) {
+ urls.push("http://example.com/foo" + i);
+ }
+ await PlacesTestUtils.addVisits(urls);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ });
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ maxResults,
+ `There should be ${maxResults} results in the view.`
+ );
+
+ urls.reverse();
+ urls.unshift(
+ (await Services.search.getDefault()).getSubmission("foo").uri.spec
+ );
+ urls.splice(result1.suggestedIndex, 0, result1.payload.url);
+ urls.splice(result2.suggestedIndex, 0, result2.payload.url);
+ urls = urls.slice(0, maxResults);
+
+ let expected = [];
+ for (let i = 0; i < maxResults; ++i) {
+ let url = (await UrlbarTestUtils.getDetailsOfResultAt(window, i)).url;
+ expected.push(url);
+ }
+ // Check all the results.
+ Assert.deepEqual(expected, urls);
+
+ await clean();
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+add_task(async function suggestedIndex_append() {
+ // When suggestedIndex is greater than the number of results the result is
+ // appended.
+ let result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/append/" }
+ );
+ result.suggestedIndex = 4;
+
+ let provider = new UrlbarTestUtils.TestProvider({ results: [result] });
+ UrlbarProvidersManager.registerProvider(provider);
+ async function clean() {
+ UrlbarProvidersManager.unregisterProvider(provider);
+ await PlacesUtils.history.clear();
+ }
+ registerCleanupFunction(clean);
+
+ await PlacesTestUtils.addVisits("http://example.com/bar");
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "bar",
+ });
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 3,
+ `There should be 3 results in the view.`
+ );
+
+ let urls = [
+ (await Services.search.getDefault()).getSubmission("bar").uri.spec,
+ "http://example.com/bar",
+ "http://mozilla.org/append/",
+ ];
+
+ let expected = [];
+ for (let i = 0; i < 3; ++i) {
+ let url = (await UrlbarTestUtils.getDetailsOfResultAt(window, i)).url;
+ expected.push(url);
+ }
+ // Check all the results.
+ Assert.deepEqual(expected, urls);
+
+ await clean();
+ await UrlbarTestUtils.promisePopupClose(window);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_switchTab_closesUrlbarPopup.js b/browser/components/urlbar/tests/browser/browser_switchTab_closesUrlbarPopup.js
new file mode 100644
index 0000000000..d172176494
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_switchTab_closesUrlbarPopup.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Checks that switching tabs closes the urlbar popup.
+ */
+
+"use strict";
+
+add_task(async function() {
+ let tab1 = BrowserTestUtils.addTab(gBrowser);
+ let tab2 = BrowserTestUtils.addTab(gBrowser);
+
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+ });
+
+ // Add a couple of dummy entries to ensure the history popup will open.
+ await PlacesTestUtils.addVisits([
+ { uri: makeURI("http://example.com/foo") },
+ { uri: makeURI("http://example.com/foo/bar") },
+ ]);
+
+ // When urlbar in a new tab is focused, and a tab switch occurs,
+ // the urlbar popup should be closed
+ await BrowserTestUtils.switchTab(gBrowser, tab2);
+ gURLBar.focus(); // focus the urlbar in the tab we will switch to
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ // Now open the popup.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ // Check that the popup closes when we switch tab.
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ return BrowserTestUtils.switchTab(gBrowser, tab2);
+ });
+ Assert.ok(true, "Popup was successfully closed");
+});
diff --git a/browser/components/urlbar/tests/browser/browser_switchTab_currentTab.js b/browser/components/urlbar/tests/browser/browser_switchTab_currentTab.js
new file mode 100644
index 0000000000..eccee800e3
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_switchTab_currentTab.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This test ensures that switch to tab still works when the URI contains an
+ * encoded part.
+ */
+
+"use strict";
+
+add_task(async function test_switchTab_currentTab() {
+ registerCleanupFunction(PlacesUtils.history.clear);
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:robots#1" },
+ async () => {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:robots#2" },
+ async () => {
+ let context = await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "robot",
+ });
+ Assert.ok(
+ context.results.some(
+ result =>
+ result.type == UrlbarUtils.RESULT_TYPE.TAB_SWITCH &&
+ result.payload.url == "about:robots#1"
+ )
+ );
+ Assert.ok(
+ !context.results.some(
+ result =>
+ result.type == UrlbarUtils.RESULT_TYPE.TAB_SWITCH &&
+ result.payload.url == "about:robots#2"
+ )
+ );
+ }
+ );
+ }
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_switchTab_decodeuri.js b/browser/components/urlbar/tests/browser/browser_switchTab_decodeuri.js
new file mode 100644
index 0000000000..fe23eceaf9
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_switchTab_decodeuri.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This test ensures that switch to tab still works when the URI contains an
+ * encoded part.
+ */
+
+"use strict";
+
+const TEST_URL = `${TEST_BASE_URL}dummy_page.html#test%7C1`;
+
+add_task(async function test_switchtab_decodeuri() {
+ info("Opening first tab");
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+ info("Opening and selecting second tab");
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+
+ info("Wait for autocomplete");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "dummy_page",
+ });
+
+ info("Select autocomplete popup entry");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ UrlbarTestUtils.getSelectedRowIndex(window)
+ );
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.TAB_SWITCH);
+
+ info("switch-to-tab");
+ let tabSelectPromise = BrowserTestUtils.waitForEvent(
+ window,
+ "TabSelect",
+ false
+ );
+ EventUtils.synthesizeKey("KEY_Enter");
+ await tabSelectPromise;
+
+ Assert.equal(
+ gBrowser.selectedTab,
+ tab,
+ "Should have switched to the right tab"
+ );
+
+ gBrowser.removeCurrentTab();
+ await PlacesUtils.history.clear();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_switchTab_override.js b/browser/components/urlbar/tests/browser/browser_switchTab_override.js
new file mode 100644
index 0000000000..dabce43612
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_switchTab_override.js
@@ -0,0 +1,100 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This test ensures that overriding switch-to-tab correctly loads the page
+ * rather than switching to it.
+ */
+
+"use strict";
+
+const TEST_URL = `${TEST_BASE_URL}dummy_page.html`;
+
+add_task(async function test_switchtab_override() {
+ info("Opening first tab");
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+ info("Opening and selecting second tab");
+ let secondTab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ registerCleanupFunction(() => {
+ try {
+ gBrowser.removeTab(tab);
+ gBrowser.removeTab(secondTab);
+ } catch (ex) {
+ /* tabs may have already been closed in case of failure */
+ }
+ });
+
+ info("Wait for autocomplete");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "dummy_page",
+ });
+
+ info("Select second autocomplete popup entry");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ UrlbarTestUtils.getSelectedRowIndex(window)
+ );
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.TAB_SWITCH);
+
+ // Check to see if the switchtab label is visible and
+ // all other labels are hidden
+ const allLabels = document.getElementById("urlbar-label-box").children;
+ for (let label of allLabels) {
+ if (label.id == "urlbar-label-switchtab") {
+ Assert.ok(BrowserTestUtils.is_visible(label));
+ } else {
+ Assert.ok(BrowserTestUtils.is_hidden(label));
+ }
+ }
+
+ info("Override switch-to-tab");
+ let deferred = PromiseUtils.defer();
+ // In case of failure this would switch tab.
+ let onTabSelect = event => {
+ deferred.reject(new Error("Should have overridden switch to tab"));
+ };
+ gBrowser.tabContainer.addEventListener("TabSelect", onTabSelect);
+ registerCleanupFunction(() => {
+ gBrowser.tabContainer.removeEventListener("TabSelect", onTabSelect);
+ });
+ // Otherwise it would load the page.
+ BrowserTestUtils.browserLoaded(secondTab.linkedBrowser).then(
+ deferred.resolve
+ );
+
+ EventUtils.synthesizeKey("KEY_Shift", { type: "keydown" });
+
+ // Checks that all labels are hidden when Shift is held down on the SwitchToTab result
+ for (let label of allLabels) {
+ Assert.ok(BrowserTestUtils.is_hidden(label));
+ }
+
+ registerCleanupFunction(() => {
+ // Avoid confusing next tests by leaving a pending keydown.
+ EventUtils.synthesizeKey("KEY_Shift", { type: "keyup" });
+ });
+
+ let attribute = "actionoverride";
+ Assert.ok(
+ gURLBar.view.panel.hasAttribute(attribute),
+ "We should be overriding"
+ );
+
+ EventUtils.synthesizeKey("KEY_Enter");
+ info(`gURLBar.value = ${gURLBar.value}`);
+ await deferred.promise;
+
+ // Blurring the urlbar should have cleared the override.
+ Assert.ok(
+ !gURLBar.view.panel.hasAttribute(attribute),
+ "We should not be overriding anymore"
+ );
+
+ await PlacesUtils.history.clear();
+ gBrowser.removeTab(tab);
+ gBrowser.removeTab(secondTab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_switchToTabHavingURI_aOpenParams.js b/browser/components/urlbar/tests/browser/browser_switchToTabHavingURI_aOpenParams.js
new file mode 100644
index 0000000000..6f9722776b
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_switchToTabHavingURI_aOpenParams.js
@@ -0,0 +1,216 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+add_task(async function test_ignoreFragment() {
+ let tabRefAboutHome = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:home#1"
+ );
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla");
+ let numTabsAtStart = gBrowser.tabs.length;
+
+ switchTab("about:home#1", true);
+ switchTab("about:mozilla", true);
+
+ let hashChangePromise = ContentTask.spawn(
+ tabRefAboutHome.linkedBrowser,
+ [],
+ async function() {
+ await ContentTaskUtils.waitForEvent(this, "hashchange", true);
+ }
+ );
+ switchTab("about:home#2", true, {
+ ignoreFragment: "whenComparingAndReplace",
+ });
+ is(
+ tabRefAboutHome,
+ gBrowser.selectedTab,
+ "The same about:home tab should be switched to"
+ );
+ await hashChangePromise;
+ is(gBrowser.currentURI.ref, "2", "The ref should be updated to the new ref");
+ switchTab("about:mozilla", true);
+ switchTab("about:home#3", true, { ignoreFragment: "whenComparing" });
+ is(
+ tabRefAboutHome,
+ gBrowser.selectedTab,
+ "The same about:home tab should be switched to"
+ );
+ is(
+ gBrowser.currentURI.ref,
+ "2",
+ "The ref should be unchanged since the fragment is only ignored when comparing"
+ );
+ switchTab("about:mozilla", true);
+ switchTab("about:home#1", false);
+ isnot(
+ tabRefAboutHome,
+ gBrowser.selectedTab,
+ "Selected tab should not be initial about:blank tab"
+ );
+ is(
+ gBrowser.tabs.length,
+ numTabsAtStart + 1,
+ "Should have one new tab opened"
+ );
+ switchTab("about:mozilla", true);
+ switchTab("about:home", true, { ignoreFragment: "whenComparingAndReplace" });
+ await BrowserTestUtils.waitForCondition(function() {
+ return tabRefAboutHome.linkedBrowser.currentURI.spec == "about:home";
+ });
+ is(
+ tabRefAboutHome.linkedBrowser.currentURI.spec,
+ "about:home",
+ "about:home shouldn't have hash"
+ );
+ switchTab("about:about", false, {
+ ignoreFragment: "whenComparingAndReplace",
+ });
+ cleanupTestTabs();
+});
+
+add_task(async function test_ignoreQueryString() {
+ let tabRefAboutHome = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:home?hello=firefox"
+ );
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla");
+
+ switchTab("about:home?hello=firefox", true);
+ switchTab("about:home?hello=firefoxos", false);
+ // Remove the last opened tab to test ignoreQueryString option.
+ gBrowser.removeCurrentTab();
+ switchTab("about:home?hello=firefoxos", true, { ignoreQueryString: true });
+ is(
+ tabRefAboutHome,
+ gBrowser.selectedTab,
+ "Selected tab should be the initial about:home tab"
+ );
+ is(
+ gBrowser.currentURI.spec,
+ "about:home?hello=firefox",
+ "The spec should NOT be updated to the new query string"
+ );
+ cleanupTestTabs();
+});
+
+add_task(async function test_replaceQueryString() {
+ let tabRefAboutHome = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:home?hello=firefox"
+ );
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla");
+
+ switchTab("about:home", false);
+ switchTab("about:home?hello=firefox", true);
+ switchTab("about:home?hello=firefoxos", false);
+ // Remove the last opened tab to test replaceQueryString option.
+ gBrowser.removeCurrentTab();
+ switchTab("about:home?hello=firefoxos", true, { replaceQueryString: true });
+ is(
+ tabRefAboutHome,
+ gBrowser.selectedTab,
+ "Selected tab should be the initial about:home tab"
+ );
+ // Wait for the tab to load the new URI spec.
+ await BrowserTestUtils.browserLoaded(tabRefAboutHome.linkedBrowser);
+ is(
+ gBrowser.currentURI.spec,
+ "about:home?hello=firefoxos",
+ "The spec should be updated to the new spec"
+ );
+ cleanupTestTabs();
+});
+
+add_task(async function test_replaceQueryStringAndFragment() {
+ let tabRefAboutHome = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:home?hello=firefox#aaa"
+ );
+ let tabRefAboutMozilla = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:mozilla?hello=firefoxos#aaa"
+ );
+
+ switchTab("about:home", false);
+ gBrowser.removeCurrentTab();
+ switchTab("about:home?hello=firefox#aaa", true);
+ is(
+ tabRefAboutHome,
+ gBrowser.selectedTab,
+ "Selected tab should be the initial about:home tab"
+ );
+ switchTab("about:mozilla?hello=firefox#bbb", true, {
+ replaceQueryString: true,
+ ignoreFragment: "whenComparingAndReplace",
+ });
+ is(
+ tabRefAboutMozilla,
+ gBrowser.selectedTab,
+ "Selected tab should be the initial about:mozilla tab"
+ );
+ switchTab("about:home?hello=firefoxos#bbb", true, {
+ ignoreQueryString: true,
+ ignoreFragment: "whenComparingAndReplace",
+ });
+ is(
+ tabRefAboutHome,
+ gBrowser.selectedTab,
+ "Selected tab should be the initial about:home tab"
+ );
+ cleanupTestTabs();
+});
+
+add_task(async function test_ignoreQueryStringIgnoresFragment() {
+ let tabRefAboutHome = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:home?hello=firefox#aaa"
+ );
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:mozilla?hello=firefoxos#aaa"
+ );
+
+ switchTab("about:home?hello=firefox#bbb", false, { ignoreQueryString: true });
+ gBrowser.removeCurrentTab();
+ switchTab("about:home?hello=firefoxos#aaa", true, {
+ ignoreQueryString: true,
+ });
+ is(
+ tabRefAboutHome,
+ gBrowser.selectedTab,
+ "Selected tab should be the initial about:home tab"
+ );
+ cleanupTestTabs();
+});
+
+// Begin helpers
+
+function cleanupTestTabs() {
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+}
+
+function switchTab(aURI, aShouldFindExistingTab, aOpenParams = {}) {
+ // Build the description before switchToTabHavingURI deletes the object properties.
+ let msg =
+ `Should switch to existing ${aURI} tab if one existed, ` +
+ `${
+ aOpenParams.ignoreFragment ? "ignoring" : "including"
+ } fragment portion, `;
+ if (aOpenParams.replaceQueryString) {
+ msg += "replacing";
+ } else if (aOpenParams.ignoreQueryString) {
+ msg += "ignoring";
+ } else {
+ msg += "including";
+ }
+ msg += " query string.";
+ aOpenParams.triggeringPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+ let tabFound = switchToTabHavingURI(aURI, true, aOpenParams);
+ is(tabFound, aShouldFindExistingTab, msg);
+}
+
+registerCleanupFunction(cleanupTestTabs);
diff --git a/browser/components/urlbar/tests/browser/browser_switchToTab_closes_newtab.js b/browser/components/urlbar/tests/browser/browser_switchToTab_closes_newtab.js
new file mode 100644
index 0000000000..5031491d7e
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_switchToTab_closes_newtab.js
@@ -0,0 +1,63 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This tests that switch to tab from a blank tab switches and then closes
+ * the blank tab.
+ */
+
+"use strict";
+
+add_task(async function test_switchToTab_closes() {
+ let testURL =
+ "http://example.org/browser/browser/components/urlbar/tests/browser/dummy_page.html";
+
+ // Open the base tab
+ let baseTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, testURL);
+
+ if (baseTab.linkedBrowser.currentURI.spec == "about:blank") {
+ return;
+ }
+
+ // Open a blank tab to start the test from.
+ let testTab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ // Functions for TabClose and TabSelect
+ let tabClosePromise = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabClose",
+ false,
+ event => {
+ return event.originalTarget == testTab;
+ }
+ );
+ let tabSelectPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabSelect",
+ false,
+ event => {
+ return event.originalTarget == baseTab;
+ }
+ );
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "dummy",
+ });
+
+ // The first result is the heuristic, the second will be the switch to tab.
+ let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1);
+ EventUtils.synthesizeMouseAtCenter(element, {}, window);
+
+ await Promise.all([tabSelectPromise, tabClosePromise]);
+
+ // Confirm that the selected tab is now the base tab
+ Assert.equal(
+ gBrowser.selectedTab,
+ baseTab,
+ "Should have switched to the correct tab"
+ );
+
+ gBrowser.removeTab(baseTab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_switchToTab_fullUrl_repeatedKeydown.js b/browser/components/urlbar/tests/browser/browser_switchToTab_fullUrl_repeatedKeydown.js
new file mode 100644
index 0000000000..8f80ac5841
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_switchToTab_fullUrl_repeatedKeydown.js
@@ -0,0 +1,60 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This tests that typing a url and picking a switch to tab actually switches
+ * to the right tab. Also tests repeated keydown/keyup events don't confuse
+ * override.
+ */
+
+"use strict";
+
+add_task(async function test_switchToTab_url() {
+ const TEST_URL = "https://example.org/browser/";
+
+ let baseTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+ let testTab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ // Functions for TabClose and TabSelect
+ let tabClosePromise = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabClose",
+ false,
+ event => event.target == testTab
+ );
+ let tabSelectPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabSelect",
+ false,
+ event => event.target == baseTab
+ );
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_URL,
+ fireInputEvent: true,
+ });
+ // The first result is the heuristic, the second will be the switch to tab.
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1);
+
+ // Simulate a long press, on some platforms (Windows) it can generate multiple
+ // keydown events.
+ EventUtils.synthesizeKey("VK_SHIFT", { type: "keydown", repeat: 3 });
+ EventUtils.synthesizeKey("VK_SHIFT", { type: "keyup" });
+
+ // Pick the switch to tab result.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_Enter");
+
+ await Promise.all([tabSelectPromise, tabClosePromise]);
+
+ // Confirm that the selected tab is now the base tab
+ Assert.equal(
+ gBrowser.selectedTab,
+ baseTab,
+ "Should have switched to the correct tab"
+ );
+
+ gBrowser.removeTab(baseTab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_tabKeyBehavior.js b/browser/components/urlbar/tests/browser/browser_tabKeyBehavior.js
new file mode 100644
index 0000000000..d08d129c20
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_tabKeyBehavior.js
@@ -0,0 +1,322 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test makes sure that the tab key properly adjusts the selection or moves
+// through toolbar items, depending on the urlbar state.
+// When the view is open, tab should go through results if the urlbar was
+// focused with the mouse, or has a typed string.
+
+"use strict";
+
+add_task(async function init() {
+ for (let i = 0; i < UrlbarPrefs.get("maxRichResults"); i++) {
+ await PlacesTestUtils.addVisits("http://example.com/" + i);
+ }
+
+ registerCleanupFunction(PlacesUtils.history.clear);
+});
+
+add_task(async function tabWithSearchString() {
+ info("Tab with a search string");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ fireInputEvent: true,
+ });
+ await expectTabThroughResults();
+ info("Reverse Tab with a search string");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ fireInputEvent: true,
+ });
+ await expectTabThroughResults({ reverse: true });
+});
+
+add_task(async function tabNoSearchString() {
+ info("Tab without a search string");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ fireInputEvent: true,
+ });
+ await expectTabThroughToolbar();
+ info("Reverse Tab without a search string");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ fireInputEvent: true,
+ });
+ await expectTabThroughToolbar({ reverse: true });
+});
+
+add_task(async function tabAfterBlur() {
+ info("Tab after closing the view");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+ await expectTabThroughToolbar();
+});
+
+add_task(async function tabNoSearchStringMouseFocus() {
+ info("Tab in a new blank tab after mouse focus");
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await expectTabThroughResults();
+ });
+ info("Tab in a loaded tab after mouse focus");
+ await BrowserTestUtils.withNewTab("example.com", async () => {
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await expectTabThroughResults();
+ });
+});
+
+add_task(async function tabNoSearchStringKeyboardFocus() {
+ info("Tab in a new blank tab after keyboard focus");
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeKey("l", { accelKey: true });
+ });
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await expectTabThroughToolbar();
+ });
+ info("Tab in a loaded tab after keyboard focus");
+ await BrowserTestUtils.withNewTab("example.com", async () => {
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeKey("l", { accelKey: true });
+ });
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await expectTabThroughToolbar();
+ });
+});
+
+add_task(async function tabRetainedResultMouseFocus() {
+ info("Tab after retained results with mouse focus");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await expectTabThroughResults();
+});
+
+add_task(async function tabRetainedResultsKeyboardFocus() {
+ info("Tab after retained results with keyboard focus");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeKey("l", { accelKey: true });
+ });
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await expectTabThroughResults();
+});
+
+add_task(async function tabRetainedResults() {
+ info("Tab with a search string after mouse focus.");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ await expectTabThroughResults();
+});
+
+add_task(async function tabSearchModePreview() {
+ info(
+ "Tab past a search mode preview keywordoffer after focusing with the keyboard."
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@",
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeKey("l", { accelKey: true });
+ });
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(
+ result.searchParams.keyword,
+ "The first result is a keyword offer."
+ );
+
+ // Sanity check: the Urlbar value is cleared when keywordoffer results are
+ // selected.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.ok(!gURLBar.value, "The Urlbar should have no value.");
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+
+ await expectTabThroughResults();
+
+ await UrlbarTestUtils.promisePopupClose(window, async () => {
+ gURLBar.blur();
+ // Verify that blur closes search mode preview.
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ });
+});
+
+add_task(async function tabTabToSearch() {
+ info("Tab past a tab-to-search result after focusing with the keyboard.");
+ let engineDomain = "example.com";
+ let testEngine = await Services.search.addEngineWithDetails("Test", {
+ template: `http://${engineDomain}/?search={searchTerms}`,
+ });
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits([`https://${engineDomain}/`]);
+ }
+
+ // Search for a tab-to-search result.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: engineDomain.slice(0, 4),
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeKey("l", { accelKey: true });
+ });
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ let tabToSearchResult = (
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)
+ ).result;
+ Assert.equal(
+ tabToSearchResult.providerName,
+ "TabToSearch",
+ "The second result is a tab-to-search result."
+ );
+
+ await expectTabThroughResults();
+
+ await UrlbarTestUtils.promisePopupClose(window, async () => {
+ gURLBar.blur();
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ });
+ await PlacesUtils.history.clear();
+ await Services.search.removeEngine(testEngine);
+});
+
+add_task(async function tabNoSearchStringSearchMode() {
+ info(
+ "Tab through the toolbar when refocusing a Urlbar in search mode with the keyboard."
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ fireInputEvent: true,
+ });
+ // Enter history search mode to avoid hitting the network.
+ await UrlbarTestUtils.enterSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeKey("l", { accelKey: true });
+ });
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ await expectTabThroughToolbar();
+
+ // We have to reopen the view to exit search mode.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+async function expectTabThroughResults(options = { reverse: false }) {
+ let resultCount = UrlbarTestUtils.getResultCount(window);
+ Assert.ok(resultCount > 0, "There should be results");
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+
+ let initiallySelectedIndex = result.heuristic ? 0 : -1;
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ initiallySelectedIndex,
+ "Check the initial selection."
+ );
+
+ for (let i = initiallySelectedIndex + 1; i < resultCount; i++) {
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: options.reverse });
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ options.reverse ? resultCount - i : i
+ );
+ }
+
+ EventUtils.synthesizeKey("KEY_Tab");
+
+ if (!options.reverse) {
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ initiallySelectedIndex,
+ "Should be back at the initial selection."
+ );
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ gURLBar.blur();
+ });
+}
+
+async function expectTabThroughToolbar(options = { reverse: false }) {
+ if (gURLBar.getAttribute("pageproxystate") == "valid") {
+ Assert.equal(document.activeElement, gURLBar.inputField);
+ EventUtils.synthesizeKey("KEY_Tab");
+ Assert.notEqual(document.activeElement, gURLBar.inputField);
+ } else {
+ let focusPromise = waitForFocusOnNextFocusableElement(options.reverse);
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: options.reverse });
+ await focusPromise;
+ }
+ Assert.ok(!gURLBar.view.isOpen, "The urlbar view should be closed.");
+}
+
+async function waitForFocusOnNextFocusableElement(reverse = false) {
+ let urlbar = document.getElementById("urlbar-container");
+ let nextFocusableElement = reverse
+ ? urlbar.previousElementSibling
+ : urlbar.nextElementSibling;
+ info(nextFocusableElement);
+ while (
+ nextFocusableElement &&
+ (!nextFocusableElement.classList.contains("toolbarbutton-1") ||
+ nextFocusableElement.hasAttribute("hidden"))
+ ) {
+ nextFocusableElement = reverse
+ ? nextFocusableElement.previousElementSibling
+ : nextFocusableElement.nextElementSibling;
+ }
+
+ Assert.ok(
+ nextFocusableElement.classList.contains("toolbarbutton-1"),
+ "We should have a reference to the next focusable element after the Urlbar."
+ );
+
+ return BrowserTestUtils.waitForCondition(
+ () => nextFocusableElement.tabIndex == -1
+ );
+}
diff --git a/browser/components/urlbar/tests/browser/browser_tabMatchesInAwesomebar.js b/browser/components/urlbar/tests/browser/browser_tabMatchesInAwesomebar.js
new file mode 100644
index 0000000000..0e8ea75c1f
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_tabMatchesInAwesomebar.js
@@ -0,0 +1,224 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim:set ts=2 sw=2 sts=2 et:
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Tests for ensuring that the tab switch results correctly match what is
+ * currently available.
+ */
+
+requestLongerTimeout(2);
+
+const TEST_URL_BASES = [
+ `${TEST_BASE_URL}dummy_page.html#tabmatch`,
+ `${TEST_BASE_URL}moz.png#tabmatch`,
+];
+
+const RESTRICT_TOKEN_OPENPAGE = "%";
+
+var gTabCounter = 0;
+
+add_task(async function step_1() {
+ info("Running step 1");
+ let maxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults");
+ let promises = [];
+ for (let i = 0; i < maxResults - 1; i++) {
+ let tab = BrowserTestUtils.addTab(gBrowser);
+ promises.push(loadTab(tab, TEST_URL_BASES[0] + ++gTabCounter));
+ }
+
+ await Promise.all(promises);
+ await ensure_opentabs_match_db();
+});
+
+add_task(async function step_2() {
+ info("Running step 2");
+ gBrowser.selectTabAtIndex(1);
+ gBrowser.removeCurrentTab();
+ gBrowser.selectTabAtIndex(1);
+ gBrowser.removeCurrentTab();
+ gBrowser.selectTabAtIndex(0);
+
+ let promises = [];
+ for (let i = 1; i < gBrowser.tabs.length; i++) {
+ promises.push(loadTab(gBrowser.tabs[i], TEST_URL_BASES[1] + ++gTabCounter));
+ }
+
+ await Promise.all(promises);
+ await ensure_opentabs_match_db();
+});
+
+add_task(async function step_3() {
+ info("Running step 3");
+ let promises = [];
+ for (let i = 1; i < gBrowser.tabs.length; i++) {
+ promises.push(loadTab(gBrowser.tabs[i], TEST_URL_BASES[0] + gTabCounter));
+ }
+
+ await Promise.all(promises);
+ await ensure_opentabs_match_db();
+});
+
+add_task(async function step_4() {
+ info("Running step 4 - ensure we don't register subframes as open pages");
+ let tab = BrowserTestUtils.addTab(
+ gBrowser,
+ 'data:text/html,<body><iframe src=""></iframe></body>'
+ );
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function() {
+ let iframe_loaded = ContentTaskUtils.waitForEvent(
+ content.document,
+ "load",
+ true
+ );
+ content.document.querySelector("iframe").src = "http://test2.example.org/";
+ await iframe_loaded;
+ });
+
+ await ensure_opentabs_match_db();
+});
+
+add_task(async function step_5() {
+ info("Running step 5 - remove tab immediately");
+ let tab = BrowserTestUtils.addTab(gBrowser, "about:logo");
+ BrowserTestUtils.removeTab(tab);
+ await ensure_opentabs_match_db();
+});
+
+add_task(async function step_6() {
+ info(
+ "Running step 6 - check swapBrowsersAndCloseOther preserves registered switch-to-tab result"
+ );
+ let tabToKeep = BrowserTestUtils.addTab(gBrowser);
+ let tab = BrowserTestUtils.addTab(gBrowser, "about:mozilla");
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ gBrowser.updateBrowserRemoteness(tabToKeep.linkedBrowser, {
+ remoteType: tab.linkedBrowser.isRemoteBrowser
+ ? E10SUtils.DEFAULT_REMOTE_TYPE
+ : E10SUtils.NOT_REMOTE,
+ });
+ gBrowser.swapBrowsersAndCloseOther(tabToKeep, tab);
+
+ await ensure_opentabs_match_db();
+
+ BrowserTestUtils.removeTab(tabToKeep);
+
+ await ensure_opentabs_match_db();
+});
+
+add_task(async function step_7() {
+ info("Running step 7 - close all tabs");
+
+ Services.prefs.clearUserPref("browser.sessionstore.restore_on_demand");
+
+ BrowserTestUtils.addTab(gBrowser, "about:blank", { skipAnimation: true });
+ while (gBrowser.tabs.length > 1) {
+ info("Removing tab: " + gBrowser.tabs[0].linkedBrowser.currentURI.spec);
+ gBrowser.selectTabAtIndex(0);
+ gBrowser.removeCurrentTab();
+ }
+
+ await ensure_opentabs_match_db();
+});
+
+add_task(async function cleanup() {
+ info("Cleaning up");
+
+ await PlacesUtils.history.clear();
+});
+
+function loadTab(tab, url) {
+ // Because adding visits is async, we will not be notified immediately.
+ let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ let visited = new Promise(resolve => {
+ Services.obs.addObserver(function observer(aSubject, aTopic, aData) {
+ if (url != aSubject.QueryInterface(Ci.nsIURI).spec) {
+ return;
+ }
+ Services.obs.removeObserver(observer, aTopic);
+ resolve();
+ }, "uri-visit-saved");
+ });
+
+ info("Loading page: " + url);
+ BrowserTestUtils.loadURI(tab.linkedBrowser, url);
+ return Promise.all([loaded, visited]);
+}
+
+function ensure_opentabs_match_db() {
+ let tabs = {};
+
+ for (let browserWin of Services.wm.getEnumerator("navigator:browser")) {
+ // skip closed-but-not-destroyed windows
+ if (browserWin.closed) {
+ continue;
+ }
+
+ for (let i = 0; i < browserWin.gBrowser.tabs.length; i++) {
+ let browser = browserWin.gBrowser.getBrowserAtIndex(i);
+ let url = browser.currentURI.spec;
+ if (browserWin.isBlankPageURL(url)) {
+ continue;
+ }
+ if (!(url in tabs)) {
+ tabs[url] = 1;
+ } else {
+ tabs[url]++;
+ }
+ }
+ }
+
+ return checkAutocompleteResults(tabs);
+}
+
+async function checkAutocompleteResults(expected) {
+ info("Searching open pages.");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: RESTRICT_TOKEN_OPENPAGE,
+ });
+
+ let resultCount = UrlbarTestUtils.getResultCount(window);
+ for (let i = 0; i < resultCount; i++) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ if (result.heuristic) {
+ info("Skip heuristic match");
+ continue;
+ }
+
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ "Should have a tab switch result"
+ );
+
+ let url = result.url;
+
+ info(`Search for ${url} in open tabs.`);
+ let inExpected = url in expected;
+ Assert.ok(
+ inExpected,
+ `${url} was found in autocomplete, was ${
+ inExpected ? "" : "not "
+ } expected`
+ );
+ // Remove the found entry from expected results.
+ delete expected[url];
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+
+ // Make sure there is no reported open page that is not open.
+ for (let entry in expected) {
+ Assert.ok(!entry, `Should have been found in autocomplete`);
+ }
+}
diff --git a/browser/components/urlbar/tests/browser/browser_tabMatchesInAwesomebar_perwindowpb.js b/browser/components/urlbar/tests/browser/browser_tabMatchesInAwesomebar_perwindowpb.js
new file mode 100644
index 0000000000..04cca75021
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_tabMatchesInAwesomebar_perwindowpb.js
@@ -0,0 +1,119 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test ensures that we don't move switch between tabs when one is in
+ * private browsing and the other is normal, or vice-versa.
+ */
+
+const TEST_URL = `${TEST_BASE_URL}dummy_page.html`;
+
+add_task(async function() {
+ let normalWindow = await BrowserTestUtils.openNewBrowserWindow();
+ let privateWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ await runTest(normalWindow, privateWindow, false);
+ await BrowserTestUtils.closeWindow(normalWindow);
+ await BrowserTestUtils.closeWindow(privateWindow);
+
+ normalWindow = await BrowserTestUtils.openNewBrowserWindow();
+ privateWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ await runTest(privateWindow, normalWindow, false);
+ await BrowserTestUtils.closeWindow(normalWindow);
+ await BrowserTestUtils.closeWindow(privateWindow);
+
+ privateWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ await runTest(privateWindow, privateWindow, false);
+ await BrowserTestUtils.closeWindow(privateWindow);
+
+ normalWindow = await BrowserTestUtils.openNewBrowserWindow();
+ await runTest(normalWindow, normalWindow, true);
+ await BrowserTestUtils.closeWindow(normalWindow);
+});
+
+async function runTest(aSourceWindow, aDestWindow, aExpectSwitch, aCallback) {
+ await BrowserTestUtils.openNewForegroundTab(aSourceWindow.gBrowser, TEST_URL);
+ let testTab = await BrowserTestUtils.openNewForegroundTab(
+ aDestWindow.gBrowser
+ );
+
+ info("waiting for focus on the window");
+ await SimpleTest.promiseFocus(aDestWindow);
+ info("got focus on the window");
+
+ // Select the testTab
+ aDestWindow.gBrowser.selectedTab = testTab;
+
+ // Ensure that this tab has no history entries
+ let sessionHistoryCount = await new Promise(resolve => {
+ SessionStore.getSessionHistory(gBrowser.selectedTab, function(
+ sessionHistory
+ ) {
+ resolve(sessionHistory.entries.length);
+ });
+ });
+
+ ok(
+ sessionHistoryCount < 2,
+ `The test tab has 1 or fewer history entries. sessionHistoryCount=${sessionHistoryCount}`
+ );
+ // Ensure that this tab is on about:blank
+ is(
+ testTab.linkedBrowser.currentURI.spec,
+ "about:blank",
+ "The test tab is on about:blank"
+ );
+ // Ensure that this tab's document has no child nodes
+ await SpecialPowers.spawn(testTab.linkedBrowser, [], async function() {
+ ok(
+ !content.document.body.hasChildNodes(),
+ "The test tab has no child nodes"
+ );
+ });
+ ok(
+ !testTab.hasAttribute("busy"),
+ "The test tab doesn't have the busy attribute"
+ );
+
+ // Wait for the Awesomebar popup to appear.
+ let searchString = TEST_URL;
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: aDestWindow,
+ value: searchString,
+ });
+
+ info(`awesomebar popup appeared. aExpectSwitch: ${aExpectSwitch}`);
+ // Make sure the last match is selected.
+ while (
+ UrlbarTestUtils.getSelectedRowIndex(aDestWindow) <
+ UrlbarTestUtils.getResultCount(aDestWindow) - 1
+ ) {
+ info("handling key navigation for DOM_VK_DOWN key");
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, aDestWindow);
+ }
+
+ let awaitTabSwitch;
+ if (aExpectSwitch) {
+ awaitTabSwitch = BrowserTestUtils.waitForTabClosing(testTab);
+ }
+
+ // Execute the selected action.
+ EventUtils.synthesizeKey("KEY_Enter", {}, aDestWindow);
+ info("sent Enter command to the controller");
+
+ if (aExpectSwitch) {
+ // If we expect a tab switch then the current tab
+ // will be closed and we switch to the other tab.
+ await awaitTabSwitch;
+ } else {
+ // If we don't expect a tab switch then wait for the tab to load.
+ await BrowserTestUtils.browserLoaded(testTab.linkedBrowser);
+ }
+}
diff --git a/browser/components/urlbar/tests/browser/browser_tabToSearch.js b/browser/components/urlbar/tests/browser/browser_tabToSearch.js
new file mode 100644
index 0000000000..42cd27ce97
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_tabToSearch.js
@@ -0,0 +1,679 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests tab-to-search results. See also
+ * browser/components/urlbar/tests/unit/test_providerTabToSearch.js.
+ */
+
+"use strict";
+
+const TEST_ENGINE_NAME = "Test";
+const TEST_ENGINE_DOMAIN = "example.com";
+
+const DYNAMIC_RESULT_TYPE = "onboardTabToSearch";
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ UrlbarProviderTabToSearch:
+ "resource:///modules/UrlbarProviderTabToSearch.jsm",
+});
+
+add_task(async function setup() {
+ await PlacesUtils.history.clear();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Disable onboarding results for general tests. They are enabled in tests
+ // that specifically address onboarding.
+ ["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0],
+ ],
+ });
+ let testEngine = await Services.search.addEngineWithDetails(
+ TEST_ENGINE_NAME,
+ {
+ alias: "@test",
+ template: `http://${TEST_ENGINE_DOMAIN}/?search={searchTerms}`,
+ }
+ );
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits([`https://${TEST_ENGINE_DOMAIN}/`]);
+ }
+
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ await Services.search.removeEngine(testEngine);
+ });
+});
+
+// Tests that tab-to-search results preview search mode when highlighted. These
+// results are worth testing separately since they do not set the
+// payload.keyword parameter.
+add_task(async function basic() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_ENGINE_DOMAIN.slice(0, 4),
+ });
+ let autofillResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(autofillResult.autofill);
+ Assert.equal(
+ autofillResult.url,
+ `https://${TEST_ENGINE_DOMAIN}/`,
+ "The autofilled URL matches the engine domain."
+ );
+
+ let tabToSearchResult = (
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)
+ ).result;
+ Assert.equal(
+ tabToSearchResult.providerName,
+ "TabToSearch",
+ "The second result is a tab-to-search result."
+ );
+ Assert.equal(
+ tabToSearchResult.payload.engine,
+ TEST_ENGINE_NAME,
+ "The tab-to-search result is for the correct engine."
+ );
+ let tabToSearchDetails = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ 1
+ );
+ let [actionTabToSearch] = await document.l10n.formatValues([
+ {
+ id: UrlbarUtils.WEB_ENGINE_NAMES.has(
+ tabToSearchDetails.searchParams.engine
+ )
+ ? "urlbar-result-action-tabtosearch-web"
+ : "urlbar-result-action-tabtosearch-other-engine",
+ args: { engine: tabToSearchDetails.searchParams.engine },
+ },
+ ]);
+ Assert.equal(
+ tabToSearchDetails.displayed.title,
+ `Search with ${tabToSearchDetails.searchParams.engine}`,
+ "The result's title is set correctly."
+ );
+ Assert.equal(
+ tabToSearchDetails.displayed.action,
+ actionTabToSearch,
+ "The correct action text is displayed in the tab-to-search result."
+ );
+
+ EventUtils.synthesizeKey("KEY_Tab");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 1,
+ "Sanity check: The second result is selected."
+ );
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ENGINE_NAME,
+ entry: "tabtosearch",
+ isPreview: true,
+ });
+
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+// Tests that we do not set aria-activedescendant after tabbing to a
+// tab-to-search result when the pref
+// browser.urlbar.accessibility.tabToSearch.announceResults is true. If that
+// pref is true, the result was already announced while the user was typing.
+add_task(async function activedescendant_tab() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.accessibility.tabToSearch.announceResults", true]],
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_ENGINE_DOMAIN.slice(0, 4),
+ });
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 2,
+ "There should be two results."
+ );
+ let tabToSearchRow = await UrlbarTestUtils.waitForAutocompleteResultAt(
+ window,
+ 1
+ );
+ Assert.equal(
+ tabToSearchRow.result.providerName,
+ "TabToSearch",
+ "The second result is a tab-to-search result."
+ );
+
+ EventUtils.synthesizeKey("KEY_Tab");
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ENGINE_NAME,
+ entry: "tabtosearch",
+ isPreview: true,
+ });
+ let aadID = gURLBar.inputField.getAttribute("aria-activedescendant");
+ Assert.ok(!aadID, "aria-activedescendant was not set.");
+
+ // Cycle through all the results then return to the tab-to-search result. It
+ // should be announced.
+ EventUtils.synthesizeKey("KEY_Tab");
+ aadID = gURLBar.inputField.getAttribute("aria-activedescendant");
+ let firstRow = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
+ Assert.equal(
+ aadID,
+ firstRow.id,
+ "aria-activedescendant was set to the row after the tab-to-search result."
+ );
+ EventUtils.synthesizeKey("KEY_Tab");
+ aadID = gURLBar.inputField.getAttribute("aria-activedescendant");
+ Assert.equal(
+ aadID,
+ tabToSearchRow.id,
+ "aria-activedescendant was set to the tab-to-search result."
+ );
+
+ // Now close and reopen the view, then do another search that yields a
+ // tab-to-search result. aria-activedescendant should not be set when it is
+ // selected.
+ await UrlbarTestUtils.promisePopupClose(window);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_ENGINE_DOMAIN.slice(0, 4),
+ });
+ tabToSearchRow = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1);
+ Assert.equal(
+ tabToSearchRow.result.providerName,
+ "TabToSearch",
+ "The second result is a tab-to-search result."
+ );
+
+ EventUtils.synthesizeKey("KEY_Tab");
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ENGINE_NAME,
+ entry: "tabtosearch",
+ isPreview: true,
+ });
+ aadID = gURLBar.inputField.getAttribute("aria-activedescendant");
+ Assert.ok(!aadID, "aria-activedescendant was not set.");
+
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+ await SpecialPowers.popPrefEnv();
+});
+
+// Tests that we set aria-activedescendant after accessing a tab-to-search
+// result with the arrow keys.
+add_task(async function activedescendant_arrow() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_ENGINE_DOMAIN.slice(0, 4),
+ });
+ let tabToSearchRow = await UrlbarTestUtils.waitForAutocompleteResultAt(
+ window,
+ 1
+ );
+ Assert.equal(
+ tabToSearchRow.result.providerName,
+ "TabToSearch",
+ "The second result is a tab-to-search result."
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ENGINE_NAME,
+ entry: "tabtosearch",
+ isPreview: true,
+ });
+ let aadID = gURLBar.inputField.getAttribute("aria-activedescendant");
+ Assert.equal(
+ aadID,
+ tabToSearchRow.id,
+ "aria-activedescendant was set to the tab-to-search result."
+ );
+
+ // Move selection away from the tab-to-search result then return. It should
+ // be announced.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ aadID = gURLBar.inputField.getAttribute("aria-activedescendant");
+ Assert.equal(
+ aadID,
+ UrlbarTestUtils.getOneOffSearchButtons(window).selectedButton.id,
+ tabToSearchRow.id,
+ "aria-activedescendant was moved to the first one-off."
+ );
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ aadID = gURLBar.inputField.getAttribute("aria-activedescendant");
+ Assert.equal(
+ aadID,
+ tabToSearchRow.id,
+ "aria-activedescendant was set to the tab-to-search result."
+ );
+
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+add_task(async function tab_key_race() {
+ // Mac Debug tinderboxes are just too slow and fail intermittently
+ // even if the EventBufferer timeout is set to an high value.
+ if (AppConstants.platform == "macosx" && AppConstants.DEBUG) {
+ return;
+ }
+ info(
+ "Test typing a letter followed shortly by Tab consistently selects a tab-to-search result"
+ );
+ Assert.equal(gURLBar.value, "", "Sanity check urlbar is empty");
+ let promiseQueryStarted = new Promise(resolve => {
+ /**
+ * A no-op test provider.
+ * We use this to wait for the query to start, because otherwise TAB will
+ * move to the next widget since the panel is closed and there's no running
+ * query. This means waiting for the UrlbarProvidersManager to at least
+ * evaluate the isActive status of providers.
+ * In the future we should try to reduce this latency, to defer user events
+ * even more efficiently.
+ */
+ class ListeningTestProvider extends UrlbarProvider {
+ constructor() {
+ super();
+ }
+ get name() {
+ return "ListeningTestProvider";
+ }
+ get type() {
+ return UrlbarUtils.PROVIDER_TYPE.PROFILE;
+ }
+ isActive(context) {
+ executeSoon(resolve);
+ return false;
+ }
+ isRestricting(context) {
+ return false;
+ }
+ async startQuery(context, addCallback) {
+ // Nothing to do.
+ }
+ }
+ let provider = new ListeningTestProvider();
+ UrlbarProvidersManager.registerProvider(provider);
+ registerCleanupFunction(async function() {
+ UrlbarProvidersManager.unregisterProvider(provider);
+ });
+ });
+ info("Type the beginning of the search string to get tab-to-search");
+ EventUtils.synthesizeKey(TEST_ENGINE_DOMAIN.slice(0, 1));
+ info("Awaiting for the query to start");
+ await promiseQueryStarted;
+ EventUtils.synthesizeKey("KEY_Tab");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await TestUtils.waitForCondition(
+ () => UrlbarTestUtils.getSelectedRowIndex(window) == 1,
+ "Wait for tab key to be handled"
+ );
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ENGINE_NAME,
+ entry: "tabtosearch",
+ isPreview: true,
+ });
+
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+// Test that large-style onboarding results appear and have the correct
+// properties.
+add_task(async function onboard() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 3]],
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_ENGINE_DOMAIN.slice(0, 4),
+ });
+ let autofillResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(autofillResult.autofill);
+ Assert.equal(
+ autofillResult.url,
+ `https://${TEST_ENGINE_DOMAIN}/`,
+ "The autofilled URL matches the engine domain."
+ );
+ EventUtils.synthesizeKey("KEY_Tab");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 1,
+ "Sanity check: The second result is selected."
+ );
+
+ // Now check the properties of the onboarding result.
+ let onboardingElement = await UrlbarTestUtils.waitForAutocompleteResultAt(
+ window,
+ 1
+ );
+ Assert.equal(
+ onboardingElement.result.payload.dynamicType,
+ DYNAMIC_RESULT_TYPE,
+ "The tab-to-search result is an onboarding result."
+ );
+ Assert.equal(
+ onboardingElement.result.resultSpan,
+ 2,
+ "The correct resultSpan was set."
+ );
+ Assert.ok(
+ onboardingElement
+ .querySelector(".urlbarView-row-inner")
+ .hasAttribute("selected"),
+ "The onboarding element set the selected attribute."
+ );
+
+ let [
+ titleOnboarding,
+ actionOnboarding,
+ descriptionOnboarding,
+ ] = await document.l10n.formatValues([
+ {
+ id: "urlbar-result-action-search-w-engine",
+ args: {
+ engine: onboardingElement.result.payload.engine,
+ },
+ },
+ {
+ id: UrlbarUtils.WEB_ENGINE_NAMES.has(
+ onboardingElement.result.payload.engine
+ )
+ ? "urlbar-result-action-tabtosearch-web"
+ : "urlbar-result-action-tabtosearch-other-engine",
+ args: { engine: onboardingElement.result.payload.engine },
+ },
+ {
+ id: "urlbar-tabtosearch-onboard",
+ },
+ ]);
+ let onboardingDetails = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(
+ onboardingDetails.displayed.title,
+ titleOnboarding,
+ "The correct title was set."
+ );
+ Assert.equal(
+ onboardingDetails.displayed.action,
+ actionOnboarding,
+ "The correct action text was set."
+ );
+ Assert.equal(
+ onboardingDetails.element.row.querySelector(
+ ".urlbarView-dynamic-onboardTabToSearch-description"
+ ).textContent,
+ descriptionOnboarding,
+ "The correct description was set."
+ );
+
+ // Check that the onboarding result enters search mode.
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ENGINE_NAME,
+ entry: "tabtosearch_onboard",
+ isPreview: true,
+ });
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ENGINE_NAME,
+ entry: "tabtosearch_onboard",
+ });
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+ UrlbarPrefs.set("tabToSearch.onboard.interactionsLeft", 3);
+ delete UrlbarProviderTabToSearch.onboardingInteractionAtTime;
+ await SpecialPowers.popPrefEnv();
+});
+
+// Tests that we show the onboarding result until the user interacts with it
+// `browser.urlbar.tabToSearch.onboard.interactionsLeft` times.
+add_task(async function onboard_limit() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 3]],
+ });
+
+ Assert.equal(
+ UrlbarPrefs.get("tabToSearch.onboard.interactionsLeft"),
+ 3,
+ "Sanity check: interactionsLeft is 3."
+ );
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_ENGINE_DOMAIN.slice(0, 4),
+ });
+ let onboardingResult = (
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)
+ ).result;
+ Assert.equal(
+ onboardingResult.payload.dynamicType,
+ DYNAMIC_RESULT_TYPE,
+ "The second result is an onboarding result."
+ );
+ await EventUtils.synthesizeKey("KEY_ArrowDown");
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ENGINE_NAME,
+ entry: "tabtosearch_onboard",
+ isPreview: true,
+ });
+ Assert.equal(UrlbarPrefs.get("tabToSearch.onboard.interactionsLeft"), 2);
+ await UrlbarTestUtils.exitSearchMode(window);
+
+ // We don't increment the counter if we showed the onboarding result less than
+ // 5 minutes ago.
+ for (let i = 0; i < 5; i++) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_ENGINE_DOMAIN.slice(0, 4),
+ });
+ onboardingResult = (
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)
+ ).result;
+ Assert.equal(
+ onboardingResult.payload.dynamicType,
+ DYNAMIC_RESULT_TYPE,
+ "The second result is an onboarding result."
+ );
+ await EventUtils.synthesizeKey("KEY_ArrowDown");
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ENGINE_NAME,
+ entry: "tabtosearch_onboard",
+ isPreview: true,
+ });
+ Assert.equal(
+ UrlbarPrefs.get("tabToSearch.onboard.interactionsLeft"),
+ 2,
+ "We shouldn't decrement interactionsLeft if an onboarding result was just shown."
+ );
+ await UrlbarTestUtils.exitSearchMode(window);
+ }
+
+ // If the user doesn't interact with the result, we don't increment the
+ // counter.
+ for (let i = 0; i < 5; i++) {
+ delete UrlbarProviderTabToSearch.onboardingInteractionAtTime;
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_ENGINE_DOMAIN.slice(0, 4),
+ });
+ let tabToSearchResult = (
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)
+ ).result;
+ Assert.equal(
+ tabToSearchResult.providerName,
+ "TabToSearch",
+ "The second result is a tab-to-search result."
+ );
+ Assert.equal(
+ tabToSearchResult.type,
+ UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ "The tab-to-search result is an onboarding result."
+ );
+ Assert.equal(
+ UrlbarPrefs.get("tabToSearch.onboard.interactionsLeft"),
+ 2,
+ "We shouldn't decrement interactionsLeft if the user doesn't interact with onboarding."
+ );
+ }
+
+ // Test that we increment the counter if the user interacts with the result
+ // and it's been 5+ minutes since they last interacted with it.
+ for (let i = 1; i >= 0; i--) {
+ delete UrlbarProviderTabToSearch.onboardingInteractionAtTime;
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_ENGINE_DOMAIN.slice(0, 4),
+ });
+ let tabToSearchResult = (
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)
+ ).result;
+ Assert.equal(
+ tabToSearchResult.providerName,
+ "TabToSearch",
+ "The second result is a tab-to-search result."
+ );
+ Assert.equal(
+ onboardingResult.payload.dynamicType,
+ DYNAMIC_RESULT_TYPE,
+ "The second result is an onboarding result."
+ );
+ await EventUtils.synthesizeKey("KEY_ArrowDown");
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ENGINE_NAME,
+ entry: "tabtosearch_onboard",
+ isPreview: true,
+ });
+ Assert.equal(
+ UrlbarPrefs.get("tabToSearch.onboard.interactionsLeft"),
+ i,
+ "We decremented interactionsLeft."
+ );
+ await UrlbarTestUtils.exitSearchMode(window);
+ }
+
+ delete UrlbarProviderTabToSearch.onboardingInteractionAtTime;
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_ENGINE_DOMAIN.slice(0, 4),
+ });
+ let tabToSearchResult = (
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)
+ ).result;
+ Assert.equal(
+ tabToSearchResult.providerName,
+ "TabToSearch",
+ "The second result is a tab-to-search result."
+ );
+ Assert.notEqual(
+ tabToSearchResult.type,
+ UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ "Now that interactionsLeft is 0, we don't show onboarding results."
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ UrlbarPrefs.set("tabToSearch.onboard.interactionsLeft", 3);
+ delete UrlbarProviderTabToSearch.onboardingInteractionAtTime;
+ await SpecialPowers.popPrefEnv();
+});
+
+// Tests that we show at most one onboarding result at a time. See
+// tests/unit/test_providerTabToSearch.js:multipleEnginesForHostname for a test
+// that ensures only one normal tab-to-search result is shown in this scenario.
+add_task(async function onboard_multipleEnginesForHostname() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 3]],
+ });
+
+ let testEngineMaps = await Services.search.addEngineWithDetails(
+ `${TEST_ENGINE_NAME}Maps`,
+ {
+ template: `http://${TEST_ENGINE_DOMAIN}/maps/?search={searchTerms}`,
+ }
+ );
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_ENGINE_DOMAIN.slice(0, 4),
+ });
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 2,
+ "Only two results are shown."
+ );
+ let firstResult = (
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0)
+ ).result;
+ Assert.notEqual(
+ firstResult.providerName,
+ "TabToSearch",
+ "The first result is not from TabToSearch."
+ );
+ let secondResult = (
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)
+ ).result;
+ Assert.equal(
+ secondResult.providerName,
+ "TabToSearch",
+ "The second result is from TabToSearch."
+ );
+ Assert.equal(
+ secondResult.type,
+ UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ "The tab-to-search result is the only onboarding result."
+ );
+ await Services.search.removeEngine(testEngineMaps);
+ await UrlbarTestUtils.promisePopupClose(window);
+ UrlbarPrefs.set("tabToSearch.onboard.interactionsLeft", 3);
+ delete UrlbarProviderTabToSearch.onboardingInteractionAtTime;
+ await SpecialPowers.popPrefEnv();
+});
+
+// Tests that engines with names containing extended Unicode characters can be
+// recognized as general-web engines and that their tab-to-search results
+// display the correct string.
+add_task(async function extended_unicode_in_engine() {
+ // Baidu's localized name. We expect this tab-to-search result shows the
+ // general-web engine string because Baidu is included in WEB_ENGINE_NAMES.
+ let engineName = "百度";
+ let engineDomain = "example-2.com";
+ let testEngine = await Services.search.addEngineWithDetails(engineName, {
+ template: `http://${engineDomain}/?search={searchTerms}`,
+ });
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits([`https://${engineDomain}/`]);
+ }
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: engineDomain.slice(0, 4),
+ });
+ let tabToSearchDetails = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ 1
+ );
+ Assert.equal(
+ tabToSearchDetails.searchParams.engine,
+ engineName,
+ "The tab-to-search engine name contains extended Unicode characters."
+ );
+ let [actionTabToSearch] = await document.l10n.formatValues([
+ {
+ id: "urlbar-result-action-tabtosearch-web",
+ args: { engine: tabToSearchDetails.searchParams.engine },
+ },
+ ]);
+ Assert.equal(
+ tabToSearchDetails.displayed.action,
+ actionTabToSearch,
+ "The correct action text is displayed in the tab-to-search result."
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ await PlacesUtils.history.clear();
+ await Services.search.removeEngine(testEngine);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_textruns.js b/browser/components/urlbar/tests/browser/browser_textruns.js
new file mode 100644
index 0000000000..f09303cd99
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_textruns.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test ensures that we limit textruns in case of very long urls or titles.
+ */
+
+add_task(async function() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.searches", true]],
+ });
+ let engine = await Services.search.addEngineWithDetails("Test", {
+ template: "http://example.com/?search={searchTerms}",
+ });
+ let oldDefaultEngine = await Services.search.getDefault();
+ await Services.search.setDefault(engine);
+
+ let lotsOfSpaces = "%20".repeat(300);
+ await PlacesTestUtils.addVisits({
+ uri: `https://textruns.mozilla.org/${lotsOfSpaces}/test/`,
+ title: `A long ${lotsOfSpaces} title`,
+ });
+ await UrlbarTestUtils.formHistory.add([
+ { value: `A long ${lotsOfSpaces} textruns suggestion` },
+ ]);
+
+ registerCleanupFunction(async function() {
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.formHistory.clear();
+ await Services.search.setDefault(oldDefaultEngine);
+ await Services.search.removeEngine(engine);
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "textruns",
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.searchParams.engine, "Test", "Sanity check engine");
+ Assert.equal(
+ result.displayed.title.length,
+ UrlbarUtils.MAX_TEXT_LENGTH,
+ "Result title should be limited"
+ );
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2);
+ Assert.equal(
+ result.displayed.title.length,
+ UrlbarUtils.MAX_TEXT_LENGTH,
+ "Result title should be limited"
+ );
+ Assert.equal(
+ result.displayed.url.length,
+ UrlbarUtils.MAX_TEXT_LENGTH,
+ "Result url should be limited"
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_tokenAlias.js b/browser/components/urlbar/tests/browser/browser_tokenAlias.js
new file mode 100644
index 0000000000..e0951ff3ec
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_tokenAlias.js
@@ -0,0 +1,861 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test checks "@" search engine aliases ("token aliases") in the urlbar.
+
+"use strict";
+
+const ALIAS = "@test";
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+
+// We make sure that aliases and search terms are correctly recognized when they
+// are separated by each of these different types of spaces and combinations of
+// spaces. U+3000 is the ideographic space in CJK and is commonly used by CJK
+// speakers.
+const TEST_SPACES = [" ", "\u3000", " \u3000", "\u3000 "];
+
+let testEngine;
+
+// Allow more time for Mac machines so they don't time out in verify mode. See
+// bug 1673062.
+if (AppConstants.platform == "macosx") {
+ requestLongerTimeout(5);
+}
+
+add_task(async function init() {
+ // Add a default engine with suggestions, to avoid hitting the network when
+ // fetching them.
+ let defaultEngine = await SearchTestUtils.promiseNewSearchEngine(
+ getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME
+ );
+ defaultEngine.alias = "@default";
+ let oldDefaultEngine = await Services.search.getDefault();
+ Services.search.setDefault(defaultEngine);
+ testEngine = await Services.search.addEngineWithDetails("Test", {
+ alias: ALIAS,
+ template: "http://example.com/?search={searchTerms}",
+ });
+ registerCleanupFunction(async function() {
+ await Services.search.removeEngine(testEngine);
+ Services.search.setDefault(oldDefaultEngine);
+ });
+
+ // Search results aren't shown in quantumbar unless search suggestions are
+ // enabled.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.searches", true]],
+ });
+});
+
+// Simple test that tries different variations of an alias, without reverting
+// the urlbar value in between.
+add_task(async function testNoRevert() {
+ await doSimpleTest(false);
+});
+
+// Simple test that tries different variations of an alias, reverting the urlbar
+// value in between.
+add_task(async function testRevert() {
+ await doSimpleTest(true);
+});
+
+async function doSimpleTest(revertBetweenSteps) {
+ // When autofill is enabled, searching for "@tes" will autofill to "@test",
+ // which gets in the way of this test task, so temporarily disable it.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.autoFill", false]],
+ });
+
+ // "@tes" -- not an alias, no search mode
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: ALIAS.substr(0, ALIAS.length - 1),
+ });
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ Assert.equal(
+ gURLBar.value,
+ ALIAS.substr(0, ALIAS.length - 1),
+ "value should be alias substring"
+ );
+
+ if (revertBetweenSteps) {
+ gURLBar.handleRevert();
+ gURLBar.blur();
+ }
+
+ // "@test" -- alias but no trailing space, no search mode
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: ALIAS,
+ });
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ Assert.equal(gURLBar.value, ALIAS, "value should be alias");
+
+ if (revertBetweenSteps) {
+ gURLBar.handleRevert();
+ gURLBar.blur();
+ }
+
+ // "@test " -- alias with trailing space, search mode
+ for (let spaces of TEST_SPACES) {
+ info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) }));
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: ALIAS + spaces,
+ });
+ // Wait for the second new search that starts when search mode is entered.
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: testEngine.name,
+ entry: "typed",
+ });
+ Assert.equal(gURLBar.value, "", "value should be empty");
+ await UrlbarTestUtils.exitSearchMode(window);
+
+ if (revertBetweenSteps) {
+ gURLBar.handleRevert();
+ gURLBar.blur();
+ }
+ }
+
+ // "@test foo" -- alias, search mode
+ for (let spaces of TEST_SPACES) {
+ info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) }));
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: ALIAS + spaces + "foo",
+ });
+ // Wait for the second new search that starts when search mode is entered.
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: testEngine.name,
+ entry: "typed",
+ });
+ Assert.equal(gURLBar.value, "foo", "value should be query");
+ await UrlbarTestUtils.exitSearchMode(window);
+
+ if (revertBetweenSteps) {
+ gURLBar.handleRevert();
+ gURLBar.blur();
+ }
+ }
+
+ // "@test " -- alias with trailing space, search mode
+ for (let spaces of TEST_SPACES) {
+ info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) }));
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: ALIAS + spaces,
+ });
+ // Wait for the second new search that starts when search mode is entered.
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: testEngine.name,
+ entry: "typed",
+ });
+ Assert.equal(gURLBar.value, "", "value should be empty");
+ await UrlbarTestUtils.exitSearchMode(window);
+
+ if (revertBetweenSteps) {
+ gURLBar.handleRevert();
+ gURLBar.blur();
+ }
+ }
+
+ // "@test" -- alias but no trailing space, no highlight
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: ALIAS,
+ });
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ Assert.equal(gURLBar.value, ALIAS, "value should be alias");
+
+ if (revertBetweenSteps) {
+ gURLBar.handleRevert();
+ gURLBar.blur();
+ }
+
+ // "@tes" -- not an alias, no highlight
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: ALIAS.substr(0, ALIAS.length - 1),
+ });
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ Assert.equal(
+ gURLBar.value,
+ ALIAS.substr(0, ALIAS.length - 1),
+ "value should be alias substring"
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+
+ await SpecialPowers.popPrefEnv();
+}
+
+// An alias should be recognized even when there are spaces before it, and
+// search mode should be entered.
+add_task(async function spacesBeforeAlias() {
+ for (let spaces of TEST_SPACES) {
+ info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) }));
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: spaces + ALIAS + spaces,
+ });
+ // Wait for the second new search that starts when search mode is entered.
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: testEngine.name,
+ entry: "typed",
+ });
+ Assert.equal(gURLBar.value, "", "value should be empty");
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+ }
+});
+
+// An alias in the middle of a string should not be recognized and search mode
+// should not be entered.
+add_task(async function charsBeforeAlias() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "not an alias " + ALIAS + " ",
+ });
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ Assert.equal(
+ gURLBar.value,
+ "not an alias " + ALIAS + " ",
+ "value should be unchanged"
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+});
+
+// While already in search mode, an alias should not be recognized.
+add_task(async function alreadyInSearchMode() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ await UrlbarTestUtils.enterSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: ALIAS + " ",
+ });
+
+ // Search mode source should still be bookmarks.
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ entry: "oneoff",
+ });
+ Assert.equal(gURLBar.value, ALIAS + " ", "value should be unchanged");
+
+ // Exit search mode, but first remove the value in the input. Since the value
+ // is "alias ", we'd actually immediately re-enter search mode otherwise.
+ gURLBar.value = "";
+
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+});
+
+// Types a space while typing an alias to ensure we stop autofilling.
+add_task(async function spaceWhileTypingAlias() {
+ for (let spaces of TEST_SPACES) {
+ if (spaces.length != 1) {
+ continue;
+ }
+ info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) }));
+
+ let value = ALIAS.substring(0, ALIAS.length - 1);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value,
+ selectionStart: value.length,
+ selectionEnd: value.length,
+ });
+ Assert.equal(gURLBar.value, ALIAS + " ", "Alias should be autofilled");
+
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey(spaces);
+ await searchPromise;
+
+ Assert.equal(
+ gURLBar.value,
+ value + spaces,
+ "Alias should not be autofilled"
+ );
+ await UrlbarTestUtils.assertSearchMode(window, null);
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ }
+});
+
+// Aliases are case insensitive. Make sure that searching with an alias using a
+// weird case still causes the alias to be recognized and search mode entered.
+add_task(async function aliasCase() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@TeSt ",
+ });
+ // Wait for the second new search that starts when search mode is entered.
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: testEngine.name,
+ entry: "typed",
+ });
+ Assert.equal(gURLBar.value, "", "value should be empty");
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+});
+
+// Same as previous but with a query.
+add_task(async function aliasCase_query() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@tEsT query",
+ });
+ // Wait for the second new search that starts when search mode is entered.
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: testEngine.name,
+ entry: "typed",
+ });
+ Assert.equal(gURLBar.value, "query", "value should be query");
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+});
+
+// Selecting a non-heuristic (non-first) search engine result with an alias and
+// empty query should put the alias in the urlbar and highlight it.
+// Also checks that internal aliases appear with the "@" keyword.
+add_task(async function nonHeuristicAliases() {
+ // Get the list of token alias engines (those with aliases that start with
+ // "@").
+ let tokenEngines = [];
+ for (let engine of await Services.search.getEngines()) {
+ let aliases = [];
+ if (engine.alias) {
+ aliases.push(engine.alias);
+ }
+ aliases.push(...engine.aliases);
+ let tokenAliases = aliases.filter(a => a.startsWith("@"));
+ if (tokenAliases.length) {
+ tokenEngines.push({ engine, tokenAliases });
+ }
+ }
+ if (!tokenEngines.length) {
+ Assert.ok(true, "No token alias engines, skipping task.");
+ return;
+ }
+ info(
+ "Got token alias engines: " + tokenEngines.map(({ engine }) => engine.name)
+ );
+
+ // Populate the results with the list of token alias engines by searching for
+ // "@".
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@",
+ });
+ await UrlbarTestUtils.waitForAutocompleteResultAt(
+ window,
+ tokenEngines.length - 1
+ );
+ // Key down to select each result in turn. The urlbar should preview search
+ // mode for each engine.
+ for (let { tokenAliases } of tokenEngines) {
+ let alias = tokenAliases[0];
+ let engineName = (await UrlbarSearchUtils.engineForAlias(alias)).name;
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ let expectedSearchMode = {
+ engineName,
+ entry: "keywordoffer",
+ isPreview: true,
+ };
+ if (UrlbarUtils.WEB_ENGINE_NAMES.has(engineName)) {
+ expectedSearchMode.source = UrlbarUtils.RESULT_SOURCE.SEARCH;
+ }
+ await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode);
+ Assert.ok(!gURLBar.value, "The Urlbar should be empty.");
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+});
+
+// Clicking on an @ alias offer (an @ alias with an empty search string) in the
+// view should enter search mode.
+add_task(async function clickAndFillAlias() {
+ // Do a search for "@" to show all the @ aliases.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@",
+ });
+
+ // Find our test engine in the results. It's probably last, but for
+ // robustness don't assume it is.
+ let testEngineItem;
+ for (let i = 0; !testEngineItem; i++) {
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ Assert.equal(
+ details.displayed.title,
+ `Search with ${details.searchParams.engine}`,
+ "The result's title is set correctly."
+ );
+ Assert.ok(!details.action, "The result should have no action text.");
+ if (details.searchParams && details.searchParams.keyword == ALIAS) {
+ testEngineItem = await UrlbarTestUtils.waitForAutocompleteResultAt(
+ window,
+ i
+ );
+ }
+ }
+
+ // Click it.
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeMouseAtCenter(testEngineItem, {});
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: testEngineItem.result.payload.engine,
+ entry: "keywordoffer",
+ });
+
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+});
+
+// Pressing enter on an @ alias offer (an @ alias with an empty search string)
+// in the view should enter search mode.
+add_task(async function enterAndFillAlias() {
+ // Do a search for "@" to show all the @ aliases.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@",
+ });
+
+ // Find our test engine in the results. It's probably last, but for
+ // robustness don't assume it is.
+ let details;
+ let index = 0;
+ for (; ; index++) {
+ details = await UrlbarTestUtils.getDetailsOfResultAt(window, index);
+ if (details.searchParams && details.searchParams.keyword == ALIAS) {
+ index++;
+ break;
+ }
+ }
+
+ // Key down to it and press enter.
+ EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: index });
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: details.searchParams.engine,
+ entry: "keywordoffer",
+ });
+
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+});
+
+// Pressing Enter on an @ alias autofill should enter search mode.
+add_task(async function enterAutofillsAlias() {
+ for (let value of [ALIAS.substring(0, ALIAS.length - 1), ALIAS]) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value,
+ selectionStart: value.length,
+ selectionEnd: value.length,
+ });
+
+ // Press Enter.
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: testEngine.name,
+ entry: "keywordoffer",
+ });
+
+ await UrlbarTestUtils.exitSearchMode(window);
+ }
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+});
+
+// Pressing Right on an @ alias autofill should enter search mode.
+add_task(async function rightEntersSearchMode() {
+ for (let value of [ALIAS.substring(0, ALIAS.length - 1), ALIAS]) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value,
+ selectionStart: value.length,
+ selectionEnd: value.length,
+ });
+
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: testEngine.name,
+ entry: "typed",
+ });
+ Assert.equal(gURLBar.value, "", "value should be empty");
+ await UrlbarTestUtils.exitSearchMode(window);
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+});
+
+// Pressing Tab when an @ alias is autofilled should enter search mode preview.
+add_task(async function rightEntersSearchMode() {
+ for (let value of [ALIAS.substring(0, ALIAS.length - 1), ALIAS]) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value,
+ selectionStart: value.length,
+ selectionEnd: value.length,
+ });
+
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ -1,
+ "There is no selected result."
+ );
+
+ EventUtils.synthesizeKey("KEY_Tab");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 0,
+ "The first result is selected."
+ );
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: testEngine.name,
+ entry: "keywordoffer",
+ isPreview: true,
+ });
+ Assert.equal(gURLBar.value, "", "value should be empty");
+
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: testEngine.name,
+ entry: "keywordoffer",
+ isPreview: false,
+ });
+ await UrlbarTestUtils.exitSearchMode(window);
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+});
+
+/**
+ * This test checks that if an engine is marked as hidden then
+ * it should not appear in the popup when using the "@" token alias in the search bar.
+ */
+add_task(async function hiddenEngine() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@",
+ fireInputEvent: true,
+ });
+
+ const defaultEngine = await Services.search.getDefault();
+
+ let foundDefaultEngineInPopup = false;
+
+ // Checks that the default engine appears in the urlbar's popup.
+ for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) {
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ if (defaultEngine.name == details.searchParams.engine) {
+ foundDefaultEngineInPopup = true;
+ break;
+ }
+ }
+ Assert.ok(foundDefaultEngineInPopup, "Default engine appears in the popup.");
+
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+
+ // Checks that a hidden default engine (i.e. an engine removed from
+ // a user's search settings) does not appear in the urlbar's popup.
+ defaultEngine.hidden = true;
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@",
+ fireInputEvent: true,
+ });
+ foundDefaultEngineInPopup = false;
+ for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) {
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ if (defaultEngine.name == details.searchParams.engine) {
+ foundDefaultEngineInPopup = true;
+ break;
+ }
+ }
+ Assert.ok(
+ !foundDefaultEngineInPopup,
+ "Hidden default engine does not appear in the popup"
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+
+ defaultEngine.hidden = false;
+});
+
+/**
+ * This test checks that if an engines alias is not prefixed with
+ * @ it still appears in the popup when using the "@" token
+ * alias in the search bar.
+ */
+add_task(async function nonPrefixedKeyword() {
+ let name = "Custom";
+ let alias = "customkeyword";
+ let engine = await Services.search.addEngineWithDetails(name, {
+ alias,
+ template: "http://example.com/?search={searchTerms}",
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@",
+ });
+
+ let foundEngineInPopup = false;
+
+ // Checks that the default engine appears in the urlbar's popup.
+ for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) {
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ if (details.searchParams.engine === name) {
+ foundEngineInPopup = true;
+ break;
+ }
+ }
+ Assert.ok(foundEngineInPopup, "Custom engine appears in the popup.");
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@" + alias,
+ });
+
+ let keywordOfferResult = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ 0
+ );
+
+ Assert.equal(
+ keywordOfferResult.searchParams.engine,
+ name,
+ "The first result should be a keyword search result with the correct engine."
+ );
+
+ await Services.search.removeEngine(engine);
+});
+
+// Tests that we show all engines with a token alias that match the search
+// string.
+add_task(async function multipleMatchingEngines() {
+ let testEngineFoo = await Services.search.addEngineWithDetails("TestFoo", {
+ alias: `${ALIAS}foo`,
+ template: "http://example-2.com/?search={searchTerms}",
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@te",
+ fireInputEvent: true,
+ });
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 2,
+ "Two results are shown."
+ );
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ -1,
+ "Neither result is selected."
+ );
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(result.autofill, "The first result is autofilling.");
+ Assert.equal(
+ result.searchParams.keyword,
+ ALIAS,
+ "The autofilled engine is shown first."
+ );
+
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(
+ result.searchParams.keyword,
+ `${ALIAS}foo`,
+ "The other engine is shown second."
+ );
+
+ EventUtils.synthesizeKey("KEY_Tab");
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), 0);
+ Assert.equal(gURLBar.value, "", "Urlbar should be empty.");
+ EventUtils.synthesizeKey("KEY_Tab");
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), 1);
+ Assert.equal(gURLBar.value, "", "Urlbar should be empty.");
+ EventUtils.synthesizeKey("KEY_Tab");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ -1,
+ "Tabbing all the way through the matching engines should return to the input."
+ );
+ Assert.equal(
+ gURLBar.value,
+ "@te",
+ "Urlbar should contain the search string."
+ );
+
+ await Services.search.removeEngine(testEngineFoo);
+});
+
+// Tests that UrlbarProviderTokenAliasEngines is disabled in search mode.
+add_task(async function doNotShowInSearchMode() {
+ // Do a search for "@" to show all the @ aliases.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@",
+ });
+
+ // Find our test engine in the results. It's probably last, but for
+ // robustness don't assume it is.
+ let testEngineItem;
+ for (let i = 0; !testEngineItem; i++) {
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ if (details.searchParams && details.searchParams.keyword == ALIAS) {
+ testEngineItem = await UrlbarTestUtils.waitForAutocompleteResultAt(
+ window,
+ i
+ );
+ }
+ }
+
+ Assert.equal(
+ testEngineItem.result.payload.keyword,
+ ALIAS,
+ "Sanity check: we found our engine."
+ );
+
+ // Click it.
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeMouseAtCenter(testEngineItem, {});
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: testEngineItem.result.payload.engine,
+ entry: "keywordoffer",
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@",
+ fireInputEvent: true,
+ });
+
+ let resultCount = UrlbarTestUtils.getResultCount(window);
+ for (let i = 0; i < resultCount; i++) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ Assert.ok(
+ !result.searchParams.keyword,
+ `Result at index ${i} is not a keywordoffer.`
+ );
+ }
+});
+
+async function assertFirstResultIsAlias(isAlias, expectedAlias) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "Should have the correct type"
+ );
+
+ if (isAlias) {
+ Assert.equal(
+ result.searchParams.keyword,
+ expectedAlias,
+ "Payload keyword should be the alias"
+ );
+ } else {
+ Assert.notEqual(
+ result.searchParams.keyword,
+ expectedAlias,
+ "Payload keyword should be absent or not the alias"
+ );
+ }
+}
+
+function assertHighlighted(highlighted, expectedAlias) {
+ let selection = gURLBar.editor.selectionController.getSelection(
+ Ci.nsISelectionController.SELECTION_FIND
+ );
+ Assert.ok(selection);
+ if (!highlighted) {
+ Assert.equal(selection.rangeCount, 0);
+ return;
+ }
+ Assert.equal(selection.rangeCount, 1);
+ let index = gURLBar.value.indexOf(expectedAlias);
+ Assert.ok(
+ index >= 0,
+ `gURLBar.value="${gURLBar.value}" expectedAlias="${expectedAlias}"`
+ );
+ let range = selection.getRangeAt(0);
+ Assert.ok(range);
+ Assert.equal(range.startOffset, index);
+ Assert.equal(range.endOffset, index + expectedAlias.length);
+}
+
+/**
+ * Returns an array of code points in the given string. Each code point is
+ * returned as a hexidecimal string.
+ *
+ * @param {string} str
+ * The code points of this string will be returned.
+ * @returns {array}
+ * Array of code points in the string, where each is a hexidecimal string.
+ */
+function codePoints(str) {
+ return str.split("").map(s => s.charCodeAt(0).toString(16));
+}
diff --git a/browser/components/urlbar/tests/browser/browser_top_sites.js b/browser/components/urlbar/tests/browser/browser_top_sites.js
new file mode 100644
index 0000000000..080d1fcac5
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_top_sites.js
@@ -0,0 +1,477 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AboutNewTab: "resource:///modules/AboutNewTab.jsm",
+ NewTabUtils: "resource://gre/modules/NewTabUtils.jsm",
+});
+
+const EN_US_TOPSITES =
+ "https://www.youtube.com/,https://www.facebook.com/,https://www.amazon.com/,https://www.reddit.com/,https://www.wikipedia.org/,https://twitter.com/";
+
+async function addTestVisits() {
+ // Add some visits to a URL.
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits("http://example.com/");
+ }
+
+ // Wait for example.com to be listed first.
+ await updateTopSites(sites => {
+ return sites && sites[0] && sites[0].url == "http://example.com/";
+ });
+
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "https://www.youtube.com/",
+ title: "YouTube",
+ });
+}
+
+async function checkDoesNotOpenOnFocus(win = window) {
+ // The view should not open when the input is focused programmatically.
+ win.gURLBar.blur();
+ win.gURLBar.focus();
+ Assert.ok(!win.gURLBar.view.isOpen, "check urlbar panel is not open");
+ win.gURLBar.blur();
+
+ // Check the keyboard shortcut.
+ win.document.getElementById("Browser:OpenLocation").doCommand();
+ // Because the panel opening may not be immediate, we must wait a bit.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 500));
+ Assert.ok(!win.gURLBar.view.isOpen, "check urlbar panel is not open");
+ win.gURLBar.blur();
+
+ // Focus with the mouse.
+ EventUtils.synthesizeMouseAtCenter(win.gURLBar.inputField, {}, win);
+ // Because the panel opening may not be immediate, we must wait a bit.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 500));
+ Assert.ok(!win.gURLBar.view.isOpen, "check urlbar panel is not open");
+ win.gURLBar.blur();
+}
+
+add_task(async function init() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.topsites", true],
+ ["browser.newtabpage.activity-stream.default.sites", EN_US_TOPSITES],
+ ],
+ });
+
+ await updateTopSites(
+ sites => sites && sites.length == EN_US_TOPSITES.split(",").length
+ );
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/"
+ );
+
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(tab);
+ });
+});
+
+add_task(async function topSitesShown() {
+ let sites = AboutNewTab.getTopSites();
+
+ for (let prefVal of [true, false]) {
+ // This test should work regardless of whether Top Sites are enabled on
+ // about:newtab.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.newtabpage.activity-stream.feeds.topsites", prefVal]],
+ });
+ // We don't expect this to change, but we run updateTopSites just in case
+ // feeds.topsites were to have an effect on the composition of Top Sites.
+ await updateTopSites(siteList => siteList.length == 6);
+
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ if (gURLBar.getAttribute("pageproxystate") == "invalid") {
+ gURLBar.handleRevert();
+ }
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ Assert.ok(gURLBar.view.isOpen, "UrlbarView should be open.");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ sites.length,
+ "The number of results should be the same as the number of Top Sites (6)."
+ );
+
+ for (let i = 0; i < sites.length; i++) {
+ let site = sites[i];
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ if (site.searchTopSite) {
+ Assert.equal(
+ result.searchParams.keyword,
+ site.label,
+ "The search Top Site should have an alias."
+ );
+ continue;
+ }
+
+ Assert.equal(
+ site.url,
+ result.url,
+ "The Top Site URL and the result URL shoud match."
+ );
+ Assert.equal(
+ site.label || site.title || site.hostname,
+ result.title,
+ "The Top Site title and the result title shoud match."
+ );
+ }
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ gURLBar.blur();
+ });
+ // This pops updateTopSites changes.
+ await SpecialPowers.popPrefEnv();
+ await SpecialPowers.popPrefEnv();
+ }
+});
+
+add_task(async function selectSearchTopSite() {
+ await updateTopSites(
+ sites => sites && sites[0] && sites[0].searchTopSite,
+ true
+ );
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ if (gURLBar.getAttribute("pageproxystate") == "invalid") {
+ gURLBar.handleRevert();
+ }
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ let amazonSearch = await UrlbarTestUtils.waitForAutocompleteResultAt(
+ window,
+ 0
+ );
+
+ Assert.equal(
+ amazonSearch.result.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "First result should have SEARCH type."
+ );
+
+ Assert.equal(
+ amazonSearch.result.payload.keyword,
+ "@amazon",
+ "First result should have the Amazon keyword."
+ );
+
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeMouseAtCenter(amazonSearch, {});
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: amazonSearch.result.payload.engine,
+ entry: "topsites_urlbar",
+ });
+ await UrlbarTestUtils.exitSearchMode(window, { backspace: true });
+
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ gURLBar.blur();
+ });
+});
+
+add_task(async function topSitesBookmarksAndTabs() {
+ await addTestVisits();
+ let sites = AboutNewTab.getTopSites();
+ Assert.equal(
+ sites.length,
+ 7,
+ "The test suite browser should have 7 Top Sites."
+ );
+
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ if (gURLBar.getAttribute("pageproxystate") == "invalid") {
+ gURLBar.handleRevert();
+ }
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ Assert.ok(gURLBar.view.isOpen, "UrlbarView should be open.");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 7,
+ "The number of results should be the same as the number of Top Sites (7)."
+ );
+
+ let exampleResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ exampleResult.url,
+ "http://example.com/",
+ "The example.com Top Site should be the first result."
+ );
+ Assert.equal(
+ exampleResult.source,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ "The example.com Top Site should appear in the view as an open tab result."
+ );
+
+ let youtubeResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(
+ youtubeResult.url,
+ "https://www.youtube.com/",
+ "The YouTube Top Site should be the second result."
+ );
+ Assert.equal(
+ youtubeResult.source,
+ UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ "The YouTube Top Site should appear in the view as a bookmark result."
+ );
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ gURLBar.blur();
+ });
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function topSitesKeywordNavigationPageproxystate() {
+ await addTestVisits();
+ Assert.equal(
+ gURLBar.getAttribute("pageproxystate"),
+ "valid",
+ "Sanity check initial state"
+ );
+
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ if (gURLBar.getAttribute("pageproxystate") == "invalid") {
+ gURLBar.handleRevert();
+ }
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ Assert.ok(gURLBar.view.isOpen, "UrlbarView should be open.");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ let count = UrlbarTestUtils.getResultCount(window);
+ Assert.equal(count, 7, "The number of results should be the expected one.");
+
+ for (let i = 0; i < count; ++i) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.equal(
+ gURLBar.getAttribute("pageproxystate"),
+ "invalid",
+ "Moving through results"
+ );
+ }
+ for (let i = 0; i < count; ++i) {
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ Assert.equal(
+ gURLBar.getAttribute("pageproxystate"),
+ "invalid",
+ "Moving through results"
+ );
+ }
+
+ // Double ESC should restore state.
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeKey("KEY_Escape");
+ });
+ EventUtils.synthesizeKey("KEY_Escape");
+ Assert.equal(
+ gURLBar.getAttribute("pageproxystate"),
+ "valid",
+ "Double ESC should restore state"
+ );
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function topSitesPinned() {
+ await addTestVisits();
+ let info = { url: "http://example.com/" };
+ NewTabUtils.pinnedLinks.pin(info, 0);
+
+ await updateTopSites(sites => sites && sites[0] && sites[0].isPinned);
+
+ let sites = AboutNewTab.getTopSites();
+ Assert.equal(
+ sites.length,
+ 7,
+ "The test suite browser should have 7 Top Sites."
+ );
+
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ Assert.ok(gURLBar.view.isOpen, "UrlbarView should be open.");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 7,
+ "The number of results should be the same as the number of Top Sites (7)."
+ );
+
+ let exampleResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ exampleResult.url,
+ "http://example.com/",
+ "The example.com Top Site should be the first result."
+ );
+
+ Assert.equal(
+ exampleResult.source,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ "The example.com Top Site should be an open tab result."
+ );
+
+ Assert.ok(
+ exampleResult.element.row.hasAttribute("pinned"),
+ "The example.com Top Site should have the pinned property."
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ gURLBar.blur();
+ });
+ NewTabUtils.pinnedLinks.unpin(info);
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function topSitesBookmarksAndTabsDisabled() {
+ await addTestVisits();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.openpage", false],
+ ["browser.urlbar.suggest.bookmark", false],
+ ],
+ });
+
+ let sites = AboutNewTab.getTopSites();
+ Assert.equal(
+ sites.length,
+ 7,
+ "The test suite browser should have 7 Top Sites."
+ );
+
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ if (gURLBar.getAttribute("pageproxystate") == "invalid") {
+ gURLBar.handleRevert();
+ }
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ Assert.ok(gURLBar.view.isOpen, "UrlbarView should be open.");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 7,
+ "The number of results should be the same as the number of Top Sites (7)."
+ );
+
+ let exampleResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ exampleResult.url,
+ "http://example.com/",
+ "The example.com Top Site should be the second result."
+ );
+ Assert.equal(
+ exampleResult.source,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ "The example.com Top Site should appear as a normal result even though it's open in a tab."
+ );
+
+ let youtubeResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(
+ youtubeResult.url,
+ "https://www.youtube.com/",
+ "The YouTube Top Site should be the third result."
+ );
+ Assert.equal(
+ youtubeResult.source,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ "The YouTube Top Site should appear as a normal result even though it's bookmarked."
+ );
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ gURLBar.blur();
+ });
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function topSitesDisabled() {
+ // Disable Top Sites feed.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.newtabpage.activity-stream.feeds.system.topsites", false]],
+ });
+ await checkDoesNotOpenOnFocus();
+ await SpecialPowers.popPrefEnv();
+
+ // Top Sites should also not be shown when Urlbar Top Sites are disabled.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.topsites", false]],
+ });
+ await checkDoesNotOpenOnFocus();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function topSitesNumber() {
+ // Add some visits
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits([
+ "http://example-a.com/",
+ "http://example-b.com/",
+ "http://example-c.com/",
+ "http://example-d.com/",
+ "http://example-e.com/",
+ ]);
+ }
+
+ // Wait for the expected number of Top sites.
+ await updateTopSites(sites => sites && sites.length == 8);
+ Assert.equal(
+ AboutNewTab.getTopSites().length,
+ 8,
+ "The test suite browser should have 8 Top Sites."
+ );
+
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ Assert.ok(gURLBar.view.isOpen, "UrlbarView should be open.");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 8,
+ "The number of results should be the default (8)."
+ );
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.newtabpage.activity-stream.topSitesRows", 2]],
+ });
+ // Wait for the expected number of Top sites.
+ await updateTopSites(sites => sites && sites.length == 11);
+ Assert.equal(
+ AboutNewTab.getTopSites().length,
+ 11,
+ "The test suite browser should have 11 Top Sites."
+ );
+
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ Assert.ok(gURLBar.view.isOpen, "UrlbarView should be open.");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 10,
+ "The number of results should be maxRichResults (10)."
+ );
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ await SpecialPowers.popPrefEnv();
+ await PlacesUtils.history.clear();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_top_sites_private.js b/browser/components/urlbar/tests/browser/browser_top_sites_private.js
new file mode 100644
index 0000000000..570e623d57
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_top_sites_private.js
@@ -0,0 +1,170 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AboutNewTab: "resource:///modules/AboutNewTab.jsm",
+ NewTabUtils: "resource://gre/modules/NewTabUtils.jsm",
+});
+
+const EN_US_TOPSITES =
+ "https://www.youtube.com/,https://www.facebook.com/,https://www.amazon.com/,https://www.reddit.com/,https://www.wikipedia.org/,https://twitter.com/";
+
+async function addTestVisits() {
+ // Add some visits to a URL.
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits("http://example.com/");
+ }
+
+ // Wait for example.com to be listed first.
+ await updateTopSites(sites => {
+ return sites && sites[0] && sites[0].url == "http://example.com/";
+ });
+
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "https://www.youtube.com/",
+ title: "YouTube",
+ });
+}
+
+add_task(async function init() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.topsites", true],
+ ["browser.newtabpage.activity-stream.default.sites", EN_US_TOPSITES],
+ ],
+ });
+
+ await updateTopSites(
+ sites => sites && sites.length == EN_US_TOPSITES.split(",").length
+ );
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/"
+ );
+
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(tab);
+ });
+});
+
+add_task(async function topSitesPrivateWindow() {
+ // Top Sites should also be shown in private windows.
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ await addTestVisits();
+ let sites = AboutNewTab.getTopSites();
+ Assert.equal(
+ sites.length,
+ 7,
+ "The test suite browser should have 7 Top Sites."
+ );
+ let urlbar = privateWin.gURLBar;
+ await UrlbarTestUtils.promisePopupOpen(privateWin, () => {
+ if (urlbar.getAttribute("pageproxystate") == "invalid") {
+ urlbar.handleRevert();
+ }
+ EventUtils.synthesizeMouseAtCenter(urlbar.inputField, {}, privateWin);
+ });
+ Assert.ok(urlbar.view.isOpen, "UrlbarView should be open.");
+ await UrlbarTestUtils.promiseSearchComplete(privateWin);
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(privateWin),
+ 7,
+ "The number of results should be the same as the number of Top Sites (7)."
+ );
+
+ // Top sites should also be shown in a private window if the search string
+ // gets cleared.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: privateWin,
+ value: "example",
+ });
+ urlbar.select();
+ EventUtils.synthesizeKey("KEY_Backspace", {}, privateWin);
+ Assert.ok(urlbar.view.isOpen, "UrlbarView should be open.");
+ await UrlbarTestUtils.promiseSearchComplete(privateWin);
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(privateWin),
+ 7,
+ "The number of results should be the same as the number of Top Sites (7)."
+ );
+
+ await BrowserTestUtils.closeWindow(privateWin);
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ gURLBar.blur();
+ });
+});
+
+add_task(async function topSitesTabSwitch() {
+ // Add some visits
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits(["http://example.com/"]);
+ }
+
+ // Switch to the originating tab, to check for switch to the current tab.
+ gBrowser.selectedTab = gBrowser.tabs[0];
+
+ // Wait for the expected number of Top sites.
+ await updateTopSites(sites => sites?.length == 7);
+ Assert.equal(
+ AboutNewTab.getTopSites().length,
+ 7,
+ "The test suite browser should have 7 Top Sites."
+ );
+
+ async function checkResults(win, expectedResultType) {
+ let resultCount = UrlbarTestUtils.getResultCount(win);
+ let result;
+ for (let i = 0; i < resultCount; ++i) {
+ result = await UrlbarTestUtils.getDetailsOfResultAt(win, i);
+ if (result.url == "http://example.com/") {
+ break;
+ }
+ }
+ Assert.equal(
+ result.type,
+ expectedResultType,
+ `Should provide a result of type ${expectedResultType}.`
+ );
+ }
+
+ info("Test in a non-private window");
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ Assert.ok(gURLBar.view.isOpen, "UrlbarView should be open.");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await checkResults(window, UrlbarUtils.RESULT_TYPE.TAB_SWITCH);
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ info("Test in a private window, switch to tab should not be offered");
+ // Top Sites should also be shown in private windows.
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ let urlbar = privateWin.gURLBar;
+ await UrlbarTestUtils.promisePopupOpen(privateWin, () => {
+ if (urlbar.getAttribute("pageproxystate") == "invalid") {
+ urlbar.handleRevert();
+ }
+ EventUtils.synthesizeMouseAtCenter(urlbar.inputField, {}, privateWin);
+ });
+
+ Assert.ok(urlbar.view.isOpen, "UrlbarView should be open.");
+ await UrlbarTestUtils.promiseSearchComplete(privateWin);
+ await checkResults(privateWin, UrlbarUtils.RESULT_TYPE.URL);
+ await UrlbarTestUtils.promisePopupClose(privateWin);
+ await BrowserTestUtils.closeWindow(privateWin);
+
+ await PlacesUtils.history.clear();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_typed_value.js b/browser/components/urlbar/tests/browser/browser_typed_value.js
new file mode 100644
index 0000000000..a01ee3e08e
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_typed_value.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test ensures that the urlbar is restored to the typed value on blur.
+
+"use strict";
+
+add_task(async function setup() {
+ registerCleanupFunction(async function() {
+ Services.prefs.clearUserPref("browser.urlbar.autoFill");
+ gURLBar.handleRevert();
+ await PlacesUtils.history.clear();
+ });
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", true);
+
+ await PlacesTestUtils.addVisits([
+ "http://example.com/",
+ "http://example.com/foo",
+ ]);
+});
+
+add_task(async function test_autofill() {
+ let typed = "ex";
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: typed,
+ fireInputEvent: true,
+ });
+ Assert.equal(gURLBar.value, "example.com/", "autofilled value as expected");
+ Assert.equal(gURLBar.selectionStart, typed.length);
+ Assert.equal(gURLBar.selectionEnd, gURLBar.value.length);
+
+ gURLBar.blur();
+ Assert.equal(gURLBar.value, typed, "Value should have been restored");
+ gURLBar.focus();
+ Assert.equal(gURLBar.value, typed, "Value should not have changed");
+ Assert.equal(gURLBar.selectionStart, typed.length);
+ Assert.equal(gURLBar.selectionEnd, typed.length);
+});
+
+add_task(async function test_complete_selection() {
+ let typed = "ex";
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: typed,
+ fireInputEvent: true,
+ });
+ Assert.equal(gURLBar.value, "example.com/", "autofilled value as expected");
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 2,
+ "Should have the correct number of matches"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.equal(
+ gURLBar.value,
+ "example.com/foo",
+ "Value should have been completed"
+ );
+
+ gURLBar.blur();
+ Assert.equal(gURLBar.value, typed, "Value should have been restored");
+ gURLBar.focus();
+ Assert.equal(gURLBar.value, typed, "Value should not have changed");
+ Assert.equal(gURLBar.selectionStart, typed.length);
+ Assert.equal(gURLBar.selectionEnd, typed.length);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_updateForDomainCompletion.js b/browser/components/urlbar/tests/browser/browser_updateForDomainCompletion.js
new file mode 100644
index 0000000000..27b097cdd7
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_updateForDomainCompletion.js
@@ -0,0 +1,24 @@
+"use strict";
+
+/**
+ * Disable keyword.enabled (so no keyword search), and check that when you type in
+ * "example" and hit enter, the browser loads and the URL bar is updated accordingly.
+ */
+add_task(async function() {
+ await SpecialPowers.pushPrefEnv({ set: [["keyword.enabled", false]] });
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async function(browser) {
+ gURLBar.value = "example";
+ gURLBar.select();
+ const loadPromise = BrowserTestUtils.waitForDocLoadAndStopIt(
+ "https://www.example.com/",
+ gBrowser.selectedBrowser
+ );
+
+ EventUtils.sendKey("return");
+ await loadPromise;
+ ok(true, "https://www.example.com is loaded correctly");
+ }
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_updateRows.js b/browser/components/urlbar/tests/browser/browser_updateRows.js
new file mode 100644
index 0000000000..b764c166bf
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_updateRows.js
@@ -0,0 +1,240 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests row updating and reuse.
+
+"use strict";
+
+let TEST_BASE_URL = "http://example.com/";
+
+add_task(async function init() {
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+// A URL result is replaced with a tip result and then vice versa.
+add_task(async function urlToTip() {
+ // Add some visits that will be matched by a "test" search string.
+ await PlacesTestUtils.addVisits([
+ "http://example.com/testxx",
+ "http://example.com/test",
+ ]);
+
+ // Add a provider that returns a tip result when the search string is "testx".
+ let tipResult = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TIP,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ text: "This is a test tip.",
+ buttonText: "OK",
+ helpUrl: "http://example.com/",
+ type: "test",
+ }
+ );
+ tipResult.suggestedIndex = 1;
+ let provider = new UrlbarTestUtils.TestProvider({
+ results: [tipResult],
+ });
+ provider.isActive = context => context.searchString == "testx";
+ UrlbarProvidersManager.registerProvider(provider);
+
+ // Search for "test".
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ value: "test",
+ window,
+ });
+
+ // The result at index 1 should be the http://example.com/test visit.
+ await checkResult(
+ 1,
+ UrlbarUtils.RESULT_TYPE.URL,
+ {
+ title: "test visit for http://example.com/test",
+ tagsContainer: null,
+ titleSeparator: null,
+ action: "",
+ url: TEST_BASE_URL + "test",
+ },
+ ["tipButton", "helpButton"]
+ );
+
+ // Type an "x" so that the search string is "testx".
+ EventUtils.synthesizeKey("x");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ // Now the result at index 1 should be the tip from our provider.
+ await checkResult(
+ 1,
+ UrlbarUtils.RESULT_TYPE.TIP,
+ {
+ title: "This is a test tip.",
+ tipButton: "OK",
+ helpButton: null,
+ },
+ ["tagsContainer", "titleSeparator", "action", "url"]
+ );
+
+ // Type another "x" so that the search string is "testxx".
+ EventUtils.synthesizeKey("x");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ // The result at index 1 should be the http://example.com/testxx visit.
+ await checkResult(
+ 1,
+ UrlbarUtils.RESULT_TYPE.URL,
+ {
+ title: "test visit for http://example.com/testxx",
+ tagsContainer: null,
+ titleSeparator: null,
+ action: "",
+ url: TEST_BASE_URL + "testxx",
+ },
+ ["tipButton", "helpButton"]
+ );
+
+ // Backspace so that the search string is "testx" again.
+ EventUtils.synthesizeKey("KEY_Backspace");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ // The result at index 1 should be the tip again.
+ await checkResult(
+ 1,
+ UrlbarUtils.RESULT_TYPE.TIP,
+ {
+ title: "This is a test tip.",
+ tipButton: "OK",
+ helpButton: null,
+ },
+ ["tagsContainer", "titleSeparator", "action", "url"]
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeKey("KEY_Escape");
+ });
+ UrlbarProvidersManager.unregisterProvider(provider);
+ await PlacesUtils.history.clear();
+});
+
+// A tip result is replaced with URL result and then vice versa.
+add_task(async function tipToURL() {
+ // Add a visit that will be matched by a "testx" search string.
+ await PlacesTestUtils.addVisits("http://example.com/testx");
+
+ // Add a provider that returns a tip result when the search string is "test"
+ // or "testxx".
+ let tipResult = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TIP,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ text: "This is a test tip.",
+ buttonText: "OK",
+ helpUrl: "http://example.com/",
+ type: "test",
+ }
+ );
+ tipResult.suggestedIndex = 1;
+ let provider = new UrlbarTestUtils.TestProvider({
+ results: [tipResult],
+ });
+ provider.isActive = context =>
+ ["test", "testxx"].includes(context.searchString);
+ UrlbarProvidersManager.registerProvider(provider);
+
+ // Search for "test".
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ value: "test",
+ window,
+ });
+
+ // The result at index 1 should be the tip from our provider.
+ await checkResult(
+ 1,
+ UrlbarUtils.RESULT_TYPE.TIP,
+ {
+ title: "This is a test tip.",
+ tipButton: "OK",
+ helpButton: null,
+ },
+ ["tagsContainer", "titleSeparator", "action", "url"]
+ );
+
+ // Type an "x" so that the search string is "testx".
+ EventUtils.synthesizeKey("x");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ // Now the result at index 1 should be the visit.
+ await checkResult(
+ 1,
+ UrlbarUtils.RESULT_TYPE.URL,
+ {
+ title: "test visit for http://example.com/testx",
+ tagsContainer: null,
+ titleSeparator: null,
+ action: "",
+ url: TEST_BASE_URL + "testx",
+ },
+ ["tipButton", "helpButton"]
+ );
+
+ // Type another "x" so that the search string is "testxx".
+ EventUtils.synthesizeKey("x");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ // The result at index 1 should be the tip again.
+ await checkResult(
+ 1,
+ UrlbarUtils.RESULT_TYPE.TIP,
+ {
+ title: "This is a test tip.",
+ tipButton: "OK",
+ helpButton: null,
+ },
+ ["tagsContainer", "titleSeparator", "action", "url"]
+ );
+
+ // Backspace so that the search string is "testx" again.
+ EventUtils.synthesizeKey("KEY_Backspace");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ // The result at index 1 should be the visit again.
+ await checkResult(
+ 1,
+ UrlbarUtils.RESULT_TYPE.URL,
+ {
+ title: "test visit for http://example.com/testx",
+ tagsContainer: null,
+ titleSeparator: null,
+ action: "",
+ url: TEST_BASE_URL + "testx",
+ },
+ ["tipButton", "helpButton"]
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeKey("KEY_Escape");
+ });
+ UrlbarProvidersManager.unregisterProvider(provider);
+ await PlacesUtils.history.clear();
+});
+
+async function checkResult(index, type, presentElements, absentElements) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index);
+ Assert.equal(result.type, type, "Expected result type");
+
+ for (let [name, value] of Object.entries(presentElements)) {
+ let element = result.element.row._elements.get(name);
+ Assert.ok(element, `${name} should be present`);
+ if (typeof value == "string") {
+ Assert.equal(
+ element.textContent,
+ value,
+ `${name} value should be correct`
+ );
+ }
+ }
+
+ for (let name of absentElements) {
+ let element = result.element.row._elements.get(name);
+ Assert.ok(!element, `${name} should be absent`);
+ }
+}
diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry.js b/browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry.js
new file mode 100644
index 0000000000..221128d36e
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry.js
@@ -0,0 +1,1498 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ UrlbarProvider: "resource:///modules/UrlbarUtils.jsm",
+ UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+});
+
+const TEST_ENGINE_NAME = "Test";
+const TEST_ENGINE_ALIAS = "@test";
+const TEST_ENGINE_DOMAIN = "example.com";
+
+function copyToClipboard(str) {
+ return new Promise((resolve, reject) => {
+ waitForClipboard(
+ str,
+ () => {
+ Cc["@mozilla.org/widget/clipboardhelper;1"]
+ .getService(Ci.nsIClipboardHelper)
+ .copyString(str);
+ },
+ resolve,
+ reject
+ );
+ });
+}
+
+// Each test is a function that executes an urlbar action and returns the
+// expected event object, or null if no event is expected.
+const tests = [
+ /*
+ * Engagement tests.
+ */
+ async function(win) {
+ info("Type something, press Enter.");
+ win.gURLBar.select();
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "x",
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "1",
+ numWords: "1",
+ selIndex: "0",
+ selType: "searchengine",
+ provider: "HeuristicFallback",
+ },
+ };
+ },
+
+ async function(win) {
+ info("Type a multi-word query, press Enter.");
+ win.gURLBar.select();
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "multi word query ",
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "17",
+ numWords: "3",
+ selIndex: "0",
+ selType: "searchengine",
+ provider: "HeuristicFallback",
+ },
+ };
+ },
+
+ async function(win) {
+ info("Paste something, press Enter.");
+ win.gURLBar.select();
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await SimpleTest.promiseClipboardChange("test", () => {
+ clipboardHelper.copyString("test");
+ });
+ win.document.commandDispatcher
+ .getControllerForCommand("cmd_paste")
+ .doCommand("cmd_paste");
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "pasted",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "4",
+ numWords: "1",
+ selIndex: "0",
+ selType: "searchengine",
+ provider: "HeuristicFallback",
+ },
+ };
+ },
+
+ async function(win) {
+ info("Type something, click one-off and press enter.");
+ win.gURLBar.select();
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "test",
+ fireInputEvent: true,
+ });
+
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(win);
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }, win);
+ let selectedOneOff = UrlbarTestUtils.getOneOffSearchButtons(win)
+ .selectedButton;
+ selectedOneOff.click();
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: selectedOneOff.engine.name,
+ entry: "oneoff",
+ });
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "4",
+ numWords: "1",
+ selIndex: "0",
+ selType: "searchengine",
+ provider: "HeuristicFallback",
+ },
+ };
+ },
+
+ async function(win) {
+ info(
+ "Type something, select one-off with enter, and select result with enter."
+ );
+ win.gURLBar.select();
+
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(win);
+
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "test",
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }, win);
+ let selectedOneOff = UrlbarTestUtils.getOneOffSearchButtons(win)
+ .selectedButton;
+ Assert.ok(selectedOneOff);
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await searchPromise;
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "4",
+ numWords: "1",
+ selIndex: "0",
+ selType: "searchengine",
+ provider: "HeuristicFallback",
+ },
+ };
+ },
+
+ async function(win) {
+ info("Type something, ESC, type something else, press Enter.");
+ win.gURLBar.select();
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ EventUtils.synthesizeKey("x", {}, win);
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, win);
+ EventUtils.synthesizeKey("y", {}, win);
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "1",
+ numWords: "1",
+ selIndex: "0",
+ selType: "searchengine",
+ provider: "HeuristicFallback",
+ },
+ };
+ },
+
+ async function(win) {
+ info("Type a keyword, Enter.");
+ win.gURLBar.select();
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "kw test",
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "7",
+ numWords: "2",
+ selIndex: "0",
+ selType: "keyword",
+ provider: "UnifiedComplete",
+ },
+ };
+ },
+
+ async function(win) {
+ let tipProvider = registerTipProvider();
+ info("Selecting a tip's main button, enter.");
+ win.gURLBar.search("x");
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ unregisterTipProvider(tipProvider);
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "1",
+ numWords: "1",
+ selIndex: "1",
+ selType: "tip",
+ provider: tipProvider.name,
+ },
+ };
+ },
+
+ async function(win) {
+ let tipProvider = registerTipProvider();
+ info("Selecting a tip's help button, enter.");
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ win.gURLBar.search("x");
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ unregisterTipProvider(tipProvider);
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "1",
+ numWords: "1",
+ selIndex: "1",
+ selType: "tiphelp",
+ provider: tipProvider.name,
+ },
+ };
+ },
+
+ async function(win) {
+ info("Type something and canonize");
+ win.gURLBar.select();
+ const promise = BrowserTestUtils.waitForDocLoadAndStopIt(
+ "https://www.example.com/",
+ gBrowser.selectedBrowser
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "example",
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("VK_RETURN", { ctrlKey: true }, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "7",
+ numWords: "1",
+ selIndex: "0",
+ selType: "canonized",
+ provider: "Autofill",
+ },
+ };
+ },
+
+ async function(win) {
+ info("Type something, click on bookmark entry.");
+ win.gURLBar.select();
+ let url = "http://example.com/?q=%s";
+ let promise = BrowserTestUtils.browserLoaded(
+ win.gBrowser.selectedBrowser,
+ false,
+ url
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "exa",
+ fireInputEvent: true,
+ });
+ while (win.gURLBar.untrimmedValue != url) {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ }
+ let element = UrlbarTestUtils.getSelectedRow(win);
+ EventUtils.synthesizeMouseAtCenter(element, {}, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "click",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "3",
+ numWords: "1",
+ selIndex: val => parseInt(val) > 0,
+ selType: "bookmark",
+ provider: "UnifiedComplete",
+ },
+ };
+ },
+
+ async function(win) {
+ info("Type an autofilled string, Enter.");
+ win.gURLBar.select();
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "exa",
+ fireInputEvent: true,
+ });
+ // Check it's autofilled.
+ Assert.equal(win.gURLBar.selectionStart, 3);
+ Assert.equal(win.gURLBar.selectionEnd, 12);
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "3",
+ numWords: "1",
+ selIndex: "0",
+ selType: "autofill",
+ provider: "Autofill",
+ },
+ };
+ },
+
+ async function(win) {
+ info("Type something, select bookmark entry, Enter.");
+ win.gURLBar.select();
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "exa",
+ fireInputEvent: true,
+ });
+ while (win.gURLBar.untrimmedValue != "http://example.com/?q=%s") {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ }
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "3",
+ numWords: "1",
+ selIndex: val => parseInt(val) > 0,
+ selType: "bookmark",
+ provider: "UnifiedComplete",
+ },
+ };
+ },
+
+ async function(win) {
+ info("Type something, select remote search suggestion, Enter.");
+ win.gURLBar.select();
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "foo",
+ fireInputEvent: true,
+ });
+ while (win.gURLBar.untrimmedValue != "foofoo") {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ }
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "3",
+ numWords: "1",
+ selIndex: val => parseInt(val) > 0,
+ selType: "searchsuggestion",
+ provider: "SearchSuggestions",
+ },
+ };
+ },
+
+ async function(win) {
+ info("Type something, select form history, Enter.");
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.maxHistoricalSearchSuggestions", 2]],
+ });
+ await UrlbarTestUtils.formHistory.add(["foofoo", "foobar"]);
+ win.gURLBar.select();
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "foo",
+ fireInputEvent: true,
+ });
+ while (win.gURLBar.untrimmedValue != "foofoo") {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ }
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ await SpecialPowers.popPrefEnv();
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "3",
+ numWords: "1",
+ selIndex: val => parseInt(val) > 0,
+ selType: "formhistory",
+ provider: "SearchSuggestions",
+ },
+ };
+ },
+
+ async function(win) {
+ info("Type @, enter on a keywordoffer, then search and press enter.");
+ win.gURLBar.select();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "@",
+ fireInputEvent: true,
+ });
+
+ while (win.gURLBar.searchMode?.engineName != TEST_ENGINE_NAME) {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ }
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await UrlbarTestUtils.promiseSearchComplete(win);
+
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "moz",
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "3",
+ numWords: "1",
+ selIndex: "0",
+ selType: "searchengine",
+ provider: "HeuristicFallback",
+ },
+ };
+ },
+
+ async function(win) {
+ info("Type an @alias, then space, then search and press enter.");
+ const alias = "testalias";
+ let aliasEngine = await Services.search.addEngineWithDetails("AliasTest", {
+ alias,
+ template: "http://example.com/?search={searchTerms}",
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: `${alias} `,
+ });
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: "AliasTest",
+ entry: "typed",
+ });
+
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "moz",
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+
+ await Services.search.removeEngine(aliasEngine);
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "3",
+ numWords: "1",
+ selIndex: "0",
+ selType: "searchengine",
+ provider: "HeuristicFallback",
+ },
+ };
+ },
+
+ async function(win) {
+ info("Drop something.");
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ EventUtils.synthesizeDrop(
+ win.document.getElementById("home-button"),
+ win.gURLBar.inputField,
+ [[{ type: "text/plain", data: "www.example.com" }]],
+ "copy",
+ win
+ );
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "drop_go",
+ value: "dropped",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "15",
+ numWords: "1",
+ selIndex: "-1",
+ selType: "none",
+ provider: "",
+ },
+ };
+ },
+
+ async function(win) {
+ info("Paste & Go something.");
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await SimpleTest.promiseClipboardChange("www.example.com", () => {
+ clipboardHelper.copyString("www.example.com");
+ });
+ let inputBox = win.gURLBar.querySelector("moz-input-box");
+ let cxmenu = inputBox.menupopup;
+ let cxmenuPromise = BrowserTestUtils.waitForEvent(cxmenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(
+ win.gURLBar.inputField,
+ {
+ type: "contextmenu",
+ button: 2,
+ },
+ win
+ );
+ await cxmenuPromise;
+ let menuitem = inputBox.getMenuItem("paste-and-go");
+ EventUtils.synthesizeMouseAtCenter(menuitem, {}, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "paste_go",
+ value: "pasted",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "15",
+ numWords: "1",
+ selIndex: "-1",
+ selType: "none",
+ provider: "",
+ },
+ };
+ },
+
+ // The URLs in the down arrow/autoOpen tests must vary from test to test,
+ // else the first Top Site results will be a switch-to-tab result and a page
+ // load will not occur.
+ async function(win) {
+ info("Open the panel with DOWN, select with DOWN, Enter.");
+ await addTopSite("http://example.org/");
+ win.gURLBar.value = "";
+ win.gURLBar.select();
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ });
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ while (win.gURLBar.untrimmedValue != "http://example.org/") {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ }
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "topsites",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "0",
+ numWords: "0",
+ selType: "history",
+ selIndex: val => parseInt(val) >= 0,
+ provider: "UrlbarProviderTopSites",
+ },
+ };
+ },
+
+ async function(win) {
+ info("Open the panel with DOWN, click on entry.");
+ await addTopSite("http://example.com/");
+ win.gURLBar.value = "";
+ win.gURLBar.select();
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ });
+ while (win.gURLBar.untrimmedValue != "http://example.com/") {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ }
+ let element = UrlbarTestUtils.getSelectedRow(win);
+ EventUtils.synthesizeMouseAtCenter(element, {}, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "click",
+ value: "topsites",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "0",
+ numWords: "0",
+ selType: "history",
+ selIndex: "0",
+ provider: "UrlbarProviderTopSites",
+ },
+ };
+ },
+
+ // The URLs in the autoOpen tests must vary from test to test, else
+ // the first Top Site results will be a switch-to-tab result and a page load
+ // will not occur.
+ async function(win) {
+ info(
+ "With pageproxystate=valid, autoopen the panel, select with DOWN, Enter."
+ );
+ await addTopSite("http://example.org/");
+ win.gURLBar.value = "";
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ win.document.getElementById("Browser:OpenLocation").doCommand();
+ });
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ while (win.gURLBar.untrimmedValue != "http://example.org/") {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ }
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "topsites",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "0",
+ numWords: "0",
+ selType: "history",
+ selIndex: val => parseInt(val) >= 0,
+ provider: "UrlbarProviderTopSites",
+ },
+ };
+ },
+
+ async function(win) {
+ info("With pageproxystate=valid, autoopen the panel, click on entry.");
+ await addTopSite("http://example.com/");
+ win.gURLBar.value = "";
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ win.document.getElementById("Browser:OpenLocation").doCommand();
+ });
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ while (win.gURLBar.untrimmedValue != "http://example.com/") {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ }
+ let element = UrlbarTestUtils.getSelectedRow(win);
+ EventUtils.synthesizeMouseAtCenter(element, {}, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "click",
+ value: "topsites",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "0",
+ numWords: "0",
+ selType: "history",
+ selIndex: "0",
+ provider: "UrlbarProviderTopSites",
+ },
+ };
+ },
+
+ async function(win) {
+ info("With pageproxystate=invalid, open retained results, Enter.");
+ await addTopSite("http://example.org/");
+ win.gURLBar.value = "example.org";
+ win.gURLBar.setPageProxyState("invalid");
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ win.document.getElementById("Browser:OpenLocation").doCommand();
+ });
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "returned",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "11",
+ numWords: "1",
+ selType: "autofill",
+ selIndex: "0",
+ provider: "Autofill",
+ },
+ };
+ },
+
+ async function(win) {
+ info("With pageproxystate=invalid, open retained results, click on entry.");
+ // This value must be different from the previous test, to avoid reopening
+ // the view.
+ win.gURLBar.value = "example.com";
+ win.gURLBar.setPageProxyState("invalid");
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ win.document.getElementById("Browser:OpenLocation").doCommand();
+ });
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ let element = UrlbarTestUtils.getSelectedRow(win);
+ EventUtils.synthesizeMouseAtCenter(element, {}, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "click",
+ value: "returned",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "11",
+ numWords: "1",
+ selType: "autofill",
+ selIndex: "0",
+ provider: "Autofill",
+ },
+ };
+ },
+
+ async function(win) {
+ info("Reopen the view: type, blur, focus, confirm.");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "search",
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.promisePopupClose(win, () => {
+ win.gURLBar.blur();
+ });
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ win.document.getElementById("Browser:OpenLocation").doCommand();
+ });
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ return [
+ {
+ category: "urlbar",
+ method: "abandonment",
+ object: "blur",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "6",
+ numWords: "1",
+ },
+ },
+ {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "returned",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "6",
+ numWords: "1",
+ selType: "searchengine",
+ selIndex: "0",
+ provider: "HeuristicFallback",
+ },
+ },
+ ];
+ },
+
+ async function(win) {
+ info("Open search mode with a keyboard shortcut.");
+ let defaultEngine = await Services.search.getDefault();
+ win.gURLBar.select();
+ EventUtils.synthesizeKey("k", { accelKey: true });
+ await UrlbarTestUtils.assertSearchMode(win, {
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ engineName: defaultEngine.name,
+ entry: "shortcut",
+ });
+
+ // Execute a search to finish the engagement.
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "moz",
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "3",
+ numWords: "1",
+ selIndex: "0",
+ selType: "searchengine",
+ provider: "HeuristicFallback",
+ },
+ };
+ },
+
+ async function(win) {
+ info("Open search mode from a tab-to-search result.");
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0]],
+ });
+
+ await PlacesUtils.history.clear();
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits([`https://${TEST_ENGINE_DOMAIN}/`]);
+ }
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: TEST_ENGINE_DOMAIN.slice(0, 4),
+ });
+
+ let tabToSearchResult = (
+ await UrlbarTestUtils.waitForAutocompleteResultAt(win, 1)
+ ).result;
+ Assert.equal(
+ tabToSearchResult.providerName,
+ "TabToSearch",
+ "The second result is a tab-to-search result."
+ );
+
+ // Select the tab-to-search result.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(win);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(win, {
+ engineName: TEST_ENGINE_NAME,
+ entry: "tabtosearch",
+ });
+
+ // Execute a search to finish the engagement.
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "moz",
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+
+ await PlacesUtils.history.clear();
+ await SpecialPowers.popPrefEnv();
+
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "3",
+ numWords: "1",
+ selIndex: "0",
+ selType: "searchengine",
+ provider: "HeuristicFallback",
+ },
+ };
+ },
+
+ async function(win) {
+ info("Sanity check we are not stuck on 'returned'");
+ win.gURLBar.select();
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "x",
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "1",
+ numWords: "1",
+ selIndex: "0",
+ selType: "searchengine",
+ provider: "HeuristicFallback",
+ },
+ };
+ },
+
+ async function(win) {
+ info("Reopen the view: type, blur, focus, backspace, type, confirm.");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "search",
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.promisePopupClose(win, () => {
+ win.gURLBar.blur();
+ });
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ win.document.getElementById("Browser:OpenLocation").doCommand();
+ });
+ EventUtils.synthesizeKey("VK_RIGHT", {}, win);
+ EventUtils.synthesizeKey("VK_BACK_SPACE", {}, win);
+ EventUtils.synthesizeKey("x", {}, win);
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ return [
+ {
+ category: "urlbar",
+ method: "abandonment",
+ object: "blur",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "6",
+ numWords: "1",
+ },
+ },
+ {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "returned",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "6",
+ numWords: "1",
+ selType: "searchengine",
+ selIndex: "0",
+ provider: "HeuristicFallback",
+ },
+ },
+ ];
+ },
+
+ async function(win) {
+ info("Reopen the view: type, blur, focus, type (overwrite), confirm.");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "search",
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.promisePopupClose(win, () => {
+ win.gURLBar.blur();
+ });
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ win.document.getElementById("Browser:OpenLocation").doCommand();
+ });
+ EventUtils.synthesizeKey("x", {}, win);
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ return [
+ {
+ category: "urlbar",
+ method: "abandonment",
+ object: "blur",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "6",
+ numWords: "1",
+ },
+ },
+ {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "restarted",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "1",
+ numWords: "1",
+ selType: "searchengine",
+ selIndex: "0",
+ provider: "HeuristicFallback",
+ },
+ },
+ ];
+ },
+
+ async function(win) {
+ info("Sanity check we are not stuck on 'restarted'");
+ win.gURLBar.select();
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "x",
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "1",
+ numWords: "1",
+ selIndex: "0",
+ selType: "searchengine",
+ provider: "HeuristicFallback",
+ },
+ };
+ },
+
+ /*
+ * Abandonment tests.
+ */
+
+ async function(win) {
+ info("Type something, blur.");
+ win.gURLBar.select();
+ EventUtils.synthesizeKey("x", {}, win);
+ win.gURLBar.blur();
+ return {
+ category: "urlbar",
+ method: "abandonment",
+ object: "blur",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "1",
+ numWords: "1",
+ },
+ };
+ },
+
+ async function(win) {
+ info("Open the panel with DOWN, don't type, blur it.");
+ await addTopSite("http://example.org/");
+ win.gURLBar.value = "";
+ win.gURLBar.select();
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ });
+ win.gURLBar.blur();
+ return {
+ category: "urlbar",
+ method: "abandonment",
+ object: "blur",
+ value: "topsites",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "0",
+ numWords: "0",
+ },
+ };
+ },
+
+ async function(win) {
+ info("With pageproxystate=valid, autoopen the panel, don't type, blur it.");
+ win.gURLBar.value = "";
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ win.document.getElementById("Browser:OpenLocation").doCommand();
+ });
+ win.gURLBar.blur();
+ return {
+ category: "urlbar",
+ method: "abandonment",
+ object: "blur",
+ value: "topsites",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "0",
+ numWords: "0",
+ },
+ };
+ },
+
+ async function(win) {
+ info("Enter search mode from Top Sites.");
+ await updateTopSites(sites => true, /* enableSearchShorcuts */ true);
+
+ win.gURLBar.value = "";
+ win.gURLBar.select();
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ });
+ while (win.gURLBar.searchMode?.engineName != "Google") {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ }
+ let element = UrlbarTestUtils.getSelectedRow(win);
+ Assert.ok(
+ element.result.source == UrlbarUtils.RESULT_SOURCE.SEARCH,
+ "The selected result is a search Top Site."
+ );
+
+ let engine = element.result.payload.engine;
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeMouseAtCenter(element, {}, win);
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(win, {
+ engineName: engine,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ entry: "topsites_urlbar",
+ });
+
+ await UrlbarTestUtils.exitSearchMode(win);
+
+ // To avoid needing to add a custom search shortcut Top Site, we just
+ // abandon this interaction.
+ await UrlbarTestUtils.promisePopupClose(win, () => {
+ win.gURLBar.blur();
+ });
+
+ return {
+ category: "urlbar",
+ method: "abandonment",
+ object: "blur",
+ value: "topsites",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "0",
+ numWords: "0",
+ },
+ };
+ },
+
+ async function(win) {
+ info("Open search mode from a tab-to-search result.");
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0]],
+ });
+
+ await PlacesUtils.history.clear();
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits([`https://${TEST_ENGINE_DOMAIN}/`]);
+ }
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: TEST_ENGINE_DOMAIN.slice(0, 4),
+ });
+
+ let tabToSearchResult = (
+ await UrlbarTestUtils.waitForAutocompleteResultAt(win, 1)
+ ).result;
+ Assert.equal(
+ tabToSearchResult.providerName,
+ "TabToSearch",
+ "The second result is a tab-to-search result."
+ );
+
+ // Select the tab-to-search result.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(win);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(win, {
+ engineName: TEST_ENGINE_NAME,
+ entry: "tabtosearch",
+ });
+
+ // Abandon the interaction since simply entering search mode is not
+ // considered the end of an engagement.
+ await UrlbarTestUtils.promisePopupClose(win, () => {
+ win.gURLBar.blur();
+ });
+
+ await PlacesUtils.history.clear();
+ await SpecialPowers.popPrefEnv();
+
+ return {
+ category: "urlbar",
+ method: "abandonment",
+ object: "blur",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "0",
+ numWords: "0",
+ },
+ };
+ },
+
+ async function(win) {
+ info(
+ "With pageproxystate=invalid, open retained results, don't type, blur it."
+ );
+ win.gURLBar.value = "mochi.test";
+ win.gURLBar.setPageProxyState("invalid");
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ win.document.getElementById("Browser:OpenLocation").doCommand();
+ });
+ win.gURLBar.blur();
+ return {
+ category: "urlbar",
+ method: "abandonment",
+ object: "blur",
+ value: "returned",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "10",
+ numWords: "1",
+ },
+ };
+ },
+];
+
+const noEventTests = [
+ async function(win) {
+ info("Type something, click on search settings.");
+ await BrowserTestUtils.withNewTab(
+ { gBrowser: win.gBrowser, url: "about:blank" },
+ async browser => {
+ win.gURLBar.select();
+ let promise = BrowserTestUtils.browserLoaded(browser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "x",
+ fireInputEvent: true,
+ });
+ UrlbarTestUtils.getOneOffSearchButtons(win).settingsButton.click();
+ await promise;
+ }
+ );
+ return null;
+ },
+
+ async function(win) {
+ info("Type something, Up, Enter on search settings.");
+ await BrowserTestUtils.withNewTab(
+ { gBrowser: win.gBrowser, url: "about:blank" },
+ async browser => {
+ win.gURLBar.select();
+ let promise = BrowserTestUtils.browserLoaded(browser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "x",
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("KEY_ArrowUp", {}, win);
+ Assert.ok(
+ UrlbarTestUtils.getOneOffSearchButtons(
+ win
+ ).selectedButton.classList.contains("search-setting-button-compact"),
+ "Should have selected the settings button"
+ );
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ }
+ );
+ return null;
+ },
+];
+
+add_task(async function test() {
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.eventTelemetry.enabled", true],
+ ["browser.urlbar.suggest.searches", true],
+ ],
+ });
+ // Create a new search engine and mark it as default
+ let engine = await SearchTestUtils.promiseNewSearchEngine(
+ getRootDirectory(gTestPath) + "searchSuggestionEngine.xml"
+ );
+ let oldDefaultEngine = await Services.search.getDefault();
+ await Services.search.setDefault(engine);
+ await Services.search.moveEngine(engine, 0);
+
+ let aliasEngine = await Services.search.addEngineWithDetails(
+ TEST_ENGINE_NAME,
+ {
+ alias: TEST_ENGINE_ALIAS,
+ template: `http://${TEST_ENGINE_DOMAIN}/?search={searchTerms}`,
+ }
+ );
+
+ // Add a bookmark and a keyword.
+ let bm = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.com/?q=%s",
+ title: "test",
+ });
+ await PlacesUtils.keywords.insert({
+ keyword: "kw",
+ url: "http://example.com/?q=%s",
+ });
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://mochi.test:8888/",
+ transition: PlacesUtils.history.TRANSITIONS.TYPED,
+ },
+ ]);
+
+ registerCleanupFunction(async function() {
+ await Services.search.setDefault(oldDefaultEngine);
+ await Services.search.removeEngine(aliasEngine);
+ await PlacesUtils.keywords.remove("kw");
+ await PlacesUtils.bookmarks.remove(bm);
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.formHistory.clear(window);
+ });
+
+ // This is not necessary after each loop, because assertEvents does it.
+ Services.telemetry.clearEvents();
+
+ for (let i = 0; i < tests.length; i++) {
+ info(`Running test at index ${i}`);
+ let events = await tests[i](window);
+ if (!Array.isArray(events)) {
+ events = [events];
+ }
+ // Always blur to ensure it's not accounted as an additional abandonment.
+ window.gURLBar.setSearchMode({});
+ gURLBar.blur();
+ TelemetryTestUtils.assertEvents(events, { category: "urlbar" });
+ await UrlbarTestUtils.formHistory.clear(window);
+ }
+
+ for (let i = 0; i < noEventTests.length; i++) {
+ info(`Running no event test at index ${i}`);
+ await noEventTests[i](window);
+ // Always blur to ensure it's not accounted as an additional abandonment.
+ gURLBar.blur();
+ TelemetryTestUtils.assertEvents([], { category: "urlbar" });
+ }
+});
+
+/**
+ * Replaces the contents of Top Sites with the specified site.
+ * @param {string} site
+ * A site to add to Top Sites.
+ */
+async function addTopSite(site) {
+ await PlacesUtils.history.clear();
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits(site);
+ }
+
+ await updateTopSites(sites => sites && sites[0] && sites[0].url == site);
+}
+
+function registerTipProvider() {
+ let provider = new UrlbarTestUtils.TestProvider({
+ results: tipMatches,
+ priority: 1,
+ });
+ UrlbarProvidersManager.registerProvider(provider);
+ return provider;
+}
+
+function unregisterTipProvider(provider) {
+ UrlbarProvidersManager.unregisterProvider(provider);
+}
+
+let tipMatches = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/a" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TIP,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ text: "This is a test intervention.",
+ buttonText: "Done",
+ type: "test",
+ helpUrl: "about:about",
+ }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/b" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/c" }
+ ),
+];
diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_selection.js b/browser/components/urlbar/tests/browser/browser_urlbar_selection.js
new file mode 100644
index 0000000000..e890ed17cf
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_urlbar_selection.js
@@ -0,0 +1,299 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const exampleSearch = "f oo bar";
+const exampleUrl = "https://example.com/1";
+
+function click(target) {
+ let promise = BrowserTestUtils.waitForEvent(target, "click");
+ EventUtils.synthesizeMouseAtCenter(target, {}, target.ownerGlobal);
+ return promise;
+}
+
+function openContextMenu(target) {
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ target.ownerGlobal,
+ "contextmenu"
+ );
+
+ EventUtils.synthesizeMouseAtCenter(
+ target,
+ {
+ type: "contextmenu",
+ button: 2,
+ },
+ target.ownerGlobal
+ );
+ return popupShownPromise;
+}
+
+function drag(target, fromX, fromY, toX, toY) {
+ let promise = BrowserTestUtils.waitForEvent(target, "mouseup");
+ EventUtils.synthesizeMouse(
+ target,
+ fromX,
+ fromY,
+ { type: "mousemove" },
+ target.ownerGlobal
+ );
+ EventUtils.synthesizeMouse(
+ target,
+ fromX,
+ fromY,
+ { type: "mousedown" },
+ target.ownerGlobal
+ );
+ EventUtils.synthesizeMouse(
+ target,
+ toX,
+ toY,
+ { type: "mousemove" },
+ target.ownerGlobal
+ );
+ EventUtils.synthesizeMouse(
+ target,
+ toX,
+ toY,
+ { type: "mouseup" },
+ target.ownerGlobal
+ );
+ return promise;
+}
+
+function resetPrimarySelection(val = "") {
+ if (Services.clipboard.supportsSelectionClipboard()) {
+ // Reset the clipboard.
+ clipboardHelper.copyStringToClipboard(
+ val,
+ Services.clipboard.kSelectionClipboard
+ );
+ }
+}
+
+function checkPrimarySelection(expectedVal = "") {
+ if (Services.clipboard.supportsSelectionClipboard()) {
+ let primaryAsText = SpecialPowers.getClipboardData(
+ "text/unicode",
+ SpecialPowers.Ci.nsIClipboard.kSelectionClipboard
+ );
+ Assert.equal(primaryAsText, expectedVal);
+ }
+}
+
+add_task(async function setup() {
+ // On macOS, we must "warm up" the Urlbar to get the first test to pass.
+ gURLBar.value = "";
+ await click(gURLBar.inputField);
+ gURLBar.blur();
+});
+
+add_task(async function leftClickSelectsAll() {
+ resetPrimarySelection();
+ gURLBar.value = exampleSearch;
+ await click(gURLBar.inputField);
+ Assert.equal(
+ gURLBar.selectionStart,
+ 0,
+ "The entire search term should be selected."
+ );
+ Assert.equal(
+ gURLBar.selectionEnd,
+ exampleSearch.length,
+ "The entire search term should be selected."
+ );
+ gURLBar.blur();
+ checkPrimarySelection();
+});
+
+add_task(async function leftClickSelectsUrl() {
+ resetPrimarySelection();
+ gURLBar.value = exampleUrl;
+ await click(gURLBar.inputField);
+ Assert.equal(gURLBar.selectionStart, 0, "The entire url should be selected.");
+ Assert.equal(
+ gURLBar.selectionEnd,
+ exampleUrl.length,
+ "The entire url should be selected."
+ );
+ gURLBar.blur();
+ checkPrimarySelection();
+});
+
+add_task(async function rightClickSelectsAll() {
+ gURLBar.inputField.focus();
+ gURLBar.value = exampleUrl;
+
+ // Remove the selection so the focus() call above doesn't influence the test.
+ gURLBar.selectionStart = gURLBar.selectionEnd = 0;
+
+ resetPrimarySelection();
+
+ await openContextMenu(gURLBar.inputField);
+
+ Assert.equal(gURLBar.selectionStart, 0, "The entire URL should be selected.");
+ Assert.equal(
+ gURLBar.selectionEnd,
+ exampleUrl.length,
+ "The entire URL should be selected."
+ );
+
+ checkPrimarySelection();
+
+ let contextMenu = gURLBar.querySelector("moz-input-box").menupopup;
+
+ // While the context menu is open, test the "Select All" button.
+ let contextMenuItem = contextMenu.firstElementChild;
+ while (
+ contextMenuItem.nextElementSibling &&
+ contextMenuItem.getAttribute("cmd") != "cmd_selectAll"
+ ) {
+ contextMenuItem = contextMenuItem.nextElementSibling;
+ }
+ Assert.ok(
+ contextMenuItem,
+ "The context menu should have the select all menu item."
+ );
+
+ let controller = document.commandDispatcher.getControllerForCommand(
+ contextMenuItem.getAttribute("cmd")
+ );
+ let enabled = controller.isCommandEnabled(
+ contextMenuItem.getAttribute("cmd")
+ );
+ Assert.ok(enabled, "The context menu select all item should be enabled.");
+
+ await click(contextMenuItem);
+ Assert.equal(
+ gURLBar.selectionStart,
+ 0,
+ "The entire URL should be selected after clicking selectAll button."
+ );
+ Assert.equal(
+ gURLBar.selectionEnd,
+ exampleUrl.length,
+ "The entire URL should be selected after clicking selectAll button."
+ );
+
+ gURLBar.querySelector("moz-input-box").menupopup.hidePopup();
+ gURLBar.blur();
+ checkPrimarySelection(gURLBar.value);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function contextMenuDoesNotCancelSelection() {
+ gURLBar.inputField.focus();
+ gURLBar.value = exampleUrl;
+
+ gURLBar.selectionStart = 3;
+ gURLBar.selectionEnd = 7;
+
+ resetPrimarySelection();
+
+ await openContextMenu(gURLBar.inputField);
+
+ Assert.equal(
+ gURLBar.selectionStart,
+ 3,
+ "The selection should not have changed."
+ );
+ Assert.equal(
+ gURLBar.selectionEnd,
+ 7,
+ "The selection should not have changed."
+ );
+
+ gURLBar.querySelector("moz-input-box").menupopup.hidePopup();
+ gURLBar.blur();
+ checkPrimarySelection();
+});
+
+add_task(async function dragSelect() {
+ resetPrimarySelection();
+ gURLBar.value = exampleSearch.repeat(10);
+ // Drags from an artibrary offset of 30 to test for bug 1562145: that the
+ // selection does not start at the beginning.
+ await drag(gURLBar.inputField, 30, 0, 60, 0);
+ Assert.greater(
+ gURLBar.selectionStart,
+ 0,
+ "Selection should not start at the beginning of the string."
+ );
+
+ let selectedVal = gURLBar.value.substring(
+ gURLBar.selectionStart,
+ gURLBar.selectionEnd
+ );
+ gURLBar.blur();
+ checkPrimarySelection(selectedVal);
+});
+
+/**
+ * Testing for bug 1571018: that the entire Urlbar isn't selected when the
+ * Urlbar is dragged following a selectsAll event then a blur.
+ */
+add_task(async function dragAfterSelectAll() {
+ resetPrimarySelection();
+ gURLBar.value = exampleSearch.repeat(10);
+ await click(gURLBar.inputField);
+ Assert.equal(
+ gURLBar.selectionStart,
+ 0,
+ "The entire search term should be selected."
+ );
+ Assert.equal(
+ gURLBar.selectionEnd,
+ exampleSearch.repeat(10).length,
+ "The entire search term should be selected."
+ );
+
+ gURLBar.blur();
+ checkPrimarySelection();
+
+ // The offset of 30 is arbitrary.
+ await drag(gURLBar.inputField, 30, 0, 60, 0);
+
+ Assert.notEqual(
+ gURLBar.selectionStart,
+ 0,
+ "Only part of the search term should be selected."
+ );
+ Assert.notEqual(
+ gURLBar.selectionEnd,
+ exampleSearch.repeat(10).length,
+ "Only part of the search term should be selected."
+ );
+
+ checkPrimarySelection(
+ gURLBar.value.substring(gURLBar.selectionStart, gURLBar.selectionEnd)
+ );
+});
+
+/**
+ * Testing for bug 1571018: that the entire Urlbar is selected when the Urlbar
+ * is refocused following a partial text selection then a blur.
+ */
+add_task(async function selectAllAfterDrag() {
+ gURLBar.value = exampleSearch;
+
+ gURLBar.selectionStart = 3;
+ gURLBar.selectionEnd = 7;
+
+ gURLBar.blur();
+
+ await click(gURLBar.inputField);
+
+ Assert.equal(
+ gURLBar.selectionStart,
+ 0,
+ "The entire search term should be selected."
+ );
+ Assert.equal(
+ gURLBar.selectionEnd,
+ exampleSearch.length,
+ "The entire search term should be selected."
+ );
+
+ gURLBar.blur();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry.js
new file mode 100644
index 0000000000..1e59b99eb1
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry.js
@@ -0,0 +1,1346 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This file tests urlbar telemetry with search related actions.
+ */
+
+"use strict";
+
+const SCALAR_URLBAR = "browser.engagement.navigation.urlbar";
+const SCALAR_SEARCHMODE = "browser.engagement.navigation.urlbar_searchmode";
+
+// The preference to enable suggestions in the urlbar.
+const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches";
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ SearchSERPTelemetry: "resource:///modules/SearchSERPTelemetry.jsm",
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.jsm",
+});
+
+function searchInAwesomebar(value, win = window) {
+ return UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ waitForFocus,
+ value,
+ fireInputEvent: true,
+ });
+}
+
+/**
+ * Click one of the entries in the urlbar suggestion popup.
+ *
+ * @param {string} resultTitle
+ * The title of the result to click on.
+ * @param {number} button [optional]
+ * which button to click.
+ * @returns {number}
+ * The index of the result that was clicked, or -1 if not found.
+ */
+async function clickURLBarSuggestion(resultTitle, button = 1) {
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ const count = UrlbarTestUtils.getResultCount(window);
+ for (let i = 0; i < count; i++) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ if (result.displayed.title == resultTitle) {
+ // This entry is the search suggestion we're looking for.
+ let element = await UrlbarTestUtils.waitForAutocompleteResultAt(
+ window,
+ i
+ );
+ if (button == 1) {
+ EventUtils.synthesizeMouseAtCenter(element, {});
+ } else if (button == 2) {
+ EventUtils.synthesizeMouseAtCenter(element, {
+ type: "mousedown",
+ button: 2,
+ });
+ }
+ return i;
+ }
+ }
+ return -1;
+}
+
+/**
+ * Create an engine to generate search suggestions and add it as default
+ * for this test.
+ *
+ * @param {function} taskFn
+ * The function to run with the new search engine as default.
+ */
+async function withNewSearchEngine(taskFn) {
+ const url =
+ getRootDirectory(gTestPath) + "urlbarTelemetrySearchSuggestions.xml";
+ let suggestionEngine = await Services.search.addOpenSearchEngine(url, "");
+ let previousEngine = await Services.search.getDefault();
+ await Services.search.setDefault(suggestionEngine);
+
+ try {
+ await taskFn(suggestionEngine);
+ } finally {
+ await Services.search.setDefault(previousEngine);
+ await Services.search.removeEngine(suggestionEngine);
+ }
+}
+
+add_task(async function setup() {
+ // Create a new search engine.
+ await Services.search.addEngineWithDetails("MozSearch", {
+ alias: "mozalias",
+ method: "GET",
+ template: "http://example.com/?q={searchTerms}",
+ });
+
+ // Make it the default search engine.
+ let engine = Services.search.getEngineByName("MozSearch");
+ let originalEngine = await Services.search.getDefault();
+ await Services.search.setDefault(engine);
+
+ // And the first one-off engine.
+ await Services.search.moveEngine(engine, 0);
+
+ // Enable search suggestions in the urlbar.
+ let suggestionsEnabled = Services.prefs.getBoolPref(SUGGEST_URLBAR_PREF);
+ Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, true);
+
+ // Enable local telemetry recording for the duration of the tests.
+ let oldCanRecord = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+
+ // Enable event recording for the events tested here.
+ Services.telemetry.setEventRecordingEnabled("navigation", true);
+
+ // Clear history so that history added by previous tests doesn't mess up this
+ // test when it selects results in the urlbar.
+ await PlacesUtils.history.clear();
+
+ // Clear historical search suggestions to avoid interference from previous
+ // tests.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.maxHistoricalSearchSuggestions", 0]],
+ });
+
+ // Use the default matching bucket configuration.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.matchBuckets", "general:5,suggestion:4"]],
+ });
+
+ // Allows UrlbarTestUtils to access this scope's test helpers, like Assert.
+ UrlbarTestUtils.init(this);
+
+ // Make sure to restore the engine once we're done.
+ registerCleanupFunction(async function() {
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ await Services.search.setDefault(originalEngine);
+ await Services.search.removeEngine(engine);
+ Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, suggestionsEnabled);
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.formHistory.clear();
+ Services.telemetry.setEventRecordingEnabled("navigation", false);
+ UrlbarTestUtils.uninit();
+ });
+});
+
+add_task(async function test_simpleQuery() {
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+
+ let resultIndexHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_INDEX"
+ );
+ let resultTypeHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_TYPE_2"
+ );
+ let resultIndexByTypeHist = TelemetryTestUtils.getAndClearKeyedHistogram(
+ "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2"
+ );
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ );
+ let search_hist = TelemetryTestUtils.getAndClearKeyedHistogram(
+ "SEARCH_COUNTS"
+ );
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Simulate entering a simple search.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("simple query");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ // Check if the scalars contain the expected values.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ SCALAR_URLBAR,
+ "search_enter",
+ 1
+ );
+ Assert.equal(
+ Object.keys(scalars[SCALAR_URLBAR]).length,
+ 1,
+ "This search must only increment one entry in the scalar."
+ );
+
+ // SEARCH_COUNTS should be incremented, but only the urlbar source since an
+ // internal @search keyword was not used.
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.urlbar",
+ 1
+ );
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.alias",
+ undefined
+ );
+
+ // Also check events.
+ TelemetryTestUtils.assertEvents(
+ [
+ [
+ "navigation",
+ "search",
+ "urlbar",
+ "enter",
+ { engine: "other-MozSearch" },
+ ],
+ ],
+ { category: "navigation", method: "search" }
+ );
+
+ // Check the histograms as well.
+ TelemetryTestUtils.assertHistogram(resultIndexHist, 0, 1);
+
+ TelemetryTestUtils.assertHistogram(
+ resultTypeHist,
+ UrlbarUtils.SELECTED_RESULT_TYPES.searchengine,
+ 1
+ );
+
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ resultIndexByTypeHist,
+ "searchengine",
+ 0,
+ 1
+ );
+
+ TelemetryTestUtils.assertHistogram(
+ resultMethodHist,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.enter,
+ 1
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_searchMode_enter() {
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+
+ let resultIndexHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_INDEX"
+ );
+ let resultTypeHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_TYPE_2"
+ );
+ let resultIndexByTypeHist = TelemetryTestUtils.getAndClearKeyedHistogram(
+ "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2"
+ );
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ );
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Enter search mode using an alias and a query.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("mozalias query");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ // Check if the scalars contain the expected values.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ SCALAR_SEARCHMODE,
+ "search_enter",
+ 1
+ );
+ Assert.equal(
+ Object.keys(scalars[SCALAR_SEARCHMODE]).length,
+ 1,
+ "This search must only increment one entry in the scalar."
+ );
+
+ // Also check events.
+ TelemetryTestUtils.assertEvents(
+ [
+ [
+ "navigation",
+ "search",
+ "urlbar_searchmode",
+ "enter",
+ { engine: "other-MozSearch" },
+ ],
+ ],
+ { category: "navigation", method: "search" }
+ );
+
+ // Check the histograms as well.
+ TelemetryTestUtils.assertHistogram(resultIndexHist, 0, 1);
+
+ TelemetryTestUtils.assertHistogram(
+ resultTypeHist,
+ UrlbarUtils.SELECTED_RESULT_TYPES.searchengine,
+ 1
+ );
+
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ resultIndexByTypeHist,
+ "searchengine",
+ 0,
+ 1
+ );
+
+ TelemetryTestUtils.assertHistogram(
+ resultMethodHist,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.enter,
+ 1
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Performs a search using the first result, a one-off button, and the Return
+// (Enter) key.
+add_task(async function test_oneOff_enter() {
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+
+ let resultIndexHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_INDEX"
+ );
+ let resultTypeHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_TYPE_2"
+ );
+ let resultIndexByTypeHist = TelemetryTestUtils.getAndClearKeyedHistogram(
+ "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2"
+ );
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ );
+ let search_hist = TelemetryTestUtils.getAndClearKeyedHistogram(
+ "SEARCH_COUNTS"
+ );
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Perform a one-off search using the first engine.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("query");
+
+ info("Pressing Alt+Down to take us to the first one-off engine.");
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ let engine = UrlbarTestUtils.getOneOffSearchButtons(window).selectedButton
+ .engine;
+ EventUtils.synthesizeKey("KEY_Enter");
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: engine.name,
+ entry: "oneoff",
+ });
+
+ // Now that we're in search mode, execute the search.
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ // Check if the scalars contain the expected values.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ SCALAR_SEARCHMODE,
+ "search_enter",
+ 1
+ );
+ Assert.equal(
+ Object.keys(scalars[SCALAR_SEARCHMODE]).length,
+ 1,
+ "This search must only increment one entry in the scalar."
+ );
+
+ // SEARCH_COUNTS should be incremented, but only the urlbar-searchmode source
+ // since aliases aren't counted separately in search mode.
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.urlbar-searchmode",
+ 1
+ );
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.alias",
+ undefined
+ );
+
+ // Also check events.
+ TelemetryTestUtils.assertEvents(
+ [
+ [
+ "navigation",
+ "search",
+ "urlbar_searchmode",
+ "enter",
+ { engine: "other-MozSearch" },
+ ],
+ ],
+ { category: "navigation", method: "search" }
+ );
+
+ // Check the histograms as well.
+ TelemetryTestUtils.assertHistogram(resultIndexHist, 0, 1);
+
+ TelemetryTestUtils.assertHistogram(
+ resultTypeHist,
+ UrlbarUtils.SELECTED_RESULT_TYPES.searchengine,
+ 1
+ );
+
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ resultIndexByTypeHist,
+ "searchengine",
+ 0,
+ 1
+ );
+
+ TelemetryTestUtils.assertHistogram(
+ resultMethodHist,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.enter,
+ 1
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Performs a search using the second result, a one-off button, and the Return
+// (Enter) key. This only tests the FX_URLBAR_SELECTED_RESULT_METHOD histogram
+// since test_oneOff_enter covers everything else.
+add_task(async function test_oneOff_enterSelection() {
+ Services.telemetry.clearScalars();
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ );
+
+ await withNewSearchEngine(async function() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Type a query. Suggestions should be generated by the test engine.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("query");
+
+ info(
+ "Select the second result, press Alt+Down to take us to the first one-off engine."
+ );
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ let engine = UrlbarTestUtils.getOneOffSearchButtons(window).selectedButton
+ .engine;
+ EventUtils.synthesizeKey("KEY_Enter");
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: engine.name,
+ entry: "oneoff",
+ });
+
+ // Now that we're in search mode, execute the search.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ TelemetryTestUtils.assertHistogram(
+ resultMethodHist,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection,
+ 1
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ });
+});
+
+// Performs a search using a click on a one-off button. This only tests the
+// FX_URLBAR_SELECTED_RESULT_METHOD histogram since test_oneOff_enter covers
+// everything else.
+add_task(async function test_oneOff_click() {
+ Services.telemetry.clearScalars();
+
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ );
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Type a query.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("query");
+
+ info("Click the first one-off button.");
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ let oneOffButton = UrlbarTestUtils.getOneOffSearchButtons(
+ window
+ ).getSelectableButtons(false)[0];
+ oneOffButton.click();
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: oneOffButton.engine.name,
+ entry: "oneoff",
+ });
+
+ // Now that we're in search mode, execute the search.
+ let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
+ Assert.ok(element, "Found result after entering search mode.");
+ EventUtils.synthesizeMouseAtCenter(element, {});
+ await p;
+
+ TelemetryTestUtils.assertHistogram(
+ resultMethodHist,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.click,
+ 1
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Clicks the first suggestion offered by the test search engine.
+add_task(async function test_suggestion_click() {
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+ await UrlbarTestUtils.formHistory.clear();
+
+ let resultIndexHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_INDEX"
+ );
+ let resultTypeHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_TYPE_2"
+ );
+ let resultIndexByTypeHist = TelemetryTestUtils.getAndClearKeyedHistogram(
+ "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2"
+ );
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ );
+ let search_hist = TelemetryTestUtils.getAndClearKeyedHistogram(
+ "SEARCH_COUNTS"
+ );
+
+ await withNewSearchEngine(async function(engine) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Type a query. Suggestions should be generated by the test engine.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("query");
+ info("Clicking the urlbar suggestion.");
+ await clickURLBarSuggestion("queryfoo");
+ await p;
+
+ // Check if the scalars contain the expected values.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ SCALAR_URLBAR,
+ "search_suggestion",
+ 1
+ );
+ Assert.equal(
+ Object.keys(scalars[SCALAR_URLBAR]).length,
+ 1,
+ "This search must only increment one entry in the scalar."
+ );
+
+ // SEARCH_COUNTS should be incremented.
+ let searchEngineId = "other-" + engine.name;
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ searchEngineId + ".urlbar",
+ 1
+ );
+
+ // Also check events.
+ TelemetryTestUtils.assertEvents(
+ [
+ [
+ "navigation",
+ "search",
+ "urlbar",
+ "suggestion",
+ { engine: searchEngineId },
+ ],
+ ],
+ { category: "navigation", method: "search" }
+ );
+
+ // Check the histograms as well.
+ TelemetryTestUtils.assertHistogram(resultIndexHist, 3, 1);
+
+ TelemetryTestUtils.assertHistogram(
+ resultTypeHist,
+ UrlbarUtils.SELECTED_RESULT_TYPES.searchsuggestion,
+ 1
+ );
+
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ resultIndexByTypeHist,
+ "searchsuggestion",
+ 3,
+ 1
+ );
+
+ TelemetryTestUtils.assertHistogram(
+ resultMethodHist,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.click,
+ 1
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ });
+});
+
+// Selects and presses the Return (Enter) key on the first suggestion offered by
+// the test search engine. This only tests the FX_URLBAR_SELECTED_RESULT_METHOD
+// histogram since test_suggestion_click covers everything else.
+add_task(async function test_suggestion_arrowEnterSelection() {
+ Services.telemetry.clearScalars();
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ );
+
+ await withNewSearchEngine(async function() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Type a query. Suggestions should be generated by the test engine.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("query");
+ info("Select the second result and press Return.");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ TelemetryTestUtils.assertHistogram(
+ resultMethodHist,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection,
+ 1
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ });
+});
+
+// Selects through tab and presses the Return (Enter) key on the first
+// suggestion offered by the test search engine.
+add_task(async function test_suggestion_tabEnterSelection() {
+ Services.telemetry.clearScalars();
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ );
+
+ await withNewSearchEngine(async function() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Type a query. Suggestions should be generated by the test engine.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("query");
+ info("Select the second result and press Return.");
+ EventUtils.synthesizeKey("KEY_Tab");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ TelemetryTestUtils.assertHistogram(
+ resultMethodHist,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.tabEnterSelection,
+ 1
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ });
+});
+
+// Selects through code and presses the Return (Enter) key on the first
+// suggestion offered by the test search engine.
+add_task(async function test_suggestion_enterSelection() {
+ Services.telemetry.clearScalars();
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ );
+
+ await withNewSearchEngine(async function() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Type a query. Suggestions should be generated by the test engine.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("query");
+ info("Select the second result and press Return.");
+ UrlbarTestUtils.setSelectedRowIndex(window, 1);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ TelemetryTestUtils.assertHistogram(
+ resultMethodHist,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.enterSelection,
+ 1
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ });
+});
+
+// Clicks the first suggestion offered by the test search engine when in search
+// mode.
+add_task(async function test_searchmode_suggestion_click() {
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+
+ let resultIndexHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_INDEX"
+ );
+ let resultTypeHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_TYPE_2"
+ );
+ let resultIndexByTypeHist = TelemetryTestUtils.getAndClearKeyedHistogram(
+ "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2"
+ );
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ );
+ let search_hist = TelemetryTestUtils.getAndClearKeyedHistogram(
+ "SEARCH_COUNTS"
+ );
+
+ await withNewSearchEngine(async function(engine) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Type a query. Suggestions should be generated by the test engine.");
+ await searchInAwesomebar("query");
+ await UrlbarTestUtils.enterSearchMode(window, {
+ engineName: engine.name,
+ });
+ info("Clicking the urlbar suggestion.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await clickURLBarSuggestion("queryfoo");
+ await p;
+
+ // Check if the scalars contain the expected values.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ SCALAR_SEARCHMODE,
+ "search_suggestion",
+ 1
+ );
+ Assert.equal(
+ Object.keys(scalars[SCALAR_SEARCHMODE]).length,
+ 1,
+ "This search must only increment one entry in the scalar."
+ );
+
+ // SEARCH_COUNTS should be incremented.
+ let searchEngineId = "other-" + engine.name;
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ searchEngineId + ".urlbar-searchmode",
+ 1
+ );
+
+ // Also check events.
+ TelemetryTestUtils.assertEvents(
+ [
+ [
+ "navigation",
+ "search",
+ "urlbar_searchmode",
+ "suggestion",
+ { engine: searchEngineId },
+ ],
+ ],
+ { category: "navigation", method: "search" }
+ );
+
+ // Check the histograms as well.
+ TelemetryTestUtils.assertHistogram(resultIndexHist, 1, 1);
+
+ TelemetryTestUtils.assertHistogram(
+ resultTypeHist,
+ UrlbarUtils.SELECTED_RESULT_TYPES.searchsuggestion,
+ 1
+ );
+
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ resultIndexByTypeHist,
+ "searchsuggestion",
+ 1,
+ 1
+ );
+
+ TelemetryTestUtils.assertHistogram(
+ resultMethodHist,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.click,
+ 1
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ });
+});
+
+// Selects and presses the Return (Enter) key on the first suggestion offered by
+// the test search engine in search mode. This only tests the
+// FX_URLBAR_SELECTED_RESULT_METHOD histogram since
+// test_searchmode_suggestion_click covers everything else.
+add_task(async function test_searchmode_suggestion_arrowEnterSelection() {
+ Services.telemetry.clearScalars();
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ );
+
+ await withNewSearchEngine(async function(engine) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Type a query. Suggestions should be generated by the test engine.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("query");
+ await UrlbarTestUtils.enterSearchMode(window, {
+ engineName: engine.name,
+ });
+ info("Select the second result and press Return.");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ TelemetryTestUtils.assertHistogram(
+ resultMethodHist,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection,
+ 1
+ );
+ BrowserTestUtils.removeTab(tab);
+ });
+});
+
+// Selects through tab and presses the Return (Enter) key on the first
+// suggestion offered by the test search engine in search mode.
+add_task(async function test_suggestion_tabEnterSelection() {
+ Services.telemetry.clearScalars();
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ );
+
+ await withNewSearchEngine(async function(engine) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Type a query. Suggestions should be generated by the test engine.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("query");
+ await UrlbarTestUtils.enterSearchMode(window, {
+ engineName: engine.name,
+ });
+ info("Select the second result and press Return.");
+ EventUtils.synthesizeKey("KEY_Tab");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ TelemetryTestUtils.assertHistogram(
+ resultMethodHist,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.tabEnterSelection,
+ 1
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ });
+});
+
+// Selects through code and presses the Return (Enter) key on the first
+// suggestion offered by the test search engine in search mode.
+add_task(async function test_suggestion_enterSelection() {
+ Services.telemetry.clearScalars();
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ );
+
+ await withNewSearchEngine(async function(engine) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Type a query. Suggestions should be generated by the test engine.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("query");
+ await UrlbarTestUtils.enterSearchMode(window, {
+ engineName: engine.name,
+ });
+ info("Select the second result and press Return.");
+ UrlbarTestUtils.setSelectedRowIndex(window, 1);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ TelemetryTestUtils.assertHistogram(
+ resultMethodHist,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.enterSelection,
+ 1
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ });
+});
+
+// Clicks a form history result.
+add_task(async function test_formHistory_click() {
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+ await UrlbarTestUtils.formHistory.clear();
+ await UrlbarTestUtils.formHistory.add(["foobar"]);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.maxHistoricalSearchSuggestions", 1]],
+ });
+
+ let resultIndexHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_INDEX"
+ );
+ let resultTypeHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_TYPE_2"
+ );
+ let resultIndexByTypeHist = TelemetryTestUtils.getAndClearKeyedHistogram(
+ "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2"
+ );
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ );
+ let search_hist = TelemetryTestUtils.getAndClearKeyedHistogram(
+ "SEARCH_COUNTS"
+ );
+
+ await withNewSearchEngine(async engine => {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Type a query. There should be form history.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("foo");
+ info("Clicking the form history.");
+ let foobarIndex = await clickURLBarSuggestion("foobar");
+ await p;
+
+ // Check if the scalars contain the expected values.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ SCALAR_URLBAR,
+ "search_formhistory",
+ 1
+ );
+ Assert.equal(
+ Object.keys(scalars[SCALAR_URLBAR]).length,
+ 1,
+ "This search must only increment one entry in the scalar."
+ );
+
+ // SEARCH_COUNTS should be incremented.
+ let searchEngineId = "other-" + engine.name;
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ searchEngineId + ".urlbar",
+ 1
+ );
+
+ // Also check events.
+ TelemetryTestUtils.assertEvents(
+ [
+ [
+ "navigation",
+ "search",
+ "urlbar",
+ "formhistory",
+ { engine: searchEngineId },
+ ],
+ ],
+ { category: "navigation", method: "search" }
+ );
+
+ // Check the histograms as well.
+ TelemetryTestUtils.assertHistogram(resultIndexHist, foobarIndex, 1);
+
+ TelemetryTestUtils.assertHistogram(
+ resultTypeHist,
+ UrlbarUtils.SELECTED_RESULT_TYPES.formhistory,
+ 1
+ );
+
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ resultIndexByTypeHist,
+ "formhistory",
+ foobarIndex,
+ 1
+ );
+
+ TelemetryTestUtils.assertHistogram(
+ resultMethodHist,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.click,
+ 1
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ await UrlbarTestUtils.formHistory.clear();
+ await SpecialPowers.popPrefEnv();
+ });
+});
+
+// Selects and presses the Return (Enter) key on a form history result. This
+// only tests the FX_URLBAR_SELECTED_RESULT_METHOD histogram since
+// test_formHistory_click covers everything else.
+add_task(async function test_formHistory_arrowEnterSelection() {
+ Services.telemetry.clearScalars();
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ );
+
+ await UrlbarTestUtils.formHistory.clear();
+ await UrlbarTestUtils.formHistory.add(["foobar"]);
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.maxHistoricalSearchSuggestions", 1]],
+ });
+
+ await withNewSearchEngine(async function() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Type a query. There should be form history.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("foo");
+ info("Select the form history result and press Return.");
+ while (gURLBar.untrimmedValue != "foobar") {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ TelemetryTestUtils.assertHistogram(
+ resultMethodHist,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection,
+ 1
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ await UrlbarTestUtils.formHistory.clear();
+ await SpecialPowers.popPrefEnv();
+ });
+});
+
+// Selects through tab and presses the Return (Enter) key on a form history
+// result.
+add_task(async function test_formHistory_tabEnterSelection() {
+ Services.telemetry.clearScalars();
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ );
+
+ await UrlbarTestUtils.formHistory.clear();
+ await UrlbarTestUtils.formHistory.add(["foobar"]);
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.maxHistoricalSearchSuggestions", 1]],
+ });
+
+ await withNewSearchEngine(async function() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Type a query. There should be form history.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("foo");
+ info("Select the form history result and press Return.");
+ while (gURLBar.untrimmedValue != "foobar") {
+ EventUtils.synthesizeKey("KEY_Tab");
+ }
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ TelemetryTestUtils.assertHistogram(
+ resultMethodHist,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.tabEnterSelection,
+ 1
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ await UrlbarTestUtils.formHistory.clear();
+ await SpecialPowers.popPrefEnv();
+ });
+});
+
+// Selects through code and presses the Return (Enter) key on a form history
+// result.
+add_task(async function test_formHistory_enterSelection() {
+ Services.telemetry.clearScalars();
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ );
+
+ await UrlbarTestUtils.formHistory.clear();
+ await UrlbarTestUtils.formHistory.add(["foobar"]);
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.maxHistoricalSearchSuggestions", 1]],
+ });
+
+ await withNewSearchEngine(async function() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Type a query. There should be form history.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("foo");
+ info("Select the second result and press Return.");
+ let index = 1;
+ while (gURLBar.untrimmedValue != "foobar") {
+ UrlbarTestUtils.setSelectedRowIndex(window, index++);
+ }
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ TelemetryTestUtils.assertHistogram(
+ resultMethodHist,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.enterSelection,
+ 1
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ await UrlbarTestUtils.formHistory.clear();
+ await SpecialPowers.popPrefEnv();
+ });
+});
+
+add_task(async function test_privateWindow() {
+ // Override the search telemetry search provider info to
+ // count in-content SEARCH_COUNTs telemetry for our test engine.
+ SearchSERPTelemetry.overrideSearchTelemetryForTests([
+ {
+ telemetryId: "example",
+ searchPageRegexp: "^http://example\\.com/",
+ queryParamName: "q",
+ },
+ ]);
+
+ let search_hist = TelemetryTestUtils.getAndClearKeyedHistogram(
+ "SEARCH_COUNTS"
+ );
+
+ // First, do a bunch of searches in a private window.
+ let win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+
+ info("Search in a private window and the pref does not exist");
+ let p = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await searchInAwesomebar("query", win);
+ EventUtils.synthesizeKey("KEY_Enter", undefined, win);
+ await p;
+
+ // SEARCH_COUNTS should be incremented.
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.urlbar",
+ 1
+ );
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "example.in-content:organic:none",
+ 1
+ );
+
+ info("Search again in a private window after setting the pref to true");
+ Services.prefs.setBoolPref("browser.engagement.search_counts.pbm", true);
+ p = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await searchInAwesomebar("another query", win);
+ EventUtils.synthesizeKey("KEY_Enter", undefined, win);
+ await p;
+
+ // SEARCH_COUNTS should *not* be incremented.
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.urlbar",
+ 1
+ );
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "example.in-content:organic:none",
+ 1
+ );
+
+ info("Search again in a private window after setting the pref to false");
+ Services.prefs.setBoolPref("browser.engagement.search_counts.pbm", false);
+ p = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await searchInAwesomebar("another query", win);
+ EventUtils.synthesizeKey("KEY_Enter", undefined, win);
+ await p;
+
+ // SEARCH_COUNTS should be incremented.
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.urlbar",
+ 2
+ );
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "example.in-content:organic:none",
+ 2
+ );
+
+ info("Search again in a private window after clearing the pref");
+ Services.prefs.clearUserPref("browser.engagement.search_counts.pbm");
+ p = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await searchInAwesomebar("another query", win);
+ EventUtils.synthesizeKey("KEY_Enter", undefined, win);
+ await p;
+
+ // SEARCH_COUNTS should be incremented.
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.urlbar",
+ 3
+ );
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "example.in-content:organic:none",
+ 3
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+
+ // Now, do a bunch of searches in a non-private window. Telemetry should
+ // always be recorded regardless of the pref's value.
+ win = await BrowserTestUtils.openNewBrowserWindow();
+
+ info("Search in a non-private window and the pref does not exist");
+ p = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await searchInAwesomebar("query", win);
+ EventUtils.synthesizeKey("KEY_Enter", undefined, win);
+ await p;
+
+ // SEARCH_COUNTS should be incremented.
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.urlbar",
+ 4
+ );
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "example.in-content:organic:none",
+ 4
+ );
+
+ info("Search again in a non-private window after setting the pref to true");
+ Services.prefs.setBoolPref("browser.engagement.search_counts.pbm", true);
+ p = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await searchInAwesomebar("another query", win);
+ EventUtils.synthesizeKey("KEY_Enter", undefined, win);
+ await p;
+
+ // SEARCH_COUNTS should be incremented.
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.urlbar",
+ 5
+ );
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "example.in-content:organic:none",
+ 5
+ );
+
+ info("Search again in a non-private window after setting the pref to false");
+ Services.prefs.setBoolPref("browser.engagement.search_counts.pbm", false);
+ p = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await searchInAwesomebar("another query", win);
+ EventUtils.synthesizeKey("KEY_Enter", undefined, win);
+ await p;
+
+ // SEARCH_COUNTS should be incremented.
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.urlbar",
+ 6
+ );
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "example.in-content:organic:none",
+ 6
+ );
+
+ info("Search again in a non-private window after clearing the pref");
+ Services.prefs.clearUserPref("browser.engagement.search_counts.pbm");
+ p = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await searchInAwesomebar("another query", win);
+ EventUtils.synthesizeKey("KEY_Enter", undefined, win);
+ await p;
+
+ // SEARCH_COUNTS should be incremented.
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.urlbar",
+ 7
+ );
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "example.in-content:organic:none",
+ 7
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+
+ // Reset the search provider info.
+ SearchSERPTelemetry.overrideSearchTelemetryForTests();
+ await UrlbarTestUtils.formHistory.clear();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_dynamic.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_dynamic.js
new file mode 100644
index 0000000000..20dadd0d7d
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_dynamic.js
@@ -0,0 +1,160 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This file tests urlbar telemetry for dynamic results.
+ */
+
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm",
+ UrlbarResult: "resource:///modules/UrlbarResult.jsm",
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.jsm",
+ UrlbarView: "resource:///modules/UrlbarView.jsm",
+});
+
+const DYNAMIC_TYPE_NAME = "test";
+
+/**
+ * A test URLBar provider.
+ */
+class TestProvider extends UrlbarTestUtils.TestProvider {
+ constructor() {
+ super({
+ priority: Infinity,
+ results: [
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ dynamicType: DYNAMIC_TYPE_NAME,
+ }
+ ),
+ { heuristic: true }
+ ),
+ ],
+ });
+ }
+
+ getViewUpdate(result, idsByName) {
+ return {
+ title: {
+ textContent: "This is a dynamic result.",
+ },
+ button: {
+ textContent: "Click Me",
+ },
+ };
+ }
+}
+
+add_task(async function test() {
+ // Add a dynamic result type.
+ UrlbarResult.addDynamicResultType(DYNAMIC_TYPE_NAME);
+ UrlbarView.addDynamicViewTemplate(DYNAMIC_TYPE_NAME, {
+ stylesheet:
+ getRootDirectory(gTestPath) + "urlbarTelemetryUrlbarDynamic.css",
+ children: [
+ {
+ name: "title",
+ tag: "span",
+ },
+ {
+ name: "buttonSpacer",
+ tag: "span",
+ },
+ {
+ name: "button",
+ tag: "span",
+ attributes: {
+ role: "button",
+ },
+ },
+ ],
+ });
+ registerCleanupFunction(() => {
+ UrlbarView.removeDynamicViewTemplate(DYNAMIC_TYPE_NAME);
+ UrlbarResult.removeDynamicResultType(DYNAMIC_TYPE_NAME);
+ });
+
+ // Register a provider that returns the dynamic result type.
+ let provider = new TestProvider();
+ UrlbarProvidersManager.registerProvider(provider);
+ registerCleanupFunction(() => {
+ UrlbarProvidersManager.unregisterProvider(provider);
+ });
+
+ const histograms = snapshotHistograms();
+
+ // Do a search to show the dynamic result.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value: "test",
+ fireInputEvent: true,
+ });
+
+ // Press enter on the result's button. It will be preselected since the
+ // result is the heuristic.
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Enter")
+ );
+
+ assertTelemetryResults(
+ histograms,
+ "dynamic",
+ 0,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.enter
+ );
+
+ // Clean up for subsequent tests.
+ gURLBar.handleRevert();
+});
+
+function snapshotHistograms() {
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+ return {
+ resultIndexHist: TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_INDEX"
+ ),
+ resultTypeHist: TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_TYPE_2"
+ ),
+ resultIndexByTypeHist: TelemetryTestUtils.getAndClearKeyedHistogram(
+ "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2"
+ ),
+ resultMethodHist: TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ ),
+ };
+}
+
+function assertTelemetryResults(histograms, type, index, method) {
+ TelemetryTestUtils.assertHistogram(histograms.resultIndexHist, index, 1);
+
+ TelemetryTestUtils.assertHistogram(
+ histograms.resultTypeHist,
+ UrlbarUtils.SELECTED_RESULT_TYPES[type],
+ 1
+ );
+
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ histograms.resultIndexByTypeHist,
+ type,
+ index,
+ 1
+ );
+
+ TelemetryTestUtils.assertHistogram(histograms.resultMethodHist, method, 1);
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ `urlbar.picked.${type}`,
+ index,
+ 1
+ );
+}
diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_extension.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_extension.js
new file mode 100644
index 0000000000..89914b10ba
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_extension.js
@@ -0,0 +1,181 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This file tests urlbar telemetry with extension actions.
+ */
+
+"use strict";
+
+const SCALAR_URLBAR = "browser.engagement.navigation.urlbar";
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.jsm",
+});
+
+function assertSearchTelemetryEmpty(search_hist) {
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false);
+ Assert.ok(
+ !(SCALAR_URLBAR in scalars),
+ `Should not have recorded ${SCALAR_URLBAR}`
+ );
+
+ // Make sure SEARCH_COUNTS contains identical values.
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.urlbar",
+ undefined
+ );
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.alias",
+ undefined
+ );
+
+ // Also check events.
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ );
+ events = (events.parent || []).filter(
+ e => e[1] == "navigation" && e[2] == "search"
+ );
+ Assert.deepEqual(
+ events,
+ [],
+ "Should not have recorded any navigation search events"
+ );
+}
+
+function snapshotHistograms() {
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+ return {
+ resultIndexHist: TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_INDEX"
+ ),
+ resultTypeHist: TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_TYPE_2"
+ ),
+ resultIndexByTypeHist: TelemetryTestUtils.getAndClearKeyedHistogram(
+ "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2"
+ ),
+ resultMethodHist: TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ ),
+ search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"),
+ };
+}
+
+function assertTelemetryResults(histograms, type, index, method) {
+ TelemetryTestUtils.assertHistogram(histograms.resultIndexHist, index, 1);
+
+ TelemetryTestUtils.assertHistogram(
+ histograms.resultTypeHist,
+ UrlbarUtils.SELECTED_RESULT_TYPES[type],
+ 1
+ );
+
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ histograms.resultIndexByTypeHist,
+ type,
+ index,
+ 1
+ );
+
+ TelemetryTestUtils.assertHistogram(histograms.resultMethodHist, method, 1);
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ `urlbar.picked.${type}`,
+ index,
+ 1
+ );
+}
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Disable search suggestions in the urlbar.
+ ["browser.urlbar.suggest.searches", false],
+ // Clear historical search suggestions to avoid interference from previous
+ // tests.
+ ["browser.urlbar.maxHistoricalSearchSuggestions", 0],
+ // Use the default matching bucket configuration.
+ ["browser.urlbar.matchBuckets", "general:5,suggestion:4"],
+ // Turn autofill off.
+ ["browser.urlbar.autoFill", false],
+ ],
+ });
+
+ // Enable local telemetry recording for the duration of the tests.
+ let oldCanRecord = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+
+ // Enable event recording for the events tested here.
+ Services.telemetry.setEventRecordingEnabled("navigation", true);
+
+ // Clear history so that history added by previous tests doesn't mess up this
+ // test when it selects results in the urlbar.
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ // Make sure to restore the engine once we're done.
+ registerCleanupFunction(async function() {
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ Services.telemetry.setEventRecordingEnabled("navigation", false);
+ });
+});
+
+add_task(async function test_extension() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ omnibox: {
+ keyword: "omniboxtest",
+ },
+
+ background() {
+ /* global browser */
+ browser.omnibox.setDefaultSuggestion({
+ description: "doit",
+ });
+ // Just do nothing for this test.
+ browser.omnibox.onInputEntered.addListener(() => {});
+ browser.omnibox.onInputChanged.addListener((text, suggest) => {
+ suggest([]);
+ });
+ },
+ },
+ });
+
+ await extension.startup();
+
+ const histograms = snapshotHistograms();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value: "omniboxtest ",
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("KEY_Enter");
+
+ assertSearchTelemetryEmpty(histograms.search_hist);
+ assertTelemetryResults(
+ histograms,
+ "extension",
+ 0,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.enter
+ );
+
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_places.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_places.js
new file mode 100644
index 0000000000..fb0c5e3b46
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_places.js
@@ -0,0 +1,330 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This file tests urlbar telemetry with places related actions (e.g. history/
+ * bookmark selection).
+ */
+
+"use strict";
+
+const SCALAR_URLBAR = "browser.engagement.navigation.urlbar";
+
+const TEST_URL = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://mochi.test:8888"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.jsm",
+});
+
+function searchInAwesomebar(value, win = window) {
+ return UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ waitForFocus,
+ value,
+ fireInputEvent: true,
+ });
+}
+
+function assertSearchTelemetryEmpty(search_hist) {
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false);
+ Assert.ok(
+ !(SCALAR_URLBAR in scalars),
+ `Should not have recorded ${SCALAR_URLBAR}`
+ );
+
+ // Make sure SEARCH_COUNTS contains identical values.
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.urlbar",
+ undefined
+ );
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.alias",
+ undefined
+ );
+
+ // Also check events.
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ );
+ events = (events.parent || []).filter(
+ e => e[1] == "navigation" && e[2] == "search"
+ );
+ Assert.deepEqual(
+ events,
+ [],
+ "Should not have recorded any navigation search events"
+ );
+}
+
+function snapshotHistograms() {
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+ return {
+ resultIndexHist: TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_INDEX"
+ ),
+ resultTypeHist: TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_TYPE_2"
+ ),
+ resultIndexByTypeHist: TelemetryTestUtils.getAndClearKeyedHistogram(
+ "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2"
+ ),
+ resultMethodHist: TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ ),
+ search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"),
+ };
+}
+
+function assertTelemetryResults(histograms, type, index, method) {
+ TelemetryTestUtils.assertHistogram(histograms.resultIndexHist, index, 1);
+
+ TelemetryTestUtils.assertHistogram(
+ histograms.resultTypeHist,
+ UrlbarUtils.SELECTED_RESULT_TYPES[type],
+ 1
+ );
+
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ histograms.resultIndexByTypeHist,
+ type,
+ index,
+ 1
+ );
+
+ TelemetryTestUtils.assertHistogram(histograms.resultMethodHist, method, 1);
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ `urlbar.picked.${type}`,
+ index,
+ 1
+ );
+}
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Disable search suggestions in the urlbar.
+ ["browser.urlbar.suggest.searches", false],
+ // Clear historical search suggestions to avoid interference from previous
+ // tests.
+ ["browser.urlbar.maxHistoricalSearchSuggestions", 0],
+ // Use the default matching bucket configuration.
+ ["browser.urlbar.matchBuckets", "general:5,suggestion:4"],
+ // Turn autofill off.
+ ["browser.urlbar.autoFill", false],
+ ],
+ });
+
+ // Enable local telemetry recording for the duration of the tests.
+ let oldCanRecord = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+
+ // Enable event recording for the events tested here.
+ Services.telemetry.setEventRecordingEnabled("navigation", true);
+
+ // Clear history so that history added by previous tests doesn't mess up this
+ // test when it selects results in the urlbar.
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ await PlacesUtils.keywords.insert({
+ keyword: "get",
+ url: TEST_URL + "?q=%s",
+ });
+
+ // Make sure to restore the engine once we're done.
+ registerCleanupFunction(async function() {
+ await PlacesUtils.keywords.remove("get");
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ Services.telemetry.setEventRecordingEnabled("navigation", false);
+ });
+});
+
+add_task(async function test_history() {
+ const histograms = snapshotHistograms();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com",
+ title: "example",
+ transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ },
+ ]);
+
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("example");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ assertSearchTelemetryEmpty(histograms.search_hist);
+ assertTelemetryResults(
+ histograms,
+ "history",
+ 1,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_bookmark() {
+ const histograms = snapshotHistograms();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ let bm = await PlacesUtils.bookmarks.insert({
+ url: "http://example.com",
+ title: "example",
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ });
+
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("example");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ assertSearchTelemetryEmpty(histograms.search_hist);
+ assertTelemetryResults(
+ histograms,
+ "bookmark",
+ 1,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection
+ );
+
+ await PlacesUtils.bookmarks.remove(bm);
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_keyword() {
+ const histograms = snapshotHistograms();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("get example");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ assertSearchTelemetryEmpty(histograms.search_hist);
+ assertTelemetryResults(
+ histograms,
+ "keyword",
+ 0,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.enter
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_switchtab() {
+ const histograms = snapshotHistograms();
+
+ let homeTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:buildconfig"
+ );
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:mozilla"
+ );
+
+ let p = BrowserTestUtils.waitForEvent(gBrowser, "TabSwitchDone");
+ await searchInAwesomebar("about:buildconfig");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ assertSearchTelemetryEmpty(histograms.search_hist);
+ assertTelemetryResults(
+ histograms,
+ "switchtab",
+ 1,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(homeTab);
+});
+
+add_task(async function test_visitURL() {
+ const histograms = snapshotHistograms();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("http://example.com");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ assertSearchTelemetryEmpty(histograms.search_hist);
+ assertTelemetryResults(
+ histograms,
+ "visiturl",
+ 0,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.enter
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_autofill() {
+ const histograms = snapshotHistograms();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com/mypage",
+ title: "example",
+ transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ },
+ ]);
+
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", true);
+
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("example.com/my");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ assertSearchTelemetryEmpty(histograms.search_hist);
+ assertTelemetryResults(
+ histograms,
+ "autofill",
+ 0,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.enter
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_remotetab.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_remotetab.js
new file mode 100644
index 0000000000..be6c0acf51
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_remotetab.js
@@ -0,0 +1,211 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This file tests urlbar telemetry with remote tab action.
+ */
+
+"use strict";
+
+const SCALAR_URLBAR = "browser.engagement.navigation.urlbar";
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ SyncedTabs: "resource://services-sync/SyncedTabs.jsm",
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.jsm",
+});
+
+function assertSearchTelemetryEmpty(search_hist) {
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false);
+ Assert.ok(
+ !(SCALAR_URLBAR in scalars),
+ `Should not have recorded ${SCALAR_URLBAR}`
+ );
+
+ // Make sure SEARCH_COUNTS contains identical values.
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.urlbar",
+ undefined
+ );
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.alias",
+ undefined
+ );
+
+ // Also check events.
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ );
+ events = (events.parent || []).filter(
+ e => e[1] == "navigation" && e[2] == "search"
+ );
+ Assert.deepEqual(
+ events,
+ [],
+ "Should not have recorded any navigation search events"
+ );
+}
+
+function snapshotHistograms() {
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+ return {
+ resultIndexHist: TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_INDEX"
+ ),
+ resultTypeHist: TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_TYPE_2"
+ ),
+ resultIndexByTypeHist: TelemetryTestUtils.getAndClearKeyedHistogram(
+ "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2"
+ ),
+ resultMethodHist: TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ ),
+ search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"),
+ };
+}
+
+function assertTelemetryResults(histograms, type, index, method) {
+ TelemetryTestUtils.assertHistogram(histograms.resultIndexHist, index, 1);
+
+ TelemetryTestUtils.assertHistogram(
+ histograms.resultTypeHist,
+ UrlbarUtils.SELECTED_RESULT_TYPES[type],
+ 1
+ );
+
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ histograms.resultIndexByTypeHist,
+ type,
+ index,
+ 1
+ );
+
+ TelemetryTestUtils.assertHistogram(histograms.resultMethodHist, method, 1);
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ `urlbar.picked.${type}`,
+ index,
+ 1
+ );
+}
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Disable search suggestions in the urlbar.
+ ["browser.urlbar.suggest.searches", false],
+ // Clear historical search suggestions to avoid interference from previous
+ // tests.
+ ["browser.urlbar.maxHistoricalSearchSuggestions", 0],
+ // Use the default matching bucket configuration.
+ ["browser.urlbar.matchBuckets", "general:5,suggestion:4"],
+ // Turn autofill off.
+ ["browser.urlbar.autoFill", false],
+ // Special prefs for remote tabs.
+ ["services.sync.username", "fake"],
+ ["services.sync.syncedTabs.showRemoteTabs", true],
+ ],
+ });
+
+ // Enable local telemetry recording for the duration of the tests.
+ let oldCanRecord = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+
+ // Enable event recording for the events tested here.
+ Services.telemetry.setEventRecordingEnabled("navigation", true);
+
+ // Clear history so that history added by previous tests doesn't mess up this
+ // test when it selects results in the urlbar.
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ const REMOTE_TAB = {
+ id: "7cqCr77ptzX3",
+ type: "client",
+ lastModified: 1492201200,
+ name: "zcarter's Nightly on MacBook-Pro-25",
+ clientType: "desktop",
+ tabs: [
+ {
+ type: "tab",
+ title: "Test Remote",
+ url: "http://example.com",
+ icon: UrlbarUtils.ICON.DEFAULT,
+ client: "7cqCr77ptzX3",
+ lastUsed: 1452124677,
+ },
+ ],
+ };
+
+ const sandbox = sinon.createSandbox();
+
+ let originalSyncedTabsInternal = SyncedTabs._internal;
+ SyncedTabs._internal = {
+ isConfiguredToSyncTabs: true,
+ hasSyncedThisSession: true,
+ getTabClients() {
+ return Promise.resolve([]);
+ },
+ syncTabs() {
+ return Promise.resolve();
+ },
+ };
+
+ // Tell the Sync XPCOM service it is initialized.
+ let weaveXPCService = Cc["@mozilla.org/weave/service;1"].getService(
+ Ci.nsISupports
+ ).wrappedJSObject;
+ let oldWeaveServiceReady = weaveXPCService.ready;
+ weaveXPCService.ready = true;
+
+ sandbox
+ .stub(SyncedTabs._internal, "getTabClients")
+ .callsFake(() => Promise.resolve(Cu.cloneInto([REMOTE_TAB], {})));
+
+ // Make sure to restore the engine once we're done.
+ registerCleanupFunction(async function() {
+ sandbox.restore();
+ weaveXPCService.ready = oldWeaveServiceReady;
+ SyncedTabs._internal = originalSyncedTabsInternal;
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ Services.telemetry.setEventRecordingEnabled("navigation", false);
+ });
+});
+
+add_task(async function test_remotetab() {
+ const histograms = snapshotHistograms();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value: "example",
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ assertSearchTelemetryEmpty(histograms.search_hist);
+ assertTelemetryResults(
+ histograms,
+ "remotetab",
+ 1,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_searchmode.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_searchmode.js
new file mode 100644
index 0000000000..54332259d4
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_searchmode.js
@@ -0,0 +1,579 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This file tests the urlbar.searchmode.* scalars telemetry with search mode
+ * related actions.
+ */
+
+"use strict";
+
+const ENTRY_SCALAR_PREFIX = "urlbar.searchmode.";
+const PICKED_SCALAR_PREFIX = "urlbar.picked.searchmode.";
+const ENGINE_ALIAS = "alias";
+const TEST_QUERY = "test";
+let engineName;
+let engineDomain;
+
+// The preference to enable suggestions.
+const SUGGEST_PREF = "browser.search.suggest.enabled";
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AppConstants: "resource://gre/modules/AppConstants.jsm",
+ UrlbarProviderTabToSearch:
+ "resource:///modules/UrlbarProviderTabToSearch.jsm",
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.jsm",
+});
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "TouchBarHelper",
+ "@mozilla.org/widget/touchbarhelper;1",
+ "nsITouchBarHelper"
+);
+
+/**
+ * Asserts that search mode telemetry was recorded correctly. Checks both the
+ * urlbar.searchmode.* and urlbar.searchmode_picked.* probes.
+ * @param {string} entry
+ * A search mode entry point.
+ * @param {string} engineOrSource
+ * An engine name or a search mode source.
+ * @param {number} [resultIndex]
+ * The index of the result picked while in search mode. Only pass this
+ * parameter if a result is picked.
+ */
+function assertSearchModeScalars(entry, engineOrSource, resultIndex = -1) {
+ // Check if the urlbar.searchmode.entry scalar contains the expected value.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ ENTRY_SCALAR_PREFIX + entry,
+ engineOrSource,
+ 1
+ );
+
+ for (let e of UrlbarUtils.SEARCH_MODE_ENTRY) {
+ if (e == entry) {
+ Assert.equal(
+ Object.keys(scalars[ENTRY_SCALAR_PREFIX + entry]).length,
+ 1,
+ `This search must only increment one entry in the correct scalar: ${e}`
+ );
+ } else {
+ Assert.ok(
+ !scalars[ENTRY_SCALAR_PREFIX + e],
+ `No other urlbar.searchmode scalars should be recorded. Checking ${e}`
+ );
+ }
+ }
+
+ if (resultIndex >= 0) {
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ PICKED_SCALAR_PREFIX + entry,
+ resultIndex,
+ 1
+ );
+ }
+
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+}
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Disable tab-to-search onboarding results for general tests. They are
+ // enabled in tests that specifically address onboarding.
+ ["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0],
+ ],
+ });
+
+ // Create an engine to generate search suggestions and add it as default
+ // for this test.
+ const url =
+ getRootDirectory(gTestPath) + "urlbarTelemetrySearchSuggestions.xml";
+ let suggestionEngine = await Services.search.addOpenSearchEngine(url, "");
+ suggestionEngine.alias = ENGINE_ALIAS;
+ engineDomain = suggestionEngine.getResultDomain();
+ engineName = suggestionEngine.name;
+
+ // Make it the default search engine.
+ let originalEngine = await Services.search.getDefault();
+ await Services.search.setDefault(suggestionEngine);
+
+ // And the first one-off engine.
+ await Services.search.moveEngine(suggestionEngine, 0);
+
+ // Enable local telemetry recording for the duration of the tests.
+ let oldCanRecord = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+
+ // Clear history so that history added by previous tests doesn't mess up this
+ // test when it selects results in the urlbar.
+ await PlacesUtils.history.clear();
+
+ // Clear historical search suggestions to avoid interference from previous
+ // tests.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.maxHistoricalSearchSuggestions", 0]],
+ });
+
+ // Allows UrlbarTestUtils to access this scope's test helpers, like Assert.
+ UrlbarTestUtils.init(this);
+
+ // Make sure to restore the engine once we're done.
+ registerCleanupFunction(async function() {
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ await Services.search.setDefault(originalEngine);
+ await Services.search.removeEngine(suggestionEngine);
+ await PlacesUtils.history.clear();
+ Services.telemetry.setEventRecordingEnabled("navigation", false);
+ UrlbarTestUtils.uninit();
+ });
+});
+
+// Clicks the first one off.
+add_task(async function test_oneoff_remote() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_QUERY,
+ });
+ // Enters search mode by clicking a one-off.
+ await UrlbarTestUtils.enterSearchMode(window);
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+ assertSearchModeScalars("oneoff", "other", 0);
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Clicks the history one off.
+add_task(async function test_oneoff_local() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_QUERY,
+ });
+ // Enters search mode by clicking a one-off.
+ await UrlbarTestUtils.enterSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ });
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+ assertSearchModeScalars("oneoff", "history", 0);
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Checks that the Amazon search mode name is collapsed to "Amazon".
+add_task(async function test_oneoff_amazon() {
+ // Disable suggestions to avoid hitting Amazon servers.
+ await SpecialPowers.pushPrefEnv({
+ set: [[SUGGEST_PREF, false]],
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_QUERY,
+ });
+ // Enters search mode by clicking a one-off.
+ await UrlbarTestUtils.enterSearchMode(window, {
+ engineName: "Amazon.com",
+ });
+ assertSearchModeScalars("oneoff", "Amazon");
+ await UrlbarTestUtils.exitSearchMode(window);
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+// Checks that the Wikipedia search mode name is collapsed to "Wikipedia".
+add_task(async function test_oneoff_wikipedia() {
+ // Disable suggestions to avoid hitting Wikipedia servers.
+ await SpecialPowers.pushPrefEnv({
+ set: [[SUGGEST_PREF, false]],
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_QUERY,
+ });
+ // Enters search mode by clicking a one-off.
+ await UrlbarTestUtils.enterSearchMode(window, {
+ engineName: "Wikipedia (en)",
+ });
+ assertSearchModeScalars("oneoff", "Wikipedia");
+ await UrlbarTestUtils.exitSearchMode(window);
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+// Enters search mode by pressing the keyboard shortcut.
+add_task(async function test_shortcut() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_QUERY,
+ });
+ // Enter search mode by pressing the keyboard shortcut.
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("k", { accelKey: true });
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ entry: "shortcut",
+ });
+ assertSearchModeScalars("shortcut", "other");
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Enters search mode by selecting a Top Site from the Urlbar.
+add_task(async function test_topsites_urlbar() {
+ // Disable suggestions to avoid hitting Amazon servers.
+ await SpecialPowers.pushPrefEnv({
+ set: [[SUGGEST_PREF, false]],
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ // Enter search mode by selecting a Top Site from the Urlbar.
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ if (gURLBar.getAttribute("pageproxystate") == "invalid") {
+ gURLBar.handleRevert();
+ }
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ let amazonSearch = await UrlbarTestUtils.waitForAutocompleteResultAt(
+ window,
+ 0
+ );
+ Assert.equal(
+ amazonSearch.result.payload.keyword,
+ "@amazon",
+ "First result should have the Amazon keyword."
+ );
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeMouseAtCenter(amazonSearch, {});
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: amazonSearch.result.payload.engine,
+ entry: "topsites_urlbar",
+ });
+ assertSearchModeScalars("topsites_urlbar", "Amazon");
+ await UrlbarTestUtils.exitSearchMode(window);
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+// Enters search mode by selecting a keyword offer result.
+add_task(async function test_keywordoffer() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ // Do a search for "@" + our test alias. It should autofill with a trailing
+ // space, and the heuristic result should be an autofill result with a keyword
+ // offer.
+ let alias = "@" + ENGINE_ALIAS;
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: alias,
+ });
+ let keywordOfferResult = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ 0
+ );
+ Assert.equal(
+ keywordOfferResult.searchParams.keyword,
+ alias,
+ "The first result should be a keyword search result with the correct alias."
+ );
+
+ // Pick the keyword offer result.
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName,
+ entry: "keywordoffer",
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ });
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+ assertSearchModeScalars("keywordoffer", "other", 0);
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Enters search mode by typing an alias.
+add_task(async function test_typed() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ // Enter search mode by selecting a keywordoffer result.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: `${ENGINE_ALIAS} `,
+ });
+
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey(" ");
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName,
+ entry: "typed",
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ });
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+ assertSearchModeScalars("typed", "other", 0);
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Enters search mode by calling the same function called by the Search
+// Bookmarks menu item in Library > Bookmarks.
+add_task(async function test_bookmarkmenu() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ PlacesCommandHook.searchBookmarks();
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ entry: "bookmarkmenu",
+ });
+ assertSearchModeScalars("bookmarkmenu", "bookmarks");
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Enters search mode by calling the same function called by the Search Tabs
+// menu item in the tab overflow menu.
+add_task(async function test_tabmenu() {
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ gTabsPanel.searchTabs();
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.TABS,
+ entry: "tabmenu",
+ });
+ assertSearchModeScalars("tabmenu", "tabs");
+});
+
+// Enters search mode by performing a search handoff on about:privatebrowsing.
+// NOTE: We don't test handoff on about:home. Running mochitests on about:home
+// is quite difficult. This subtest verifies that `handoff` is a valid scalar
+// suffix and that a call to
+// UrlbarInput.search(value, { searchEngine, searchModeEntry: "handoff" }) records values in
+// the urlbar.searchmode.handoff scalar. PlacesFeed.test.js verfies that
+// about:home handoff makes that exact call.
+add_task(async function test_handoff_pbm() {
+ let win = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ waitForTabURL: "about:privatebrowsing",
+ });
+ let tab = win.gBrowser.selectedBrowser;
+
+ await SpecialPowers.spawn(tab, [], async function() {
+ let btn = content.document.getElementById("search-handoff-button");
+ btn.click();
+ });
+
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(win);
+ await new Promise(r => EventUtils.synthesizeKey("f", {}, win, r));
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(win, {
+ engineName,
+ entry: "handoff",
+ });
+ assertSearchModeScalars("handoff", "other");
+
+ await UrlbarTestUtils.exitSearchMode(win);
+ await UrlbarTestUtils.promisePopupClose(win);
+ await BrowserTestUtils.closeWindow(win);
+});
+
+// Enters search mode by tapping a search shortcut on the Touch Bar.
+add_task(async function test_touchbar() {
+ if (AppConstants.platform != "macosx") {
+ return;
+ }
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_QUERY,
+ });
+ // We have to fake the tap on the Touch Bar since mochitests have no way of
+ // interacting with the Touch Bar.
+ TouchBarHelper.insertRestrictionInUrlbar(UrlbarTokenizer.RESTRICT.HISTORY);
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ entry: "touchbar",
+ });
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+ assertSearchModeScalars("touchbar", "history", 0);
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Enters search mode by selecting a tab-to-search result.
+// Tests that tab-to-search results preview search mode when highlighted. These
+// results are worth testing separately since they do not set the
+// payload.keyword parameter.
+add_task(async function test_tabtosearch() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Do not show the onboarding result for this subtest.
+ ["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0],
+ ],
+ });
+ await PlacesTestUtils.addVisits([`http://${engineDomain}/`]);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: engineDomain.slice(0, 4),
+ });
+ let tabToSearchResult = (
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)
+ ).result;
+ Assert.equal(
+ tabToSearchResult.providerName,
+ "TabToSearch",
+ "The second result is a tab-to-search result."
+ );
+ Assert.equal(
+ tabToSearchResult.payload.engine,
+ engineName,
+ "The tab-to-search result is for the correct engine."
+ );
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 1,
+ "Sanity check: The second result is selected."
+ );
+ // Pick the tab-to-search result.
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName,
+ entry: "tabtosearch",
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ });
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+ assertSearchModeScalars("tabtosearch", "other", 0);
+
+ BrowserTestUtils.removeTab(tab);
+
+ await PlacesUtils.history.clear();
+ await SpecialPowers.popPrefEnv();
+});
+
+// Enters search mode by selecting a tab-to-search onboarding result.
+add_task(async function test_tabtosearch_onboard() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 3]],
+ });
+ await PlacesTestUtils.addVisits([`http://${engineDomain}/`]);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: engineDomain.slice(0, 4),
+ });
+ let tabToSearchResult = (
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)
+ ).result;
+ Assert.equal(
+ tabToSearchResult.providerName,
+ "TabToSearch",
+ "The second result is a tab-to-search result."
+ );
+ Assert.equal(
+ tabToSearchResult.payload.engine,
+ engineName,
+ "The tab-to-search result is for the correct engine."
+ );
+ Assert.equal(
+ tabToSearchResult.payload.dynamicType,
+ "onboardTabToSearch",
+ "The tab-to-search result is an onboarding result."
+ );
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 1,
+ "Sanity check: The second result is selected."
+ );
+ // Pick the tab-to-search onboarding result.
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName,
+ entry: "tabtosearch_onboard",
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ });
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+ assertSearchModeScalars("tabtosearch_onboard", "other", 0);
+
+ UrlbarPrefs.set("tabToSearch.onboard.interactionsLeft", 3);
+ delete UrlbarProviderTabToSearch.onboardingInteractionAtTime;
+
+ BrowserTestUtils.removeTab(tab);
+ await PlacesUtils.history.clear();
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tabtosearch.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tabtosearch.js
new file mode 100644
index 0000000000..386e4268ea
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tabtosearch.js
@@ -0,0 +1,356 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This file tests telemetry for tabtosearch results.
+ * NB: This file does not test the search mode `entry` field for tab-to-search
+ * results. That is tested in browser_UsageTelemetry_urlbar_searchmode.js.
+ */
+
+"use strict";
+
+const ENGINE_NAME = "MozSearch";
+const ENGINE_DOMAIN = "example.com";
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.jsm",
+});
+
+function snapshotHistograms() {
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+ return {
+ resultIndexHist: TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_INDEX"
+ ),
+ resultTypeHist: TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_TYPE_2"
+ ),
+ resultIndexByTypeHist: TelemetryTestUtils.getAndClearKeyedHistogram(
+ "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2"
+ ),
+ resultMethodHist: TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ ),
+ search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"),
+ };
+}
+
+function assertTelemetryResults(histograms, type, index, method) {
+ TelemetryTestUtils.assertHistogram(histograms.resultIndexHist, index, 1);
+
+ TelemetryTestUtils.assertHistogram(
+ histograms.resultTypeHist,
+ UrlbarUtils.SELECTED_RESULT_TYPES[type],
+ 1
+ );
+
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ histograms.resultIndexByTypeHist,
+ type,
+ index,
+ 1
+ );
+
+ TelemetryTestUtils.assertHistogram(histograms.resultMethodHist, method, 1);
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ `urlbar.picked.${type}`,
+ index,
+ 1
+ );
+}
+
+/**
+ * Checks to see if the second result in the Urlbar is an onboarding result
+ * with the correct engine.
+ *
+ * @param {string} engineName
+ * The expected engine name.
+ */
+async function checkForOnboardingResult(engineName) {
+ Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Popup should be open.");
+ let tabToSearchResult = (
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)
+ ).result;
+ Assert.equal(
+ tabToSearchResult.providerName,
+ "TabToSearch",
+ "The second result is a tab-to-search result."
+ );
+ Assert.equal(
+ tabToSearchResult.payload.engine,
+ engineName,
+ "The tab-to-search result is for the first engine."
+ );
+ Assert.equal(
+ tabToSearchResult.payload.dynamicType,
+ "onboardTabToSearch",
+ "The tab-to-search result is an onboarding result."
+ );
+}
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0]],
+ });
+
+ let engine = await Services.search.addEngineWithDetails(ENGINE_NAME, {
+ template: `http://${ENGINE_DOMAIN}/?q={searchTerms}`,
+ });
+
+ UrlbarTestUtils.init(this);
+
+ registerCleanupFunction(async () => {
+ UrlbarTestUtils.uninit();
+ await Services.search.removeEngine(engine);
+ });
+});
+
+add_task(async function test() {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ const histograms = snapshotHistograms();
+
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits([`https://${ENGINE_DOMAIN}/`]);
+ }
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: ENGINE_DOMAIN.slice(0, 4),
+ });
+
+ let tabToSearchResult = (
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)
+ ).result;
+ Assert.equal(
+ tabToSearchResult.providerName,
+ "TabToSearch",
+ "The second result is a tab-to-search result."
+ );
+ Assert.equal(
+ tabToSearchResult.payload.engine,
+ ENGINE_NAME,
+ "The tab-to-search result is for the correct engine."
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 1,
+ "Sanity check: The second result is selected."
+ );
+
+ // Select the tab-to-search result.
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: ENGINE_NAME,
+ entry: "tabtosearch",
+ });
+
+ assertTelemetryResults(
+ histograms,
+ "tabtosearch",
+ 1,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection
+ );
+
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ gURLBar.blur();
+ });
+ await PlacesUtils.history.clear();
+ });
+});
+
+add_task(async function onboarding_impressions() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 3]],
+ });
+
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ const firstEngineHost = "example";
+ let secondEngine = await Services.search.addEngineWithDetails(
+ `${ENGINE_NAME}2`,
+ { template: `http://${firstEngineHost}-2.com/?q={searchTerms}` }
+ );
+
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits([`https://${firstEngineHost}-2.com`]);
+ await PlacesTestUtils.addVisits([`https://${ENGINE_DOMAIN}/`]);
+ }
+
+ // First do multiple searches for substrings of firstEngineHost. The view
+ // should show the same tab-to-search onboarding result the entire time, so
+ // we should not continue to increment urlbar.tips.
+ for (let i = 1; i < firstEngineHost.length; i++) {
+ info(
+ `Search for "${firstEngineHost.slice(
+ 0,
+ i
+ )}". Only record one impression for this sequence.`
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: firstEngineHost.slice(0, i),
+ fireInputEvent: true,
+ });
+ await checkForOnboardingResult(ENGINE_NAME);
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true),
+ "urlbar.tips",
+ "tabtosearch_onboard-shown",
+ 1
+ );
+
+ info("Type through autofill to second engine hostname. Record impression.");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: firstEngineHost,
+ fireInputEvent: true,
+ });
+ await checkForOnboardingResult(ENGINE_NAME);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: `${firstEngineHost}-`,
+ fireInputEvent: true,
+ });
+ await checkForOnboardingResult(`${ENGINE_NAME}2`);
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+ // Since the user typed past the autofill for the first engine, we showed a
+ // different onboarding result and now we increment
+ // tabtosearch_onboard-shown.
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true),
+ "urlbar.tips",
+ "tabtosearch_onboard-shown",
+ 3
+ );
+
+ info("Make a typo and return to autofill. Do not record impression.");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: `${firstEngineHost}-`,
+ fireInputEvent: true,
+ });
+ await checkForOnboardingResult(`${ENGINE_NAME}2`);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: `${firstEngineHost}-3`,
+ fireInputEvent: true,
+ });
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 1,
+ "We are not showing a tab-to-search result."
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: `${firstEngineHost}-2`,
+ fireInputEvent: true,
+ });
+ await checkForOnboardingResult(`${ENGINE_NAME}2`);
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true),
+ "urlbar.tips",
+ "tabtosearch_onboard-shown",
+ 4
+ );
+
+ info("Cancel then restart autofill. Do not record impression.");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: `${firstEngineHost}-2`,
+ fireInputEvent: true,
+ });
+ await checkForOnboardingResult(`${ENGINE_NAME}2`);
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_Backspace");
+ await searchPromise;
+ Assert.greater(
+ UrlbarTestUtils.getResultCount(window),
+ 1,
+ "Sanity check: we have more than one result."
+ );
+ let result = (await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1))
+ .result;
+ Assert.notEqual(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "The second result is not a tab-to-search result."
+ );
+ searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ // Type the "." from `example-2.com`.
+ EventUtils.synthesizeKey(".");
+ await searchPromise;
+ await checkForOnboardingResult(`${ENGINE_NAME}2`);
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true),
+ "urlbar.tips",
+ "tabtosearch_onboard-shown",
+ 5
+ );
+
+ // See javadoc for UrlbarProviderTabToSearch.onEngagement for discussion
+ // about retained results.
+ info("Reopen the result set with retained results. Record impression.");
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ await checkForOnboardingResult(`${ENGINE_NAME}2`);
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true),
+ "urlbar.tips",
+ "tabtosearch_onboard-shown",
+ 6
+ );
+
+ info(
+ "Open a result page and then autofill engine host. Record impression."
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: firstEngineHost,
+ fireInputEvent: true,
+ });
+ await checkForOnboardingResult(ENGINE_NAME);
+ // Press enter on the heuristic result so we visit example.com without
+ // doing an additional search.
+ let loadPromise = BrowserTestUtils.browserLoaded(browser);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+ // Click the Urlbar and type to simulate what a user would actually do. If
+ // we use promiseAutocompleteResultPopup, no query would be made between
+ // this one and the previous tab-to-search query. Thus
+ // `onboardingEnginesShown` would not be cleared. This would not happen
+ // in real-world usage.
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey(firstEngineHost.slice(0, 4));
+ await searchPromise;
+ await checkForOnboardingResult(ENGINE_NAME);
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+ // We clear the scalar this time.
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "urlbar.tips",
+ "tabtosearch_onboard-shown",
+ 8
+ );
+
+ await PlacesUtils.history.clear();
+ await Services.search.removeEngine(secondEngine);
+ });
+});
diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tip.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tip.js
new file mode 100644
index 0000000000..a23fffdb3f
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tip.js
@@ -0,0 +1,149 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This file tests urlbar telemetry for tip results.
+ */
+
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ UrlbarProvider: "resource:///modules/UrlbarUtils.jsm",
+ UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm",
+ UrlbarResult: "resource:///modules/UrlbarResult.jsm",
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.jsm",
+});
+
+function snapshotHistograms() {
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+ return {
+ resultIndexHist: TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_INDEX"
+ ),
+ resultTypeHist: TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_TYPE_2"
+ ),
+ resultIndexByTypeHist: TelemetryTestUtils.getAndClearKeyedHistogram(
+ "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2"
+ ),
+ resultMethodHist: TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ ),
+ search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"),
+ };
+}
+
+function assertTelemetryResults(histograms, type, index, method) {
+ TelemetryTestUtils.assertHistogram(histograms.resultIndexHist, index, 1);
+
+ TelemetryTestUtils.assertHistogram(
+ histograms.resultTypeHist,
+ UrlbarUtils.SELECTED_RESULT_TYPES[type],
+ 1
+ );
+
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ histograms.resultIndexByTypeHist,
+ type,
+ index,
+ 1
+ );
+
+ TelemetryTestUtils.assertHistogram(histograms.resultMethodHist, method, 1);
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ `urlbar.picked.${type}`,
+ index,
+ 1
+ );
+}
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Disable search suggestions in the urlbar.
+ ["browser.urlbar.suggest.searches", false],
+ // Turn autofill off.
+ ["browser.urlbar.autoFill", false],
+ ],
+ });
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test() {
+ // Add a restricting provider that returns a preselected heuristic tip result.
+ let provider = new TipProvider([
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TIP,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ icon: "",
+ text: "This is a test tip.",
+ buttonText: "OK",
+ type: "test",
+ }
+ ),
+ { heuristic: true }
+ ),
+ ]);
+ UrlbarProvidersManager.registerProvider(provider);
+
+ const histograms = snapshotHistograms();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ // Show the view and press enter to select the tip.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value: "test",
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("KEY_Enter");
+
+ assertTelemetryResults(
+ histograms,
+ "tip",
+ 0,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.enter
+ );
+
+ UrlbarProvidersManager.unregisterProvider(provider);
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * A test URLBar provider.
+ */
+class TipProvider extends UrlbarProvider {
+ constructor(results) {
+ super();
+ this._results = results;
+ }
+ get name() {
+ return "TestProviderTip";
+ }
+ get type() {
+ return UrlbarUtils.PROVIDER_TYPE.PROFILE;
+ }
+ isActive(context) {
+ return true;
+ }
+ getPriority(context) {
+ return 1;
+ }
+ async startQuery(context, addCallback) {
+ context.preselected = true;
+ for (const result of this._results) {
+ addCallback(this, result);
+ }
+ }
+}
diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_topsite.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_topsite.js
new file mode 100644
index 0000000000..352e08cf8f
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_topsite.js
@@ -0,0 +1,151 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This file tests urlbar telemetry for topsite results.
+ */
+
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AboutNewTab: "resource:///modules/AboutNewTab.jsm",
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.jsm",
+});
+
+const EN_US_TOPSITES =
+ "https://www.youtube.com/,https://www.facebook.com/,https://www.amazon.com/,https://www.reddit.com/,https://www.wikipedia.org/,https://twitter.com/";
+
+function snapshotHistograms() {
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+ return {
+ resultIndexHist: TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_INDEX"
+ ),
+ resultTypeHist: TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_TYPE_2"
+ ),
+ resultIndexByTypeHist: TelemetryTestUtils.getAndClearKeyedHistogram(
+ "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2"
+ ),
+ resultMethodHist: TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ ),
+ search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"),
+ };
+}
+
+function assertTelemetryResults(histograms, type, index, method) {
+ TelemetryTestUtils.assertHistogram(histograms.resultIndexHist, index, 1);
+
+ TelemetryTestUtils.assertHistogram(
+ histograms.resultTypeHist,
+ UrlbarUtils.SELECTED_RESULT_TYPES[type],
+ 1
+ );
+
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ histograms.resultIndexByTypeHist,
+ type,
+ index,
+ 1
+ );
+
+ TelemetryTestUtils.assertHistogram(histograms.resultMethodHist, method, 1);
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ `urlbar.picked.${type}`,
+ index,
+ 1
+ );
+}
+
+/**
+ * Updates the Top Sites feed.
+ * @param {function} condition
+ * A callback that returns true after Top Sites are successfully updated.
+ * @param {boolean} searchShortcuts
+ * True if Top Sites search shortcuts should be enabled.
+ */
+async function updateTopSites(condition, searchShortcuts = false) {
+ // Toggle the pref to clear the feed cache and force an update.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.newtabpage.activity-stream.feeds.system.topsites", false],
+ ["browser.newtabpage.activity-stream.feeds.system.topsites", true],
+ [
+ "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts",
+ searchShortcuts,
+ ],
+ ],
+ });
+
+ // Wait for the feed to be updated.
+ await TestUtils.waitForCondition(() => {
+ let sites = AboutNewTab.getTopSites();
+ return condition(sites);
+ }, "Waiting for top sites to be updated");
+}
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.topsites", true],
+ ["browser.newtabpage.activity-stream.default.sites", EN_US_TOPSITES],
+ ],
+ });
+ await updateTopSites(
+ sites => sites && sites.length == EN_US_TOPSITES.split(",").length
+ );
+});
+
+add_task(async function test() {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ let sites = AboutNewTab.getTopSites();
+ Assert.equal(
+ sites.length,
+ 6,
+ "The test suite browser should have 6 Top Sites."
+ );
+
+ const histograms = snapshotHistograms();
+
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ sites.length,
+ "The number of results should be the same as the number of Top Sites (6)."
+ );
+ // Select the first resultm and confirm it.
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 0,
+ "The first result should be selected"
+ );
+
+ let loadPromise = BrowserTestUtils.waitForDocLoadAndStopIt(
+ result.url,
+ gBrowser.selectedBrowser
+ );
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+
+ assertTelemetryResults(
+ histograms,
+ "topsite",
+ 0,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection
+ );
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ gURLBar.blur();
+ });
+ });
+});
diff --git a/browser/components/urlbar/tests/browser/browser_userTypedValue.js b/browser/components/urlbar/tests/browser/browser_userTypedValue.js
new file mode 100644
index 0000000000..8319b37962
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_userTypedValue.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+ const URI = TEST_BASE_URL + "file_userTypedValue.html";
+ window.browserDOMWindow.openURI(
+ makeURI(URI),
+ null,
+ Ci.nsIBrowserDOMWindow.OPEN_NEWTAB,
+ Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL,
+ Services.scriptSecurityManager.getSystemPrincipal()
+ );
+
+ is(gBrowser.userTypedValue, URI, "userTypedValue matches test URI");
+ is(gURLBar.value, URI, "location bar value matches test URI");
+
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.removeCurrentTab({ skipPermitUnload: true });
+ is(
+ gBrowser.userTypedValue,
+ URI,
+ "userTypedValue matches test URI after switching tabs"
+ );
+ is(
+ gURLBar.value,
+ URI,
+ "location bar value matches test URI after switching tabs"
+ );
+
+ waitForExplicitFinish();
+ BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(() => {
+ is(
+ gBrowser.userTypedValue,
+ null,
+ "userTypedValue is null as the page has loaded"
+ );
+ is(
+ gURLBar.value,
+ URI,
+ "location bar value matches test URI as the page has loaded"
+ );
+
+ gBrowser.removeCurrentTab({ skipPermitUnload: true });
+ finish();
+ });
+}
diff --git a/browser/components/urlbar/tests/browser/browser_valueOnTabSwitch.js b/browser/components/urlbar/tests/browser/browser_valueOnTabSwitch.js
new file mode 100644
index 0000000000..d1053dfc8e
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_valueOnTabSwitch.js
@@ -0,0 +1,166 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This tests for the correct URL being displayed in the URL bar after switching
+ * tabs which are in different states (e.g. deleted, partially deleted).
+ */
+
+"use strict";
+
+const TEST_URL = `${TEST_BASE_URL}dummy_page.html`;
+
+add_task(async function() {
+ // autofill may conflict with the test scope, by filling missing parts of
+ // the url due to autoOpen.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.autoFill", false]],
+ });
+
+ let charsToDelete,
+ deletedURLTab,
+ fullURLTab,
+ partialURLTab,
+ testPartialURL,
+ testURL;
+
+ charsToDelete = 5;
+ deletedURLTab = BrowserTestUtils.addTab(gBrowser);
+ fullURLTab = BrowserTestUtils.addTab(gBrowser);
+ partialURLTab = BrowserTestUtils.addTab(gBrowser);
+ testURL = TEST_URL;
+
+ let loaded1 = BrowserTestUtils.browserLoaded(
+ deletedURLTab.linkedBrowser,
+ false,
+ testURL
+ );
+ let loaded2 = BrowserTestUtils.browserLoaded(
+ fullURLTab.linkedBrowser,
+ false,
+ testURL
+ );
+ let loaded3 = BrowserTestUtils.browserLoaded(
+ partialURLTab.linkedBrowser,
+ false,
+ testURL
+ );
+ BrowserTestUtils.loadURI(deletedURLTab.linkedBrowser, testURL);
+ BrowserTestUtils.loadURI(fullURLTab.linkedBrowser, testURL);
+ BrowserTestUtils.loadURI(partialURLTab.linkedBrowser, testURL);
+ await Promise.all([loaded1, loaded2, loaded3]);
+
+ testURL = BrowserUtils.trimURL(testURL);
+ testPartialURL = testURL.substr(0, testURL.length - charsToDelete);
+
+ function cleanUp() {
+ gBrowser.removeTab(fullURLTab);
+ gBrowser.removeTab(partialURLTab);
+ gBrowser.removeTab(deletedURLTab);
+ }
+
+ async function cycleTabs() {
+ await BrowserTestUtils.switchTab(gBrowser, fullURLTab);
+ is(
+ gURLBar.value,
+ testURL,
+ "gURLBar.value should be testURL after switching back to fullURLTab"
+ );
+
+ await BrowserTestUtils.switchTab(gBrowser, partialURLTab);
+ is(
+ gURLBar.value,
+ testPartialURL,
+ "gURLBar.value should be testPartialURL after switching back to partialURLTab"
+ );
+ await BrowserTestUtils.switchTab(gBrowser, deletedURLTab);
+ is(
+ gURLBar.value,
+ "",
+ 'gURLBar.value should be "" after switching back to deletedURLTab'
+ );
+
+ await BrowserTestUtils.switchTab(gBrowser, fullURLTab);
+ is(
+ gURLBar.value,
+ testURL,
+ "gURLBar.value should be testURL after switching back to fullURLTab"
+ );
+ }
+
+ function urlbarBackspace(removeAll) {
+ return new Promise((resolve, reject) => {
+ gBrowser.selectedBrowser.focus();
+ gURLBar.addEventListener(
+ "input",
+ function() {
+ resolve();
+ },
+ { once: true }
+ );
+ gURLBar.focus();
+ if (removeAll) {
+ gURLBar.select();
+ } else {
+ gURLBar.selectionStart = gURLBar.selectionEnd = gURLBar.value.length;
+ }
+ EventUtils.synthesizeKey("KEY_Backspace");
+ });
+ }
+
+ async function prepareDeletedURLTab() {
+ await BrowserTestUtils.switchTab(gBrowser, deletedURLTab);
+ is(
+ gURLBar.value,
+ testURL,
+ "gURLBar.value should be testURL after initial switch to deletedURLTab"
+ );
+
+ // simulate the user removing the whole url from the location bar
+ await urlbarBackspace(true);
+ is(gURLBar.value, "", 'gURLBar.value should be "" (just set)');
+ }
+
+ async function prepareFullURLTab() {
+ await BrowserTestUtils.switchTab(gBrowser, fullURLTab);
+ is(
+ gURLBar.value,
+ testURL,
+ "gURLBar.value should be testURL after initial switch to fullURLTab"
+ );
+ }
+
+ async function preparePartialURLTab() {
+ await BrowserTestUtils.switchTab(gBrowser, partialURLTab);
+ is(
+ gURLBar.value,
+ testURL,
+ "gURLBar.value should be testURL after initial switch to partialURLTab"
+ );
+
+ // simulate the user removing part of the url from the location bar
+ let deleted = 0;
+ while (deleted < charsToDelete) {
+ await urlbarBackspace(false);
+ deleted++;
+ }
+
+ is(
+ gURLBar.value,
+ testPartialURL,
+ "gURLBar.value should be testPartialURL (just set)"
+ );
+ }
+
+ // prepare the three tabs required by this test
+
+ // First tab
+ await prepareFullURLTab();
+ await preparePartialURLTab();
+ await prepareDeletedURLTab();
+
+ // now cycle the tabs and make sure everything looks good
+ await cycleTabs();
+ cleanUp();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_view_emptyResultSet.js b/browser/components/urlbar/tests/browser/browser_view_emptyResultSet.js
new file mode 100644
index 0000000000..77b11adba8
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_view_emptyResultSet.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the view results are cleared and the view is closed, when an empty
+// result set arrives after a non-empty one.
+
+add_task(async function() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ });
+ Assert.ok(
+ UrlbarTestUtils.getResultCount(window) > 0,
+ `There should be some results in the view.`
+ );
+ Assert.ok(gURLBar.view.isOpen, `The view should be open.`);
+
+ // Register an high priority empty result provider.
+ let provider = new UrlbarTestUtils.TestProvider({
+ results: [],
+ priority: 999,
+ });
+ UrlbarProvidersManager.registerProvider(provider);
+ registerCleanupFunction(async function() {
+ UrlbarProvidersManager.unregisterProvider(provider);
+ await PlacesUtils.history.clear();
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ });
+ Assert.ok(
+ UrlbarTestUtils.getResultCount(window) == 0,
+ `There should be no results in the view.`
+ );
+ Assert.ok(!gURLBar.view.isOpen, `The view should have been closed.`);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_view_resultDisplay.js b/browser/components/urlbar/tests/browser/browser_view_resultDisplay.js
new file mode 100644
index 0000000000..63de190935
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_view_resultDisplay.js
@@ -0,0 +1,354 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that a result has the various elements displayed in the URL bar as
+ * we expect them to be.
+ */
+
+add_task(async function setup() {
+ await PlacesUtils.history.clear();
+
+ registerCleanupFunction(async function() {
+ await PlacesUtils.history.clear();
+ Services.prefs.clearUserPref("browser.urlbar.trimURLs");
+ });
+});
+
+async function testResult(input, expected, index = 1) {
+ const ESCAPED_URL = encodeURI(input.url);
+
+ await PlacesUtils.history.clear();
+ if (index > 0) {
+ await PlacesTestUtils.addVisits({
+ uri: input.url,
+ title: input.title,
+ });
+ }
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: input.query,
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index);
+ Assert.equal(result.url, ESCAPED_URL, "Should have the correct url to load");
+ Assert.equal(
+ result.displayed.url,
+ expected.displayedUrl,
+ "Should have the correct displayed url"
+ );
+ Assert.equal(
+ result.displayed.title,
+ input.title,
+ "Should have the expected title"
+ );
+ Assert.equal(
+ result.displayed.typeIcon,
+ "none",
+ "Should not have a type icon"
+ );
+ if (index > 0) {
+ Assert.equal(
+ result.image,
+ `page-icon:${ESCAPED_URL}`,
+ "Should have the correct favicon"
+ );
+ }
+
+ assertDisplayedHighlights(
+ "title",
+ result.element.title,
+ expected.highlightedTitle
+ );
+
+ assertDisplayedHighlights("url", result.element.url, expected.highlightedUrl);
+}
+
+function assertDisplayedHighlights(elementName, element, expectedResults) {
+ Assert.equal(
+ element.childNodes.length,
+ expectedResults.length,
+ `Should have the correct number of child nodes for ${elementName}`
+ );
+
+ for (let i = 0; i < element.childNodes.length; i++) {
+ let child = element.childNodes[i];
+ Assert.equal(
+ child.textContent,
+ expectedResults[i][0],
+ `Should have the correct text for the ${i} part of the ${elementName}`
+ );
+ Assert.equal(
+ child.nodeName,
+ expectedResults[i][1] ? "strong" : "#text",
+ `Should have the correct text/strong status for the ${i} part of the ${elementName}`
+ );
+ }
+}
+
+add_task(async function test_url_result() {
+ await testResult(
+ {
+ query: "\u6e2C\u8a66",
+ title: "The \u6e2C\u8a66 URL",
+ url: "https://example.com/\u6e2C\u8a66test",
+ },
+ {
+ displayedUrl: "example.com/\u6e2C\u8a66test",
+ highlightedTitle: [
+ ["The ", false],
+ ["\u6e2C\u8a66", true],
+ [" URL", false],
+ ],
+ highlightedUrl: [
+ ["example.com/", false],
+ ["\u6e2C\u8a66", true],
+ ["test", false],
+ ],
+ }
+ );
+});
+
+add_task(async function test_url_result_no_path() {
+ await testResult(
+ {
+ query: "ample",
+ title: "The Title",
+ url: "https://example.com/",
+ },
+ {
+ displayedUrl: "example.com",
+ highlightedTitle: [["The Title", false]],
+ highlightedUrl: [
+ ["ex", false],
+ ["ample", true],
+ [".com", false],
+ ],
+ }
+ );
+});
+
+add_task(async function test_url_result_www() {
+ await testResult(
+ {
+ query: "ample",
+ title: "The Title",
+ url: "https://www.example.com/",
+ },
+ {
+ displayedUrl: "example.com",
+ highlightedTitle: [["The Title", false]],
+ highlightedUrl: [
+ ["ex", false],
+ ["ample", true],
+ [".com", false],
+ ],
+ }
+ );
+});
+
+add_task(async function test_url_result_no_trimming() {
+ Services.prefs.setBoolPref("browser.urlbar.trimURLs", false);
+
+ await testResult(
+ {
+ query: "\u6e2C\u8a66",
+ title: "The \u6e2C\u8a66 URL",
+ url: "http://example.com/\u6e2C\u8a66test",
+ },
+ {
+ displayedUrl: "http://example.com/\u6e2C\u8a66test",
+ highlightedTitle: [
+ ["The ", false],
+ ["\u6e2C\u8a66", true],
+ [" URL", false],
+ ],
+ highlightedUrl: [
+ ["http://example.com/", false],
+ ["\u6e2C\u8a66", true],
+ ["test", false],
+ ],
+ }
+ );
+
+ Services.prefs.clearUserPref("browser.urlbar.trimURLs");
+});
+
+add_task(async function test_case_insensitive_highlights_1() {
+ await testResult(
+ {
+ query: "exam",
+ title: "The examPLE URL EXAMple",
+ url: "https://example.com/ExAm",
+ },
+ {
+ displayedUrl: "example.com/ExAm",
+ highlightedTitle: [
+ ["The ", false],
+ ["exam", true],
+ ["PLE URL ", false],
+ ["EXAM", true],
+ ["ple", false],
+ ],
+ highlightedUrl: [
+ ["exam", true],
+ ["ple.com/", false],
+ ["ExAm", true],
+ ],
+ }
+ );
+});
+
+add_task(async function test_case_insensitive_highlights_2() {
+ await testResult(
+ {
+ query: "EXAM",
+ title: "The examPLE URL EXAMple",
+ url: "https://example.com/ExAm",
+ },
+ {
+ displayedUrl: "example.com/ExAm",
+ highlightedTitle: [
+ ["The ", false],
+ ["exam", true],
+ ["PLE URL ", false],
+ ["EXAM", true],
+ ["ple", false],
+ ],
+ highlightedUrl: [
+ ["exam", true],
+ ["ple.com/", false],
+ ["ExAm", true],
+ ],
+ }
+ );
+});
+
+add_task(async function test_case_insensitive_highlights_3() {
+ await testResult(
+ {
+ query: "eXaM",
+ title: "The examPLE URL EXAMple",
+ url: "https://example.com/ExAm",
+ },
+ {
+ displayedUrl: "example.com/ExAm",
+ highlightedTitle: [
+ ["The ", false],
+ ["exam", true],
+ ["PLE URL ", false],
+ ["EXAM", true],
+ ["ple", false],
+ ],
+ highlightedUrl: [
+ ["exam", true],
+ ["ple.com/", false],
+ ["ExAm", true],
+ ],
+ }
+ );
+});
+
+add_task(async function test_case_insensitive_highlights_4() {
+ await testResult(
+ {
+ query: "ExAm",
+ title: "The examPLE URL EXAMple",
+ url: "https://example.com/ExAm",
+ },
+ {
+ displayedUrl: "example.com/ExAm",
+ highlightedTitle: [
+ ["The ", false],
+ ["exam", true],
+ ["PLE URL ", false],
+ ["EXAM", true],
+ ["ple", false],
+ ],
+ highlightedUrl: [
+ ["exam", true],
+ ["ple.com/", false],
+ ["ExAm", true],
+ ],
+ }
+ );
+});
+
+add_task(async function test_case_insensitive_highlights_5() {
+ await testResult(
+ {
+ query: "exam foo",
+ title: "The examPLE URL foo EXAMple FOO",
+ url: "https://example.com/ExAm/fOo",
+ },
+ {
+ displayedUrl: "example.com/ExAm/fOo",
+ highlightedTitle: [
+ ["The ", false],
+ ["exam", true],
+ ["PLE URL ", false],
+ ["foo", true],
+ [" ", false],
+ ["EXAM", true],
+ ["ple ", false],
+ ["FOO", true],
+ ],
+ highlightedUrl: [
+ ["exam", true],
+ ["ple.com/", false],
+ ["ExAm", true],
+ ["/", false],
+ ["fOo", true],
+ ],
+ }
+ );
+});
+
+add_task(async function test_case_insensitive_highlights_6() {
+ await testResult(
+ {
+ query: "EXAM FOO",
+ title: "The examPLE URL foo EXAMple FOO",
+ url: "https://example.com/ExAm/fOo",
+ },
+ {
+ displayedUrl: "example.com/ExAm/fOo",
+ highlightedTitle: [
+ ["The ", false],
+ ["exam", true],
+ ["PLE URL ", false],
+ ["foo", true],
+ [" ", false],
+ ["EXAM", true],
+ ["ple ", false],
+ ["FOO", true],
+ ],
+ highlightedUrl: [
+ ["exam", true],
+ ["ple.com/", false],
+ ["ExAm", true],
+ ["/", false],
+ ["fOo", true],
+ ],
+ }
+ );
+});
+
+add_task(async function test_no_highlight_fallback_heuristic_url() {
+ info("Test unvisited heuristic (fallback provider)");
+ await testResult(
+ {
+ query: "nonexisting.com",
+ title: "http://nonexisting.com/",
+ url: "http://nonexisting.com/",
+ },
+ {
+ displayedUrl: "", // URL heuristic only has title.
+ highlightedTitle: [["http://nonexisting.com/", false]],
+ highlightedUrl: [],
+ },
+ 0 // Test the heuristic result.
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_view_resultTypes_display.js b/browser/components/urlbar/tests/browser/browser_view_resultTypes_display.js
new file mode 100644
index 0000000000..494f728f75
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_view_resultTypes_display.js
@@ -0,0 +1,319 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { SyncedTabs } = ChromeUtils.import(
+ "resource://services-sync/SyncedTabs.jsm"
+);
+
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+
+function assertElementsDisplayed(details, expected) {
+ Assert.equal(
+ details.type,
+ expected.type,
+ "Should be displaying a row of the correct type"
+ );
+ Assert.equal(
+ details.title,
+ expected.title,
+ "Should be displaying the correct title"
+ );
+ let separatorVisible =
+ window.getComputedStyle(details.element.separator).display != "none" &&
+ window.getComputedStyle(details.element.separator).visibility != "collapse";
+ Assert.equal(
+ expected.separator,
+ separatorVisible,
+ `Should${expected.separator ? " " : " not "}be displaying a separator`
+ );
+}
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.searches", false],
+ // Disable search suggestions in the urlbar.
+ ["browser.urlbar.suggest.searches", false],
+ // Clear historical search suggestions to avoid interference from previous
+ // tests.
+ ["browser.urlbar.maxHistoricalSearchSuggestions", 0],
+ // Use the default matching bucket configuration.
+ ["browser.urlbar.matchBuckets", "general:5,suggestion:4"],
+ // Turn autofill off.
+ ["browser.urlbar.autoFill", false],
+ ],
+ });
+
+ let engine = await SearchTestUtils.promiseNewSearchEngine(
+ getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME
+ );
+ let oldDefaultEngine = await Services.search.getDefault();
+ await Services.search.setDefault(engine);
+
+ registerCleanupFunction(async () => {
+ await Services.search.setDefault(oldDefaultEngine);
+ });
+
+ // Move the mouse away from the results panel, because hovering a result may
+ // change its aspect (e.g. by showing a " - search with Engine" suffix).
+ await EventUtils.synthesizeNativeMouseMove(gURLBar.inputField);
+});
+
+add_task(async function test_tab_switch_result() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:mozilla"
+ );
+
+ await BrowserTestUtils.withNewTab({ gBrowser }, async () => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "about:mozilla",
+ fireInputEvent: true,
+ });
+
+ const details = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+
+ assertElementsDisplayed(details, {
+ separator: true,
+ title: "about:mozilla",
+ type: UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ });
+ });
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_search_result() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", true);
+
+ await BrowserTestUtils.withNewTab({ gBrowser }, async () => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ fireInputEvent: true,
+ });
+
+ let index = await UrlbarTestUtils.promiseSuggestionsPresent(window);
+ const details = await UrlbarTestUtils.getDetailsOfResultAt(window, index);
+
+ // We'll initially display no separator.
+ assertElementsDisplayed(details, {
+ separator: false,
+ title: "foofoo",
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ });
+
+ // Down to select the first search suggestion.
+ for (let i = index; i > 0; --i) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+
+ // We should now be displaying one.
+ assertElementsDisplayed(details, {
+ separator: true,
+ title: "foofoo",
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ });
+ });
+
+ await PlacesUtils.history.clear();
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+});
+
+add_task(async function test_url_result() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com",
+ title: "example",
+ transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ },
+ ]);
+
+ await BrowserTestUtils.withNewTab({ gBrowser }, async () => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "example",
+ fireInputEvent: true,
+ });
+
+ const details = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+
+ assertElementsDisplayed(details, {
+ separator: true,
+ title: "example",
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ });
+ });
+
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function test_keyword_result() {
+ const TEST_URL = `${TEST_BASE_URL}print_postdata.sjs`;
+
+ await PlacesUtils.keywords.insert({
+ keyword: "get",
+ url: TEST_URL + "?q=%s",
+ });
+
+ await BrowserTestUtils.withNewTab({ gBrowser }, async () => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "get ",
+ fireInputEvent: true,
+ });
+
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+
+ // Because only the keyword is typed, we show the bookmark url.
+ assertElementsDisplayed(details, {
+ separator: true,
+ title: TEST_URL + "?q=",
+ type: UrlbarUtils.RESULT_TYPE.KEYWORD,
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "get test",
+ fireInputEvent: true,
+ });
+
+ details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+
+ assertElementsDisplayed(details, {
+ separator: false,
+ title: "example.com: test",
+ type: UrlbarUtils.RESULT_TYPE.KEYWORD,
+ });
+ });
+});
+
+add_task(async function test_omnibox_result() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ omnibox: {
+ keyword: "omniboxtest",
+ },
+
+ background() {
+ /* global browser */
+ browser.omnibox.setDefaultSuggestion({
+ description: "doit",
+ });
+ // Just do nothing for this test.
+ browser.omnibox.onInputEntered.addListener(() => {});
+ browser.omnibox.onInputChanged.addListener((text, suggest) => {
+ suggest([]);
+ });
+ },
+ },
+ });
+
+ await extension.startup();
+
+ await BrowserTestUtils.withNewTab({ gBrowser }, async () => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "omniboxtest ",
+ fireInputEvent: true,
+ });
+
+ const details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+
+ assertElementsDisplayed(details, {
+ separator: true,
+ title: "Generated extension",
+ type: UrlbarUtils.RESULT_TYPE.OMNIBOX,
+ });
+ });
+
+ await extension.unload();
+});
+
+add_task(async function test_remote_tab_result() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["services.sync.username", "fake"],
+ ["services.sync.syncedTabs.showRemoteTabs", true],
+ ],
+ });
+ // Clear history so that history added by previous tests doesn't mess up this
+ // test when it selects results in the urlbar.
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ const REMOTE_TAB = {
+ id: "7cqCr77ptzX3",
+ type: "client",
+ lastModified: 1492201200,
+ name: "zcarter's Nightly on MacBook-Pro-25",
+ clientType: "desktop",
+ tabs: [
+ {
+ type: "tab",
+ title: "Test Remote",
+ url: "http://example.com",
+ icon: UrlbarUtils.ICON.DEFAULT,
+ client: "7cqCr77ptzX3",
+ lastUsed: parseInt(Date.now() / 1000),
+ },
+ ],
+ };
+
+ const sandbox = sinon.createSandbox();
+
+ let originalSyncedTabsInternal = SyncedTabs._internal;
+ SyncedTabs._internal = {
+ isConfiguredToSyncTabs: true,
+ hasSyncedThisSession: true,
+ getTabClients() {
+ return Promise.resolve([]);
+ },
+ syncTabs() {
+ return Promise.resolve();
+ },
+ };
+
+ // Tell the Sync XPCOM service it is initialized.
+ let weaveXPCService = Cc["@mozilla.org/weave/service;1"].getService(
+ Ci.nsISupports
+ ).wrappedJSObject;
+ let oldWeaveServiceReady = weaveXPCService.ready;
+ weaveXPCService.ready = true;
+
+ sandbox
+ .stub(SyncedTabs._internal, "getTabClients")
+ .callsFake(() => Promise.resolve(Cu.cloneInto([REMOTE_TAB], {})));
+
+ // Reset internal cache in PlacesRemoteTabsAutocompleteProvider.
+ Services.obs.notifyObservers(null, "weave:engine:sync:finish", "tabs");
+
+ registerCleanupFunction(async function() {
+ sandbox.restore();
+ weaveXPCService.ready = oldWeaveServiceReady;
+ SyncedTabs._internal = originalSyncedTabsInternal;
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ Services.telemetry.setEventRecordingEnabled("navigation", false);
+ });
+
+ await BrowserTestUtils.withNewTab({ gBrowser }, async () => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "example",
+ fireInputEvent: true,
+ });
+
+ const details = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+
+ assertElementsDisplayed(details, {
+ separator: true,
+ title: "Test Remote",
+ type: UrlbarUtils.RESULT_TYPE.REMOTE_TAB,
+ });
+ });
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_waitForLoadOrTimeout.js b/browser/components/urlbar/tests/browser/browser_waitForLoadOrTimeout.js
new file mode 100644
index 0000000000..352e37b9d0
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_waitForLoadOrTimeout.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the waitForLoadOrTimeout test helper function in head.js.
+ */
+
+"use strict";
+
+add_task(async function load() {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ let url = "http://example.com/";
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: url,
+ });
+
+ let loadPromise = waitForLoadOrTimeout();
+ EventUtils.synthesizeKey("KEY_Enter");
+ let loadEvent = await loadPromise;
+
+ Assert.ok(loadEvent, "Page should have loaded before timeout");
+ Assert.equal(
+ loadEvent.target.currentURI.spec,
+ url,
+ "example.com should have loaded"
+ );
+ });
+});
+
+add_task(async function timeout() {
+ let loadEvent = await waitForLoadOrTimeout();
+ Assert.ok(
+ !loadEvent,
+ "No page should have loaded, and timeout should have fired"
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_whereToOpen.js b/browser/components/urlbar/tests/browser/browser_whereToOpen.js
new file mode 100644
index 0000000000..a2d0d907df
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_whereToOpen.js
@@ -0,0 +1,194 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const NON_EMPTY_TAB = "example.com/non-empty";
+const EMPTY_TAB = "about:blank";
+const META_KEY = AppConstants.platform == "macosx" ? "metaKey" : "ctrlKey";
+const ENTER = new KeyboardEvent("keydown", {});
+const ALT_ENTER = new KeyboardEvent("keydown", { altKey: true });
+const ALTGR_ENTER = new KeyboardEvent("keydown", { modifierAltGraph: true });
+const CLICK = new MouseEvent("click", { button: 0 });
+const META_CLICK = new MouseEvent("click", { button: 0, [META_KEY]: true });
+const MIDDLE_CLICK = new MouseEvent("click", { button: 1 });
+
+let old_openintab = Preferences.get("browser.urlbar.openintab");
+registerCleanupFunction(async function() {
+ Preferences.set("browser.urlbar.openintab", old_openintab);
+});
+
+add_task(async function openInTab() {
+ // Open a non-empty tab.
+ let tab = (gBrowser.selectedTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ NON_EMPTY_TAB
+ ));
+
+ for (let test of [
+ {
+ pref: false,
+ event: ALT_ENTER,
+ desc: "Alt+Enter, non-empty tab, default prefs",
+ },
+ {
+ pref: false,
+ event: ALTGR_ENTER,
+ desc: "AltGr+Enter, non-empty tab, default prefs",
+ },
+ {
+ pref: false,
+ event: META_CLICK,
+ desc: "Meta+click, non-empty tab, default prefs",
+ },
+ {
+ pref: false,
+ event: MIDDLE_CLICK,
+ desc: "Middle click, non-empty tab, default prefs",
+ },
+ { pref: true, event: ENTER, desc: "Enter, non-empty tab, openInTab" },
+ {
+ pref: true,
+ event: CLICK,
+ desc: "Normal click, non-empty tab, openInTab",
+ },
+ ]) {
+ info(test.desc);
+
+ Preferences.set("browser.urlbar.openintab", test.pref);
+ let where = gURLBar._whereToOpen(test.event);
+ is(where, "tab", "URL would be loaded in a new tab");
+ }
+
+ // Clean up.
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function keepEmptyTab() {
+ // Open an empty tab.
+ let tab = (gBrowser.selectedTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ EMPTY_TAB
+ ));
+
+ for (let test of [
+ {
+ pref: false,
+ event: META_CLICK,
+ desc: "Meta+click, empty tab, default prefs",
+ },
+ {
+ pref: false,
+ event: MIDDLE_CLICK,
+ desc: "Middle click, empty tab, default prefs",
+ },
+ ]) {
+ info(test.desc);
+
+ Preferences.set("browser.urlbar.openintab", test.pref);
+ let where = gURLBar._whereToOpen(test.event);
+ is(where, "tab", "URL would be loaded in a new tab");
+ }
+
+ // Clean up.
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function reuseEmptyTab() {
+ // Open an empty tab.
+ let tab = (gBrowser.selectedTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ EMPTY_TAB
+ ));
+
+ for (let test of [
+ {
+ pref: false,
+ event: ALT_ENTER,
+ desc: "Alt+Enter, empty tab, default prefs",
+ },
+ {
+ pref: false,
+ event: ALTGR_ENTER,
+ desc: "AltGr+Enter, empty tab, default prefs",
+ },
+ { pref: true, event: ENTER, desc: "Enter, empty tab, openInTab" },
+ { pref: true, event: CLICK, desc: "Normal click, empty tab, openInTab" },
+ ]) {
+ info(test.desc);
+ Preferences.set("browser.urlbar.openintab", test.pref);
+ let where = gURLBar._whereToOpen(test.event);
+ is(where, "current", "New URL would reuse the current empty tab");
+ }
+
+ // Clean up.
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function openInCurrentTab() {
+ for (let test of [
+ {
+ pref: false,
+ url: NON_EMPTY_TAB,
+ event: ENTER,
+ desc: "Enter, non-empty tab, default prefs",
+ },
+ {
+ pref: false,
+ url: NON_EMPTY_TAB,
+ event: CLICK,
+ desc: "Normal click, non-empty tab, default prefs",
+ },
+ {
+ pref: false,
+ url: EMPTY_TAB,
+ event: ENTER,
+ desc: "Enter, empty tab, default prefs",
+ },
+ {
+ pref: false,
+ url: EMPTY_TAB,
+ event: CLICK,
+ desc: "Normal click, empty tab, default prefs",
+ },
+ {
+ pref: true,
+ url: NON_EMPTY_TAB,
+ event: ALT_ENTER,
+ desc: "Alt+Enter, non-empty tab, openInTab",
+ },
+ {
+ pref: true,
+ url: NON_EMPTY_TAB,
+ event: ALTGR_ENTER,
+ desc: "AltGr+Enter, non-empty tab, openInTab",
+ },
+ {
+ pref: true,
+ url: NON_EMPTY_TAB,
+ event: META_CLICK,
+ desc: "Meta+click, non-empty tab, openInTab",
+ },
+ {
+ pref: true,
+ url: NON_EMPTY_TAB,
+ event: MIDDLE_CLICK,
+ desc: "Middle click, non-empty tab, openInTab",
+ },
+ ]) {
+ info(test.desc);
+
+ // Open a new tab.
+ let tab = (gBrowser.selectedTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ test.url
+ ));
+
+ Preferences.set("browser.urlbar.openintab", test.pref);
+ let where = gURLBar._whereToOpen(test.event);
+ is(where, "current", "URL would open in the current tab");
+
+ // Clean up.
+ BrowserTestUtils.removeTab(tab);
+ }
+});
diff --git a/browser/components/urlbar/tests/browser/dummy_page.html b/browser/components/urlbar/tests/browser/dummy_page.html
new file mode 100644
index 0000000000..1a87e28408
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/dummy_page.html
@@ -0,0 +1,9 @@
+<html>
+<head>
+<title>Dummy test page</title>
+<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta>
+</head>
+<body>
+<p>Dummy test page</p>
+</body>
+</html>
diff --git a/browser/components/urlbar/tests/browser/dynamicResult0.css b/browser/components/urlbar/tests/browser/dynamicResult0.css
new file mode 100644
index 0000000000..328127b594
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/dynamicResult0.css
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+#urlbar {
+ --testDynamicResult0: ok0;
+}
+
+.urlbarView-row[dynamicType=test] > .urlbarView-row-inner {
+ display: flex;
+ align-items: center;
+ min-height: 32px;
+ width: 100%;
+}
+
+.urlbarView-dynamic-test-buttonBox {
+ display: flex;
+ align-items: center;
+ min-height: 32px;
+}
+
+.urlbarView-dynamic-test-text {
+ flex-grow: 1;
+ flex-shrink: 1;
+ padding: 10px;
+}
+
+.urlbarView-dynamic-test-selectable,
+.urlbarView-dynamic-test-button1,
+.urlbarView-dynamic-test-button2 {
+ min-height: 16px;
+ padding: 8px;
+ border: none;
+ border-radius: 2px;
+ font-size: 0.93em;
+ color: inherit;
+ background-color: var(--urlbarView-button-background);
+ min-width: 8.75em;
+ text-align: center;
+ flex-basis: initial;
+ flex-shrink: 0;
+ margin-inline-end: 10px;
+}
+
+.urlbarView-dynamic-test-selectable[selected],
+.urlbarView-dynamic-test-button1[selected],
+.urlbarView-dynamic-test-button2[selected] {
+ color: white;
+ background-color: var(--urlbarView-primary-button-background);
+ box-shadow: 0 0 0 1px #0a84ff inset, 0 0 0 1px #0a84ff, 0 0 0 4px rgba(10, 132, 255, 0.3);
+}
diff --git a/browser/components/urlbar/tests/browser/dynamicResult1.css b/browser/components/urlbar/tests/browser/dynamicResult1.css
new file mode 100644
index 0000000000..ae43fd3f9a
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/dynamicResult1.css
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+#urlbar {
+ --testDynamicResult1: ok1;
+}
+
+.urlbarView-row[dynamicType=test] > .urlbarView-row-inner {
+ display: flex;
+ align-items: center;
+ min-height: 32px;
+ width: 100%;
+}
+
+.urlbarView-dynamic-test-buttonBox {
+ display: flex;
+ align-items: center;
+ min-height: 32px;
+}
+
+.urlbarView-dynamic-test-text {
+ flex-grow: 1;
+ flex-shrink: 1;
+ padding: 10px;
+}
+
+.urlbarView-dynamic-test-selectable,
+.urlbarView-dynamic-test-button1,
+.urlbarView-dynamic-test-button2 {
+ min-height: 16px;
+ padding: 8px;
+ border: none;
+ border-radius: 2px;
+ font-size: 0.93em;
+ color: inherit;
+ background-color: var(--urlbarView-button-background);
+ min-width: 8.75em;
+ text-align: center;
+ flex-basis: initial;
+ flex-shrink: 0;
+ margin-inline-end: 10px;
+}
+
+.urlbarView-dynamic-test-selectable[selected],
+.urlbarView-dynamic-test-button1[selected],
+.urlbarView-dynamic-test-button2[selected] {
+ color: white;
+ background-color: var(--urlbarView-primary-button-background);
+ box-shadow: 0 0 0 1px #0a84ff inset, 0 0 0 1px #0a84ff, 0 0 0 4px rgba(10, 132, 255, 0.3);
+}
diff --git a/browser/components/urlbar/tests/browser/file_blank_but_not_blank.html b/browser/components/urlbar/tests/browser/file_blank_but_not_blank.html
new file mode 100644
index 0000000000..1f5fea8dcf
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/file_blank_but_not_blank.html
@@ -0,0 +1,2 @@
+<script>var q = "1";</script>
+<a href="javascript:q">Click me</a>
diff --git a/browser/components/urlbar/tests/browser/file_urlbar_edit_dos.html b/browser/components/urlbar/tests/browser/file_urlbar_edit_dos.html
new file mode 100644
index 0000000000..e02242f6a1
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/file_urlbar_edit_dos.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Try editing the URL bar</title>
+<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta>
+</head>
+<body>
+<script>
+function dos_hash() {
+ location.hash = "#";
+}
+
+function dos_pushState() {
+ history.pushState({}, "Some title", "");
+}
+</script>
+</body>
+</html>
diff --git a/browser/components/urlbar/tests/browser/file_userTypedValue.html b/browser/components/urlbar/tests/browser/file_userTypedValue.html
new file mode 100644
index 0000000000..a787b70898
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/file_userTypedValue.html
@@ -0,0 +1 @@
+<html><body>bug562649</body></html>
diff --git a/browser/components/urlbar/tests/browser/head-common.js b/browser/components/urlbar/tests/browser/head-common.js
new file mode 100644
index 0000000000..e03b45851c
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/head-common.js
@@ -0,0 +1,85 @@
+/* eslint-env mozilla/frame-script */
+
+var { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ HttpServer: "resource://testing-common/httpd.js",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.jsm",
+ Preferences: "resource://gre/modules/Preferences.jsm",
+ UrlbarProvider: "resource:///modules/UrlbarUtils.jsm",
+ UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm",
+ UrlbarResult: "resource:///modules/UrlbarResult.jsm",
+ UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.jsm",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(this, "TEST_BASE_URL", () =>
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ )
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "clipboardHelper",
+ "@mozilla.org/widget/clipboardhelper;1",
+ "nsIClipboardHelper"
+);
+
+XPCOMUtils.defineLazyGetter(this, "UrlbarTestUtils", () => {
+ const { UrlbarTestUtils: module } = ChromeUtils.import(
+ "resource://testing-common/UrlbarTestUtils.jsm"
+ );
+ module.init(this);
+ return module;
+});
+
+XPCOMUtils.defineLazyGetter(this, "SearchTestUtils", () => {
+ const { SearchTestUtils: module } = ChromeUtils.import(
+ "resource://testing-common/SearchTestUtils.jsm"
+ );
+ module.init(this);
+ return module;
+});
+
+/**
+ * Initializes an HTTP Server, and runs a task with it.
+ * @param {object} details {scheme, host, port}
+ * @param {function} taskFn The task to run, gets the server as argument.
+ */
+async function withHttpServer(
+ details = { scheme: "http", host: "localhost", port: -1 },
+ taskFn
+) {
+ let server = new HttpServer();
+ let url = `${details.scheme}://${details.host}:${details.port}`;
+ try {
+ info(`starting HTTP Server for ${url}`);
+ try {
+ server.start(details.port);
+ details.port = server.identity.primaryPort;
+ server.identity.setPrimary(details.scheme, details.host, details.port);
+ } catch (ex) {
+ throw new Error("We can't launch our http server successfully. " + ex);
+ }
+ Assert.ok(
+ server.identity.has(details.scheme, details.host, details.port),
+ `${url} is listening.`
+ );
+ try {
+ await taskFn(server);
+ } catch (ex) {
+ throw new Error("Exception in the task function " + ex);
+ }
+ } finally {
+ server.identity.remove(details.scheme, details.host, details.port);
+ try {
+ await new Promise(resolve => server.stop(resolve));
+ } catch (ex) {}
+ server = null;
+ }
+}
diff --git a/browser/components/urlbar/tests/browser/head.js b/browser/components/urlbar/tests/browser/head.js
new file mode 100644
index 0000000000..f8e0527240
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/head.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * These tests unit test the result/url loading functionality of UrlbarController.
+ */
+
+"use strict";
+
+let sandbox;
+
+var { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AboutNewTab: "resource:///modules/AboutNewTab.jsm",
+ AppConstants: "resource://gre/modules/AppConstants.jsm",
+ ObjectUtils: "resource://gre/modules/ObjectUtils.jsm",
+ PromiseUtils: "resource://gre/modules/PromiseUtils.jsm",
+ ResetProfile: "resource://gre/modules/ResetProfile.jsm",
+ TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.jsm",
+ UrlbarController: "resource:///modules/UrlbarController.jsm",
+ UrlbarQueryContext: "resource:///modules/UrlbarUtils.jsm",
+ UrlbarResult: "resource:///modules/UrlbarResult.jsm",
+ UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.jsm",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+ UrlbarView: "resource:///modules/UrlbarView.jsm",
+});
+
+/* import-globals-from head-common.js */
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/urlbar/tests/browser/head-common.js",
+ this
+);
+
+const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
+
+registerCleanupFunction(async () => {
+ // Ensure the Urlbar popup is always closed at the end of a test, to save having
+ // to do it within each test.
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+async function selectAndPaste(str, win = window) {
+ await SimpleTest.promiseClipboardChange(str, () => {
+ clipboardHelper.copyString(str);
+ });
+ win.gURLBar.select();
+ win.document.commandDispatcher
+ .getControllerForCommand("cmd_paste")
+ .doCommand("cmd_paste");
+}
+
+/**
+ * Updates the Top Sites feed.
+ *
+ * @param {function} condition
+ * A callback that returns true after Top Sites are successfully updated.
+ * @param {boolean} searchShortcuts
+ * True if Top Sites search shortcuts should be enabled.
+ */
+async function updateTopSites(condition, searchShortcuts = false) {
+ // Toggle the pref to clear the feed cache and force an update.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.newtabpage.activity-stream.feeds.system.topsites", false],
+ ["browser.newtabpage.activity-stream.feeds.system.topsites", true],
+ [
+ "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts",
+ searchShortcuts,
+ ],
+ ],
+ });
+
+ // Wait for the feed to be updated.
+ await TestUtils.waitForCondition(() => {
+ let sites = AboutNewTab.getTopSites();
+ return condition(sites);
+ }, "Waiting for top sites to be updated");
+}
+
+/**
+ * Waits for a load in any browser or a timeout, whichever comes first.
+ *
+ * @param {window} win
+ * The top-level browser window to listen in.
+ * @param {number} timeoutMs
+ * The timeout in ms.
+ * @returns {event|null}
+ * If a load event was detected before the timeout fired, then the event is
+ * returned. event.target will be the browser in which the load occurred. If
+ * the timeout fired before a load was detected, null is returned.
+ */
+async function waitForLoadOrTimeout(win = window, timeoutMs = 1000) {
+ let event;
+ let listener;
+ let timeout;
+ let eventName = "BrowserTestUtils:ContentEvent:load";
+ try {
+ event = await Promise.race([
+ new Promise(resolve => {
+ listener = resolve;
+ win.addEventListener(eventName, listener, true);
+ }),
+ new Promise(resolve => {
+ timeout = win.setTimeout(resolve, timeoutMs);
+ }),
+ ]);
+ } finally {
+ win.removeEventListener(eventName, listener, true);
+ win.clearTimeout(timeout);
+ }
+ return event || null;
+}
diff --git a/browser/components/urlbar/tests/browser/moz.png b/browser/components/urlbar/tests/browser/moz.png
new file mode 100644
index 0000000000..769c636340
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/moz.png
Binary files differ
diff --git a/browser/components/urlbar/tests/browser/print_postdata.sjs b/browser/components/urlbar/tests/browser/print_postdata.sjs
new file mode 100644
index 0000000000..4175a24805
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/print_postdata.sjs
@@ -0,0 +1,22 @@
+const CC = Components.Constructor;
+const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream");
+
+function handleRequest(request, response) {
+ response.setHeader("Content-Type", "text/plain", false);
+ if (request.method == "GET") {
+ response.write(request.queryString);
+ } else {
+ var body = new BinaryInputStream(request.bodyInputStream);
+
+ var avail;
+ var bytes = [];
+
+ while ((avail = body.available()) > 0)
+ Array.prototype.push.apply(bytes, body.readByteArray(avail));
+
+ var data = String.fromCharCode.apply(null, bytes);
+ response.bodyOutputStream.write(data, data.length);
+ }
+}
diff --git a/browser/components/urlbar/tests/browser/redirect_error.sjs b/browser/components/urlbar/tests/browser/redirect_error.sjs
new file mode 100644
index 0000000000..64c6f143b0
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/redirect_error.sjs
@@ -0,0 +1,16 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const REDIRECT_TO = "https://www.bank1.com/"; // Bad-cert host.
+
+function handleRequest(aRequest, aResponse) {
+ // Set HTTP Status
+ aResponse.setStatusLine(aRequest.httpVersion, 301, "Moved Permanently");
+
+ // Set redirect URI, mirroring the hash value.
+ let hash = (/\#.+/.test(aRequest.path))?
+ "#" + aRequest.path.split("#")[1]:
+ "";
+ aResponse.setHeader("Location", REDIRECT_TO + hash);
+}
diff --git a/browser/components/urlbar/tests/browser/searchSuggestionEngine.sjs b/browser/components/urlbar/tests/browser/searchSuggestionEngine.sjs
new file mode 100644
index 0000000000..770474e745
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/searchSuggestionEngine.sjs
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let gTimer;
+
+function handleRequest(req, resp) {
+ // Parse the query params. If the params aren't in the form "foo=bar", then
+ // treat the entire query string as a search string.
+ let params = req.queryString.split("&").reduce((memo, pair) => {
+ let [key, val] = pair.split("=");
+ if (!val) {
+ // This part isn't in the form "foo=bar". Treat it as the search string
+ // (the "query").
+ val = key;
+ key = "query";
+ }
+ memo[decode(key)] = decode(val);
+ return memo;
+ }, {});
+
+ let timeout = parseInt(params["timeout"]);
+ if (timeout) {
+ // Write the response after a timeout.
+ resp.processAsync();
+ gTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ gTimer.init(() => {
+ writeResponse(params, resp);
+ resp.finish();
+ }, timeout, Ci.nsITimer.TYPE_ONE_SHOT);
+ return;
+ }
+
+ writeResponse(params, resp);
+}
+
+function writeResponse(params, resp) {
+ // Echo back the search string with "foo" and "bar" appended.
+ let suffixes = ["foo", "bar"];
+ if (params["count"]) {
+ // Add more suffixes.
+ let serial = 0;
+ while (suffixes.length < params["count"]) {
+ suffixes.push(++serial);
+ }
+ }
+ let data = [params["query"], suffixes.map(s => params["query"] + s)];
+ resp.setHeader("Content-Type", "application/json", false);
+ resp.write(JSON.stringify(data));
+}
+
+function decode(str) {
+ return decodeURIComponent(str.replace(/\+/g, encodeURIComponent(" ")));
+}
diff --git a/browser/components/urlbar/tests/browser/searchSuggestionEngine.xml b/browser/components/urlbar/tests/browser/searchSuggestionEngine.xml
new file mode 100644
index 0000000000..142c91849c
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/searchSuggestionEngine.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>browser_searchSuggestionEngine searchSuggestionEngine.xml</ShortName>
+<Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/searchSuggestionEngine.sjs?{searchTerms}"/>
+<Url type="text/html" method="GET" template="http://mochi.test:8888/" rel="searchform">
+ <Param name="terms" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/browser/components/urlbar/tests/browser/searchSuggestionEngine2.xml b/browser/components/urlbar/tests/browser/searchSuggestionEngine2.xml
new file mode 100644
index 0000000000..565aaf2bc0
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/searchSuggestionEngine2.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>browser_searchSuggestionEngine2 searchSuggestionEngine2.xml</ShortName>
+<!-- Redirect the actual search request to the test-server because of proxy restriction -->
+<Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/searchSuggestionEngine.sjs?{searchTerms}"/>
+<!-- Redirect speculative connect to a local http server we run for this test -->
+<Url type="text/html" method="GET" template="http://localhost:20709/" rel="searchform">
+ <Param name="terms" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/browser/components/urlbar/tests/browser/searchSuggestionEngineMany.xml b/browser/components/urlbar/tests/browser/searchSuggestionEngineMany.xml
new file mode 100644
index 0000000000..7e77e32029
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/searchSuggestionEngineMany.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>browser_searchSuggestionEngineMany searchSuggestionEngineMany.xml</ShortName>
+<Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/searchSuggestionEngine.sjs?{searchTerms}&amp;count=10"/>
+<Url type="text/html" method="GET" template="http://mochi.test:8888/" rel="searchform">
+ <Param name="terms" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/browser/components/urlbar/tests/browser/searchSuggestionEngineSlow.xml b/browser/components/urlbar/tests/browser/searchSuggestionEngineSlow.xml
new file mode 100644
index 0000000000..e7214e65cc
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/searchSuggestionEngineSlow.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>searchSuggestionEngineSlow.xml</ShortName>
+<Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/searchSuggestionEngine.sjs?query={searchTerms}&amp;timeout=3000"/>
+<Url type="text/html" method="GET" template="http://mochi.test:8888/" rel="searchform">
+ <Param name="terms" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/browser/components/urlbar/tests/browser/slow-page.sjs b/browser/components/urlbar/tests/browser/slow-page.sjs
new file mode 100644
index 0000000000..ce9a759744
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/slow-page.sjs
@@ -0,0 +1,23 @@
+"use strict";
+
+let timer;
+
+const DELAY_MS = 5000;
+function handleRequest(request, response) {
+ if (request.queryString.endsWith("faster")) {
+ response.setHeader("Content-Type", "text/html", false);
+ response.write("<body>Not so slow!</body>");
+ return;
+ }
+ response.processAsync();
+ timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.init(
+ () => {
+ response.setHeader("Content-Type", "text/html", false);
+ response.write("<body>This is a slow loading page.</body>");
+ response.finish();
+ },
+ DELAY_MS,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+}
diff --git a/browser/components/urlbar/tests/browser/urlbarTelemetrySearchSuggestions.sjs b/browser/components/urlbar/tests/browser/urlbarTelemetrySearchSuggestions.sjs
new file mode 100644
index 0000000000..1978b4f665
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/urlbarTelemetrySearchSuggestions.sjs
@@ -0,0 +1,9 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(req, resp) {
+ let suffixes = ["foo", "bar"];
+ let data = [req.queryString, suffixes.map(s => req.queryString + s)];
+ resp.setHeader("Content-Type", "application/json", false);
+ resp.write(JSON.stringify(data));
+}
diff --git a/browser/components/urlbar/tests/browser/urlbarTelemetrySearchSuggestions.xml b/browser/components/urlbar/tests/browser/urlbarTelemetrySearchSuggestions.xml
new file mode 100644
index 0000000000..8ed4fef6f1
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/urlbarTelemetrySearchSuggestions.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>browser_urlbar_telemetry urlbarTelemetrySearchSuggestions.xml</ShortName>
+<Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/urlbarTelemetrySearchSuggestions.sjs?{searchTerms}"/>
+<Url type="text/html" method="GET" template="http://example.com" rel="searchform"/>
+</SearchPlugin>
diff --git a/browser/components/urlbar/tests/browser/urlbarTelemetryUrlbarDynamic.css b/browser/components/urlbar/tests/browser/urlbarTelemetryUrlbarDynamic.css
new file mode 100644
index 0000000000..e81052522f
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/urlbarTelemetryUrlbarDynamic.css
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+.urlbarView-row[dynamicType=test] > .urlbarView-row-inner {
+ display: flex;
+ align-items: center;
+ min-height: 32px;
+ width: 100%;
+}
+
+.urlbarView-dynamic-test-button {
+ min-height: 16px;
+ padding: 8px;
+ border: none;
+ border-radius: 2px;
+ font-size: 0.93em;
+ color: inherit;
+ background-color: var(--urlbarView-button-background);
+ min-width: 8.75em;
+ text-align: center;
+ flex-basis: initial;
+ flex-shrink: 0;
+}
+
+.urlbarView-dynamic-test-button[selected] {
+ color: white;
+ background-color: var(--urlbarView-primary-button-background);
+ box-shadow: 0 0 0 1px #0a84ff inset, 0 0 0 1px #0a84ff, 0 0 0 4px rgba(10, 132, 255, 0.3);
+}
+
+.urlbarView-dynamic-test-button:hover {
+ color: white;
+ background-color: var(--urlbarView-primary-button-background-hover);
+}
+
+.urlbarView-dynamic-test-button:active {
+ color: white;
+ background-color: var(--urlbarView-primary-button-background-active);
+}
+
+.urlbarView-dynamic-test-buttonSpacer {
+ flex-basis: 48px;
+ flex-grow: 1;
+ flex-shrink: 1;
+}
diff --git a/browser/components/urlbar/tests/ext/api.js b/browser/components/urlbar/tests/ext/api.js
new file mode 100644
index 0000000000..44f4802db1
--- /dev/null
+++ b/browser/components/urlbar/tests/ext/api.js
@@ -0,0 +1,256 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* global ExtensionAPI, ExtensionCommon */
+
+"use strict";
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
+ Preferences: "resource://gre/modules/Preferences.jsm",
+ UrlbarProviderExtension: "resource:///modules/UrlbarProviderExtension.jsm",
+ UrlbarResult: "resource:///modules/UrlbarResult.jsm",
+ UrlbarView: "resource:///modules/UrlbarView.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(
+ this,
+ "defaultPreferences",
+ () => new Preferences({ defaultBranch: true })
+);
+
+let { EventManager } = ExtensionCommon;
+
+this.experiments_urlbar = class extends ExtensionAPI {
+ getAPI(context) {
+ return {
+ experiments: {
+ urlbar: {
+ addDynamicResultType: (name, type) => {
+ this._addDynamicResultType(name, type);
+ },
+
+ addDynamicViewTemplate: (name, viewTemplate) => {
+ this._addDynamicViewTemplate(name, viewTemplate);
+ },
+
+ attributionURL: this._getDefaultSettingsAPI(
+ "browser.partnerlink.attributionURL"
+ ),
+
+ clearInput() {
+ let window = BrowserWindowTracker.getTopWindow();
+ window.gURLBar.value = "";
+ window.gURLBar.setPageProxyState("invalid");
+ },
+
+ engagementTelemetry: this._getDefaultSettingsAPI(
+ "browser.urlbar.eventTelemetry.enabled"
+ ),
+
+ extensionTimeout: this._getDefaultSettingsAPI(
+ "browser.urlbar.extension.timeout"
+ ),
+
+ onViewUpdateRequested: new EventManager({
+ context,
+ name: "experiments.urlbar.onViewUpdateRequested",
+ register: (fire, providerName) => {
+ let provider = UrlbarProviderExtension.getOrCreate(providerName);
+ provider.setEventListener(
+ "getViewUpdate",
+ (result, idsByName) => {
+ return fire.async(result.payload, idsByName).catch(error => {
+ throw context.normalizeError(error);
+ });
+ }
+ );
+ return () => provider.setEventListener("getViewUpdate", null);
+ },
+ }).api(),
+ },
+ },
+ };
+ }
+
+ onShutdown() {
+ // Reset the default prefs. This is necessary because
+ // ExtensionPreferencesManager doesn't properly reset prefs set on the
+ // default branch. See bug 1586543, bug 1578513, bug 1578508.
+ if (this._initialDefaultPrefs) {
+ for (let [pref, value] of this._initialDefaultPrefs.entries()) {
+ defaultPreferences.set(pref, value);
+ }
+ }
+
+ this._removeDynamicViewTemplates();
+ this._removeDynamicResultTypes();
+ }
+
+ _getDefaultSettingsAPI(pref) {
+ return {
+ get: details => {
+ return {
+ value: Preferences.get(pref),
+
+ // Nothing actually uses this, but on debug builds there are extra
+ // checks enabled in Schema.jsm that fail if it's not present. The
+ // value doesn't matter.
+ levelOfControl: "controllable_by_this_extension",
+ };
+ },
+ set: details => {
+ if (!this._initialDefaultPrefs) {
+ this._initialDefaultPrefs = new Map();
+ }
+ if (!this._initialDefaultPrefs.has(pref)) {
+ this._initialDefaultPrefs.set(pref, defaultPreferences.get(pref));
+ }
+ defaultPreferences.set(pref, details.value);
+ return true;
+ },
+ clear: details => {
+ if (this._initialDefaultPrefs && this._initialDefaultPrefs.has(pref)) {
+ defaultPreferences.set(pref, this._initialDefaultPrefs.get(pref));
+ return true;
+ }
+ return false;
+ },
+ };
+ }
+
+ // We use the following four properties as bookkeeping to keep track of
+ // dynamic result types and view templates registered by extensions so that
+ // they can be properly removed on extension shutdown.
+
+ // Names of dynamic result types added by this extension.
+ _dynamicResultTypeNames = new Set();
+
+ // Names of dynamic result type view templates added by this extension.
+ _dynamicViewTemplateNames = new Set();
+
+ // Maps dynamic result type names to Sets of IDs of extensions that have
+ // registered those types.
+ static extIDsByDynamicResultTypeName = new Map();
+
+ // Maps dynamic result type view template names to Sets of IDs of extensions
+ // that have registered those view templates.
+ static extIDsByDynamicViewTemplateName = new Map();
+
+ /**
+ * Adds a dynamic result type and includes it in our bookkeeping. See
+ * UrlbarResult.addDynamicResultType().
+ *
+ * @param {string} name
+ * The name of the dynamic result type.
+ * @param {object} type
+ * The type.
+ */
+ _addDynamicResultType(name, type) {
+ this._dynamicResultTypeNames.add(name);
+ this._addExtIDToDynamicResultTypeMap(
+ experiments_urlbar.extIDsByDynamicResultTypeName,
+ name
+ );
+ UrlbarResult.addDynamicResultType(name, type);
+ }
+
+ /**
+ * Removes all dynamic result types added by the extension.
+ */
+ _removeDynamicResultTypes() {
+ for (let name of this._dynamicResultTypeNames) {
+ let allRemoved = this._removeExtIDFromDynamicResultTypeMap(
+ experiments_urlbar.extIDsByDynamicResultTypeName,
+ name
+ );
+ if (allRemoved) {
+ UrlbarResult.removeDynamicResultType(name);
+ }
+ }
+ }
+
+ /**
+ * Adds a dynamic result type view template and includes it in our
+ * bookkeeping. See UrlbarView.addDynamicViewTemplate().
+ *
+ * @param {string} name
+ * The view template will be registered for the dynamic result type with
+ * this name.
+ * @param {object} viewTemplate
+ * The view template.
+ */
+ _addDynamicViewTemplate(name, viewTemplate) {
+ this._dynamicViewTemplateNames.add(name);
+ this._addExtIDToDynamicResultTypeMap(
+ experiments_urlbar.extIDsByDynamicViewTemplateName,
+ name
+ );
+ if (viewTemplate.stylesheet) {
+ viewTemplate.stylesheet = this.extension.baseURI.resolve(
+ viewTemplate.stylesheet
+ );
+ }
+ UrlbarView.addDynamicViewTemplate(name, viewTemplate);
+ }
+
+ /**
+ * Removes all dynamic result type view templates added by the extension.
+ */
+ _removeDynamicViewTemplates() {
+ for (let name of this._dynamicViewTemplateNames) {
+ let allRemoved = this._removeExtIDFromDynamicResultTypeMap(
+ experiments_urlbar.extIDsByDynamicViewTemplateName,
+ name
+ );
+ if (allRemoved) {
+ UrlbarView.removeDynamicViewTemplate(name);
+ }
+ }
+ }
+
+ /**
+ * Adds a dynamic result type name and this extension's ID to a bookkeeping
+ * map.
+ *
+ * @param {Map} map
+ * Either extIDsByDynamicResultTypeName or extIDsByDynamicViewTemplateName.
+ * @param {string} dynamicTypeName
+ * The dynamic result type name.
+ */
+ _addExtIDToDynamicResultTypeMap(map, dynamicTypeName) {
+ let extIDs = map.get(dynamicTypeName);
+ if (!extIDs) {
+ extIDs = new Set();
+ map.set(dynamicTypeName, extIDs);
+ }
+ extIDs.add(this.extension.id);
+ }
+
+ /**
+ * Removes a dynamic result type name and this extension's ID from a
+ * bookkeeping map.
+ *
+ * @param {Map} map
+ * Either extIDsByDynamicResultTypeName or extIDsByDynamicViewTemplateName.
+ * @param {string} dynamicTypeName
+ * The dynamic result type name.
+ * @returns {boolean}
+ * True if no other extension IDs are in the map under the same
+ * dynamicTypeName, and false otherwise.
+ */
+ _removeExtIDFromDynamicResultTypeMap(map, dynamicTypeName) {
+ let extIDs = map.get(dynamicTypeName);
+ extIDs.delete(this.extension.id);
+ if (!extIDs.size) {
+ map.delete(dynamicTypeName);
+ return true;
+ }
+ return false;
+ }
+};
diff --git a/browser/components/urlbar/tests/ext/browser/.eslintrc.js b/browser/components/urlbar/tests/ext/browser/.eslintrc.js
new file mode 100644
index 0000000000..e57058ecb1
--- /dev/null
+++ b/browser/components/urlbar/tests/ext/browser/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ env: {
+ webextensions: true,
+ },
+};
diff --git a/browser/components/urlbar/tests/ext/browser/browser.ini b/browser/components/urlbar/tests/ext/browser/browser.ini
new file mode 100644
index 0000000000..416fc52eb3
--- /dev/null
+++ b/browser/components/urlbar/tests/ext/browser/browser.ini
@@ -0,0 +1,18 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+[DEFAULT]
+support-files =
+ ../../browser/head-common.js
+ ../api.js
+ ../schema.json
+ head.js
+
+[browser_ext_urlbar_attributionURL.js]
+[browser_ext_urlbar_clearInput.js]
+[browser_ext_urlbar_dynamicResult.js]
+support-files =
+ dynamicResult.css
+[browser_ext_urlbar_engagementTelemetry.js]
+[browser_ext_urlbar_extensionTimeout.js]
diff --git a/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_attributionURL.js b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_attributionURL.js
new file mode 100644
index 0000000000..a5bccc8eba
--- /dev/null
+++ b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_attributionURL.js
@@ -0,0 +1,16 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* global browser */
+
+// This tests the browser.experiments.urlbar.engagementTelemetry WebExtension
+// Experiment API.
+
+"use strict";
+
+add_settings_tasks("browser.partnerlink.attributionURL", "string", () => {
+ browser.test.onMessage.addListener(async (method, arg) => {
+ let result = await browser.experiments.urlbar.attributionURL[method](arg);
+ browser.test.sendMessage("done", result);
+ });
+});
diff --git a/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_clearInput.js b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_clearInput.js
new file mode 100644
index 0000000000..afeff3b8a1
--- /dev/null
+++ b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_clearInput.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* global browser */
+
+// This tests the browser.experiments.urlbar.clearInput WebExtension Experiment
+// API.
+
+"use strict";
+
+add_task(async function test() {
+ // Load a page so that pageproxystate is valid. When the extension calls
+ // clearInput, the pageproxystate should become invalid.
+ await BrowserTestUtils.withNewTab("http://example.com/", async () => {
+ Assert.notEqual(gURLBar.value, "", "Input is not empty");
+ Assert.equal(gURLBar.getAttribute("pageproxystate"), "valid");
+
+ let ext = await loadExtension({
+ background: async () => {
+ await browser.experiments.urlbar.clearInput();
+ browser.test.sendMessage("done");
+ },
+ });
+ await ext.awaitMessage("done");
+
+ Assert.equal(gURLBar.value, "", "Input is empty");
+ Assert.equal(gURLBar.getAttribute("pageproxystate"), "invalid");
+
+ await ext.unload();
+ });
+});
diff --git a/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_dynamicResult.js b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_dynamicResult.js
new file mode 100644
index 0000000000..a710d8949d
--- /dev/null
+++ b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_dynamicResult.js
@@ -0,0 +1,137 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* global browser */
+
+// This tests dynamic results using the WebExtension Experiment API.
+
+"use strict";
+
+add_task(async function test() {
+ let ext = await loadExtension({
+ extraFiles: {
+ "dynamicResult.css": await (
+ await fetch("file://" + getTestFilePath("dynamicResult.css"))
+ ).text(),
+ },
+ background: async () => {
+ browser.experiments.urlbar.addDynamicResultType("testDynamicType");
+ browser.experiments.urlbar.addDynamicViewTemplate("testDynamicType", {
+ stylesheet: "dynamicResult.css",
+ children: [
+ {
+ name: "text",
+ tag: "span",
+ },
+ {
+ name: "button",
+ tag: "span",
+ attributes: {
+ role: "button",
+ },
+ },
+ ],
+ });
+ browser.urlbar.onBehaviorRequested.addListener(query => {
+ return "restricting";
+ }, "test");
+ browser.urlbar.onResultsRequested.addListener(query => {
+ return [
+ {
+ type: "dynamic",
+ source: "local",
+ heuristic: true,
+ payload: {
+ dynamicType: "testDynamicType",
+ },
+ },
+ ];
+ }, "test");
+ browser.experiments.urlbar.onViewUpdateRequested.addListener(payload => {
+ return {
+ text: {
+ textContent: "This is a dynamic result.",
+ },
+ button: {
+ textContent: "Click Me",
+ },
+ };
+ }, "test");
+ browser.urlbar.onResultPicked.addListener((payload, elementName) => {
+ browser.test.sendMessage("onResultPicked", [payload, elementName]);
+ }, "test");
+ },
+ });
+
+ // Wait for the provider and dynamic type to be registered before continuing.
+ await TestUtils.waitForCondition(
+ () =>
+ UrlbarProvidersManager.getProvider("test") &&
+ UrlbarResult.getDynamicResultType("testDynamicType"),
+ "Waiting for provider and dynamic type to be registered"
+ );
+ Assert.ok(
+ UrlbarProvidersManager.getProvider("test"),
+ "Provider should be registered"
+ );
+ Assert.ok(
+ UrlbarResult.getDynamicResultType("testDynamicType"),
+ "Dynamic type should be registered"
+ );
+
+ // Do a search.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ waitForFocus: SimpleTest.waitForFocus,
+ });
+
+ // Get the row.
+ let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
+ Assert.equal(
+ row.result.type,
+ UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ "row.result.type"
+ );
+ Assert.equal(
+ row.getAttribute("dynamicType"),
+ "testDynamicType",
+ "row[dynamicType]"
+ );
+
+ let text = row.querySelector(".urlbarView-dynamic-testDynamicType-text");
+
+ // The view's call to provider.getViewUpdate is async, so we need to make sure
+ // the update has been applied before continuing to avoid intermittent
+ // failures.
+ await TestUtils.waitForCondition(
+ () => text.textContent == "This is a dynamic result."
+ );
+
+ // Check the elements.
+ Assert.equal(
+ text.textContent,
+ "This is a dynamic result.",
+ "text.textContent"
+ );
+ let button = row.querySelector(".urlbarView-dynamic-testDynamicType-button");
+ Assert.equal(button.textContent, "Click Me", "button.textContent");
+
+ // The result's button should be selected since the result is the heuristic.
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElement(window),
+ button,
+ "Button should be selected"
+ );
+
+ // Pick the button.
+ let pickPromise = ext.awaitMessage("onResultPicked");
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Enter")
+ );
+ let [payload, elementName] = await pickPromise;
+ Assert.equal(payload.dynamicType, "testDynamicType", "Picked payload");
+ Assert.equal(elementName, "button", "Picked element name");
+
+ await ext.unload();
+});
diff --git a/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_engagementTelemetry.js b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_engagementTelemetry.js
new file mode 100644
index 0000000000..50ded14d4e
--- /dev/null
+++ b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_engagementTelemetry.js
@@ -0,0 +1,18 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* global browser */
+
+// This tests the browser.experiments.urlbar.engagementTelemetry WebExtension
+// Experiment API.
+
+"use strict";
+
+add_settings_tasks("browser.urlbar.eventTelemetry.enabled", "boolean", () => {
+ browser.test.onMessage.addListener(async (method, arg) => {
+ let result = await browser.experiments.urlbar.engagementTelemetry[method](
+ arg
+ );
+ browser.test.sendMessage("done", result);
+ });
+});
diff --git a/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_extensionTimeout.js b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_extensionTimeout.js
new file mode 100644
index 0000000000..de09ef263c
--- /dev/null
+++ b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_extensionTimeout.js
@@ -0,0 +1,16 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* global browser */
+
+// This tests the browser.experiments.urlbar.engagementTelemetry WebExtension
+// Experiment API.
+
+"use strict";
+
+add_settings_tasks("browser.urlbar.extension.timeout", "number", () => {
+ browser.test.onMessage.addListener(async (method, arg) => {
+ let result = await browser.experiments.urlbar.extensionTimeout[method](arg);
+ browser.test.sendMessage("done", result);
+ });
+});
diff --git a/browser/components/urlbar/tests/ext/browser/dynamicResult.css b/browser/components/urlbar/tests/ext/browser/dynamicResult.css
new file mode 100644
index 0000000000..efd0c8c950
--- /dev/null
+++ b/browser/components/urlbar/tests/ext/browser/dynamicResult.css
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+.urlbarView-row[dynamicType=testDynamicType] > .urlbarView-row-inner {
+ display: flex;
+ align-items: center;
+ min-height: 32px;
+ width: 100%;
+}
+
+.urlbarView-dynamic-testDynamicType-text {
+ flex-grow: 1;
+ flex-shrink: 1;
+ padding: 10px;
+}
+
+.urlbarView-dynamic-testDynamicType-button {
+ min-height: 16px;
+ padding: 8px;
+ border: none;
+ border-radius: 2px;
+ font-size: 0.93em;
+ color: inherit;
+ background-color: var(--urlbarView-button-background);
+ min-width: 8.75em;
+ text-align: center;
+ flex-basis: initial;
+ flex-shrink: 0;
+ margin-inline-end: 10px;
+}
+
+.urlbarView-dynamic-testDynamicType-button[selected] {
+ color: white;
+ background-color: var(--urlbarView-primary-button-background);
+ box-shadow: 0 0 0 1px #0a84ff inset, 0 0 0 1px #0a84ff, 0 0 0 4px rgba(10, 132, 255, 0.3);
+}
diff --git a/browser/components/urlbar/tests/ext/browser/head.js b/browser/components/urlbar/tests/ext/browser/head.js
new file mode 100644
index 0000000000..e5b2d7631d
--- /dev/null
+++ b/browser/components/urlbar/tests/ext/browser/head.js
@@ -0,0 +1,251 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * The files in this directory test the browser.urlbarExperiments WebExtension
+ * Experiment APIs, which are the WebExtension APIs we ship in our urlbar
+ * experiment extensions. Unlike the WebExtension APIs we ship in mozilla-
+ * central, which have continuous test coverage [1], our WebExtension Experiment
+ * APIs would not have continuous test coverage were it not for the fact that we
+ * copy and test them here. This is especially useful for APIs that are used in
+ * experiments that target multiple versions of Firefox, and for APIs that are
+ * reused in multiple experiments. See [2] and [3] for more info on
+ * experiments.
+ *
+ * [1] See browser/components/extensions/test
+ * [2] browser/components/urlbar/docs/experiments.rst
+ * [3] https://firefox-source-docs.mozilla.org/toolkit/components/extensions/webextensions/basics.html#webextensions-experiments
+ */
+
+"use strict";
+
+/* import-globals-from ../../browser/head-common.js */
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/urlbar/tests/ext/browser/head-common.js",
+ this
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ Preferences: "resource://gre/modules/Preferences.jsm",
+});
+
+const SCHEMA_BASENAME = "schema.json";
+const SCRIPT_BASENAME = "api.js";
+
+const SCHEMA_PATH = getTestFilePath(SCHEMA_BASENAME);
+const SCRIPT_PATH = getTestFilePath(SCRIPT_BASENAME);
+
+let schemaSource;
+let scriptSource;
+
+add_task(async function loadSource() {
+ schemaSource = await (await fetch("file://" + SCHEMA_PATH)).text();
+ scriptSource = await (await fetch("file://" + SCRIPT_PATH)).text();
+});
+
+/**
+ * Loads a mock extension with our browser.experiments.urlbar API and a
+ * background script. Be sure to call `await ext.unload()` when you're done
+ * with it.
+ *
+ * @param {function} background
+ * This function is serialized and becomes the background script.
+ * @param {object} extraFiles
+ * Extra files to load in the extension.
+ * @returns {object}
+ * The extension.
+ */
+async function loadExtension({ background, extraFiles = {} }) {
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["urlbar"],
+ experiment_apis: {
+ experiments_urlbar: {
+ schema: SCHEMA_BASENAME,
+ parent: {
+ scopes: ["addon_parent"],
+ paths: [["experiments", "urlbar"]],
+ script: SCRIPT_BASENAME,
+ },
+ },
+ },
+ },
+ files: {
+ [SCHEMA_BASENAME]: schemaSource,
+ [SCRIPT_BASENAME]: scriptSource,
+ ...extraFiles,
+ },
+ isPrivileged: true,
+ background,
+ });
+ await ext.startup();
+ return ext;
+}
+
+/**
+ * Tests toggling a preference value via an experiments.urlbar API.
+ *
+ * @param {string} prefName
+ * The name of the pref to be tested.
+ * @param {string} type
+ * The type of the pref being set. One of "string", "boolean", or "number".
+ * @param {function} background
+ * Boilerplate function that returns the value from calling the
+ * browser.experiments.urlbar.prefName[method] APIs.
+ */
+function add_settings_tasks(prefName, type, background) {
+ let defaultPreferences = new Preferences({ defaultBranch: true });
+
+ let originalValue = defaultPreferences.get(prefName);
+ registerCleanupFunction(() => {
+ defaultPreferences.set(prefName, originalValue);
+ });
+
+ let firstValue, secondValue;
+ switch (type) {
+ case "string":
+ firstValue = "test value 1";
+ secondValue = "test value 2";
+ break;
+ case "number":
+ firstValue = 10;
+ secondValue = 100;
+ break;
+ case "boolean":
+ firstValue = false;
+ secondValue = true;
+ break;
+ default:
+ Assert.fail(
+ `"type" parameter must be one of "string", "number", or "boolean"`
+ );
+ }
+
+ add_task(async function get() {
+ let ext = await loadExtension({ background });
+
+ defaultPreferences.set(prefName, firstValue);
+ ext.sendMessage("get", {});
+ let result = await ext.awaitMessage("done");
+ Assert.strictEqual(result.value, firstValue);
+
+ defaultPreferences.set(prefName, secondValue);
+ ext.sendMessage("get", {});
+ result = await ext.awaitMessage("done");
+ Assert.strictEqual(result.value, secondValue);
+
+ await ext.unload();
+ });
+
+ add_task(async function set() {
+ let ext = await loadExtension({ background });
+
+ defaultPreferences.set(prefName, firstValue);
+ ext.sendMessage("set", { value: secondValue });
+ let result = await ext.awaitMessage("done");
+ Assert.strictEqual(result, true);
+ Assert.strictEqual(defaultPreferences.get(prefName), secondValue);
+
+ ext.sendMessage("set", { value: firstValue });
+ result = await ext.awaitMessage("done");
+ Assert.strictEqual(result, true);
+ Assert.strictEqual(defaultPreferences.get(prefName), firstValue);
+
+ await ext.unload();
+ });
+
+ add_task(async function clear() {
+ // no set()
+ defaultPreferences.set(prefName, firstValue);
+ let ext = await loadExtension({ background });
+ ext.sendMessage("clear", {});
+ let result = await ext.awaitMessage("done");
+ Assert.strictEqual(result, false);
+ Assert.strictEqual(defaultPreferences.get(prefName), firstValue);
+ await ext.unload();
+
+ // firstValue -> secondValue
+ defaultPreferences.set(prefName, firstValue);
+ ext = await loadExtension({ background });
+ ext.sendMessage("set", { value: secondValue });
+ await ext.awaitMessage("done");
+ ext.sendMessage("clear", {});
+ result = await ext.awaitMessage("done");
+ Assert.strictEqual(result, true);
+ Assert.strictEqual(defaultPreferences.get(prefName), firstValue);
+ await ext.unload();
+
+ // secondValue -> firstValue
+ defaultPreferences.set(prefName, secondValue);
+ ext = await loadExtension({ background });
+ ext.sendMessage("set", { value: firstValue });
+ await ext.awaitMessage("done");
+ ext.sendMessage("clear", {});
+ result = await ext.awaitMessage("done");
+ Assert.strictEqual(result, true);
+ Assert.strictEqual(defaultPreferences.get(prefName), secondValue);
+ await ext.unload();
+
+ // firstValue -> firstValue
+ defaultPreferences.set(prefName, firstValue);
+ ext = await loadExtension({ background });
+ ext.sendMessage("set", { value: firstValue });
+ await ext.awaitMessage("done");
+ ext.sendMessage("clear", {});
+ result = await ext.awaitMessage("done");
+ Assert.strictEqual(result, true);
+ Assert.strictEqual(defaultPreferences.get(prefName), firstValue);
+ await ext.unload();
+
+ // secondValue -> secondValue
+ defaultPreferences.set(prefName, secondValue);
+ ext = await loadExtension({ background });
+ ext.sendMessage("set", { value: secondValue });
+ await ext.awaitMessage("done");
+ ext.sendMessage("clear", {});
+ result = await ext.awaitMessage("done");
+ Assert.strictEqual(result, true);
+ Assert.strictEqual(defaultPreferences.get(prefName), secondValue);
+ await ext.unload();
+ });
+
+ add_task(async function shutdown() {
+ // no set()
+ defaultPreferences.set(prefName, firstValue);
+ let ext = await loadExtension({ background });
+ await ext.unload();
+ Assert.strictEqual(defaultPreferences.get(prefName), firstValue);
+
+ // firstValue -> secondValue
+ defaultPreferences.set(prefName, firstValue);
+ ext = await loadExtension({ background });
+ ext.sendMessage("set", { value: secondValue });
+ await ext.awaitMessage("done");
+ await ext.unload();
+ Assert.strictEqual(defaultPreferences.get(prefName), firstValue);
+
+ // secondValue -> firstValue
+ defaultPreferences.set(prefName, secondValue);
+ ext = await loadExtension({ background });
+ ext.sendMessage("set", { value: firstValue });
+ await ext.awaitMessage("done");
+ await ext.unload();
+ Assert.strictEqual(defaultPreferences.get(prefName), secondValue);
+
+ // firstValue -> firstValue
+ defaultPreferences.set(prefName, firstValue);
+ ext = await loadExtension({ background });
+ ext.sendMessage("set", { value: firstValue });
+ await ext.awaitMessage("done");
+ await ext.unload();
+ Assert.strictEqual(defaultPreferences.get(prefName), firstValue);
+
+ // secondValue -> secondValue
+ defaultPreferences.set(prefName, secondValue);
+ ext = await loadExtension({ background });
+ ext.sendMessage("set", { value: secondValue });
+ await ext.awaitMessage("done");
+ await ext.unload();
+ Assert.strictEqual(defaultPreferences.get(prefName), secondValue);
+ });
+}
diff --git a/browser/components/urlbar/tests/ext/schema.json b/browser/components/urlbar/tests/ext/schema.json
new file mode 100644
index 0000000000..26b0df5a34
--- /dev/null
+++ b/browser/components/urlbar/tests/ext/schema.json
@@ -0,0 +1,113 @@
+[
+ {
+ "namespace": "experiments.urlbar",
+ "description": "APIs supporting urlbar experiments",
+ "types": [
+ {
+ "id": "DynamicResultType",
+ "type": "object",
+ "description": "Describes a dynamic result type.",
+ "properties": {
+ "viewTemplate": {
+ "type": "object",
+ "description": "An object describing the type's view.",
+ "additionalProperties": true
+ }
+ }
+ }
+ ],
+ "properties": {
+ "attributionURL": {
+ "$ref": "types.Setting",
+ "description": "Gets or sets the attribution URL for the current browser session."
+ },
+ "engagementTelemetry": {
+ "$ref": "types.Setting",
+ "description": "Enables or disables the engagement telemetry for the current browser session."
+ },
+ "extensionTimeout": {
+ "$ref": "types.Setting",
+ "description": "Sets the amount of time in ms that extensions have to return results to the browser.urlbar API."
+ }
+ },
+ "events": [
+ {
+ "name": "onViewUpdateRequested",
+ "type": "function",
+ "description": "Fired when the urlbar view updates the view of one of the results of the provider.",
+ "parameters": [
+ {
+ "name": "payload",
+ "type": "object",
+ "description": "The result's payload."
+ },
+ {
+ "name": "idsByName",
+ "type": "object",
+ "description": "A Map from an element's name, as defined by the provider; to its ID in the DOM, as defined by the browser."
+ }
+ ],
+ "extraParameters": [
+ {
+ "name": "providerName",
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9_-]+$",
+ "description": "The name of the provider you want to provide updates for."
+ }
+ ],
+ "returns": {
+ "type": "object",
+ "description": "An object describing the view update."
+ }
+ }
+ ],
+ "functions": [
+ {
+ "name": "addDynamicResultType",
+ "type": "function",
+ "async": true,
+ "description": "Adds a dynamic result type. See UrlbarResult.addDynamicResultType().",
+ "parameters": [
+ {
+ "name": "name",
+ "type": "string",
+ "description": "The name of the result type."
+ },
+ {
+ "name": "type",
+ "type": "object",
+ "default": {},
+ "optional": true,
+ "description": "The result type. Currently this should be an empty object (which is the default value)."
+ }
+ ]
+ },
+ {
+ "name": "addDynamicViewTemplate",
+ "type": "function",
+ "async": true,
+ "description": "Adds a view template for a dynamic result type. See UrlbarView.addDynamicViewTemplate().",
+ "parameters": [
+ {
+ "name": "name",
+ "type": "string",
+ "description": "The view template will be registered for the dynamic result type with this name."
+ },
+ {
+ "name": "viewTemplate",
+ "type": "object",
+ "additionalProperties": true,
+ "description": "The view template."
+ }
+ ]
+ },
+ {
+ "name": "clearInput",
+ "type": "function",
+ "async": true,
+ "description": "Sets urlbar.value to the empty string and the pageproxystate to invalid.",
+ "parameters": []
+ }
+ ]
+ }
+]
diff --git a/browser/components/urlbar/tests/unit/data/engine-suggestions.xml b/browser/components/urlbar/tests/unit/data/engine-suggestions.xml
new file mode 100644
index 0000000000..2f7a6f7b09
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/data/engine-suggestions.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>engine-suggestions.xml</ShortName>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="http://localhost:9000/suggest?{searchTerms}"/>
+<Url type="text/html"
+ method="GET"
+ template="http://localhost:9000/search"
+ rel="searchform">
+ <Param name="terms" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/browser/components/urlbar/tests/unit/data/engine-tail-suggestions.xml b/browser/components/urlbar/tests/unit/data/engine-tail-suggestions.xml
new file mode 100644
index 0000000000..65f208884d
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/data/engine-tail-suggestions.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>engine-tail-suggestions.xml</ShortName>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="http://localhost:9001/suggest?{searchTerms}"/>
+<Url type="text/html"
+ method="GET"
+ template="http://localhost:9001/search"
+ rel="searchform"/>
+</SearchPlugin>
diff --git a/browser/components/urlbar/tests/unit/head.js b/browser/components/urlbar/tests/unit/head.js
new file mode 100644
index 0000000000..04c5e42eb9
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/head.js
@@ -0,0 +1,946 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+var {
+ UrlbarMuxer,
+ UrlbarProvider,
+ UrlbarQueryContext,
+ UrlbarUtils,
+} = ChromeUtils.import("resource:///modules/UrlbarUtils.jsm");
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AddonTestUtils: "resource://testing-common/AddonTestUtils.jsm",
+ AppConstants: "resource://gre/modules/AppConstants.jsm",
+ HttpServer: "resource://testing-common/httpd.js",
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.jsm",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
+ PromiseUtils: "resource://gre/modules/PromiseUtils.jsm",
+ Services: "resource://gre/modules/Services.jsm",
+ TestUtils: "resource://testing-common/TestUtils.jsm",
+ UrlbarController: "resource:///modules/UrlbarController.jsm",
+ UrlbarInput: "resource:///modules/UrlbarInput.jsm",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
+ UrlbarProviderOpenTabs: "resource:///modules/UrlbarProviderOpenTabs.jsm",
+ UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm",
+ UrlbarResult: "resource:///modules/UrlbarResult.jsm",
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.jsm",
+ UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.jsm",
+});
+const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
+
+AddonTestUtils.init(this, false);
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "42",
+ "42"
+);
+
+add_task(async function initXPCShellDependencies() {
+ await UrlbarTestUtils.initXPCShellDependencies();
+});
+
+/**
+ * Gets the database connection. If the Places connection is invalid it will
+ * try to create a new connection.
+ *
+ * @param [optional] aForceNewConnection
+ * Forces creation of a new connection to the database. When a
+ * connection is asyncClosed it cannot anymore schedule async statements,
+ * though connectionReady will keep returning true (Bug 726990).
+ *
+ * @return The database connection or null if unable to get one.
+ */
+var gDBConn;
+function DBConn(aForceNewConnection) {
+ if (!aForceNewConnection) {
+ let db = PlacesUtils.history.DBConnection;
+ if (db.connectionReady) {
+ return db;
+ }
+ }
+
+ // If the Places database connection has been closed, create a new connection.
+ if (!gDBConn || aForceNewConnection) {
+ let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ file.append("places.sqlite");
+ let dbConn = (gDBConn = Services.storage.openDatabase(file));
+
+ TestUtils.topicObserved("profile-before-change").then(() =>
+ dbConn.asyncClose()
+ );
+ }
+
+ return gDBConn.connectionReady ? gDBConn : null;
+}
+
+/**
+ * @param {string} searchString The search string to insert into the context.
+ * @param {object} properties Overrides for the default values.
+ * @returns {UrlbarQueryContext} Creates a dummy query context with pre-filled
+ * required options.
+ */
+function createContext(searchString = "foo", properties = {}) {
+ info(`Creating new queryContext with searchString: ${searchString}`);
+ let context = new UrlbarQueryContext(
+ Object.assign(
+ {
+ allowAutofill: UrlbarPrefs.get("autoFill"),
+ isPrivate: true,
+ maxResults: UrlbarPrefs.get("maxRichResults"),
+ searchString,
+ },
+ properties
+ )
+ );
+ UrlbarTokenizer.tokenize(context);
+ return context;
+}
+
+/**
+ * Waits for the given notification from the supplied controller.
+ *
+ * @param {UrlbarController} controller The controller to wait for a response from.
+ * @param {string} notification The name of the notification to wait for.
+ * @param {boolean} expected Wether the notification is expected.
+ * @returns {Promise} A promise that is resolved with the arguments supplied to
+ * the notification.
+ */
+function promiseControllerNotification(
+ controller,
+ notification,
+ expected = true
+) {
+ return new Promise((resolve, reject) => {
+ let proxifiedObserver = new Proxy(
+ {},
+ {
+ get: (target, name) => {
+ if (name == notification) {
+ return (...args) => {
+ controller.removeQueryListener(proxifiedObserver);
+ if (expected) {
+ resolve(args);
+ } else {
+ reject();
+ }
+ };
+ }
+ return () => false;
+ },
+ }
+ );
+ controller.addQueryListener(proxifiedObserver);
+ });
+}
+
+/**
+ * A basic test provider, returning all the provided matches.
+ */
+class TestProvider extends UrlbarTestUtils.TestProvider {
+ isActive(context) {
+ Assert.ok(context, "context is passed-in");
+ return true;
+ }
+ getPriority(context) {
+ Assert.ok(context, "context is passed-in");
+ return 0;
+ }
+ async startQuery(context, add) {
+ Assert.ok(context, "context is passed-in");
+ Assert.equal(typeof add, "function", "add is a callback");
+ this._context = context;
+ for (const result of this._results) {
+ add(this, result);
+ }
+ }
+ cancelQuery(context) {
+ // If the query was created but didn't run, this_context will be undefined.
+ if (this._context) {
+ Assert.equal(this._context, context, "cancelQuery: context is the same");
+ }
+ if (this._onCancel) {
+ this._onCancel();
+ }
+ }
+}
+
+function convertToUtf8(str) {
+ return String.fromCharCode(...new TextEncoder().encode(str));
+}
+
+/**
+ * Helper function to clear the existing providers and register a basic provider
+ * that returns only the results given.
+ *
+ * @param {array} results The results for the provider to return.
+ * @param {function} [onCancel] Optional, called when the query provider
+ * receives a cancel instruction.
+ * @param {UrlbarUtils.PROVIDER_TYPE} type The provider type.
+ * @returns {string} name of the registered provider
+ */
+function registerBasicTestProvider(results = [], onCancel, type) {
+ let provider = new TestProvider({ results, onCancel, type });
+ UrlbarProvidersManager.registerProvider(provider);
+ return provider.name;
+}
+
+// Creates an HTTP server for the test.
+function makeTestServer(port = -1) {
+ let httpServer = new HttpServer();
+ httpServer.start(port);
+ registerCleanupFunction(() => httpServer.stop(() => {}));
+ return httpServer;
+}
+
+/**
+ * Adds a search engine to the Search Service.
+ *
+ * @param {string} basename
+ * Basename for the engine.
+ * @param {object} httpServer [optional] HTTP Server to use.
+ * @returns {Promise} Resolved once the addition is complete.
+ */
+async function addTestEngine(basename, httpServer = undefined) {
+ httpServer = httpServer || makeTestServer();
+ httpServer.registerDirectory("/", do_get_cwd());
+ let dataUrl =
+ "http://localhost:" + httpServer.identity.primaryPort + "/data/";
+
+ // Before initializing the search service, set the geo IP url pref to a dummy
+ // string. When the search service is initialized, it contacts the URI named
+ // in this pref, causing unnecessary error logs.
+ let geoPref = "browser.search.geoip.url";
+ Services.prefs.setCharPref(geoPref, "");
+ registerCleanupFunction(() => Services.prefs.clearUserPref(geoPref));
+
+ info("Adding engine: " + basename);
+ return new Promise(resolve => {
+ Services.obs.addObserver(function obs(subject, topic, data) {
+ let engine = subject.QueryInterface(Ci.nsISearchEngine);
+ info("Observed " + data + " for " + engine.name);
+ if (data != "engine-added" || engine.name != basename) {
+ return;
+ }
+
+ Services.obs.removeObserver(obs, "browser-search-engine-modified");
+ registerCleanupFunction(() => Services.search.removeEngine(engine));
+ resolve(engine);
+ }, "browser-search-engine-modified");
+
+ info("Adding engine from URL: " + dataUrl + basename);
+ Services.search.addOpenSearchEngine(dataUrl + basename, null);
+ });
+}
+
+/**
+ * Sets up a search engine that provides some suggestions by appending strings
+ * onto the search query.
+ *
+ * @param {function} suggestionsFn
+ * A function that returns an array of suggestion strings given a
+ * search string. If not given, a default function is used.
+ * @returns {nsISearchEngine} The new engine.
+ */
+async function addTestSuggestionsEngine(suggestionsFn = null) {
+ // This port number should match the number in engine-suggestions.xml.
+ let server = makeTestServer(9000);
+ server.registerPathHandler("/suggest", (req, resp) => {
+ // URL query params are x-www-form-urlencoded, which converts spaces into
+ // plus signs, so un-convert any plus signs back to spaces.
+ let searchStr = decodeURIComponent(req.queryString.replace(/\+/g, " "));
+ let suggestions = suggestionsFn
+ ? suggestionsFn(searchStr)
+ : [searchStr].concat(["foo", "bar"].map(s => searchStr + " " + s));
+ let data = [searchStr, suggestions];
+ resp.setHeader("Content-Type", "application/json", false);
+ resp.write(JSON.stringify(data));
+ });
+ let engine = await addTestEngine("engine-suggestions.xml", server);
+ return engine;
+}
+
+/**
+ * Sets up a search engine that provides some tail suggestions by creating an
+ * array that mimics Google's tail suggestion responses.
+ *
+ * @param {function} suggestionsFn
+ * A function that returns an array that mimics Google's tail suggestion
+ * responses. See bug 1626897.
+ * NOTE: Consumers specifying suggestionsFn must include searchStr as a
+ * part of the array returned by suggestionsFn.
+ * @returns {nsISearchEngine} The new engine.
+ */
+async function addTestTailSuggestionsEngine(suggestionsFn = null) {
+ // This port number should match the number in engine-tail-suggestions.xml.
+ let server = makeTestServer(9001);
+ server.registerPathHandler("/suggest", (req, resp) => {
+ // URL query params are x-www-form-urlencoded, which converts spaces into
+ // plus signs, so un-convert any plus signs back to spaces.
+ let searchStr = decodeURIComponent(req.queryString.replace(/\+/g, " "));
+ let suggestions = suggestionsFn
+ ? suggestionsFn(searchStr)
+ : [
+ "what time is it in t",
+ ["what is the time today texas"].concat(
+ ["toronto", "tunisia"].map(s => searchStr + s.slice(1))
+ ),
+ [],
+ {
+ "google:irrelevantparameter": [],
+ "google:suggestdetail": [{}].concat(
+ ["toronto", "tunisia"].map(s => ({
+ mp: "… ",
+ t: s,
+ }))
+ ),
+ },
+ ];
+ let data = suggestions;
+ let jsonString = JSON.stringify(data);
+ // This script must be evaluated as UTF-8 for this to write out the bytes of
+ // the string in UTF-8. If it's evaluated as Latin-1, the written bytes
+ // will be the result of UTF-8-encoding the result-string *twice*, which
+ // will break the "… " match prefixes.
+ let stringOfUtf8Bytes = convertToUtf8(jsonString);
+ resp.setHeader("Content-Type", "application/json", false);
+ resp.write(stringOfUtf8Bytes);
+ });
+ let engine = await addTestEngine("engine-tail-suggestions.xml", server);
+ return engine;
+}
+
+/**
+ * Helper for tests that generate search results but aren't interested in
+ * suggestions, such as autofill tests. Installs a test engine and disables
+ * suggestions.
+ */
+function testEngine_setup() {
+ add_task(async function setup() {
+ await cleanupPlaces();
+ let engine = await addTestSuggestionsEngine();
+ let oldDefaultEngine = await Services.search.getDefault();
+
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ Services.prefs.clearUserPref(
+ "browser.search.separatePrivateDefault.ui.enabled"
+ );
+ Services.search.setDefault(oldDefaultEngine);
+ });
+
+ Services.search.setDefault(engine);
+ Services.prefs.setBoolPref(
+ "browser.search.separatePrivateDefault.ui.enabled",
+ false
+ );
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ });
+}
+
+async function cleanupPlaces() {
+ Services.prefs.clearUserPref("browser.urlbar.autoFill");
+ Services.prefs.clearUserPref("browser.urlbar.autoFill.searchEngines");
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+}
+
+/**
+ * Returns the frecency of a url.
+ *
+ * @param {string} aURI The URI or spec to get frecency for.
+ * @returns {number} the frecency value.
+ */
+function frecencyForUrl(aURI) {
+ let url = aURI;
+ if (aURI instanceof Ci.nsIURI) {
+ url = aURI.spec;
+ } else if (aURI instanceof URL) {
+ url = aURI.href;
+ }
+ let stmt = DBConn().createStatement(
+ "SELECT frecency FROM moz_places WHERE url_hash = hash(?1) AND url = ?1"
+ );
+ stmt.bindByIndex(0, url);
+ try {
+ if (!stmt.executeStep()) {
+ throw new Error("No result for frecency.");
+ }
+ return stmt.getInt32(0);
+ } finally {
+ stmt.finalize();
+ }
+}
+
+/**
+ * Creates a UrlbarResult for a bookmark result.
+ * @param {UrlbarQueryContext} queryContext
+ * The context that this result will be displayed in.
+ * @param {string} options.title
+ * The page title.
+ * @param {string} options.uri
+ * The page URI.
+ * @param {string} [options.iconUri]
+ * A URI for the page's icon.
+ * @param {array} [options.tags]
+ * An array of string tags. Defaults to an empty array.
+ * @param {boolean} [options.heuristic]
+ * True if this is a heuristic result. Defaults to false.
+ * @returns {UrlbarResult}
+ */
+function makeBookmarkResult(
+ queryContext,
+ {
+ title,
+ uri,
+ iconUri,
+ tags = [],
+ heuristic = false,
+ source = UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ }
+) {
+ let result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ source,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ url: [uri, UrlbarUtils.HIGHLIGHT.TYPED],
+ // Check against undefined so consumers can pass in the empty string.
+ icon: [typeof iconUri != "undefined" ? iconUri : `page-icon:${uri}`],
+ title: [title, UrlbarUtils.HIGHLIGHT.TYPED],
+ tags: [tags, UrlbarUtils.HIGHLIGHT.TYPED],
+ })
+ );
+
+ result.heuristic = heuristic;
+ return result;
+}
+
+/**
+ * Creates a UrlbarResult for a form history result.
+ * @param {UrlbarQueryContext} queryContext
+ * The context that this result will be displayed in.
+ * @param {string} options.suggestion
+ * The form history suggestion.
+ * @param {string} options.engineName
+ * The name of the engine that will do the search when the result is picked.
+ * @returns {UrlbarResult}
+ */
+function makeFormHistoryResult(queryContext, { suggestion, engineName }) {
+ return new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ engine: engineName,
+ suggestion: [suggestion, UrlbarUtils.HIGHLIGHT.SUGGESTED],
+ lowerCaseSuggestion: suggestion.toLocaleLowerCase(),
+ })
+ );
+}
+
+/**
+ * Creates a UrlbarResult for an omnibox extension result. For more information,
+ * see the documentation for omnibox.SuggestResult:
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/omnibox/SuggestResult
+ * @param {UrlbarQueryContext} queryContext
+ * The context that this result will be displayed in.
+ * @param {string} options.content
+ * The string displayed when the result is highlighted.
+ * @param {string} options.description
+ * The string displayed in the address bar dropdown.
+ * @param {string} options.keyword
+ * The keyword associated with the extension returning the result.
+ * @param {boolean} [options.heuristic]
+ * True if this is a heuristic result. Defaults to false.
+ * @returns {UrlbarResult}
+ */
+function makeOmniboxResult(
+ queryContext,
+ { content, description, keyword, heuristic = false }
+) {
+ let result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.OMNIBOX,
+ UrlbarUtils.RESULT_SOURCE.OTHER_NETWORK,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ title: [description, UrlbarUtils.HIGHLIGHT.TYPED],
+ content: [content, UrlbarUtils.HIGHLIGHT.TYPED],
+ keyword: [keyword, UrlbarUtils.HIGHLIGHT.TYPED],
+ icon: [UrlbarUtils.ICON.EXTENSION],
+ })
+ );
+ result.heuristic = heuristic;
+ return result;
+}
+
+/**
+ * Creates a UrlbarResult for a keyword search result.
+ * @param {UrlbarQueryContext} queryContext
+ * The context that this result will be displayed in.
+ * @param {string} options.uri
+ * The page URI.
+ * @param {string} options.keyword
+ * The page's search keyword.
+ * @param {string} [options.title]
+ * The title for the bookmarked keyword page.
+ * @param {string} [options.iconUri]
+ * A URI for the engine's icon.
+ * @param {string} [options.postData]
+ * The search POST data.
+ * @param {boolean} [options.heuristic]
+ * True if this is a heuristic result. Defaults to false.
+ * @returns {UrlbarResult}
+ */
+function makeKeywordSearchResult(
+ queryContext,
+ { uri, keyword, title, iconUri, postData, heuristic = false }
+) {
+ let result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.KEYWORD,
+ UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ title: [title ? title : uri, UrlbarUtils.HIGHLIGHT.TYPED],
+ url: [uri, UrlbarUtils.HIGHLIGHT.TYPED],
+ keyword: [keyword, UrlbarUtils.HIGHLIGHT.TYPED],
+ input: [queryContext.searchString, UrlbarUtils.HIGHLIGHT.TYPED],
+ postData,
+ icon: typeof iconUri != "undefined" ? iconUri : `page-icon:${uri}`,
+ })
+ );
+
+ if (heuristic) {
+ result.heuristic = heuristic;
+ }
+ return result;
+}
+
+/**
+ * Creates a UrlbarResult for a priority search result.
+ * @param {UrlbarQueryContext} queryContext
+ * The context that this result will be displayed in.
+ * @param {string} [options.engineName]
+ * The name of the engine providing the suggestion. Leave blank if there
+ * is no suggestion.
+ * @param {string} [options.engineIconUri]
+ * A URI for the engine's icon.
+ * @param {boolean} [options.heuristic]
+ * True if this is a heuristic result. Defaults to false.
+ * @returns {UrlbarResult}
+ */
+function makePrioritySearchResult(
+ queryContext,
+ { engineName, engineIconUri, heuristic }
+) {
+ let result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ engine: [engineName, UrlbarUtils.HIGHLIGHT.TYPED],
+ icon: engineIconUri,
+ })
+ );
+
+ if (heuristic) {
+ result.heuristic = heuristic;
+ }
+ return result;
+}
+
+/**
+ * Creates a UrlbarResult for a search result.
+ * @param {UrlbarQueryContext} queryContext
+ * The context that this result will be displayed in.
+ * @param {string} [options.suggestion]
+ * The suggestion offered by the search engine.
+ * @param {string} [options.engineName]
+ * The name of the engine providing the suggestion. Leave blank if there
+ * is no suggestion.
+ * @param {string} [options.uri]
+ * The URI that the search result will navigate to.
+ * @param {string} [options.query]
+ * The query that started the search. This overrides
+ * `queryContext.searchString`. This is useful when the query that will show
+ * up in the result object will be different from what was typed. For example,
+ * if a leading restriction token will be used.
+ * @param {string} [options.alias]
+ * The alias for the search engine, if the search is an alias search.
+ * @param {string} [options.engineIconUri]
+ * A URI for the engine's icon.
+ * @param {boolean} [options.heuristic]
+ * True if this is a heuristic result. Defaults to false.
+ * @param {boolean} [options.providesSearchMode]
+ * Whether search mode is entered when this result is selected.
+ * @param {string} [options.providerName]
+ * The name of the provider offering this result. The test suite will not
+ * check which provider offered a result unless this option is specified.
+ * @returns {UrlbarResult}
+ */
+function makeSearchResult(
+ queryContext,
+ {
+ suggestion,
+ tailPrefix,
+ tail,
+ tailOffsetIndex,
+ engineName,
+ alias,
+ uri,
+ query,
+ engineIconUri,
+ providesSearchMode,
+ providerName,
+ inPrivateWindow,
+ isPrivateEngine,
+ heuristic = false,
+ type = UrlbarUtils.RESULT_TYPE.SEARCH,
+ source = UrlbarUtils.RESULT_SOURCE.SEARCH,
+ satisfiesAutofillThreshold = false,
+ }
+) {
+ // Tail suggestion common cases, handled here to reduce verbosity in tests.
+ if (tail) {
+ if (!tailPrefix) {
+ tailPrefix = "… ";
+ }
+ if (!tailOffsetIndex) {
+ tailOffsetIndex = suggestion.indexOf(tail);
+ }
+ }
+
+ let payload = {
+ engine: [engineName, UrlbarUtils.HIGHLIGHT.TYPED],
+ suggestion: [suggestion, UrlbarUtils.HIGHLIGHT.SUGGESTED],
+ tailPrefix,
+ tail: [tail, UrlbarUtils.HIGHLIGHT.SUGGESTED],
+ tailOffsetIndex,
+ keyword: [
+ alias,
+ providesSearchMode
+ ? UrlbarUtils.HIGHLIGHT.TYPED
+ : UrlbarUtils.HIGHLIGHT.NONE,
+ ],
+ // Check against undefined so consumers can pass in the empty string.
+ query: [
+ typeof query != "undefined" ? query : queryContext.trimmedSearchString,
+ UrlbarUtils.HIGHLIGHT.TYPED,
+ ],
+ icon: engineIconUri,
+ providesSearchMode,
+ inPrivateWindow,
+ isPrivateEngine,
+ };
+
+ // Passing even an undefined URL in the payload creates a potentially-unwanted
+ // displayUrl parameter, so we add it only if specified.
+ if (uri) {
+ payload.url = uri;
+ }
+ if (providerName == "TabToSearch") {
+ payload.satisfiesAutofillThreshold = satisfiesAutofillThreshold;
+ if (payload.url.startsWith("www.")) {
+ payload.url = payload.url.substring(4);
+ }
+ }
+
+ let result = new UrlbarResult(
+ type,
+ source,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, payload)
+ );
+
+ if (typeof suggestion == "string") {
+ result.payload.lowerCaseSuggestion = result.payload.suggestion.toLocaleLowerCase();
+ }
+
+ if (providerName) {
+ result.providerName = providerName;
+ }
+
+ result.heuristic = heuristic;
+ return result;
+}
+
+/**
+ * Creates a UrlbarResult for a history result.
+ * @param {UrlbarQueryContext} queryContext
+ * The context that this result will be displayed in.
+ * @param {string} options.title
+ * The page title.
+ * @param {string} options.uri
+ * The page URI.
+ * @param {array} [options.tags]
+ * An array of string tags. Defaults to an empty array.
+ * @param {string} [options.iconUri]
+ * A URI for the page's icon.
+ * @param {boolean} [options.heuristic]
+ * True if this is a heuristic result. Defaults to false.
+ * * @param {string} providerName
+ * The name of the provider offering this result. The test suite will not
+ * check which provider offered a result unless this option is specified.
+ * @returns {UrlbarResult}
+ */
+function makeVisitResult(
+ queryContext,
+ {
+ title,
+ uri,
+ iconUri,
+ providerName,
+ tags = null,
+ heuristic = false,
+ source = UrlbarUtils.RESULT_SOURCE.HISTORY,
+ }
+) {
+ let payload = {
+ url: [uri, UrlbarUtils.HIGHLIGHT.TYPED],
+ title: [title, UrlbarUtils.HIGHLIGHT.TYPED],
+ };
+
+ if (iconUri) {
+ payload.icon = iconUri;
+ } else if (
+ iconUri === undefined &&
+ source != UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL
+ ) {
+ payload.icon = `page-icon:${uri}`;
+ }
+
+ if (!heuristic || tags) {
+ payload.tags = [tags || [], UrlbarUtils.HIGHLIGHT.TYPED];
+ }
+
+ let result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ source,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, payload)
+ );
+
+ if (providerName) {
+ result.providerName = providerName;
+ }
+
+ result.heuristic = heuristic;
+ return result;
+}
+
+/**
+ * Checks that the results returned by a UrlbarController match those in
+ * the param `matches`.
+ * @param {UrlbarQueryContext} context
+ * The context for this query.
+ * @param {string} [incompleteSearch]
+ * A search will be fired for this string and then be immediately canceled by
+ * the query in `context`.
+ * @param {string} [autofilled]
+ * The autofilled value in the first result.
+ * @param {string} [completed]
+ * The value that would be filled if the autofill result was confirmed.
+ * Has no effect if `autofilled` is not specified.
+ * @param {array} matches
+ * An array of UrlbarResults.
+ * @param {boolean} [isPrivate]
+ * Set this to `true` to simulate a search in a private window.
+ */
+async function check_results({
+ context,
+ incompleteSearch,
+ autofilled,
+ completed,
+ matches = [],
+} = {}) {
+ if (!context) {
+ return;
+ }
+
+ // At this point frecency could still be updating due to latest pages
+ // updates.
+ // This is not a problem in real life, but autocomplete tests should
+ // return reliable resultsets, thus we have to wait.
+ await PlacesTestUtils.promiseAsyncUpdates();
+
+ let controller = UrlbarTestUtils.newMockController({
+ input: {
+ isPrivate: context.isPrivate,
+ onFirstResult() {
+ return false;
+ },
+ window: {
+ location: {
+ href: AppConstants.BROWSER_CHROME_URL,
+ },
+ },
+ },
+ });
+
+ if (incompleteSearch) {
+ let incompleteContext = createContext(incompleteSearch, {
+ isPrivate: context.isPrivate,
+ });
+ controller.startQuery(incompleteContext);
+ }
+ await controller.startQuery(context);
+
+ if (autofilled) {
+ Assert.ok(context.results[0], "There is a first result.");
+ Assert.ok(
+ context.results[0].autofill,
+ "The first result is an autofill result"
+ );
+ Assert.equal(
+ context.results[0].autofill.value,
+ autofilled,
+ "The correct value was autofilled."
+ );
+ if (completed) {
+ Assert.equal(
+ context.results[0].payload.url,
+ completed,
+ "The completed autofill value is correct."
+ );
+ }
+ }
+ if (context.results.length != matches.length) {
+ info("Actual results: " + JSON.stringify(context.results));
+ }
+ Assert.equal(
+ context.results.length,
+ matches.length,
+ "Found the expected number of results."
+ );
+
+ function getPayload(result) {
+ let payload = {};
+ for (let [key, value] of Object.entries(result.payload)) {
+ if (value !== undefined) {
+ payload[key] = value;
+ }
+ }
+ return payload;
+ }
+
+ for (let i = 0; i < matches.length; i++) {
+ let actual = context.results[i];
+ let expected = matches[i];
+ info(
+ `Comparing results at index ${i}:` +
+ " actual=" +
+ JSON.stringify(actual) +
+ " expected=" +
+ JSON.stringify(expected)
+ );
+ Assert.equal(
+ actual.type,
+ expected.type,
+ `result.type at result index ${i}`
+ );
+ Assert.equal(
+ actual.source,
+ expected.source,
+ `result.source at result index ${i}`
+ );
+ Assert.equal(
+ actual.heuristic,
+ expected.heuristic,
+ `result.heuristic at result index ${i}`
+ );
+ if (expected.providerName) {
+ Assert.equal(
+ actual.providerName,
+ expected.providerName,
+ `result.providerName at result index ${i}`
+ );
+ }
+ Assert.deepEqual(
+ getPayload(actual),
+ getPayload(expected),
+ `result.payload at result index ${i}`
+ );
+ }
+}
+
+/**
+ * Returns the frecency of an origin.
+ *
+ * @param {string} prefix
+ * The origin's prefix, e.g., "http://".
+ * @param {string} aHost
+ * The origin's host.
+ * @returns {number} The origin's frecency.
+ */
+async function getOriginFrecency(prefix, aHost) {
+ let db = await PlacesUtils.promiseDBConnection();
+ let rows = await db.execute(
+ `
+ SELECT frecency
+ FROM moz_origins
+ WHERE prefix = :prefix AND host = :host
+ `,
+ { prefix, host: aHost }
+ );
+ Assert.equal(rows.length, 1);
+ return rows[0].getResultByIndex(0);
+}
+
+/**
+ * Returns the origin frecency stats.
+ *
+ * @returns {object}
+ * An object { count, sum, squares }.
+ */
+async function getOriginFrecencyStats() {
+ let db = await PlacesUtils.promiseDBConnection();
+ let rows = await db.execute(`
+ SELECT
+ IFNULL((SELECT value FROM moz_meta WHERE key = 'origin_frecency_count'), 0),
+ IFNULL((SELECT value FROM moz_meta WHERE key = 'origin_frecency_sum'), 0),
+ IFNULL((SELECT value FROM moz_meta WHERE key = 'origin_frecency_sum_of_squares'), 0)
+ `);
+ let count = rows[0].getResultByIndex(0);
+ let sum = rows[0].getResultByIndex(1);
+ let squares = rows[0].getResultByIndex(2);
+ return { count, sum, squares };
+}
+
+/**
+ * Returns the origin autofill frecency threshold.
+ *
+ * @returns {number}
+ * The threshold.
+ */
+async function getOriginAutofillThreshold() {
+ let { count, sum, squares } = await getOriginFrecencyStats();
+ if (!count) {
+ return 0;
+ }
+ if (count == 1) {
+ return sum;
+ }
+ let stddevMultiplier = UrlbarPrefs.get("autoFill.stddevMultiplier");
+ return (
+ sum / count +
+ stddevMultiplier * Math.sqrt((squares - (sum * sum) / count) / count)
+ );
+}
+
+/**
+ * Checks that origins appear in a given order in the database.
+ * @param {string} host The "fixed" host, without "www."
+ * @param {Array} prefixOrder The prefixes (scheme + www.) sorted appropriately.
+ */
+async function checkOriginsOrder(host, prefixOrder) {
+ await PlacesUtils.withConnectionWrapper("checkOriginsOrder", async db => {
+ let prefixes = (
+ await db.execute(
+ `SELECT prefix || iif(instr(host, "www.") = 1, "www.", "")
+ FROM moz_origins
+ WHERE host = :host OR host = "www." || :host
+ ORDER BY ROWID ASC
+ `,
+ { host }
+ )
+ ).map(r => r.getResultByIndex(0));
+ Assert.deepEqual(prefixes, prefixOrder);
+ });
+}
diff --git a/browser/components/urlbar/tests/unit/test_UrlbarController_integration.js b/browser/components/urlbar/tests/unit/test_UrlbarController_integration.js
new file mode 100644
index 0000000000..6da8255b5a
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_UrlbarController_integration.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * These tests test the UrlbarController in association with the model.
+ */
+
+"use strict";
+
+const { PromiseUtils } = ChromeUtils.import(
+ "resource://gre/modules/PromiseUtils.jsm"
+);
+
+const TEST_URL = "http://example.com";
+const match = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: TEST_URL }
+);
+let controller;
+
+/**
+ * Asserts that the query context has the expected values.
+ *
+ * @param {UrlbarQueryContext} context
+ * @param {object} expectedValues The expected values for the UrlbarQueryContext.
+ */
+function assertContextMatches(context, expectedValues) {
+ Assert.ok(
+ context instanceof UrlbarQueryContext,
+ "Should be a UrlbarQueryContext"
+ );
+
+ for (let [key, value] of Object.entries(expectedValues)) {
+ Assert.equal(
+ context[key],
+ value,
+ `Should have the expected value for ${key} in the UrlbarQueryContext`
+ );
+ }
+}
+
+add_task(async function setup() {
+ controller = UrlbarTestUtils.newMockController();
+});
+
+add_task(async function test_basic_search() {
+ let providerName = registerBasicTestProvider([match]);
+ const context = createContext(TEST_URL, { providers: [providerName] });
+
+ let startedPromise = promiseControllerNotification(
+ controller,
+ "onQueryStarted"
+ );
+ let resultsPromise = promiseControllerNotification(
+ controller,
+ "onQueryResults"
+ );
+
+ controller.startQuery(context);
+
+ let params = await startedPromise;
+
+ Assert.equal(params[0], context);
+
+ params = await resultsPromise;
+
+ Assert.deepEqual(
+ params[0].results,
+ [match],
+ "Should have the expected match"
+ );
+});
+
+add_task(async function test_cancel_search() {
+ let providerCanceledDeferred = PromiseUtils.defer();
+ let providerName = registerBasicTestProvider(
+ [match],
+ providerCanceledDeferred.resolve
+ );
+ const context = createContext(TEST_URL, { providers: [providerName] });
+
+ let startedPromise = promiseControllerNotification(
+ controller,
+ "onQueryStarted"
+ );
+ let cancelPromise = promiseControllerNotification(
+ controller,
+ "onQueryCancelled"
+ );
+
+ controller.startQuery(context);
+
+ let params = await startedPromise;
+
+ controller.cancelQuery(context);
+
+ Assert.equal(params[0], context);
+
+ info("Should tell the provider the query is canceled");
+ await providerCanceledDeferred.promise;
+
+ params = await cancelPromise;
+});
diff --git a/browser/components/urlbar/tests/unit/test_UrlbarController_telemetry.js b/browser/components/urlbar/tests/unit/test_UrlbarController_telemetry.js
new file mode 100644
index 0000000000..d103eb10f5
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_UrlbarController_telemetry.js
@@ -0,0 +1,256 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * These tests unit test the functionality of UrlbarController by stubbing out the
+ * model and providing stubs to be called.
+ */
+
+"use strict";
+
+const TEST_URL = "http://example.com";
+const MATCH = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: TEST_URL }
+);
+const TELEMETRY_1ST_RESULT = "PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS";
+const TELEMETRY_6_FIRST_RESULTS = "PLACES_AUTOCOMPLETE_6_FIRST_RESULTS_TIME_MS";
+
+let controller;
+let firstHistogram;
+let sixthHistogram;
+
+/**
+ * A delayed test provider, allowing the query to be delayed for an amount of time.
+ */
+class DelayedProvider extends TestProvider {
+ async startQuery(context, add) {
+ Assert.ok(context, "context is passed-in");
+ Assert.equal(typeof add, "function", "add is a callback");
+ this._add = add;
+ await new Promise(resolve => {
+ this._resultsAdded = resolve;
+ });
+ }
+ async addResults(matches, finish = true) {
+ // startQuery may have not been invoked yet, so wait for it
+ await TestUtils.waitForCondition(
+ () => !!this._add,
+ "Waiting for the _add callback"
+ );
+ for (const match of matches) {
+ this._add(this, match);
+ }
+ if (finish) {
+ this._add = null;
+ this._resultsAdded();
+ }
+ }
+}
+
+/**
+ * Returns the number of reports sent recorded within the histogram results.
+ *
+ * @param {object} results a snapshot of histogram results to check.
+ * @returns {number} The count of reports recorded in the histogram.
+ */
+function getHistogramReportsCount(results) {
+ let sum = 0;
+ for (let [, value] of Object.entries(results.values)) {
+ sum += value;
+ }
+ return sum;
+}
+
+add_task(function setup() {
+ controller = UrlbarTestUtils.newMockController();
+
+ firstHistogram = Services.telemetry.getHistogramById(TELEMETRY_1ST_RESULT);
+ sixthHistogram = Services.telemetry.getHistogramById(
+ TELEMETRY_6_FIRST_RESULTS
+ );
+});
+
+add_task(async function test_n_autocomplete_cancel() {
+ firstHistogram.clear();
+ sixthHistogram.clear();
+
+ let providerCanceledDeferred = PromiseUtils.defer();
+ let provider = new TestProvider({
+ results: [],
+ onCancel: providerCanceledDeferred.resolve,
+ });
+ UrlbarProvidersManager.registerProvider(provider);
+ const context = createContext(TEST_URL, { providers: [provider.name] });
+
+ Assert.ok(
+ !TelemetryStopwatch.running(TELEMETRY_1ST_RESULT, context),
+ "Should not have started first result stopwatch"
+ );
+ Assert.ok(
+ !TelemetryStopwatch.running(TELEMETRY_6_FIRST_RESULTS, context),
+ "Should not have started first 6 results stopwatch"
+ );
+
+ controller.startQuery(context);
+
+ Assert.ok(
+ TelemetryStopwatch.running(TELEMETRY_1ST_RESULT, context),
+ "Should have started first result stopwatch"
+ );
+ Assert.ok(
+ TelemetryStopwatch.running(TELEMETRY_6_FIRST_RESULTS, context),
+ "Should have started first 6 results stopwatch"
+ );
+
+ controller.cancelQuery(context);
+
+ await providerCanceledDeferred.promise;
+
+ Assert.ok(
+ !TelemetryStopwatch.running(TELEMETRY_1ST_RESULT, context),
+ "Should have canceled first result stopwatch"
+ );
+ Assert.ok(
+ !TelemetryStopwatch.running(TELEMETRY_6_FIRST_RESULTS, context),
+ "Should have canceled first 6 results stopwatch"
+ );
+
+ let results = firstHistogram.snapshot();
+ Assert.equal(
+ results.sum,
+ 0,
+ "Should not have recorded any times (first result)"
+ );
+ results = sixthHistogram.snapshot();
+ Assert.equal(
+ results.sum,
+ 0,
+ "Should not have recorded any times (first 6 results)"
+ );
+});
+
+add_task(async function test_n_autocomplete_results() {
+ firstHistogram.clear();
+ sixthHistogram.clear();
+
+ let provider = new DelayedProvider();
+ UrlbarProvidersManager.registerProvider(provider);
+ const context = createContext(TEST_URL, { providers: [provider.name] });
+
+ let resultsPromise = promiseControllerNotification(
+ controller,
+ "onQueryResults"
+ );
+
+ Assert.ok(
+ !TelemetryStopwatch.running(TELEMETRY_1ST_RESULT, context),
+ "Should not have started first result stopwatch"
+ );
+ Assert.ok(
+ !TelemetryStopwatch.running(TELEMETRY_6_FIRST_RESULTS, context),
+ "Should not have started first 6 results stopwatch"
+ );
+
+ controller.startQuery(context);
+
+ Assert.ok(
+ TelemetryStopwatch.running(TELEMETRY_1ST_RESULT, context),
+ "Should have started first result stopwatch"
+ );
+ Assert.ok(
+ TelemetryStopwatch.running(TELEMETRY_6_FIRST_RESULTS, context),
+ "Should have started first 6 results stopwatch"
+ );
+
+ await provider.addResults([MATCH], false);
+ await resultsPromise;
+
+ Assert.ok(
+ !TelemetryStopwatch.running(TELEMETRY_1ST_RESULT, context),
+ "Should have stopped the first stopwatch"
+ );
+ Assert.ok(
+ TelemetryStopwatch.running(TELEMETRY_6_FIRST_RESULTS, context),
+ "Should have kept the first 6 results stopwatch running"
+ );
+
+ let firstResults = firstHistogram.snapshot();
+ let first6Results = sixthHistogram.snapshot();
+ Assert.equal(
+ getHistogramReportsCount(firstResults),
+ 1,
+ "Should have recorded one time for the first result"
+ );
+ Assert.equal(
+ getHistogramReportsCount(first6Results),
+ 0,
+ "Should not have recorded any times (first 6 results)"
+ );
+
+ // Now add 5 more results, so that the first 6 results is triggered.
+ for (let i = 0; i < 5; i++) {
+ resultsPromise = promiseControllerNotification(
+ controller,
+ "onQueryResults"
+ );
+ await provider.addResults(
+ [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: TEST_URL + "/i" }
+ ),
+ ],
+ false
+ );
+ await resultsPromise;
+ }
+
+ Assert.ok(
+ !TelemetryStopwatch.running(TELEMETRY_1ST_RESULT, context),
+ "Should have stopped the first stopwatch"
+ );
+ Assert.ok(
+ !TelemetryStopwatch.running(TELEMETRY_6_FIRST_RESULTS, context),
+ "Should have stopped the first 6 results stopwatch"
+ );
+
+ let updatedResults = firstHistogram.snapshot();
+ let updated6Results = sixthHistogram.snapshot();
+ Assert.deepEqual(
+ updatedResults,
+ firstResults,
+ "Should not have changed the histogram for the first result"
+ );
+ Assert.equal(
+ getHistogramReportsCount(updated6Results),
+ 1,
+ "Should have recorded one time for the first 6 results"
+ );
+
+ // Add one more, to check neither are updated.
+ resultsPromise = promiseControllerNotification(controller, "onQueryResults");
+ await provider.addResults([
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: TEST_URL + "/6" }
+ ),
+ ]);
+ await resultsPromise;
+
+ let secondUpdateResults = firstHistogram.snapshot();
+ let secondUpdate6Results = sixthHistogram.snapshot();
+ Assert.deepEqual(
+ secondUpdateResults,
+ firstResults,
+ "Should not have changed the histogram for the first result"
+ );
+ Assert.equal(
+ getHistogramReportsCount(secondUpdate6Results),
+ 1,
+ "Should not have changed the histogram for the first 6 results"
+ );
+});
diff --git a/browser/components/urlbar/tests/unit/test_UrlbarController_unit.js b/browser/components/urlbar/tests/unit/test_UrlbarController_unit.js
new file mode 100644
index 0000000000..c62df478f1
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_UrlbarController_unit.js
@@ -0,0 +1,389 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * These tests unit test the functionality of UrlbarController by stubbing out the
+ * model and providing stubs to be called.
+ */
+
+"use strict";
+
+// A fake ProvidersManager.
+let fPM;
+let sandbox;
+let generalListener;
+let controller;
+
+/**
+ * Asserts that the query context has the expected values.
+ *
+ * @param {UrlbarQueryContext} context
+ * @param {object} expectedValues The expected values for the UrlbarQueryContext.
+ */
+function assertContextMatches(context, expectedValues) {
+ Assert.ok(
+ context instanceof UrlbarQueryContext,
+ "Should be a UrlbarQueryContext"
+ );
+
+ for (let [key, value] of Object.entries(expectedValues)) {
+ Assert.equal(
+ context[key],
+ value,
+ `Should have the expected value for ${key} in the UrlbarQueryContext`
+ );
+ }
+}
+
+add_task(function setup() {
+ sandbox = sinon.createSandbox();
+
+ fPM = {
+ startQuery: sandbox.stub(),
+ cancelQuery: sandbox.stub(),
+ };
+
+ generalListener = {
+ onQueryStarted: sandbox.stub(),
+ onQueryResults: sandbox.stub(),
+ onQueryCancelled: sandbox.stub(),
+ };
+
+ controller = UrlbarTestUtils.newMockController({
+ manager: fPM,
+ });
+ controller.addQueryListener(generalListener);
+});
+
+add_task(function test_constructor_throws() {
+ Assert.throws(
+ () => new UrlbarController(),
+ /Missing options: input/,
+ "Should throw if the input was not supplied"
+ );
+ Assert.throws(
+ () => new UrlbarController({ input: {} }),
+ /input is missing 'window' property/,
+ "Should throw if the input is not a UrlbarInput"
+ );
+ Assert.throws(
+ () => new UrlbarController({ input: { window: {} } }),
+ /input.window should be an actual browser window/,
+ "Should throw if the input.window is not a window"
+ );
+ Assert.throws(
+ () =>
+ new UrlbarController({
+ input: {
+ window: {
+ location: "about:fake",
+ },
+ },
+ }),
+ /input.window should be an actual browser window/,
+ "Should throw if the input.window is not an object"
+ );
+ Assert.throws(
+ () =>
+ new UrlbarController({
+ input: {
+ window: {
+ location: {
+ href: "about:fake",
+ },
+ },
+ },
+ }),
+ /input.window should be an actual browser window/,
+ "Should throw if the input.window does not have the correct location"
+ );
+ Assert.throws(
+ () =>
+ new UrlbarController({
+ input: {
+ window: {
+ location: {
+ href: AppConstants.BROWSER_CHROME_URL,
+ },
+ },
+ },
+ }),
+ /input.isPrivate must be set/,
+ "Should throw if input.isPrivate is not set"
+ );
+
+ new UrlbarController({
+ input: {
+ isPrivate: false,
+ window: {
+ location: {
+ href: AppConstants.BROWSER_CHROME_URL,
+ },
+ },
+ },
+ });
+ Assert.ok(true, "Correct call should not throw");
+});
+
+add_task(function test_add_and_remove_listeners() {
+ Assert.throws(
+ () => controller.addQueryListener(null),
+ /Expected listener to be an object/,
+ "Should throw for a null listener"
+ );
+ Assert.throws(
+ () => controller.addQueryListener(123),
+ /Expected listener to be an object/,
+ "Should throw for a non-object listener"
+ );
+
+ const listener = {};
+
+ controller.addQueryListener(listener);
+
+ Assert.ok(
+ controller._listeners.has(listener),
+ "Should have added the listener to the list."
+ );
+
+ // Adding a non-existent listener shouldn't throw.
+ controller.removeQueryListener(123);
+
+ controller.removeQueryListener(listener);
+
+ Assert.ok(
+ !controller._listeners.has(listener),
+ "Should have removed the listener from the list"
+ );
+
+ sandbox.resetHistory();
+});
+
+add_task(function test__notify() {
+ const listener1 = {
+ onFake: sandbox.stub().callsFake(() => {
+ throw new Error("fake error");
+ }),
+ };
+ const listener2 = {
+ onFake: sandbox.stub(),
+ };
+
+ controller.addQueryListener(listener1);
+ controller.addQueryListener(listener2);
+
+ const param = "1234";
+
+ controller.notify("onFake", param);
+
+ Assert.equal(
+ listener1.onFake.callCount,
+ 1,
+ "Should have called the first listener method."
+ );
+ Assert.deepEqual(
+ listener1.onFake.args[0],
+ [param],
+ "Should have called the first listener with the correct argument"
+ );
+ Assert.equal(
+ listener2.onFake.callCount,
+ 1,
+ "Should have called the second listener method."
+ );
+ Assert.deepEqual(
+ listener2.onFake.args[0],
+ [param],
+ "Should have called the first listener with the correct argument"
+ );
+
+ controller.removeQueryListener(listener2);
+ controller.removeQueryListener(listener1);
+
+ // This should succeed without errors.
+ controller.notify("onNewFake");
+
+ sandbox.resetHistory();
+});
+
+add_task(function test_handle_query_starts_search() {
+ const context = createContext();
+ controller.startQuery(context);
+
+ Assert.equal(
+ fPM.startQuery.callCount,
+ 1,
+ "Should have called startQuery once"
+ );
+ Assert.equal(
+ fPM.startQuery.args[0].length,
+ 2,
+ "Should have called startQuery with two arguments"
+ );
+
+ assertContextMatches(fPM.startQuery.args[0][0], {});
+ Assert.equal(
+ fPM.startQuery.args[0][1],
+ controller,
+ "Should have passed the controller as the second argument"
+ );
+
+ Assert.equal(
+ generalListener.onQueryStarted.callCount,
+ 1,
+ "Should have called onQueryStarted for the listener"
+ );
+ Assert.deepEqual(
+ generalListener.onQueryStarted.args[0],
+ [context],
+ "Should have called onQueryStarted with the context"
+ );
+
+ sandbox.resetHistory();
+});
+
+add_task(async function test_handle_query_starts_search_sets_allowAutofill() {
+ let originalValue = Services.prefs.getBoolPref("browser.urlbar.autoFill");
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", !originalValue);
+
+ await controller.startQuery(createContext());
+
+ Assert.equal(
+ fPM.startQuery.callCount,
+ 1,
+ "Should have called startQuery once"
+ );
+ Assert.equal(
+ fPM.startQuery.args[0].length,
+ 2,
+ "Should have called startQuery with two arguments"
+ );
+
+ assertContextMatches(fPM.startQuery.args[0][0], {
+ allowAutofill: !originalValue,
+ });
+ Assert.equal(
+ fPM.startQuery.args[0][1],
+ controller,
+ "Should have passed the controller as the second argument"
+ );
+
+ sandbox.resetHistory();
+
+ Services.prefs.clearUserPref("browser.urlbar.autoFill");
+});
+
+add_task(function test_cancel_query() {
+ const context = createContext();
+ controller.startQuery(context);
+
+ controller.cancelQuery();
+
+ Assert.equal(
+ fPM.cancelQuery.callCount,
+ 1,
+ "Should have called cancelQuery once"
+ );
+ Assert.equal(
+ fPM.cancelQuery.args[0].length,
+ 1,
+ "Should have called cancelQuery with one argument"
+ );
+
+ Assert.equal(
+ generalListener.onQueryCancelled.callCount,
+ 1,
+ "Should have called onQueryCancelled for the listener"
+ );
+ Assert.deepEqual(
+ generalListener.onQueryCancelled.args[0],
+ [context],
+ "Should have called onQueryCancelled with the context"
+ );
+
+ sandbox.resetHistory();
+});
+
+add_task(function test_receiveResults() {
+ const context = createContext();
+ context.results = [];
+ controller.receiveResults(context);
+
+ Assert.equal(
+ generalListener.onQueryResults.callCount,
+ 1,
+ "Should have called onQueryResults for the listener"
+ );
+ Assert.deepEqual(
+ generalListener.onQueryResults.args[0],
+ [context],
+ "Should have called onQueryResults with the context"
+ );
+
+ sandbox.resetHistory();
+});
+
+add_task(async function test_notifications_order() {
+ // Clear any pending notifications.
+ const context = createContext();
+ await controller.startQuery(context);
+
+ // Check that when multiple queries are executed, the notifications arrive
+ // in the proper order.
+ let collectingListener = new Proxy(
+ {},
+ {
+ _notifications: [],
+ get(target, name) {
+ if (name == "notifications") {
+ return this._notifications;
+ }
+ return () => {
+ this._notifications.push(name);
+ };
+ },
+ }
+ );
+ controller.addQueryListener(collectingListener);
+ controller.startQuery(context);
+ Assert.deepEqual(
+ ["onQueryStarted"],
+ collectingListener.notifications,
+ "Check onQueryStarted is fired synchronously"
+ );
+ controller.startQuery(context);
+ Assert.deepEqual(
+ ["onQueryStarted", "onQueryCancelled", "onQueryFinished", "onQueryStarted"],
+ collectingListener.notifications,
+ "Check order of notifications"
+ );
+ controller.cancelQuery();
+ Assert.deepEqual(
+ [
+ "onQueryStarted",
+ "onQueryCancelled",
+ "onQueryFinished",
+ "onQueryStarted",
+ "onQueryCancelled",
+ "onQueryFinished",
+ ],
+ collectingListener.notifications,
+ "Check order of notifications"
+ );
+ await controller.startQuery(context);
+ controller.cancelQuery();
+ Assert.deepEqual(
+ [
+ "onQueryStarted",
+ "onQueryCancelled",
+ "onQueryFinished",
+ "onQueryStarted",
+ "onQueryCancelled",
+ "onQueryFinished",
+ "onQueryStarted",
+ "onQueryFinished",
+ ],
+ collectingListener.notifications,
+ "Check order of notifications"
+ );
+});
diff --git a/browser/components/urlbar/tests/unit/test_UrlbarPrefs.js b/browser/components/urlbar/tests/unit/test_UrlbarPrefs.js
new file mode 100644
index 0000000000..b08a5fff2a
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_UrlbarPrefs.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(function test() {
+ Assert.throws(
+ () => UrlbarPrefs.get("browser.migration.version"),
+ /Trying to access an unknown pref/,
+ "Should throw when passing an untracked pref"
+ );
+
+ Assert.throws(
+ () => UrlbarPrefs.set("browser.migration.version", 100),
+ /Trying to access an unknown pref/,
+ "Should throw when passing an untracked pref"
+ );
+ Assert.throws(
+ () => UrlbarPrefs.set("maxRichResults", "10"),
+ /Invalid value/,
+ "Should throw when passing an invalid value type"
+ );
+
+ Assert.deepEqual(UrlbarPrefs.get("formatting.enabled"), true);
+ UrlbarPrefs.set("formatting.enabled", false);
+ Assert.deepEqual(UrlbarPrefs.get("formatting.enabled"), false);
+
+ Assert.deepEqual(UrlbarPrefs.get("maxRichResults"), 10);
+ UrlbarPrefs.set("maxRichResults", 6);
+ Assert.deepEqual(UrlbarPrefs.get("maxRichResults"), 6);
+
+ Assert.deepEqual(UrlbarPrefs.get("autoFill.stddevMultiplier"), 0.0);
+ UrlbarPrefs.set("autoFill.stddevMultiplier", 0.01);
+ // Due to rounding errors, floats are slightly imprecise, so we can't
+ // directly compare what we set to what we retrieve.
+ Assert.deepEqual(
+ parseFloat(UrlbarPrefs.get("autoFill.stddevMultiplier").toFixed(2)),
+ 0.01
+ );
+});
diff --git a/browser/components/urlbar/tests/unit/test_UrlbarQueryContext.js b/browser/components/urlbar/tests/unit/test_UrlbarQueryContext.js
new file mode 100644
index 0000000000..e30e2fa0eb
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_UrlbarQueryContext.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(function test_constructor() {
+ Assert.throws(
+ () => new UrlbarQueryContext(),
+ /Missing or empty allowAutofill provided to UrlbarQueryContext/,
+ "Should throw with no arguments"
+ );
+
+ Assert.throws(
+ () =>
+ new UrlbarQueryContext({
+ allowAutofill: true,
+ isPrivate: false,
+ searchString: "foo",
+ }),
+ /Missing or empty maxResults provided to UrlbarQueryContext/,
+ "Should throw with a missing maxResults parameter"
+ );
+
+ Assert.throws(
+ () =>
+ new UrlbarQueryContext({
+ allowAutofill: true,
+ maxResults: 1,
+ searchString: "foo",
+ }),
+ /Missing or empty isPrivate provided to UrlbarQueryContext/,
+ "Should throw with a missing isPrivate parameter"
+ );
+
+ Assert.throws(
+ () =>
+ new UrlbarQueryContext({
+ isPrivate: false,
+ maxResults: 1,
+ searchString: "foo",
+ }),
+ /Missing or empty allowAutofill provided to UrlbarQueryContext/,
+ "Should throw with a missing allowAutofill parameter"
+ );
+
+ let qc = new UrlbarQueryContext({
+ allowAutofill: false,
+ isPrivate: true,
+ maxResults: 1,
+ searchString: "foo",
+ });
+
+ Assert.strictEqual(
+ qc.allowAutofill,
+ false,
+ "Should have saved the correct value for allowAutofill"
+ );
+ Assert.strictEqual(
+ qc.isPrivate,
+ true,
+ "Should have saved the correct value for isPrivate"
+ );
+ Assert.equal(
+ qc.maxResults,
+ 1,
+ "Should have saved the correct value for maxResults"
+ );
+ Assert.equal(
+ qc.searchString,
+ "foo",
+ "Should have saved the correct value for searchString"
+ );
+});
diff --git a/browser/components/urlbar/tests/unit/test_UrlbarQueryContext_restrictSource.js b/browser/components/urlbar/tests/unit/test_UrlbarQueryContext_restrictSource.js
new file mode 100644
index 0000000000..056110fed9
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_UrlbarQueryContext_restrictSource.js
@@ -0,0 +1,145 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test for restrictions set through UrlbarQueryContext.sources.
+ */
+
+add_task(async function setup() {
+ let engine = await addTestSuggestionsEngine();
+ let oldDefaultEngine = await Services.search.getDefault();
+ Services.search.setDefault(engine);
+ registerCleanupFunction(async () =>
+ Services.search.setDefault(oldDefaultEngine)
+ );
+});
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "unifiedComplete",
+ "@mozilla.org/autocomplete/search;1?name=unifiedcomplete",
+ "nsIAutoCompleteSearch"
+);
+
+add_task(async function test_restrictions() {
+ await PlacesTestUtils.addVisits([
+ { uri: "http://history.com/", title: "match" },
+ ]);
+ await PlacesUtils.bookmarks.insert({
+ url: "http://bookmark.com/",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "match",
+ });
+ await UrlbarProviderOpenTabs.registerOpenTab("http://openpagematch.com/");
+
+ info("Bookmark restrict");
+ let results = await get_results({
+ sources: [UrlbarUtils.RESULT_SOURCE.BOOKMARKS],
+ searchString: "match",
+ });
+ // Skip the heuristic result.
+ Assert.deepEqual(
+ results.filter(r => !r.heuristic).map(r => r.payload.url),
+ ["http://bookmark.com/"]
+ );
+
+ info("History restrict");
+ results = await get_results({
+ sources: [UrlbarUtils.RESULT_SOURCE.HISTORY],
+ searchString: "match",
+ });
+ // Skip the heuristic result.
+ Assert.deepEqual(
+ results.filter(r => !r.heuristic).map(r => r.payload.url),
+ ["http://history.com/"]
+ );
+
+ info("tabs restrict");
+ results = await get_results({
+ sources: [UrlbarUtils.RESULT_SOURCE.TABS],
+ searchString: "match",
+ });
+ // Skip the heuristic result.
+ Assert.deepEqual(
+ results.filter(r => !r.heuristic).map(r => r.payload.url),
+ ["http://openpagematch.com/"]
+ );
+
+ info("search restrict");
+ results = await get_results({
+ sources: [UrlbarUtils.RESULT_SOURCE.SEARCH],
+ searchString: "match",
+ });
+ Assert.ok(
+ !results.some(r => r.payload.engine != "engine-suggestions.xml"),
+ "All the results should be search results"
+ );
+
+ info("search restrict should ignore restriction token");
+ results = await get_results({
+ sources: [UrlbarUtils.RESULT_SOURCE.SEARCH],
+ searchString: `${UrlbarTokenizer.RESTRICT.BOOKMARKS} match`,
+ });
+ Assert.ok(
+ !results.some(r => r.payload.engine != "engine-suggestions.xml"),
+ "All the results should be search results"
+ );
+ Assert.equal(
+ results[0].payload.query,
+ `${UrlbarTokenizer.RESTRICT.BOOKMARKS} match`,
+ "The restriction token should be ignored and not stripped"
+ );
+
+ info("search restrict with alias");
+ let aliasEngine = await Services.search.addEngineWithDetails("Test", {
+ alias: "match",
+ template: "http://example.com/?search={searchTerms}",
+ });
+ registerCleanupFunction(async function() {
+ await Services.search.removeEngine(aliasEngine);
+ });
+ results = await get_results({
+ sources: [UrlbarUtils.RESULT_SOURCE.SEARCH],
+ searchString: "match this",
+ });
+ Assert.ok(
+ !results.some(r => r.payload.engine != "engine-suggestions.xml"),
+ "All the results should be search results and the alias should be ignored"
+ );
+ Assert.equal(
+ results[0].payload.query,
+ `match this`,
+ "The restriction token should be ignored and not stripped"
+ );
+
+ info("search restrict with other engine");
+ results = await get_results({
+ sources: [UrlbarUtils.RESULT_SOURCE.SEARCH],
+ searchString: "match",
+ engineName: "Test",
+ });
+ Assert.ok(
+ !results.some(r => r.payload.engine != "Test"),
+ "All the results should be search results from the Test engine"
+ );
+});
+
+async function get_results(test) {
+ let controller = UrlbarTestUtils.newMockController();
+ let options = {
+ allowAutofill: false,
+ isPrivate: false,
+ maxResults: 10,
+ sources: test.sources,
+ };
+ if (test.engineName) {
+ options.searchMode = {
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ engineName: test.engineName,
+ };
+ }
+ let queryContext = createContext(test.searchString, options);
+ await controller.startQuery(queryContext);
+ return queryContext.results;
+}
diff --git a/browser/components/urlbar/tests/unit/test_UrlbarSearchUtils.jsm b/browser/components/urlbar/tests/unit/test_UrlbarSearchUtils.jsm
new file mode 100644
index 0000000000..92b2cfbb4b
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_UrlbarSearchUtils.jsm
@@ -0,0 +1,292 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { UrlbarSearchUtils } = ChromeUtils.import(
+ "resource:///modules/UrlbarSearchUtils.jsm"
+);
+
+add_task(async function() {
+ await UrlbarSearchUtils.init();
+ // Tell the search service we are running in the US. This also has the
+ // desired side-effect of preventing our geoip lookup.
+ Services.prefs.setCharPref("browser.search.region", "US");
+
+ Services.search.restoreDefaultEngines();
+ Services.search.resetToOriginalDefaultEngine();
+});
+
+add_task(async function search_engine_match() {
+ let engine = await Services.search.getDefault();
+ let domain = engine.getResultDomain();
+ let token = domain.substr(0, 1);
+ let matchedEngine = (
+ await UrlbarSearchUtils.enginesForDomainPrefix(token)
+ )[0];
+ Assert.equal(matchedEngine, engine);
+});
+
+add_task(async function no_match() {
+ Assert.equal(
+ 0,
+ (await UrlbarSearchUtils.enginesForDomainPrefix("test")).length
+ );
+});
+
+add_task(async function hide_search_engine_nomatch() {
+ let engine = await Services.search.getDefault();
+ let domain = engine.getResultDomain();
+ let token = domain.substr(0, 1);
+ let promiseTopic = promiseSearchTopic("engine-changed");
+ await Promise.all([Services.search.removeEngine(engine), promiseTopic]);
+ Assert.ok(engine.hidden);
+ let matchedEngines = await UrlbarSearchUtils.enginesForDomainPrefix(token);
+ Assert.ok(
+ !matchedEngines.length || matchedEngines[0].getResultDomain() != domain
+ );
+ engine.hidden = false;
+ await TestUtils.waitForCondition(
+ async () => (await UrlbarSearchUtils.enginesForDomainPrefix(token)).length
+ );
+ let matchedEngine2 = (
+ await UrlbarSearchUtils.enginesForDomainPrefix(token)
+ )[0];
+ Assert.ok(matchedEngine2);
+});
+
+add_task(async function onlyEnabled_option_nomatch() {
+ let engine = await Services.search.getDefault();
+ let domain = engine.getResultDomain();
+ let token = domain.substr(0, 1);
+ Services.prefs.setCharPref("browser.search.hiddenOneOffs", engine.name);
+ let matchedEngines = await UrlbarSearchUtils.enginesForDomainPrefix(token, {
+ onlyEnabled: true,
+ });
+ Assert.ok(
+ !matchedEngines.length || matchedEngines[0].getResultDomain() != domain
+ );
+ Services.prefs.clearUserPref("browser.search.hiddenOneOffs");
+ matchedEngines = await UrlbarSearchUtils.enginesForDomainPrefix(token, {
+ onlyEnabled: true,
+ });
+ Assert.ok(
+ matchedEngines.length && matchedEngines[0].getResultDomain() == domain
+ );
+});
+
+add_task(async function add_search_engine_match() {
+ let promiseTopic = promiseSearchTopic("engine-added");
+ Assert.equal(
+ 0,
+ (await UrlbarSearchUtils.enginesForDomainPrefix("bacon")).length
+ );
+ await Promise.all([
+ Services.search.addEngineWithDetails("bacon", {
+ alias: "pork",
+ description: "Search Bacon",
+ method: "GET",
+ template: "http://www.bacon.moz/?search={searchTerms}",
+ }),
+ promiseTopic,
+ ]);
+ await promiseTopic;
+ let matchedEngine = (
+ await UrlbarSearchUtils.enginesForDomainPrefix("bacon")
+ )[0];
+ Assert.ok(matchedEngine);
+ Assert.equal(matchedEngine.searchForm, "http://www.bacon.moz");
+ Assert.equal(matchedEngine.name, "bacon");
+ Assert.equal(matchedEngine.iconURI, null);
+ info("also type part of the public suffix");
+ matchedEngine = (
+ await UrlbarSearchUtils.enginesForDomainPrefix("bacon.m")
+ )[0];
+ Assert.ok(matchedEngine);
+ Assert.equal(matchedEngine.searchForm, "http://www.bacon.moz");
+ Assert.equal(matchedEngine.name, "bacon");
+ Assert.equal(matchedEngine.iconURI, null);
+});
+
+add_task(async function match_multiple_search_engines() {
+ let promiseTopic = promiseSearchTopic("engine-added");
+ Assert.equal(
+ 0,
+ (await UrlbarSearchUtils.enginesForDomainPrefix("baseball")).length
+ );
+ await Promise.all([
+ Services.search.addEngineWithDetails("baseball", {
+ description: "Search Baseball",
+ method: "GET",
+ template: "http://www.baseball.moz/?search={searchTerms}",
+ }),
+ promiseTopic,
+ ]);
+ await promiseTopic;
+ let matchedEngines = await UrlbarSearchUtils.enginesForDomainPrefix("ba");
+ Assert.equal(
+ matchedEngines.length,
+ 2,
+ "enginesForDomainPrefix returned two engines."
+ );
+ Assert.equal(matchedEngines[0].searchForm, "http://www.bacon.moz");
+ Assert.equal(matchedEngines[0].name, "bacon");
+ Assert.equal(matchedEngines[1].searchForm, "http://www.baseball.moz");
+ Assert.equal(matchedEngines[1].name, "baseball");
+});
+
+add_task(async function test_aliased_search_engine_match() {
+ Assert.equal(null, await UrlbarSearchUtils.engineForAlias("sober"));
+ // Lower case
+ let matchedEngine = await UrlbarSearchUtils.engineForAlias("pork");
+ Assert.ok(matchedEngine);
+ Assert.equal(matchedEngine.name, "bacon");
+ Assert.ok(matchedEngine.aliases.includes("pork"));
+ Assert.equal(matchedEngine.iconURI, null);
+ // Upper case
+ matchedEngine = await UrlbarSearchUtils.engineForAlias("PORK");
+ Assert.ok(matchedEngine);
+ Assert.equal(matchedEngine.name, "bacon");
+ Assert.ok(matchedEngine.aliases.includes("pork"));
+ Assert.equal(matchedEngine.iconURI, null);
+ // Cap case
+ matchedEngine = await UrlbarSearchUtils.engineForAlias("Pork");
+ Assert.ok(matchedEngine);
+ Assert.equal(matchedEngine.name, "bacon");
+ Assert.ok(matchedEngine.aliases.includes("pork"));
+ Assert.equal(matchedEngine.iconURI, null);
+});
+
+add_task(async function test_aliased_search_engine_match_upper_case_alias() {
+ let promiseTopic = promiseSearchTopic("engine-added");
+ Assert.equal(
+ 0,
+ (await UrlbarSearchUtils.enginesForDomainPrefix("patch")).length
+ );
+ await Promise.all([
+ Services.search.addEngineWithDetails("patch", {
+ alias: "PR",
+ description: "Search Patch",
+ method: "GET",
+ template: "http://www.patch.moz/?search={searchTerms}",
+ }),
+ promiseTopic,
+ ]);
+ // lower case
+ let matchedEngine = await UrlbarSearchUtils.engineForAlias("pr");
+ Assert.ok(matchedEngine);
+ Assert.equal(matchedEngine.name, "patch");
+ Assert.ok(matchedEngine.aliases.includes("PR"));
+ Assert.equal(matchedEngine.iconURI, null);
+ // Upper case
+ matchedEngine = await UrlbarSearchUtils.engineForAlias("PR");
+ Assert.ok(matchedEngine);
+ Assert.equal(matchedEngine.name, "patch");
+ Assert.ok(matchedEngine.aliases.includes("PR"));
+ Assert.equal(matchedEngine.iconURI, null);
+ // Cap case
+ matchedEngine = await UrlbarSearchUtils.engineForAlias("Pr");
+ Assert.ok(matchedEngine);
+ Assert.equal(matchedEngine.name, "patch");
+ Assert.ok(matchedEngine.aliases.includes("PR"));
+ Assert.equal(matchedEngine.iconURI, null);
+});
+
+add_task(async function remove_search_engine_nomatch() {
+ let engine = Services.search.getEngineByName("bacon");
+ let promiseTopic = promiseSearchTopic("engine-removed");
+ await Promise.all([Services.search.removeEngine(engine), promiseTopic]);
+ Assert.equal(
+ 0,
+ (await UrlbarSearchUtils.enginesForDomainPrefix("bacon")).length
+ );
+});
+
+add_task(async function test_builtin_aliased_search_engine_match() {
+ let engine = await UrlbarSearchUtils.engineForAlias("@google");
+ Assert.ok(engine);
+ Assert.equal(engine.name, "Google");
+ let promiseTopic = promiseSearchTopic("engine-changed");
+ await Promise.all([Services.search.removeEngine(engine), promiseTopic]);
+ let matchedEngine = await UrlbarSearchUtils.engineForAlias("@google");
+ Assert.ok(!matchedEngine);
+ engine.hidden = false;
+ await TestUtils.waitForCondition(() =>
+ UrlbarSearchUtils.engineForAlias("@google")
+ );
+ engine = await UrlbarSearchUtils.engineForAlias("@google");
+ Assert.ok(engine);
+});
+
+add_task(async function test_serps_are_equivalent() {
+ info("Subset URL has extraneous parameters.");
+ let url1 = "https://example.com/search?q=test&type=images";
+ let url2 = "https://example.com/search?q=test";
+ Assert.ok(!UrlbarSearchUtils.serpsAreEquivalent(url1, url2));
+ info("Superset URL has extraneous parameters.");
+ Assert.ok(UrlbarSearchUtils.serpsAreEquivalent(url2, url1));
+
+ info("Same keys, different values.");
+ url1 = "https://example.com/search?q=test&type=images";
+ url2 = "https://example.com/search?q=test123&type=maps";
+ Assert.ok(!UrlbarSearchUtils.serpsAreEquivalent(url1, url2));
+ Assert.ok(!UrlbarSearchUtils.serpsAreEquivalent(url2, url1));
+
+ info("Subset matching isn't strict (URL is subset of itself).");
+ Assert.ok(UrlbarSearchUtils.serpsAreEquivalent(url1, url1));
+
+ info("Origin and pathname are ignored.");
+ url1 = "https://example.com/search?q=test";
+ url2 = "https://example-1.com/maps?q=test";
+ Assert.ok(UrlbarSearchUtils.serpsAreEquivalent(url1, url2));
+ Assert.ok(UrlbarSearchUtils.serpsAreEquivalent(url2, url1));
+
+ info("Params can be optionally ignored");
+ url1 = "https://example.com/search?q=test&abc=123&foo=bar";
+ url2 = "https://example.com/search?q=test";
+ Assert.ok(!UrlbarSearchUtils.serpsAreEquivalent(url1, url2));
+ Assert.ok(UrlbarSearchUtils.serpsAreEquivalent(url1, url2, ["abc", "foo"]));
+});
+
+add_task(async function test_get_root_domain_from_engine() {
+ let engine = await Services.search.addEngineWithDetails("TestEngine2", {
+ template: "http://example.com",
+ });
+ Assert.equal(UrlbarSearchUtils.getRootDomainFromEngine(engine), "example");
+ await Services.search.removeEngine(engine);
+
+ engine = await Services.search.addEngineWithDetails("TestEngine", {
+ template: "http://www.subdomain.othersubdomain.example.com",
+ });
+ Assert.equal(UrlbarSearchUtils.getRootDomainFromEngine(engine), "example");
+ await Services.search.removeEngine(engine);
+
+ // We let engines with URL ending in .test through even though its not a valid
+ // TLD.
+ engine = await Services.search.addEngineWithDetails("TestMalformed", {
+ template: `http://mochi.test/?search={searchTerms}`,
+ });
+ Assert.equal(UrlbarSearchUtils.getRootDomainFromEngine(engine), "mochi");
+ await Services.search.removeEngine(engine);
+
+ // We return the domain for engines with a malformed URL.
+ engine = await Services.search.addEngineWithDetails("TestMalformed", {
+ template: `http://subdomain.foobar/?search={searchTerms}`,
+ });
+ Assert.equal(
+ UrlbarSearchUtils.getRootDomainFromEngine(engine),
+ "subdomain.foobar"
+ );
+ await Services.search.removeEngine(engine);
+});
+
+function promiseSearchTopic(expectedVerb) {
+ return new Promise(resolve => {
+ Services.obs.addObserver(function observe(subject, topic, verb) {
+ info("browser-search-engine-modified: " + verb);
+ if (verb == expectedVerb) {
+ Services.obs.removeObserver(observe, "browser-search-engine-modified");
+ resolve();
+ }
+ }, "browser-search-engine-modified");
+ });
+}
diff --git a/browser/components/urlbar/tests/unit/test_UrlbarUtils_addToUrlbarHistory.js b/browser/components/urlbar/tests/unit/test_UrlbarUtils_addToUrlbarHistory.js
new file mode 100644
index 0000000000..d2d30e1516
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_UrlbarUtils_addToUrlbarHistory.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * These tests unit test the functionality of the functions in UrlbarUtils.
+ * Some functions are bigger, and split out into sepearate test_UrlbarUtils_* files.
+ */
+
+"use strict";
+
+const { PrivateBrowsingUtils } = ChromeUtils.import(
+ "resource://gre/modules/PrivateBrowsingUtils.jsm"
+);
+const { PlacesUIUtils } = ChromeUtils.import(
+ "resource:///modules/PlacesUIUtils.jsm"
+);
+
+let sandbox;
+
+add_task(function setup() {
+ sandbox = sinon.createSandbox();
+});
+
+add_task(function test_addToUrlbarHistory() {
+ sandbox.stub(PlacesUIUtils, "markPageAsTyped");
+ sandbox.stub(PrivateBrowsingUtils, "isWindowPrivate").returns(false);
+
+ UrlbarUtils.addToUrlbarHistory("http://example.com");
+ Assert.ok(
+ PlacesUIUtils.markPageAsTyped.calledOnce,
+ "Should have marked a simple URL as typed."
+ );
+ PlacesUIUtils.markPageAsTyped.resetHistory();
+
+ UrlbarUtils.addToUrlbarHistory();
+ Assert.ok(
+ PlacesUIUtils.markPageAsTyped.notCalled,
+ "Should not have attempted to mark a null URL as typed."
+ );
+ PlacesUIUtils.markPageAsTyped.resetHistory();
+
+ UrlbarUtils.addToUrlbarHistory("http://exam ple.com");
+ Assert.ok(
+ PlacesUIUtils.markPageAsTyped.notCalled,
+ "Should not have marked a URL containing a space as typed."
+ );
+ PlacesUIUtils.markPageAsTyped.resetHistory();
+
+ UrlbarUtils.addToUrlbarHistory("http://exam\x01ple.com");
+ Assert.ok(
+ PlacesUIUtils.markPageAsTyped.notCalled,
+ "Should not have marked a URL containing a control character as typed."
+ );
+ PlacesUIUtils.markPageAsTyped.resetHistory();
+
+ PrivateBrowsingUtils.isWindowPrivate.returns(true);
+ UrlbarUtils.addToUrlbarHistory("http://example.com");
+ Assert.ok(
+ PlacesUIUtils.markPageAsTyped.notCalled,
+ "Should not have marked a URL provided by a private browsing page as typed."
+ );
+ PlacesUIUtils.markPageAsTyped.resetHistory();
+});
diff --git a/browser/components/urlbar/tests/unit/test_UrlbarUtils_getShortcutOrURIAndPostData.js b/browser/components/urlbar/tests/unit/test_UrlbarUtils_getShortcutOrURIAndPostData.js
new file mode 100644
index 0000000000..c363b4b6ec
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_UrlbarUtils_getShortcutOrURIAndPostData.js
@@ -0,0 +1,257 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * These tests unit test the functionality of UrlbarController by stubbing out the
+ * model and providing stubs to be called.
+ */
+
+"use strict";
+
+function getPostDataString(aIS) {
+ if (!aIS) {
+ return null;
+ }
+
+ let sis = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ sis.init(aIS);
+ let dataLines = sis.read(aIS.available()).split("\n");
+
+ // only want the last line
+ return dataLines[dataLines.length - 1];
+}
+
+function keywordResult(aURL, aPostData, aIsUnsafe) {
+ this.url = aURL;
+ this.postData = aPostData;
+ this.isUnsafe = aIsUnsafe;
+}
+
+function keyWordData() {}
+keyWordData.prototype = {
+ init(aKeyWord, aURL, aPostData, aSearchWord) {
+ this.keyword = aKeyWord;
+ this.uri = Services.io.newURI(aURL);
+ this.postData = aPostData;
+ this.searchWord = aSearchWord;
+
+ this.method = this.postData ? "POST" : "GET";
+ },
+};
+
+function bmKeywordData(aKeyWord, aURL, aPostData, aSearchWord) {
+ this.init(aKeyWord, aURL, aPostData, aSearchWord);
+}
+bmKeywordData.prototype = new keyWordData();
+
+function searchKeywordData(aKeyWord, aURL, aPostData, aSearchWord) {
+ this.init(aKeyWord, aURL, aPostData, aSearchWord);
+}
+searchKeywordData.prototype = new keyWordData();
+
+var testData = [
+ [
+ new bmKeywordData("bmget", "http://bmget/search=%s", null, "foo"),
+ new keywordResult("http://bmget/search=foo", null),
+ ],
+
+ [
+ new bmKeywordData("bmpost", "http://bmpost/", "search=%s", "foo2"),
+ new keywordResult("http://bmpost/", "search=foo2"),
+ ],
+
+ [
+ new bmKeywordData(
+ "bmpostget",
+ "http://bmpostget/search1=%s",
+ "search2=%s",
+ "foo3"
+ ),
+ new keywordResult("http://bmpostget/search1=foo3", "search2=foo3"),
+ ],
+
+ [
+ new bmKeywordData("bmget-nosearch", "http://bmget-nosearch/", null, ""),
+ new keywordResult("http://bmget-nosearch/", null),
+ ],
+
+ [
+ new searchKeywordData(
+ "searchget",
+ "http://searchget/?search={searchTerms}",
+ null,
+ "foo4"
+ ),
+ new keywordResult("http://searchget/?search=foo4", null, true),
+ ],
+
+ [
+ new searchKeywordData(
+ "searchpost",
+ "http://searchpost/",
+ "search={searchTerms}",
+ "foo5"
+ ),
+ new keywordResult("http://searchpost/", "search=foo5", true),
+ ],
+
+ [
+ new searchKeywordData(
+ "searchpostget",
+ "http://searchpostget/?search1={searchTerms}",
+ "search2={searchTerms}",
+ "foo6"
+ ),
+ new keywordResult(
+ "http://searchpostget/?search1=foo6",
+ "search2=foo6",
+ true
+ ),
+ ],
+
+ // Bookmark keywords that don't take parameters should not be activated if a
+ // parameter is passed (bug 420328).
+ [
+ new bmKeywordData("bmget-noparam", "http://bmget-noparam/", null, "foo7"),
+ new keywordResult(null, null, true),
+ ],
+ [
+ new bmKeywordData(
+ "bmpost-noparam",
+ "http://bmpost-noparam/",
+ "not_a=param",
+ "foo8"
+ ),
+ new keywordResult(null, null, true),
+ ],
+
+ // Test escaping (%s = escaped, %S = raw)
+ // UTF-8 default
+ [
+ new bmKeywordData(
+ "bmget-escaping",
+ "http://bmget/?esc=%s&raw=%S",
+ null,
+ "fo\xE9"
+ ),
+ new keywordResult("http://bmget/?esc=fo%C3%A9&raw=fo\xE9", null),
+ ],
+ // Explicitly-defined ISO-8859-1
+ [
+ new bmKeywordData(
+ "bmget-escaping2",
+ "http://bmget/?esc=%s&raw=%S&mozcharset=ISO-8859-1",
+ null,
+ "fo\xE9"
+ ),
+ new keywordResult("http://bmget/?esc=fo%E9&raw=fo\xE9", null),
+ ],
+
+ // Bug 359809: Test escaping +, /, and @
+ // UTF-8 default
+ [
+ new bmKeywordData(
+ "bmget-escaping",
+ "http://bmget/?esc=%s&raw=%S",
+ null,
+ "+/@"
+ ),
+ new keywordResult("http://bmget/?esc=%2B%2F%40&raw=+/@", null),
+ ],
+ // Explicitly-defined ISO-8859-1
+ [
+ new bmKeywordData(
+ "bmget-escaping2",
+ "http://bmget/?esc=%s&raw=%S&mozcharset=ISO-8859-1",
+ null,
+ "+/@"
+ ),
+ new keywordResult("http://bmget/?esc=%2B%2F%40&raw=+/@", null),
+ ],
+
+ // Test using a non-bmKeywordData object, to test the behavior of
+ // getShortcutOrURIAndPostData for non-keywords (setupKeywords only adds keywords for
+ // bmKeywordData objects)
+ [{ keyword: "http://gavinsharp.com" }, new keywordResult(null, null, true)],
+];
+
+add_task(async function test_getshortcutoruri() {
+ await setupKeywords();
+
+ for (let item of testData) {
+ let [data, result] = item;
+
+ let query = data.keyword;
+ if (data.searchWord) {
+ query += " " + data.searchWord;
+ }
+ let returnedData = await UrlbarUtils.getShortcutOrURIAndPostData(query);
+ // null result.url means we should expect the same query we sent in
+ let expected = result.url || query;
+ Assert.equal(
+ returnedData.url,
+ expected,
+ "got correct URL for " + data.keyword
+ );
+ Assert.equal(
+ getPostDataString(returnedData.postData),
+ result.postData,
+ "got correct postData for " + data.keyword
+ );
+ Assert.equal(
+ returnedData.mayInheritPrincipal,
+ !result.isUnsafe,
+ "got correct mayInheritPrincipal for " + data.keyword
+ );
+ }
+
+ await cleanupKeywords();
+});
+
+var folder = null;
+var gAddedEngines = [];
+
+async function setupKeywords() {
+ folder = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "keyword-test",
+ });
+ for (let item of testData) {
+ let data = item[0];
+ if (data instanceof bmKeywordData) {
+ await PlacesUtils.bookmarks.insert({
+ url: data.uri,
+ parentGuid: folder.guid,
+ });
+ await PlacesUtils.keywords.insert({
+ keyword: data.keyword,
+ url: data.uri.spec,
+ postData: data.postData,
+ });
+ }
+
+ if (data instanceof searchKeywordData) {
+ let addedEngine = await Services.search.addEngineWithDetails(
+ data.keyword,
+ {
+ alias: data.keyword,
+ method: data.method,
+ template: data.uri.spec,
+ searchPostParams: data.postData,
+ }
+ );
+ gAddedEngines.push(addedEngine);
+ }
+ }
+}
+
+async function cleanupKeywords() {
+ await PlacesUtils.bookmarks.remove(folder);
+ for (let engine of gAddedEngines) {
+ await Services.search.removeEngine(engine);
+ }
+ gAddedEngines = [];
+}
diff --git a/browser/components/urlbar/tests/unit/test_UrlbarUtils_getTokenMatches.js b/browser/components/urlbar/tests/unit/test_UrlbarUtils_getTokenMatches.js
new file mode 100644
index 0000000000..bae6ffc879
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_UrlbarUtils_getTokenMatches.js
@@ -0,0 +1,294 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests UrlbarUtils.getTokenMatches.
+ */
+
+"use strict";
+
+add_task(function test() {
+ const tests = [
+ {
+ tokens: ["mozilla", "is", "i"],
+ phrase: "mozilla is for the Open Web",
+ expected: [
+ [0, 7],
+ [8, 2],
+ ],
+ },
+ {
+ tokens: ["mozilla", "is", "i"],
+ phrase: "MOZILLA IS for the Open Web",
+ expected: [
+ [0, 7],
+ [8, 2],
+ ],
+ },
+ {
+ tokens: ["mozilla", "is", "i"],
+ phrase: "MoZiLlA Is for the Open Web",
+ expected: [
+ [0, 7],
+ [8, 2],
+ ],
+ },
+ {
+ tokens: ["MOZILLA", "IS", "I"],
+ phrase: "mozilla is for the Open Web",
+ expected: [
+ [0, 7],
+ [8, 2],
+ ],
+ },
+ {
+ tokens: ["MoZiLlA", "Is", "I"],
+ phrase: "mozilla is for the Open Web",
+ expected: [
+ [0, 7],
+ [8, 2],
+ ],
+ },
+ {
+ tokens: ["mo", "b"],
+ phrase: "mozilla is for the Open Web",
+ expected: [
+ [0, 2],
+ [26, 1],
+ ],
+ },
+ {
+ tokens: ["mo", "b"],
+ phrase: "MOZILLA is for the OPEN WEB",
+ expected: [
+ [0, 2],
+ [26, 1],
+ ],
+ },
+ {
+ tokens: ["MO", "B"],
+ phrase: "mozilla is for the Open Web",
+ expected: [
+ [0, 2],
+ [26, 1],
+ ],
+ },
+ {
+ tokens: ["mo", ""],
+ phrase: "mozilla is for the Open Web",
+ expected: [[0, 2]],
+ },
+ {
+ tokens: ["mozilla"],
+ phrase: "mozilla",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["mozilla"],
+ phrase: "MOZILLA",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["mozilla"],
+ phrase: "MoZiLlA",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["mozilla"],
+ phrase: "mOzIlLa",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["MOZILLA"],
+ phrase: "mozilla",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["MoZiLlA"],
+ phrase: "mozilla",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["mOzIlLa"],
+ phrase: "mozilla",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["\u9996"],
+ phrase: "Test \u9996\u9875 Test",
+ expected: [[5, 1]],
+ },
+ {
+ tokens: ["mo", "zilla"],
+ phrase: "mozilla",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["mo", "zilla"],
+ phrase: "MOZILLA",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["mo", "zilla"],
+ phrase: "MoZiLlA",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["mo", "zilla"],
+ phrase: "mOzIlLa",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["MO", "ZILLA"],
+ phrase: "mozilla",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["Mo", "Zilla"],
+ phrase: "mozilla",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["moz", "zilla"],
+ phrase: "mozilla",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: [""], // Should never happen in practice.
+ phrase: "mozilla",
+ expected: [],
+ },
+ {
+ tokens: ["mo", "om"],
+ phrase: "mozilla mozzarella momo",
+ expected: [
+ [0, 2],
+ [8, 2],
+ [19, 4],
+ ],
+ },
+ {
+ tokens: ["mo", "om"],
+ phrase: "MOZILLA MOZZARELLA MOMO",
+ expected: [
+ [0, 2],
+ [8, 2],
+ [19, 4],
+ ],
+ },
+ {
+ tokens: ["MO", "OM"],
+ phrase: "mozilla mozzarella momo",
+ expected: [
+ [0, 2],
+ [8, 2],
+ [19, 4],
+ ],
+ },
+ {
+ tokens: ["resume"],
+ phrase: "résumé",
+ expected: [[0, 6]],
+ },
+ {
+ // This test should succeed even in a Spanish locale where N and Ñ are
+ // considered distinct letters.
+ tokens: ["jalapeno"],
+ phrase: "jalapeño",
+ expected: [[0, 8]],
+ },
+ ];
+ for (let { tokens, phrase, expected } of tests) {
+ tokens = tokens.map(t => ({
+ value: t,
+ lowerCaseValue: t.toLocaleLowerCase(),
+ }));
+ Assert.deepEqual(
+ UrlbarUtils.getTokenMatches(tokens, phrase, UrlbarUtils.HIGHLIGHT.TYPED),
+ expected,
+ `Match "${tokens.map(t => t.value).join(", ")}" on "${phrase}"`
+ );
+ }
+});
+
+/**
+ * Tests suggestion highlighting. Note that suggestions are only highlighted if
+ * the matching token is at the beginning of a word in the matched string.
+ */
+add_task(function testSuggestions() {
+ const tests = [
+ {
+ tokens: ["mozilla", "is", "i"],
+ phrase: "mozilla is for the Open Web",
+ expected: [
+ [7, 1],
+ [10, 17],
+ ],
+ },
+ {
+ tokens: ["\u9996"],
+ phrase: "Test \u9996\u9875 Test",
+ expected: [
+ [0, 5],
+ [6, 6],
+ ],
+ },
+ {
+ tokens: ["mo", "zilla"],
+ phrase: "mOzIlLa",
+ expected: [[2, 5]],
+ },
+ {
+ tokens: ["MO", "ZILLA"],
+ phrase: "mozilla",
+ expected: [[2, 5]],
+ },
+ {
+ tokens: [""], // Should never happen in practice.
+ phrase: "mozilla",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["mo", "om", "la"],
+ phrase: "mozilla mozzarella momo",
+ expected: [
+ [2, 6],
+ [10, 9],
+ [21, 2],
+ ],
+ },
+ {
+ tokens: ["mo", "om", "la"],
+ phrase: "MOZILLA MOZZARELLA MOMO",
+ expected: [
+ [2, 6],
+ [10, 9],
+ [21, 2],
+ ],
+ },
+ {
+ tokens: ["MO", "OM", "LA"],
+ phrase: "mozilla mozzarella momo",
+ expected: [
+ [2, 6],
+ [10, 9],
+ [21, 2],
+ ],
+ },
+ ];
+ for (let { tokens, phrase, expected } of tests) {
+ tokens = tokens.map(t => ({
+ value: t,
+ lowerCaseValue: t.toLocaleLowerCase(),
+ }));
+ Assert.deepEqual(
+ UrlbarUtils.getTokenMatches(
+ tokens,
+ phrase,
+ UrlbarUtils.HIGHLIGHT.SUGGESTED
+ ),
+ expected,
+ `Match "${tokens.map(t => t.value).join(", ")}" on "${phrase}"`
+ );
+ }
+});
diff --git a/browser/components/urlbar/tests/unit/test_autofill_about_urls.js b/browser/components/urlbar/tests/unit/test_autofill_about_urls.js
new file mode 100644
index 0000000000..4a15aa5e9b
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_about_urls.js
@@ -0,0 +1,100 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const ENGINE_NAME = "engine-suggestions.xml";
+
+testEngine_setup();
+
+// "about:ab" should match "about:about"
+add_task(async function aboutAb() {
+ let context = createContext("about:ab", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "about:about",
+ completed: "about:about",
+ matches: [
+ makeVisitResult(context, {
+ uri: "about:about",
+ title: "about:about",
+ heuristic: true,
+ }),
+ ],
+ });
+});
+
+// "about:Ab" should match "about:about"
+add_task(async function aboutAb() {
+ let context = createContext("about:Ab", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "about:About",
+ completed: "about:about",
+ matches: [
+ makeVisitResult(context, {
+ uri: "about:about",
+ title: "about:about",
+ heuristic: true,
+ }),
+ ],
+ });
+});
+
+// "about:about" should match "about:about"
+add_task(async function aboutAbout() {
+ let context = createContext("about:about", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "about:about",
+ completed: "about:about",
+ matches: [
+ makeVisitResult(context, {
+ uri: "about:about",
+ title: "about:about",
+ heuristic: true,
+ }),
+ ],
+ });
+});
+
+// "about:a" should complete to "about:about" and also match "about:addons"
+add_task(async function aboutAboutAndAboutAddons() {
+ let context = createContext("about:a", { isPrivate: false });
+ await check_results({
+ context,
+ search: "about:a",
+ autofilled: "about:about",
+ completed: "about:about",
+ matches: [
+ makeVisitResult(context, {
+ uri: "about:about",
+ title: "about:about",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "about:addons",
+ title: "about:addons",
+ iconUri: "",
+ providerName: "UnifiedComplete",
+ }),
+ ],
+ });
+});
+
+// "about:" should *not* match anything
+add_task(async function aboutColonHasNoMatch() {
+ let context = createContext("about:", { isPrivate: false });
+ await check_results({
+ context,
+ search: "about:",
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ providerName: "HeuristicFallback",
+ heuristic: true,
+ }),
+ ],
+ });
+});
diff --git a/browser/components/urlbar/tests/unit/test_autofill_bookmarked.js b/browser/components/urlbar/tests/unit/test_autofill_bookmarked.js
new file mode 100644
index 0000000000..39483e993a
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_bookmarked.js
@@ -0,0 +1,148 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This is a specific autofill test to ensure we pick the correct bookmarked
+// state of an origin. Regardless of the order of origins, we should always pick
+// the correct bookmarked status.
+
+add_task(async function() {
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+
+ let host = "example.com";
+ // Add a bookmark to the http version, but ensure the https version has an
+ // higher frecency.
+ let bookmark = await PlacesUtils.bookmarks.insert({
+ url: `http://${host}`,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits(`https://${host}`);
+ }
+ // ensure both fall below the threshold.
+ for (let i = 0; i < 15; i++) {
+ await PlacesTestUtils.addVisits(`https://not-${host}`);
+ }
+
+ async function check_autofill() {
+ let threshold = await getOriginAutofillThreshold();
+ let httpOriginFrecency = await getOriginFrecency("http://", host);
+ Assert.less(
+ httpOriginFrecency,
+ threshold,
+ "Http origin frecency should be below the threshold"
+ );
+ let httpsOriginFrecency = await getOriginFrecency("https://", host);
+ Assert.less(
+ httpsOriginFrecency,
+ threshold,
+ "Https origin frecency should be below the threshold"
+ );
+ Assert.less(
+ httpOriginFrecency,
+ httpsOriginFrecency,
+ "Http origin frecency should be below the https origin frecency"
+ );
+
+ // The http version should be filled because it's bookmarked, but with the
+ // https prefix that is more frecent.
+ let context = createContext("ex", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: `${host}/`,
+ completed: `https://${host}/`,
+ matches: [
+ makeVisitResult(context, {
+ uri: `https://${host}/`,
+ title: `https://${host}`,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: `https://not-${host}/`,
+ title: `test visit for https://not-${host}/`,
+ }),
+ ],
+ });
+ }
+
+ await check_autofill();
+
+ // Now remove the bookmark, ensure to remove the orphans, then reinsert the
+ // bookmark; thus we physically invert the order of the rows in the table.
+ await checkOriginsOrder(host, ["http://", "https://"]);
+ await PlacesUtils.bookmarks.remove(bookmark);
+ await PlacesUtils.withConnectionWrapper("removeOrphans", async db => {
+ db.execute(`DELETE FROM moz_places WHERE url = :url`, {
+ url: `http://${host}/`,
+ });
+ db.execute(
+ `DELETE FROM moz_origins WHERE prefix = "http://" AND host = :host`,
+ { host }
+ );
+ });
+ bookmark = await PlacesUtils.bookmarks.insert({
+ url: `http://${host}`,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+
+ await checkOriginsOrder(host, ["https://", "http://"]);
+
+ await check_autofill();
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.remove(bookmark);
+});
+
+add_task(async function test_www() {
+ // Add a bookmark to the www version
+ let host = "example.com";
+ await PlacesUtils.bookmarks.insert({
+ url: `http://www.${host}`,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+
+ info("search for start of www.");
+ let context = createContext("w", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: `www.${host}/`,
+ completed: `http://www.${host}/`,
+ matches: [
+ makeVisitResult(context, {
+ uri: `http://www.${host}/`,
+ title: `www.${host}`,
+ heuristic: true,
+ }),
+ ],
+ });
+ info("search for full www.");
+ context = createContext("www.", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: `www.${host}/`,
+ completed: `http://www.${host}/`,
+ matches: [
+ makeVisitResult(context, {
+ uri: `http://www.${host}/`,
+ title: `www.${host}`,
+ heuristic: true,
+ }),
+ ],
+ });
+ info("search for host without www.");
+ context = createContext("ex", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: `${host}/`,
+ completed: `http://www.${host}/`,
+ matches: [
+ makeVisitResult(context, {
+ uri: `http://www.${host}/`,
+ title: `www.${host}`,
+ heuristic: true,
+ }),
+ ],
+ });
+});
diff --git a/browser/components/urlbar/tests/unit/test_autofill_functional.js b/browser/components/urlbar/tests/unit/test_autofill_functional.js
new file mode 100644
index 0000000000..662cf420b8
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_functional.js
@@ -0,0 +1,112 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Functional tests for inline autocomplete
+
+add_task(async function setup() {
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+});
+
+add_task(async function test_urls_order() {
+ info("Add urls, check for correct order");
+ let places = [
+ { uri: Services.io.newURI("http://visit1.mozilla.org") },
+ { uri: Services.io.newURI("http://visit2.mozilla.org") },
+ ];
+ await PlacesTestUtils.addVisits(places);
+ let context = createContext("vis", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "visit2.mozilla.org/",
+ completed: "http://visit2.mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://visit2.mozilla.org/",
+ title: "visit2.mozilla.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://visit1.mozilla.org/",
+ title: "test visit for http://visit1.mozilla.org/",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_bookmark_first() {
+ info("With a bookmark and history, the query result should be the bookmark");
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: Services.io.newURI("http://bookmark1.mozilla.org/"),
+ });
+ await PlacesTestUtils.addVisits(
+ Services.io.newURI("http://bookmark1.mozilla.org/foo")
+ );
+ let context = createContext("bookmark", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "bookmark1.mozilla.org/",
+ completed: "http://bookmark1.mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://bookmark1.mozilla.org/",
+ title: "bookmark1.mozilla.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://bookmark1.mozilla.org/foo",
+ title: "test visit for http://bookmark1.mozilla.org/foo",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_complete_querystring() {
+ info("Check to make sure we autocomplete after ?");
+ await PlacesTestUtils.addVisits(
+ Services.io.newURI("http://smokey.mozilla.org/foo?bacon=delicious")
+ );
+ let context = createContext("smokey.mozilla.org/foo?", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "smokey.mozilla.org/foo?bacon=delicious",
+ completed: "http://smokey.mozilla.org/foo?bacon=delicious",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://smokey.mozilla.org/foo?bacon=delicious",
+ title: "smokey.mozilla.org/foo?bacon=delicious",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_complete_fragment() {
+ info("Check to make sure we autocomplete after #");
+ await PlacesTestUtils.addVisits(
+ Services.io.newURI("http://smokey.mozilla.org/foo?bacon=delicious#bar")
+ );
+ let context = createContext("smokey.mozilla.org/foo?bacon=delicious#bar", {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ autofilled: "smokey.mozilla.org/foo?bacon=delicious#bar",
+ completed: "http://smokey.mozilla.org/foo?bacon=delicious#bar",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://smokey.mozilla.org/foo?bacon=delicious#bar",
+ title: "smokey.mozilla.org/foo?bacon=delicious#bar",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_autofill_origins.js b/browser/components/urlbar/tests/unit/test_autofill_origins.js
new file mode 100644
index 0000000000..af9685286f
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_origins.js
@@ -0,0 +1,638 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const ENGINE_NAME = "engine-suggestions.xml";
+const HEURISTIC_FALLBACK_PROVIDERNAME = "HeuristicFallback";
+
+const origin = "example.com";
+
+async function cleanup() {
+ let suggestPrefs = ["history", "bookmark", "openpage"];
+ for (let type of suggestPrefs) {
+ Services.prefs.clearUserPref("browser.urlbar.suggest." + type);
+ }
+ await cleanupPlaces();
+}
+
+testEngine_setup();
+
+// "example.com/" should match http://example.com/. i.e., the search string
+// should be treated as if it didn't have the trailing slash.
+add_task(async function trailingSlash() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com/",
+ },
+ ]);
+
+ let context = createContext(`${origin}/`, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: `${origin}/`,
+ completed: `http://${origin}/`,
+ matches: [
+ makeVisitResult(context, {
+ uri: `http://${origin}/`,
+ title: `${origin}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "example.com/" should match http://www.example.com/. i.e., the search string
+// should be treated as if it didn't have the trailing slash.
+add_task(async function trailingSlashWWW() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://www.example.com/",
+ },
+ ]);
+ let context = createContext(`${origin}/`, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ completed: "http://www.example.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: `http://www.${origin}/`,
+ title: `www.${origin}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "ex" should match http://example.com:8888/, and the port should be completed.
+add_task(async function port() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com:8888/",
+ },
+ ]);
+ let context = createContext("ex", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com:8888/",
+ completed: "http://example.com:8888/",
+ matches: [
+ makeVisitResult(context, {
+ uri: `http://${origin}:8888/`,
+ title: `${origin}:8888`,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "example.com:8" should match http://example.com:8888/, and the port should
+// be completed.
+add_task(async function portPartial() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com:8888/",
+ },
+ ]);
+ let context = createContext(`${origin}:8`, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com:8888/",
+ completed: "http://example.com:8888/",
+ matches: [
+ makeVisitResult(context, {
+ uri: `http://${origin}:8888/`,
+ title: `${origin}:8888`,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "EXaM" should match http://example.com/ and the case of the search string
+// should be preserved in the autofilled value.
+add_task(async function preserveCase() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com/",
+ },
+ ]);
+ let context = createContext("EXaM", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "EXaMple.com/",
+ completed: "http://example.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: `http://${origin}/`,
+ title: `${origin}`,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "EXaM" should match http://example.com:8888/, the port should be completed,
+// and the case of the search string should be preserved in the autofilled
+// value.
+add_task(async function preserveCasePort() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com:8888/",
+ },
+ ]);
+ let context = createContext("EXaM", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "EXaMple.com:8888/",
+ completed: "http://example.com:8888/",
+ matches: [
+ makeVisitResult(context, {
+ uri: `http://${origin}:8888/`,
+ title: `${origin}:8888`,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "example.com:89" should *not* match http://example.com:8888/.
+add_task(async function portNoMatch1() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com:8888/",
+ },
+ ]);
+ let context = createContext(`${origin}:89`, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${origin}:89/`,
+ title: `http://${origin}:89/`,
+ iconUri: "",
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "example.com:9" should *not* match http://example.com:8888/.
+add_task(async function portNoMatch2() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com:8888/",
+ },
+ ]);
+ let context = createContext(`${origin}:9`, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${origin}:9/`,
+ title: `http://${origin}:9/`,
+ iconUri: "",
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "example/" should *not* match http://example.com/.
+add_task(async function trailingSlash_2() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com/",
+ },
+ ]);
+ let context = createContext("example/", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://example/",
+ title: "http://example/",
+ iconUri: "page-icon:http://example/",
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// multi.dotted.domain, search up to dot.
+add_task(async function multidotted() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://www.example.co.jp:8888/",
+ },
+ ]);
+ let context = createContext("www.example.co.", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "www.example.co.jp:8888/",
+ completed: "http://www.example.co.jp:8888/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://www.example.co.jp:8888/",
+ title: "www.example.co.jp:8888",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+add_task(async function test_ip() {
+ // IP addresses have complicated rules around whether they show
+ // HeuristicFallback's backup search result. Flip this pref to disable that
+ // backup search and simplify ths subtest.
+ Services.prefs.setBoolPref("keyword.enabled", false);
+ for (let str of [
+ "192.168.1.1/",
+ "255.255.255.255:8080/",
+ "[2001:db8::1428:57ab]/",
+ "[::c0a8:5909]/",
+ "[::1]/",
+ ]) {
+ info("testing " + str);
+ await PlacesTestUtils.addVisits("http://" + str);
+ for (let i = 1; i < str.length; ++i) {
+ let context = createContext(str.substring(0, i), { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: str,
+ completed: "http://" + str,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + str,
+ title: str.replace(/\/$/, ""), // strip trailing slash
+ heuristic: true,
+ }),
+ ],
+ });
+ }
+ await cleanup();
+ }
+ Services.prefs.clearUserPref("keyword.enabled");
+});
+
+// host starting with large number.
+add_task(async function large_number_host() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://12345example.it:8888/",
+ },
+ ]);
+ let context = createContext("1234", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "12345example.it:8888/",
+ completed: "http://12345example.it:8888/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://12345example.it:8888/",
+ title: "12345example.it:8888",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// When determining which origins should be autofilled, all the origins sharing
+// a host should be added together to get their combined frecency -- i.e.,
+// prefixes should be collapsed. And then from that list, the origin with the
+// highest frecency should be chosen.
+add_task(async function groupByHost() {
+ // Add some visits to the same host, example.com. Add one http and two https
+ // so that https has a higher frecency and is therefore the origin that should
+ // be autofilled. Also add another origin that has a higher frecency than
+ // both so that alone, neither http nor https would be autofilled, but added
+ // together they should be.
+ await PlacesTestUtils.addVisits([
+ { uri: "http://example.com/" },
+
+ { uri: "https://example.com/" },
+ { uri: "https://example.com/" },
+
+ { uri: "https://mozilla.org/" },
+ { uri: "https://mozilla.org/" },
+ { uri: "https://mozilla.org/" },
+ { uri: "https://mozilla.org/" },
+ ]);
+
+ let httpFrec = frecencyForUrl("http://example.com/");
+ let httpsFrec = frecencyForUrl("https://example.com/");
+ let otherFrec = frecencyForUrl("https://mozilla.org/");
+ Assert.less(httpFrec, httpsFrec, "Sanity check");
+ Assert.less(httpsFrec, otherFrec, "Sanity check");
+
+ // Make sure the frecencies of the three origins are as expected in relation
+ // to the threshold.
+ let threshold = await getOriginAutofillThreshold();
+ Assert.less(httpFrec, threshold, "http origin should be < threshold");
+ Assert.less(httpsFrec, threshold, "https origin should be < threshold");
+ Assert.ok(threshold <= otherFrec, "Other origin should cross threshold");
+
+ Assert.ok(
+ threshold <= httpFrec + httpsFrec,
+ "http and https origin added together should cross threshold"
+ );
+
+ // The https origin should be autofilled.
+ let context = createContext("ex", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ completed: "https://example.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://example.com/",
+ title: "https://example.com",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanup();
+});
+
+// This is the same as the previous (groupByHost), but it changes the standard
+// deviation multiplier by setting the corresponding pref. This makes sure that
+// the pref is respected.
+add_task(async function groupByHostNonDefaultStddevMultiplier() {
+ let stddevMultiplier = 1.5;
+ Services.prefs.setCharPref(
+ "browser.urlbar.autoFill.stddevMultiplier",
+ Number(stddevMultiplier).toFixed(1)
+ );
+
+ await PlacesTestUtils.addVisits([
+ { uri: "http://example.com/" },
+ { uri: "http://example.com/" },
+
+ { uri: "https://example.com/" },
+ { uri: "https://example.com/" },
+ { uri: "https://example.com/" },
+
+ { uri: "https://foo.com/" },
+ { uri: "https://foo.com/" },
+ { uri: "https://foo.com/" },
+
+ { uri: "https://mozilla.org/" },
+ { uri: "https://mozilla.org/" },
+ { uri: "https://mozilla.org/" },
+ { uri: "https://mozilla.org/" },
+ { uri: "https://mozilla.org/" },
+ ]);
+
+ let httpFrec = frecencyForUrl("http://example.com/");
+ let httpsFrec = frecencyForUrl("https://example.com/");
+ let otherFrec = frecencyForUrl("https://mozilla.org/");
+ Assert.less(httpFrec, httpsFrec, "Sanity check");
+ Assert.less(httpsFrec, otherFrec, "Sanity check");
+
+ // Make sure the frecencies of the three origins are as expected in relation
+ // to the threshold.
+ let threshold = await getOriginAutofillThreshold();
+ Assert.less(httpFrec, threshold, "http origin should be < threshold");
+ Assert.less(httpsFrec, threshold, "https origin should be < threshold");
+ Assert.ok(threshold <= otherFrec, "Other origin should cross threshold");
+
+ Assert.ok(
+ threshold <= httpFrec + httpsFrec,
+ "http and https origin added together should cross threshold"
+ );
+
+ // The https origin should be autofilled.
+ let context = createContext("ex", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ completed: "https://example.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://example.com/",
+ title: "https://example.com",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ Services.prefs.clearUserPref("browser.urlbar.autoFill.stddevMultiplier");
+
+ await cleanup();
+});
+
+// This is similar to suggestHistoryFalse_bookmark_0 in test_autofill_tasks.js,
+// but it adds unbookmarked visits for multiple URLs with the same origin.
+add_task(async function suggestHistoryFalse_bookmark_multiple() {
+ // Force only bookmarked pages to be suggested and therefore only bookmarked
+ // pages to be completed.
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+
+ let search = "ex";
+ let baseURL = "http://example.com/";
+ let bookmarkedURL = baseURL + "bookmarked";
+
+ // Add visits for three different URLs all sharing the same origin, and then
+ // bookmark the second one. After that, the origin should be autofilled. The
+ // reason for adding unbookmarked visits before and after adding the
+ // bookmarked visit is to make sure our aggregate SQL query for determining
+ // whether an origin is bookmarked is correct.
+
+ await PlacesTestUtils.addVisits([
+ {
+ uri: baseURL + "other1",
+ },
+ ]);
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await PlacesTestUtils.addVisits([
+ {
+ uri: bookmarkedURL,
+ },
+ ]);
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await PlacesTestUtils.addVisits([
+ {
+ uri: baseURL + "other2",
+ },
+ ]);
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // Now bookmark the second URL. It should be suggested and completed.
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: bookmarkedURL,
+ });
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ completed: baseURL,
+ matches: [
+ makeVisitResult(context, {
+ uri: baseURL,
+ title: "example.com",
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: bookmarkedURL,
+ title: "A bookmark",
+ }),
+ ],
+ });
+
+ await cleanup();
+});
+
+// This is similar to suggestHistoryFalse_bookmark_prefix_0 in
+// autofill_tasks.js, but it adds unbookmarked visits for multiple URLs with the
+// same origin.
+add_task(async function suggestHistoryFalse_bookmark_prefix_multiple() {
+ // Force only bookmarked pages to be suggested and therefore only bookmarked
+ // pages to be completed.
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+
+ let search = "http://ex";
+ let baseURL = "http://example.com/";
+ let bookmarkedURL = baseURL + "bookmarked";
+
+ // Add visits for three different URLs all sharing the same origin, and then
+ // bookmark the second one. After that, the origin should be autofilled. The
+ // reason for adding unbookmarked visits before and after adding the
+ // bookmarked visit is to make sure our aggregate SQL query for determining
+ // whether an origin is bookmarked is correct.
+
+ await PlacesTestUtils.addVisits([
+ {
+ uri: baseURL + "other1",
+ },
+ ]);
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `${search}/`,
+ title: `${search}/`,
+ iconUri: "",
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+
+ await PlacesTestUtils.addVisits([
+ {
+ uri: bookmarkedURL,
+ },
+ ]);
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `${search}/`,
+ title: `${search}/`,
+ iconUri: "",
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+
+ await PlacesTestUtils.addVisits([
+ {
+ uri: baseURL + "other2",
+ },
+ ]);
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `${search}/`,
+ title: `${search}/`,
+ iconUri: "",
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+
+ // Now bookmark the second URL. It should be suggested and completed.
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: bookmarkedURL,
+ });
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://example.com/",
+ completed: baseURL,
+ matches: [
+ makeVisitResult(context, {
+ uri: baseURL,
+ title: "example.com",
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: bookmarkedURL,
+ title: "A bookmark",
+ }),
+ ],
+ });
+
+ await cleanup();
+});
diff --git a/browser/components/urlbar/tests/unit/test_autofill_originsAndQueries.js b/browser/components/urlbar/tests/unit/test_autofill_originsAndQueries.js
new file mode 100644
index 0000000000..5ad440596b
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_originsAndQueries.js
@@ -0,0 +1,2408 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const ENGINE_NAME = "engine-suggestions.xml";
+const HEURISTIC_FALLBACK_PROVIDERNAME = "HeuristicFallback";
+const UNIFIEDCOMPLETE_PROVIDERNAME = "UnifiedComplete";
+
+/**
+ * Helpful reminder of the `autofilled` and `completed` properties in the
+ * object passed to check_results:
+ * autofilled: expected input.value after autofill
+ * completed: expected input.value after autofill and enter is pressed
+ *
+ * `completed` is the URL that the controller sets to input.value, and the URL
+ * that will ultimately be loaded when you press enter.
+ **/
+
+async function cleanup() {
+ let suggestPrefs = ["history", "bookmark", "openpage"];
+ for (let type of suggestPrefs) {
+ Services.prefs.clearUserPref("browser.urlbar.suggest." + type);
+ }
+ await cleanupPlaces();
+}
+
+testEngine_setup();
+
+let path;
+let search;
+let searchCase;
+let title;
+let url;
+const host = "example.com";
+let origins;
+
+function add_autofill_task(callback) {
+ let func = async () => {
+ info(`Running subtest with origins disabled: ${callback.name}`);
+ origins = false;
+ path = "/foo";
+ search = "example.com/f";
+ searchCase = "EXAMPLE.COM/f";
+ title = "example.com/foo";
+ url = host + path;
+ await callback();
+
+ info(`Running subtest with origins enabled: ${callback.name}`);
+ origins = true;
+ path = "/";
+ search = "ex";
+ searchCase = "EX";
+ title = "example.com";
+ url = host + path;
+ await callback();
+ };
+ Object.defineProperty(func, "name", { value: callback.name });
+ add_task(func);
+}
+
+// "ex" should match http://example.com/.
+add_autofill_task(async function basic() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://" + url,
+ },
+ ]);
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "EX" should match http://example.com/.
+add_autofill_task(async function basicCase() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://" + url,
+ },
+ ]);
+ let context = createContext(searchCase, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: searchCase + url.substr(searchCase.length),
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "ex" should match http://www.example.com/.
+add_autofill_task(async function noWWWShouldMatchWWW() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://www." + url,
+ },
+ ]);
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://www." + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://www." + url,
+ title: "www." + title,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "EX" should match http://www.example.com/.
+add_autofill_task(async function noWWWShouldMatchWWWCase() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://www." + url,
+ },
+ ]);
+ let context = createContext(searchCase, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: searchCase + url.substr(searchCase.length),
+ completed: "http://www." + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://www." + url,
+ title: "www." + title,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "www.ex" should *not* match http://example.com/.
+add_autofill_task(async function wwwShouldNotMatchNoWWW() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://" + url,
+ },
+ ]);
+ let context = createContext("www." + search, { isPrivate: false });
+ if (origins) {
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://www." + search + "/",
+ title: "http://www." + search + "/",
+ displayUrl: "http://www." + search,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ } else {
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://www." + search,
+ title: "http://www." + search,
+ iconUri: `page-icon:http://www.${host}/`,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ }
+ await cleanup();
+});
+
+// "http://ex" should match http://example.com/.
+add_autofill_task(async function prefix() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://" + url,
+ },
+ ]);
+ let context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://" + url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "HTTP://EX" should match http://example.com/.
+add_autofill_task(async function prefixCase() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://" + url,
+ },
+ ]);
+ let context = createContext("HTTP://" + searchCase, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "HTTP://" + searchCase + url.substr(searchCase.length),
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "http://ex" should match http://www.example.com/.
+add_autofill_task(async function prefixNoWWWShouldMatchWWW() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://www." + url,
+ },
+ ]);
+ let context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://" + url,
+ completed: "http://www." + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://www." + url,
+ title: "www." + title,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "HTTP://EX" should match http://www.example.com/.
+add_autofill_task(async function prefixNoWWWShouldMatchWWWCase() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://www." + url,
+ },
+ ]);
+ let context = createContext("HTTP://" + searchCase, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "HTTP://" + searchCase + url.substr(searchCase.length),
+ completed: "http://www." + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://www." + url,
+ title: "www." + title,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "http://www.ex" should *not* match http://example.com/.
+add_autofill_task(async function prefixWWWShouldNotMatchNoWWW() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://" + url,
+ },
+ ]);
+ let context = createContext("http://www." + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://www.${search}/` : `http://www.${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ title: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://www.${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "http://ex" should *not* match https://example.com/.
+add_autofill_task(async function httpPrefixShouldNotMatchHTTPS() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "https://" + url,
+ },
+ ]);
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ title: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: "test visit for https://" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "ex" should match https://example.com/.
+add_autofill_task(async function httpsBasic() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "https://" + url,
+ },
+ ]);
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "https://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: "https://" + title,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "ex" should match https://www.example.com/.
+add_autofill_task(async function httpsNoWWWShouldMatchWWW() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "https://www." + url,
+ },
+ ]);
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "https://www." + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www." + url,
+ title: "https://www." + title,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "www.ex" should *not* match https://example.com/.
+add_autofill_task(async function httpsWWWShouldNotMatchNoWWW() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "https://" + url,
+ },
+ ]);
+ let context = createContext("www." + search, { isPrivate: false });
+ if (origins) {
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://www." + search + "/",
+ title: "http://www." + search + "/",
+ displayUrl: "http://www." + search,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ } else {
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://www." + search,
+ title: "http://www." + search,
+ iconUri: `page-icon:http://www.${host}/`,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ }
+ await cleanup();
+});
+
+// "https://ex" should match https://example.com/.
+add_autofill_task(async function httpsPrefix() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "https://" + url,
+ },
+ ]);
+ let context = createContext("https://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "https://" + url,
+ completed: "https://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: "https://" + title,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "https://ex" should match https://www.example.com/.
+add_autofill_task(async function httpsPrefixNoWWWShouldMatchWWW() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "https://www." + url,
+ },
+ ]);
+ let context = createContext("https://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "https://" + url,
+ completed: "https://www." + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www." + url,
+ title: "https://www." + title,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "https://www.ex" should *not* match https://example.com/.
+add_autofill_task(async function httpsPrefixWWWShouldNotMatchNoWWW() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "https://" + url,
+ },
+ ]);
+ let context = createContext("https://www." + search, { isPrivate: false });
+ let prefixedUrl = origins
+ ? `https://www.${search}/`
+ : `https://www.${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ title: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:https://www.${host}/`,
+ providerame: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "https://ex" should *not* match http://example.com/.
+add_autofill_task(async function httpsPrefixShouldNotMatchHTTP() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://" + url,
+ },
+ ]);
+ let context = createContext("https://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `https://${search}/` : `https://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ title: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:https://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: "test visit for http://" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "https://ex" should *not* match http://example.com/, even if the latter is
+// more frecent and both could be autofilled.
+add_autofill_task(async function httpsPrefixShouldNotMatchMoreFrecentHTTP() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://" + url,
+ transition: PlacesUtils.history.TRANSITIONS.TYPED,
+ },
+ {
+ uri: "http://" + url,
+ },
+ {
+ uri: "https://" + url,
+ transition: PlacesUtils.history.TRANSITIONS.TYPED,
+ },
+ {
+ uri: "http://otherpage",
+ },
+ ]);
+ let context = createContext("https://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "https://" + url,
+ completed: "https://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: "https://" + title,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Autofill should respond to frecency changes.
+add_autofill_task(async function frecency() {
+ // Start with an http visit. It should be completed.
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://" + url,
+ },
+ ]);
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // Add two https visits. https should now be completed.
+ for (let i = 0; i < 2; i++) {
+ await PlacesTestUtils.addVisits([{ uri: "https://" + url }]);
+ }
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "https://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: "https://" + title,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // Add two more http visits, three total. http should now be completed
+ // again.
+ for (let i = 0; i < 2; i++) {
+ await PlacesTestUtils.addVisits([{ uri: "http://" + url }]);
+ }
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: "test visit for https://" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+
+ // Add four www https visits. www https should now be completed.
+ for (let i = 0; i < 4; i++) {
+ await PlacesTestUtils.addVisits([{ uri: "https://www." + url }]);
+ }
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "https://www." + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www." + url,
+ title: "https://www." + title,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: "test visit for https://" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+
+ // Remove the www https page.
+ await PlacesUtils.history.remove(["https://www." + url]);
+
+ // http should now be completed again.
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: "test visit for https://" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+
+ // Remove the http page.
+ await PlacesUtils.history.remove(["http://" + url]);
+
+ // https should now be completed again.
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "https://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: "https://" + title,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // Add a visit with a different host so that "ex" doesn't autofill it.
+ // https://example.com/ should still have a higher frecency though, so it
+ // should still be autofilled.
+ await PlacesTestUtils.addVisits([{ uri: "https://not-" + url }]);
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "https://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: "https://" + title,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://not-" + url,
+ title: "test visit for https://not-" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+
+ // Now add 10 more visits to the different host so that the frecency of
+ // https://example.com/ falls below the autofill threshold. It should not
+ // be autofilled now.
+ for (let i = 0; i < 10; i++) {
+ await PlacesTestUtils.addVisits([{ uri: "https://not-" + url }]);
+ }
+
+ // In the `origins` case, the failure to make an autofill
+ // match means UnifiedComplete should not create a heuristic result. In the
+ // `!origins` case, autofill should still happen since there's no threshold
+ // comparison.
+ context = createContext(search, { isPrivate: false });
+ if (origins) {
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "https://not-" + url,
+ title: "test visit for https://not-" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: "test visit for https://" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ } else {
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "https://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: "https://" + title,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://not-" + url,
+ title: "test visit for https://not-" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ }
+
+ // Remove the visits to the different host.
+ await PlacesUtils.history.remove(["https://not-" + url]);
+
+ // https should be completed again.
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "https://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: "https://" + title,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // Remove the https visits.
+ await PlacesUtils.history.remove(["https://" + url]);
+
+ // Now nothing should be completed.
+ context = createContext(search, { isPrivate: false });
+ if (origins) {
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ } else {
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://" + search,
+ title: "http://" + search,
+ iconUri: `page-icon:http://${host}/`,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ }
+
+ await cleanup();
+});
+
+// Bookmarked places should always be autofilled, even when they don't meet
+// the threshold.
+add_autofill_task(async function bookmarkBelowThreshold() {
+ // Add some visits to a URL so that the origin autofill threshold is large.
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://not-" + url,
+ },
+ ]);
+ }
+
+ // Now bookmark another URL.
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://" + url,
+ });
+
+ // Make sure the bookmarked origin and place frecencies are below the
+ // threshold so that the origin/URL otherwise would not be autofilled.
+ let placeFrecency = await PlacesTestUtils.fieldInDB(
+ "http://" + url,
+ "frecency"
+ );
+ let originFrecency = await getOriginFrecency("http://", host);
+ let threshold = await getOriginAutofillThreshold();
+ Assert.ok(
+ placeFrecency < threshold,
+ `Place frecency should be below the threshold: ` +
+ `placeFrecency=${placeFrecency} threshold=${threshold}`
+ );
+ Assert.ok(
+ originFrecency < threshold,
+ `Origin frecency should be below the threshold: ` +
+ `originFrecency=${originFrecency} threshold=${threshold}`
+ );
+
+ // The bookmark should be autofilled.
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://not-" + url,
+ title: "test visit for http://not-" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+
+ await cleanup();
+});
+
+// Bookmarked places should be autofilled when they *do* meet the threshold.
+add_autofill_task(async function bookmarkAboveThreshold() {
+ // Bookmark a URL.
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://" + url,
+ });
+
+ // The frecencies of the place and origin should be >= the threshold. In
+ // fact they should be the same as the threshold since the place is the only
+ // place in the database.
+ let placeFrecency = await PlacesTestUtils.fieldInDB(
+ "http://" + url,
+ "frecency"
+ );
+ let originFrecency = await getOriginFrecency("http://", host);
+ let threshold = await getOriginAutofillThreshold();
+ Assert.equal(placeFrecency, threshold);
+ Assert.equal(originFrecency, threshold);
+
+ // The bookmark should be autofilled.
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanup();
+});
+
+// Bookmark a page and then clear history. The bookmarked origin/URL should
+// be autofilled even though its frecency is <= 0 since the autofill threshold
+// is 0.
+add_autofill_task(async function zeroThreshold() {
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://" + url,
+ });
+
+ await PlacesUtils.history.clear();
+
+ // Make sure the place's frecency is <= 0. (It will be reset to -1 on the
+ // history.clear() above, and then on idle it will be reset to 0. xpcshell
+ // tests disable the idle service, so in practice it should always be -1,
+ // but in order to avoid possible intermittent failures in the future, don't
+ // assume that.)
+ let placeFrecency = await PlacesTestUtils.fieldInDB(
+ "http://" + url,
+ "frecency"
+ );
+ Assert.ok(placeFrecency <= 0);
+
+ // Make sure the origin's frecency is 0.
+ let originFrecency = await getOriginFrecency("http://", host);
+ Assert.equal(originFrecency, 0);
+
+ // Make sure the autofill threshold is 0.
+ let threshold = await getOriginAutofillThreshold();
+ Assert.equal(threshold, 0);
+
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = false
+// suggest.bookmark = true
+// search for: visit
+// prefix search: no
+// prefix matches search: n/a
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(async function suggestHistoryFalse_visit() {
+ await PlacesTestUtils.addVisits("http://" + url);
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ ],
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ context = createContext(search, { isPrivate: false });
+ if (origins) {
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ } else {
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://" + search,
+ title: "http://" + search,
+ iconUri: `page-icon:http://${host}/`,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ }
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = false
+// suggest.bookmark = true
+// search for: visit
+// prefix search: yes
+// prefix matches search: yes
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(async function suggestHistoryFalse_visit_prefix() {
+ await PlacesTestUtils.addVisits("http://" + url);
+ let context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://" + url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ ],
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ context = createContext(search, { isPrivate: false });
+ if (origins) {
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ } else {
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://" + search,
+ title: "http://" + search,
+ iconUri: `page-icon:http://${host}/`,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ }
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = false
+// suggest.bookmark = true
+// search for: bookmark
+// prefix search: no
+// prefix matches search: n/a
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: yes
+add_autofill_task(async function suggestHistoryFalse_bookmark_0() {
+ // Add the bookmark.
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://" + url,
+ });
+
+ // Make the bookmark fall below the autofill frecency threshold so we ensure
+ // the bookmark is always autofilled in this case, even if it doesn't meet
+ // the threshold.
+ let meetsThreshold = true;
+ while (meetsThreshold) {
+ // Add a visit to another origin to boost the threshold.
+ await PlacesTestUtils.addVisits("http://foo-" + url);
+ let originFrecency = await getOriginFrecency("http://", host);
+ let threshold = await getOriginAutofillThreshold();
+ meetsThreshold = threshold <= originFrecency;
+ }
+
+ // At this point, the bookmark doesn't meet the threshold, but it should
+ // still be autofilled.
+ let originFrecency = await getOriginFrecency("http://", host);
+ let threshold = await getOriginAutofillThreshold();
+ Assert.ok(originFrecency < threshold);
+
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = false
+// suggest.bookmark = true
+// search for: bookmark
+// prefix search: no
+// prefix matches search: n/a
+// origin matches search: no
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(async function suggestHistoryFalse_bookmark_1() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://non-matching-" + url,
+ });
+ let context = createContext(search, { isPrivate: false });
+ let matches = [
+ makeBookmarkResult(context, {
+ uri: "http://non-matching-" + url,
+ title: "A bookmark",
+ }),
+ ];
+ if (origins) {
+ matches.unshift(
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ })
+ );
+ } else {
+ matches.unshift(
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://" + search,
+ title: "http://" + search,
+ iconUri: `page-icon:http://${host}/`,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ })
+ );
+ }
+ await check_results({
+ context,
+ matches,
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = false
+// suggest.bookmark = true
+// search for: bookmark
+// prefix search: yes
+// prefix matches search: yes
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: yes
+add_autofill_task(async function suggestHistoryFalse_bookmark_prefix_0() {
+ // Add the bookmark.
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://" + url,
+ });
+
+ // Make the bookmark fall below the autofill frecency threshold so we ensure
+ // the bookmark is always autofilled in this case, even if it doesn't meet
+ // the threshold.
+ let meetsThreshold = true;
+ while (meetsThreshold) {
+ // Add a visit to another origin to boost the threshold.
+ await PlacesTestUtils.addVisits("http://foo-" + url);
+ let originFrecency = await getOriginFrecency("http://", host);
+ let threshold = await getOriginAutofillThreshold();
+ meetsThreshold = threshold <= originFrecency;
+ }
+
+ // At this point, the bookmark doesn't meet the threshold, but it should
+ // still be autofilled.
+ let originFrecency = await getOriginFrecency("http://", host);
+ let threshold = await getOriginAutofillThreshold();
+ Assert.ok(originFrecency < threshold);
+
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://" + url,
+ });
+ let context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://" + url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = false
+// suggest.bookmark = true
+// search for: bookmark
+// prefix search: yes
+// prefix matches search: no
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(async function suggestHistoryFalse_bookmark_prefix_1() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "ftp://" + url,
+ });
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ title: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeBookmarkResult(context, {
+ uri: "ftp://" + url,
+ title: "A bookmark",
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = false
+// suggest.bookmark = true
+// search for: bookmark
+// prefix search: yes
+// prefix matches search: yes
+// origin matches search: no
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(async function suggestHistoryFalse_bookmark_prefix_2() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://non-matching-" + url,
+ });
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ title: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://non-matching-" + url,
+ title: "A bookmark",
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = false
+// suggest.bookmark = true
+// search for: bookmark
+// prefix search: yes
+// prefix matches search: no
+// origin matches search: no
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(async function suggestHistoryFalse_bookmark_prefix_3() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "ftp://non-matching-" + url,
+ });
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ title: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeBookmarkResult(context, {
+ uri: "ftp://non-matching-" + url,
+ title: "A bookmark",
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visit
+// prefix search: no
+// prefix matches search: n/a
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: yes
+add_autofill_task(async function suggestBookmarkFalse_visit_0() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ await PlacesTestUtils.addVisits("http://" + url);
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visit
+// prefix search: no
+// prefix matches search: n/a
+// origin matches search: no
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(async function suggestBookmarkFalse_visit_1() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ await PlacesTestUtils.addVisits("http://non-matching-" + url);
+ let context = createContext(search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ let matches = [
+ makeVisitResult(context, {
+ uri: "http://non-matching-" + url,
+ title: "test visit for http://non-matching-" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ];
+ if (origins) {
+ matches.unshift(
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ })
+ );
+ } else {
+ matches.unshift(
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ title: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ })
+ );
+ }
+ await check_results({
+ context,
+ matches,
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visit
+// prefix search: yes
+// prefix matches search: yes
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: yes
+add_autofill_task(async function suggestBookmarkFalse_visit_prefix_0() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ await PlacesTestUtils.addVisits("http://" + url);
+ let context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://" + url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visit
+// prefix search: yes
+// prefix matches search: no
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(async function suggestBookmarkFalse_visit_prefix_1() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ await PlacesTestUtils.addVisits("ftp://" + url);
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ title: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "ftp://" + url,
+ title: "test visit for ftp://" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visit
+// prefix search: yes
+// prefix matches search: yes
+// origin matches search: no
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(async function suggestBookmarkFalse_visit_prefix_2() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ await PlacesTestUtils.addVisits("http://non-matching-" + url);
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ title: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://non-matching-" + url,
+ title: "test visit for http://non-matching-" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visit
+// prefix search: yes
+// prefix matches search: no
+// origin matches search: no
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(async function suggestBookmarkFalse_visit_prefix_3() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ await PlacesTestUtils.addVisits("ftp://non-matching-" + url);
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ title: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "ftp://non-matching-" + url,
+ title: "test visit for ftp://non-matching-" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: unvisited bookmark
+// prefix search: no
+// prefix matches search: n/a
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(async function suggestBookmarkFalse_unvisitedBookmark() {
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://" + url,
+ });
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ ],
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ context = createContext(search, { isPrivate: false });
+ if (origins) {
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ } else {
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://" + search,
+ title: "http://" + search,
+ iconUri: `page-icon:http://${host}/`,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ }
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: unvisited bookmark
+// prefix search: yes
+// prefix matches search: yes
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(
+ async function suggestBookmarkFalse_unvisitedBookmark_prefix_0() {
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://" + url,
+ });
+ let context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://" + url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ ],
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ title: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+ }
+);
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: unvisited bookmark
+// prefix search: yes
+// prefix matches search: no
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(
+ async function suggestBookmarkFalse_unvisitedBookmark_prefix_1() {
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "ftp://" + url,
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ title: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+ }
+);
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: unvisited bookmark
+// prefix search: yes
+// prefix matches search: yes
+// origin matches search: no
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(
+ async function suggestBookmarkFalse_unvisitedBookmark_prefix_2() {
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://non-matching-" + url,
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ title: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+ }
+);
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: unvisited bookmark
+// prefix search: yes
+// prefix matches search: no
+// origin matches search: no
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(
+ async function suggestBookmarkFalse_unvisitedBookmark_prefix_3() {
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "ftp://non-matching-" + url,
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ title: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+ }
+);
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visited bookmark above autofill threshold
+// prefix search: no
+// prefix matches search: n/a
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: yes
+add_autofill_task(async function suggestBookmarkFalse_visitedBookmark_above() {
+ await PlacesTestUtils.addVisits("http://" + url);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://" + url,
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visited bookmark above autofill threshold
+// prefix search: yes
+// prefix matches search: yes
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: yes
+add_autofill_task(
+ async function suggestBookmarkFalse_visitedBookmarkAbove_prefix_0() {
+ await PlacesTestUtils.addVisits("http://" + url);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://" + url,
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ let context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://" + url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+ }
+);
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visited bookmark above autofill threshold
+// prefix search: yes
+// prefix matches search: no
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(
+ async function suggestBookmarkFalse_visitedBookmarkAbove_prefix_1() {
+ await PlacesTestUtils.addVisits("ftp://" + url);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "ftp://" + url,
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ title: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeBookmarkResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ uri: "ftp://" + url,
+ title: "A bookmark",
+ }),
+ ],
+ });
+ await cleanup();
+ }
+);
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visited bookmark above autofill threshold
+// prefix search: yes
+// prefix matches search: yes
+// origin matches search: no
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(
+ async function suggestBookmarkFalse_visitedBookmarkAbove_prefix_2() {
+ await PlacesTestUtils.addVisits("http://non-matching-" + url);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://non-matching-" + url,
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ title: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeBookmarkResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ uri: "http://non-matching-" + url,
+ title: "A bookmark",
+ }),
+ ],
+ });
+ await cleanup();
+ }
+);
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visited bookmark above autofill threshold
+// prefix search: yes
+// prefix matches search: no
+// origin matches search: no
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(
+ async function suggestBookmarkFalse_visitedBookmarkAbove_prefix_3() {
+ await PlacesTestUtils.addVisits("ftp://non-matching-" + url);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "ftp://non-matching-" + url,
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ title: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeBookmarkResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ uri: "ftp://non-matching-" + url,
+ title: "A bookmark",
+ }),
+ ],
+ });
+ await cleanup();
+ }
+);
+
+// The following suggestBookmarkFalse_visitedBookmarkBelow* tests are similar
+// to the suggestBookmarkFalse_visitedBookmarkAbove* tests, but instead of
+// checking visited bookmarks above the autofill threshold, they check visited
+// bookmarks below the threshold. These tests don't make sense for URL
+// queries (as opposed to origin queries) because URL queries don't use the
+// same autofill threshold, so we skip them when !origins.
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visited bookmark below autofill threshold
+// prefix search: no
+// prefix matches search: n/a
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(async function suggestBookmarkFalse_visitedBookmarkBelow() {
+ if (!origins) {
+ // See comment above suggestBookmarkFalse_visitedBookmarkBelow.
+ return;
+ }
+ // First, make sure that `url` is below the autofill threshold.
+ await PlacesTestUtils.addVisits("http://" + url);
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits("http://some-other-" + url);
+ }
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://some-other-" + url,
+ title: "test visit for http://some-other-" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: "test visit for http://" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ // Now bookmark it and set suggest.bookmark to false.
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://" + url,
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://some-other-" + url,
+ title: "test visit for http://some-other-" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: "A bookmark",
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visited bookmark below autofill threshold
+// prefix search: yes
+// prefix matches search: yes
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(
+ async function suggestBookmarkFalse_visitedBookmarkBelow_prefix_0() {
+ if (!origins) {
+ // See comment above suggestBookmarkFalse_visitedBookmarkBelow.
+ return;
+ }
+ // First, make sure that `url` is below the autofill threshold.
+ await PlacesTestUtils.addVisits("http://" + url);
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits("http://some-other-" + url);
+ }
+ let context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${search}/`,
+ title: `http://${search}/`,
+ heuristic: true,
+ iconUri: "",
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://some-other-" + url,
+ title: "test visit for http://some-other-" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: "test visit for http://" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ // Now bookmark it and set suggest.bookmark to false.
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://" + url,
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${search}/`,
+ title: `http://${search}/`,
+ heuristic: true,
+ iconUri: "",
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://some-other-" + url,
+ title: "test visit for http://some-other-" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: "A bookmark",
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+ }
+);
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visited bookmark below autofill threshold
+// prefix search: yes
+// prefix matches search: no
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(
+ async function suggestBookmarkFalse_visitedBookmarkBelow_prefix_1() {
+ if (!origins) {
+ // See comment above suggestBookmarkFalse_visitedBookmarkBelow.
+ return;
+ }
+ // First, make sure that `url` is below the autofill threshold.
+ await PlacesTestUtils.addVisits("ftp://" + url);
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits("ftp://some-other-" + url);
+ }
+ let context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${search}/`,
+ title: `http://${search}/`,
+ heuristic: true,
+ iconUri: "",
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "ftp://some-other-" + url,
+ title: "test visit for ftp://some-other-" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "ftp://" + url,
+ title: "test visit for ftp://" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ // Now bookmark it and set suggest.bookmark to false.
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "ftp://" + url,
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${search}/`,
+ title: `http://${search}/`,
+ heuristic: true,
+ iconUri: "",
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "ftp://some-other-" + url,
+ title: "test visit for ftp://some-other-" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "ftp://" + url,
+ title: "A bookmark",
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+ }
+);
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visited bookmark below autofill threshold
+// prefix search: yes
+// prefix matches search: yes
+// origin matches search: no
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(
+ async function suggestBookmarkFalse_visitedBookmarkBelow_prefix_2() {
+ if (!origins) {
+ // See comment above suggestBookmarkFalse_visitedBookmarkBelow.
+ return;
+ }
+ // First, make sure that `url` is below the autofill threshold.
+ await PlacesTestUtils.addVisits("http://non-matching-" + url);
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits("http://some-other-" + url);
+ }
+ let context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${search}/`,
+ title: `http://${search}/`,
+ heuristic: true,
+ iconUri: "",
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://some-other-" + url,
+ title: "test visit for http://some-other-" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://non-matching-" + url,
+ title: "test visit for http://non-matching-" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ // Now bookmark it and set suggest.bookmark to false.
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://non-matching-" + url,
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${search}/`,
+ title: `http://${search}/`,
+ heuristic: true,
+ iconUri: "",
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://some-other-" + url,
+ title: "test visit for http://some-other-" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://non-matching-" + url,
+ title: "A bookmark",
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+ }
+);
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visited bookmark below autofill threshold
+// prefix search: yes
+// prefix matches search: no
+// origin matches search: no
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(
+ async function suggestBookmarkFalse_visitedBookmarkBelow_prefix_3() {
+ if (!origins) {
+ // See comment above suggestBookmarkFalse_visitedBookmarkBelow.
+ return;
+ }
+ // First, make sure that `url` is below the autofill threshold.
+ await PlacesTestUtils.addVisits("ftp://non-matching-" + url);
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits("ftp://some-other-" + url);
+ }
+ let context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${search}/`,
+ title: `http://${search}/`,
+ heuristic: true,
+ iconUri: "",
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "ftp://some-other-" + url,
+ title: "test visit for ftp://some-other-" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "ftp://non-matching-" + url,
+ title: "test visit for ftp://non-matching-" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ // Now bookmark it and set suggest.bookmark to false.
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "ftp://non-matching-" + url,
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${search}/`,
+ title: `http://${search}/`,
+ heuristic: true,
+ iconUri: "",
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "ftp://some-other-" + url,
+ title: "test visit for ftp://some-other-" + url,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "ftp://non-matching-" + url,
+ title: "A bookmark",
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+ }
+);
diff --git a/browser/components/urlbar/tests/unit/test_autofill_prefix_fallback.js b/browser/components/urlbar/tests/unit/test_autofill_prefix_fallback.js
new file mode 100644
index 0000000000..ef3366d8db
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_prefix_fallback.js
@@ -0,0 +1,74 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This tests autofill prefix fallback in case multiple origins have the same
+// exact frecency.
+// We should prefer https, or in case of other prefixes just sort by descending
+// id.
+
+add_task(async function() {
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+
+ let host = "example.com";
+ let prefixes = ["https://", "https://www.", "http://", "http://www."];
+ for (let prefix of prefixes) {
+ await PlacesUtils.bookmarks.insert({
+ url: `${prefix}${host}`,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ }
+ await checkOriginsOrder(host, prefixes);
+
+ // The https://www version should be filled because it's https and the www
+ // version has been added later so it has an higher id.
+ let context = createContext("ex", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: `${host}/`,
+ completed: `https://www.${host}/`,
+ matches: [
+ makeVisitResult(context, {
+ uri: `https://www.${host}/`,
+ title: `https://www.${host}`,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: `https://${host}/`,
+ title: `${host}`,
+ }),
+ ],
+ });
+
+ // Remove and reinsert bookmarks in another order.
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+ prefixes = ["https://www.", "http://", "https://", "http://www."];
+ for (let prefix of prefixes) {
+ await PlacesUtils.bookmarks.insert({
+ url: `${prefix}${host}`,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ }
+ await checkOriginsOrder(host, prefixes);
+
+ await check_results({
+ context,
+ autofilled: `${host}/`,
+ completed: `https://${host}/`,
+ matches: [
+ makeVisitResult(context, {
+ uri: `https://${host}/`,
+ title: `https://${host}`,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: `https://www.${host}/`,
+ title: `www.${host}`,
+ }),
+ ],
+ });
+});
diff --git a/browser/components/urlbar/tests/unit/test_autofill_search_engine_aliases.js b/browser/components/urlbar/tests/unit/test_autofill_search_engine_aliases.js
new file mode 100644
index 0000000000..9f87947770
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_search_engine_aliases.js
@@ -0,0 +1,90 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Tests autofilling search engine token ("@") aliases.
+
+"use strict";
+
+const TEST_ENGINE_NAME = "test autofill aliases";
+const TEST_ENGINE_ALIAS = "@autofilltest";
+
+add_task(async function init() {
+ // Add an engine with an "@" alias.
+ await Services.search.addEngineWithDetails(TEST_ENGINE_NAME, {
+ alias: TEST_ENGINE_ALIAS,
+ template: "http://example.com/?search={searchTerms}",
+ });
+ registerCleanupFunction(async () => {
+ let engine = Services.search.getEngineByName(TEST_ENGINE_NAME);
+ Assert.ok(engine);
+ await Services.search.removeEngine(engine);
+ });
+});
+
+// Searching for @autofi should autofill to @autofilltest.
+add_task(async function basic() {
+ // Add a history visit that should normally match but for the fact that the
+ // search uses an @ alias. When an @ alias is autofilled, there should be no
+ // other matches except the autofill heuristic match.
+ await PlacesTestUtils.addVisits({
+ uri: "http://example.com/",
+ title: TEST_ENGINE_ALIAS,
+ });
+
+ let search = TEST_ENGINE_ALIAS.substr(
+ 0,
+ Math.round(TEST_ENGINE_ALIAS.length / 2)
+ );
+ let autofilledValue = TEST_ENGINE_ALIAS + " ";
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: autofilledValue,
+ matches: [
+ makeSearchResult(context, {
+ engineName: TEST_ENGINE_NAME,
+ alias: TEST_ENGINE_ALIAS,
+ query: "",
+ providesSearchMode: true,
+ heuristic: false,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+// Searching for @AUTOFI should autofill to @AUTOFIlltest, preserving the case
+// in the search string.
+add_task(async function preserveCase() {
+ // Add a history visit that should normally match but for the fact that the
+ // search uses an @ alias. When an @ alias is autofilled, there should be no
+ // other matches except the autofill heuristic match.
+ await PlacesTestUtils.addVisits({
+ uri: "http://example.com/",
+ title: TEST_ENGINE_ALIAS,
+ });
+
+ let search = TEST_ENGINE_ALIAS.toUpperCase().substr(
+ 0,
+ Math.round(TEST_ENGINE_ALIAS.length / 2)
+ );
+ let alias = search + TEST_ENGINE_ALIAS.substr(search.length);
+
+ let autofilledValue = alias + " ";
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: autofilledValue,
+ matches: [
+ makeSearchResult(context, {
+ engineName: TEST_ENGINE_NAME,
+ alias,
+ query: "",
+ providesSearchMode: true,
+ heuristic: false,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_autofill_search_engines.js b/browser/components/urlbar/tests/unit/test_autofill_search_engines.js
new file mode 100644
index 0000000000..63af7115f2
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_search_engines.js
@@ -0,0 +1,234 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// The autoFill.searchEngines pref autofills the domains of engines registered
+// with the search service. That's what this test checks. It's a different
+// path in UnifiedComplete.js from normal moz_places autofill, which is tested
+// in test_autofill_origins.js and test_autofill_urls.js.
+
+"use strict";
+
+const ENGINE_NAME = "TestEngine";
+
+add_task(async function searchEngines() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ Services.prefs.setBoolPref(
+ "browser.search.separatePrivateDefault.ui.enabled",
+ false
+ );
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.autoFill.searchEngines");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ Services.prefs.clearUserPref(
+ "browser.search.separatePrivateDefault.ui.enabled"
+ );
+ });
+
+ let schemes = ["http", "https"];
+ for (let i = 0; i < schemes.length; i++) {
+ let scheme = schemes[i];
+ let engine = await Services.search.addEngineWithDetails(ENGINE_NAME, {
+ method: "GET",
+ template: scheme + "://www.example.com/",
+ searchGetParams: "q={searchTerms}",
+ });
+
+ let context = createContext("ex", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("example.com", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("example.com/", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("www.ex", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "www.example.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("www.example.com", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "www.example.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("www.example.com/", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "www.example.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext(scheme + "://ex", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: scheme + "://example.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext(scheme + "://example.com", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: scheme + "://example.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext(scheme + "://example.com/", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: scheme + "://example.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext(scheme + "://www.ex", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: scheme + "://www.example.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext(scheme + "://www.example.com", {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ autofilled: scheme + "://www.example.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext(scheme + "://www.example.com/", {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ search: scheme + "://www.example.com/",
+ autofilled: scheme + "://www.example.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // We should just get a normal heuristic result from HeuristicFallback for
+ // these queries.
+ let otherScheme = schemes[(i + 1) % schemes.length];
+ context = createContext(otherScheme + "://ex", { isPrivate: false });
+ await check_results({
+ context,
+ search: otherScheme + "://ex",
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: otherScheme + "://ex/",
+ title: otherScheme + "://ex/",
+ heuristic: true,
+ }),
+ ],
+ });
+ context = createContext(otherScheme + "://www.ex", { isPrivate: false });
+ await check_results({
+ context,
+ search: otherScheme + "://www.ex",
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: otherScheme + "://www.ex/",
+ title: otherScheme + "://www.ex/",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("example/", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://example/",
+ title: "http://example/",
+ iconUri: "page-icon:http://example/",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await Services.search.removeEngine(engine);
+ }
+});
diff --git a/browser/components/urlbar/tests/unit/test_autofill_urls.js b/browser/components/urlbar/tests/unit/test_autofill_urls.js
new file mode 100644
index 0000000000..d0424a4b8d
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_urls.js
@@ -0,0 +1,218 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const HEURISTIC_FALLBACK_PROVIDERNAME = "HeuristicFallback";
+const UNIFIEDCOMPLETE_PROVIDERNAME = "UnifiedComplete";
+
+// "example.com/foo/" should match http://example.com/foo/.
+testEngine_setup();
+
+add_task(async function multipleSlashes() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com/foo/",
+ },
+ ]);
+ let context = createContext("example.com/foo/", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/foo/",
+ completed: "http://example.com/foo/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://example.com/foo/",
+ title: "example.com/foo/",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+// "example.com:8888/f" should match http://example.com:8888/foo.
+add_task(async function port() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com:8888/foo",
+ },
+ ]);
+ let context = createContext("example.com:8888/f", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com:8888/foo",
+ completed: "http://example.com:8888/foo",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://example.com:8888/foo",
+ title: "example.com:8888/foo",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+// "example.com:8999/f" should *not* autofill http://example.com:8888/foo.
+add_task(async function portNoMatch() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com:8888/foo",
+ },
+ ]);
+ let context = createContext("example.com:8999/f", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://example.com:8999/f",
+ title: "http://example.com:8999/f",
+ iconUri: "page-icon:http://example.com:8999/",
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+// autofill to the next slash
+add_task(async function port() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com:8888/foo/bar/baz",
+ },
+ ]);
+ let context = createContext("example.com:8888/foo/b", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com:8888/foo/bar/",
+ completed: "http://example.com:8888/foo/bar/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://example.com:8888/foo/bar/",
+ title: "example.com:8888/foo/bar/",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com:8888/foo/bar/baz",
+ title: "test visit for http://example.com:8888/foo/bar/baz",
+ tags: [],
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+// autofill to the next slash, end of url
+add_task(async function port() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com:8888/foo/bar/baz",
+ },
+ ]);
+ let context = createContext("example.com:8888/foo/bar/b", {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ autofilled: "example.com:8888/foo/bar/baz",
+ completed: "http://example.com:8888/foo/bar/baz",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://example.com:8888/foo/bar/baz",
+ title: "example.com:8888/foo/bar/baz",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+// autofill with case insensitive from history and bookmark.
+add_task(async function caseInsensitiveFromHistoryAndBookmark() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", true);
+
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com/foo",
+ },
+ ]);
+
+ await testCaseInsensitive();
+
+ Services.prefs.clearUserPref("browser.urlbar.suggest.bookmark");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.history");
+ await cleanupPlaces();
+});
+
+// autofill with case insensitive from history.
+add_task(async function caseInsensitiveFromHistory() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", true);
+
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com/foo",
+ },
+ ]);
+
+ await testCaseInsensitive();
+
+ Services.prefs.clearUserPref("browser.urlbar.suggest.bookmark");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.history");
+ await cleanupPlaces();
+});
+
+// autofill with case insensitive from bookmark.
+add_task(async function caseInsensitiveFromBookmark() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://example.com/foo",
+ });
+
+ await testCaseInsensitive();
+
+ Services.prefs.clearUserPref("browser.urlbar.suggest.bookmark");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.history");
+ await cleanupPlaces();
+});
+
+async function testCaseInsensitive() {
+ const testData = [
+ {
+ input: "example.com/F",
+ expectedAutofill: "example.com/Foo",
+ },
+ {
+ // Test with prefix.
+ input: "http://example.com/F",
+ expectedAutofill: "http://example.com/Foo",
+ },
+ ];
+
+ for (const { input, expectedAutofill } of testData) {
+ const context = createContext(input, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ autofilled: expectedAutofill,
+ completed: "http://example.com/foo",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://example.com/foo",
+ title: "example.com/foo",
+ heuristic: true,
+ }),
+ ],
+ });
+ }
+}
diff --git a/browser/components/urlbar/tests/unit/test_avoid_middle_complete.js b/browser/components/urlbar/tests/unit/test_avoid_middle_complete.js
new file mode 100644
index 0000000000..a6de9f3a3a
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_avoid_middle_complete.js
@@ -0,0 +1,284 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const ENGINE_NAME = "engine-suggestions.xml";
+
+testEngine_setup();
+
+add_task(async function test_prefix_space_noautofill() {
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://moz.org/test/"),
+ });
+
+ info("Should not try to autoFill if search string contains a space");
+ let context = createContext(" mo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ query: " mo",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://moz.org/test/",
+ title: "test visit for http://moz.org/test/",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_trailing_space_noautofill() {
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://moz.org/test/"),
+ });
+
+ info("Should not try to autoFill if search string contains a space");
+ let context = createContext("mo ", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ query: "mo ",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://moz.org/test/",
+ title: "test visit for http://moz.org/test/",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_searchEngine_autofill() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
+ let engine = await Services.search.addEngineWithDetails("CakeSearch", {
+ method: "GET",
+ template: "http://cake.search/",
+ searchGetParams: "q={searchTerms}",
+ });
+ registerCleanupFunction(async () => Services.search.removeEngine(engine));
+
+ info(
+ "Should autoFill search engine if search string does not contains a space"
+ );
+ let context = createContext("ca", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: "CakeSearch",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_searchEngine_prefix_space_noautofill() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
+ let engine = await Services.search.addEngineWithDetails("CupcakeSearch", {
+ method: "GET",
+ template: "http://cupcake.search/",
+ searchGetParams: "q={searchTerms}",
+ });
+ registerCleanupFunction(async () => Services.search.removeEngine(engine));
+
+ info(
+ "Should not try to autoFill search engine if search string contains a space"
+ );
+ let context = createContext(" cu", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ query: " cu",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_searchEngine_trailing_space_noautofill() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
+ let engine = await Services.search.addEngineWithDetails("BaconSearch", {
+ method: "GET",
+ template: "http://bacon.search/",
+ searchGetParams: "q={searchTerms}",
+ });
+ registerCleanupFunction(async () => Services.search.removeEngine(engine));
+
+ info(
+ "Should not try to autoFill search engine if search string contains a space"
+ );
+ let context = createContext("ba ", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ query: "ba ",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_searchEngine_www_noautofill() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
+ let engine = await Services.search.addEngineWithDetails("HamSearch", {
+ method: "GET",
+ template: "http://ham.search/",
+ searchGetParams: "q={searchTerms}",
+ });
+ registerCleanupFunction(async () => Services.search.removeEngine(engine));
+
+ info(
+ "Should not autoFill search engine if search string contains www. but engine doesn't"
+ );
+ let context = createContext("www.ham", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://www.ham/",
+ title: "http://www.ham/",
+ displayUrl: "http://www.ham",
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ query: "www.ham",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_searchEngine_different_scheme_noautofill() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
+ let engine = await Services.search.addEngineWithDetails("PieSearch", {
+ method: "GET",
+ template: "https://pie.search/",
+ searchGetParams: "q={searchTerms}",
+ });
+ registerCleanupFunction(async () => Services.search.removeEngine(engine));
+
+ info(
+ "Should not autoFill search engine if search string has a different scheme."
+ );
+ let context = createContext("http://pie", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://pie/",
+ title: "http://pie/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_searchEngine_matching_prefix_autofill() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
+ let engine = await Services.search.addEngineWithDetails("BeanSearch", {
+ method: "GET",
+ template: "http://www.bean.search/",
+ searchGetParams: "q={searchTerms}",
+ });
+ registerCleanupFunction(async () => Services.search.removeEngine(engine));
+
+ info("Should autoFill search engine if search string has matching prefix.");
+ let context = createContext("http://www.be", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://www.bean.search/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: "BeanSearch",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Should autoFill search engine if search string has www prefix.");
+ context = createContext("www.be", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "www.bean.search/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: "BeanSearch",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Should autoFill search engine if search string has matching scheme.");
+ context = createContext("http://be", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://bean.search/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: "BeanSearch",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_prefix_autofill() {
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ });
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://moz.org/test/"),
+ });
+
+ info(
+ "Should not try to autoFill in-the-middle if a search is canceled immediately"
+ );
+ let context = createContext("mozi", { isPrivate: false });
+ await check_results({
+ context,
+ incompleteSearch: "moz",
+ autofilled: "mozilla.org/",
+ completed: "http://mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/",
+ title: "mozilla.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/test/",
+ title: "test visit for http://mozilla.org/test/",
+ providerName: "UnifiedComplete",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_avoid_stripping_to_empty_tokens.js b/browser/components/urlbar/tests/unit/test_avoid_stripping_to_empty_tokens.js
new file mode 100644
index 0000000000..b9e3227874
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_avoid_stripping_to_empty_tokens.js
@@ -0,0 +1,121 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const ENGINE_NAME = "engine-suggestions.xml";
+
+testEngine_setup();
+
+add_task(async function test_protocol_trimming() {
+ for (let prot of ["http", "https", "ftp"]) {
+ let visit = {
+ // Include the protocol in the query string to ensure we get matches (see bug 1059395)
+ uri: Services.io.newURI(
+ prot +
+ "://www.mozilla.org/test/?q=" +
+ prot +
+ encodeURIComponent("://") +
+ "www.foo"
+ ),
+ title: "Test title",
+ };
+ await PlacesTestUtils.addVisits(visit);
+
+ let input = prot + "://www.";
+ info("Searching for: " + input);
+ let context = createContext(input, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: prot + "://www.mozilla.org/",
+ completed: prot + "://www.mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: prot + "://www.mozilla.org/",
+ title:
+ prot == "http" ? "www.mozilla.org" : prot + "://www.mozilla.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: visit.uri.spec,
+ title: visit.title,
+ }),
+ ],
+ });
+
+ input = "www.";
+ info("Searching for: " + input);
+ context = createContext(input, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "www.mozilla.org/",
+ completed: prot + "://www.mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: prot + "://www.mozilla.org/",
+ title:
+ prot == "http" ? "www.mozilla.org" : prot + "://www.mozilla.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: visit.uri.spec,
+ title: visit.title,
+ }),
+ ],
+ });
+
+ input = prot + "://www. ";
+ info("Searching for: " + input);
+ context = createContext(input, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `${input.trim()}/`,
+ title: `${input.trim()}/`,
+ iconUri: "",
+ heuristic: true,
+ providerName: "HeuristicFallback",
+ }),
+ makeVisitResult(context, {
+ uri: visit.uri.spec,
+ title: visit.title,
+ providerName: "UnifiedComplete",
+ }),
+ ],
+ });
+
+ let inputs = [
+ prot + "://",
+ prot + ":// ",
+ prot + ":// mo",
+ prot + "://mo te",
+ prot + "://www. mo",
+ prot + "://www.mo te",
+ "www. ",
+ "www. mo",
+ "www.mo te",
+ ];
+ for (input of inputs) {
+ info("Searching for: " + input);
+ context = createContext(input, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ query: input,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: visit.uri.spec,
+ title: visit.title,
+ providerName: "UnifiedComplete",
+ }),
+ ],
+ });
+ }
+
+ await cleanupPlaces();
+ }
+});
diff --git a/browser/components/urlbar/tests/unit/test_casing.js b/browser/components/urlbar/tests/unit/test_casing.js
new file mode 100644
index 0000000000..89e58c45a9
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_casing.js
@@ -0,0 +1,356 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const ENGINE_NAME = "engine-suggestions.xml";
+const AUTOFILL_PROVIDERNAME = "Autofill";
+const HEURISTIC_FALLBACK_PROVIDERNAME = "HeuristicFallback";
+const UNIFIEDCOMPLETE_PROVIDERNAME = "UnifiedComplete";
+
+testEngine_setup();
+
+add_task(async function test_casing_1() {
+ info("Searching for cased entry 1");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ });
+ let context = createContext("MOZ", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "MOZilla.org/",
+ completed: "http://mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/",
+ title: "mozilla.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/test/",
+ title: "test visit for http://mozilla.org/test/",
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_casing_2() {
+ info("Searching for cased entry 2");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ });
+ let context = createContext("mozilla.org/T", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.org/Test/",
+ completed: "http://mozilla.org/test/",
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ uri: "http://mozilla.org/test/",
+ title: "mozilla.org/test/",
+ iconUri: "page-icon:http://mozilla.org/test/",
+ heuristic: true,
+ providerName: AUTOFILL_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_casing_3() {
+ info("Searching for cased entry 3");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/Test/"),
+ });
+ let context = createContext("mozilla.org/T", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.org/Test/",
+ completed: "http://mozilla.org/Test/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/Test/",
+ title: "mozilla.org/Test/",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_casing_4() {
+ info("Searching for cased entry 4");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/Test/"),
+ });
+ let context = createContext("mOzilla.org/t", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mOzilla.org/test/",
+ completed: "http://mozilla.org/Test/",
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ uri: "http://mozilla.org/Test/",
+ title: "mozilla.org/Test/",
+ iconUri: "page-icon:http://mozilla.org/Test/",
+ heuristic: true,
+ providerName: AUTOFILL_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_casing_5() {
+ info("Searching for cased entry 5");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/Test/"),
+ });
+ let context = createContext("mOzilla.org/T", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mOzilla.org/Test/",
+ completed: "http://mozilla.org/Test/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/Test/",
+ title: "mozilla.org/Test/",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_untrimmed_casing() {
+ info("Searching for untrimmed cased entry");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/Test/"),
+ });
+ let context = createContext("http://mOz", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://mOzilla.org/",
+ completed: "http://mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/",
+ title: "mozilla.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/Test/",
+ title: "test visit for http://mozilla.org/Test/",
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_untrimmed_www_casing() {
+ info("Searching for untrimmed cased entry with www");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://www.mozilla.org/Test/"),
+ });
+ let context = createContext("http://www.mOz", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://www.mOzilla.org/",
+ completed: "http://www.mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://www.mozilla.org/",
+ title: "www.mozilla.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://www.mozilla.org/Test/",
+ title: "test visit for http://www.mozilla.org/Test/",
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_untrimmed_path_casing() {
+ info("Searching for untrimmed cased entry with path");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/Test/"),
+ });
+ let context = createContext("http://mOzilla.org/t", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://mOzilla.org/test/",
+ completed: "http://mozilla.org/Test/",
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ uri: "http://mozilla.org/Test/",
+ title: "mozilla.org/Test/",
+ iconUri: "page-icon:http://mozilla.org/Test/",
+ heuristic: true,
+ providerName: AUTOFILL_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_untrimmed_path_casing_2() {
+ info("Searching for untrimmed cased entry with path 2");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/Test/"),
+ });
+ let context = createContext("http://mOzilla.org/T", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://mOzilla.org/Test/",
+ completed: "http://mozilla.org/Test/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/Test/",
+ title: "mozilla.org/Test/",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_untrimmed_path_www_casing() {
+ info("Searching for untrimmed cased entry with www and path");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://www.mozilla.org/Test/"),
+ });
+ let context = createContext("http://www.mOzilla.org/t", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://www.mOzilla.org/test/",
+ completed: "http://www.mozilla.org/Test/",
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ uri: "http://www.mozilla.org/Test/",
+ title: "www.mozilla.org/Test/",
+ iconUri: "page-icon:http://www.mozilla.org/Test/",
+ heuristic: true,
+ providerName: AUTOFILL_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_untrimmed_path_www_casing_2() {
+ info("Searching for untrimmed cased entry with www and path 2");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://www.mozilla.org/Test/"),
+ });
+ let context = createContext("http://www.mOzilla.org/T", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://www.mOzilla.org/Test/",
+ completed: "http://www.mozilla.org/Test/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://www.mozilla.org/Test/",
+ title: "www.mozilla.org/Test/",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_searching() {
+ let uri1 = Services.io.newURI("http://dummy/1/");
+ let uri2 = Services.io.newURI("http://dummy/2/");
+ let uri3 = Services.io.newURI("http://dummy/3/");
+ let uri4 = Services.io.newURI("http://dummy/4/");
+ let uri5 = Services.io.newURI("http://dummy/5/");
+
+ await PlacesTestUtils.addVisits([
+ { uri: uri1, title: "uppercase lambda \u039B" },
+ { uri: uri2, title: "lowercase lambda \u03BB" },
+ { uri: uri3, title: "symbol \u212A" }, // kelvin
+ { uri: uri4, title: "uppercase K" },
+ { uri: uri5, title: "lowercase k" },
+ ]);
+
+ info("Search for lowercase lambda");
+ let context = createContext("\u03BB", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeVisitResult(context, {
+ uri: uri2.spec,
+ title: "lowercase lambda \u03BB",
+ }),
+ makeVisitResult(context, {
+ uri: uri1.spec,
+ title: "uppercase lambda \u039B",
+ }),
+ ],
+ });
+
+ info("Search for uppercase lambda");
+ context = createContext("\u039B", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeVisitResult(context, {
+ uri: uri2.spec,
+ title: "lowercase lambda \u03BB",
+ }),
+ makeVisitResult(context, {
+ uri: uri1.spec,
+ title: "uppercase lambda \u039B",
+ }),
+ ],
+ });
+
+ info("Search for kelvin sign");
+ context = createContext("\u212A", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeVisitResult(context, { uri: uri5.spec, title: "lowercase k" }),
+ makeVisitResult(context, { uri: uri4.spec, title: "uppercase K" }),
+ makeVisitResult(context, { uri: uri3.spec, title: "symbol \u212A" }),
+ ],
+ });
+
+ info("Search for lowercase k");
+ context = createContext("k", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeVisitResult(context, { uri: uri5.spec, title: "lowercase k" }),
+ makeVisitResult(context, { uri: uri4.spec, title: "uppercase K" }),
+ makeVisitResult(context, { uri: uri3.spec, title: "symbol \u212A" }),
+ ],
+ });
+
+ info("Search for uppercase k");
+ context = createContext("K", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeVisitResult(context, { uri: uri5.spec, title: "lowercase k" }),
+ makeVisitResult(context, { uri: uri4.spec, title: "uppercase K" }),
+ makeVisitResult(context, { uri: uri3.spec, title: "symbol \u212A" }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_dedupe_prefix.js b/browser/components/urlbar/tests/unit/test_dedupe_prefix.js
new file mode 100644
index 0000000000..58f223fbc4
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_dedupe_prefix.js
@@ -0,0 +1,116 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Testing that we dedupe results that have the same URL and title as another
+// except for their prefix (e.g. http://www.).
+add_task(async function dedupe_prefix() {
+ // We need to set the title or else we won't dedupe. We only dedupe when
+ // titles match up to mitigate deduping when the www. version of a site is
+ // completely different from it's www-less counterpart and thus presumably
+ // has a different title.
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com/foo/",
+ title: "Example Page",
+ },
+ {
+ uri: "http://www.example.com/foo/",
+ title: "Example Page",
+ },
+ {
+ uri: "https://example.com/foo/",
+ title: "Example Page",
+ },
+ {
+ uri: "https://www.example.com/foo/",
+ title: "Example Page",
+ },
+ {
+ uri: "https://www.example.com/foo/",
+ title: "Example Page",
+ },
+ ]);
+
+ // We should get https://www. as the heuristic result but https:// in the
+ // results since the latter's prefix is a higher priority.
+ let context = createContext("example.com/foo/", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/foo/",
+ completed: "https://www.example.com/foo/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www.example.com/foo/",
+ title: "https://www.example.com/foo/",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://example.com/foo/",
+ title: "Example Page",
+ }),
+ ],
+ });
+
+ // Add more visits to the lowest-priority prefix. It should be the heuristic
+ // result but we should still show our highest-priority result. https://www.
+ // should not appear at all.
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://www.example.com/foo/",
+ title: "Example Page",
+ },
+ ]);
+ }
+
+ context = createContext("example.com/foo/", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/foo/",
+ completed: "http://www.example.com/foo/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://www.example.com/foo/",
+ title: "www.example.com/foo/",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://example.com/foo/",
+ title: "Example Page",
+ }),
+ ],
+ });
+
+ // Add enough https:// vists for it to have the highest frecency. It should
+ // be the heuristic result. We should still get the https://www. result
+ // because we still show results with the same key and protocol if they differ
+ // from the heuristic result in having www.
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "https://example.com/foo/",
+ title: "Example Page",
+ },
+ ]);
+ }
+
+ context = createContext("example.com/foo/", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/foo/",
+ completed: "https://example.com/foo/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://example.com/foo/",
+ title: "https://example.com/foo/",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://www.example.com/foo/",
+ title: "Example Page",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_dupe_urls.js b/browser/components/urlbar/tests/unit/test_dupe_urls.js
new file mode 100644
index 0000000000..9707233279
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_dupe_urls.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Ensure inline autocomplete doesn't return zero frecency pages.
+
+add_task(async function setup() {
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+});
+
+add_task(async function test_dupe_urls() {
+ info("Searching for urls with dupes should only show one");
+ await PlacesTestUtils.addVisits(
+ {
+ uri: Services.io.newURI("http://mozilla.org/"),
+ },
+ {
+ uri: Services.io.newURI("http://mozilla.org/?"),
+ }
+ );
+ let context = createContext("moz", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.org/",
+ completed: "http://mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/",
+ title: "mozilla.org",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_dupe_secure_urls() {
+ await PlacesTestUtils.addVisits(
+ {
+ uri: Services.io.newURI("https://example.org/"),
+ },
+ {
+ uri: Services.io.newURI("https://example.org/?"),
+ }
+ );
+ let context = createContext("exam", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.org/",
+ completed: "https://example.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://example.org/",
+ title: "https://example.org",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_encoded_urls.js b/browser/components/urlbar/tests/unit/test_encoded_urls.js
new file mode 100644
index 0000000000..5260683ddd
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_encoded_urls.js
@@ -0,0 +1,97 @@
+add_task(async function test_encoded() {
+ info("Searching for over encoded url should not break it");
+ let url = "https://www.mozilla.com/search/top/?q=%25%32%35";
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI(url),
+ title: url,
+ });
+ let context = createContext(url, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: url,
+ matches: [
+ makeVisitResult(context, {
+ uri: url,
+ title: url,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_encoded_trimmed() {
+ info("Searching for over encoded url should not break it");
+ let url = "https://www.mozilla.com/search/top/?q=%25%32%35";
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI(url),
+ title: url,
+ });
+ let context = createContext("mozilla.com/search/top/?q=%25%32%35", {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ autofilled: "mozilla.com/search/top/?q=%25%32%35",
+ completed: url,
+ matches: [
+ makeVisitResult(context, {
+ uri: url,
+ title: url,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_encoded_partial() {
+ info("Searching for over encoded url should not break it");
+ let url = "https://www.mozilla.com/search/top/?q=%25%32%35";
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI(url),
+ title: url,
+ });
+ let context = createContext("https://www.mozilla.com/search/top/?q=%25", {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: url,
+ matches: [
+ makeVisitResult(context, {
+ uri: url,
+ title: url,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_encoded_path() {
+ info("Searching for over encoded url should not break it");
+ let url = "https://www.mozilla.com/%25%32%35/top/";
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI(url),
+ title: url,
+ });
+ let context = createContext("https://www.mozilla.com/%25%32%35/t", {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: url,
+ title: url,
+ heuristic: true,
+ }),
+ ],
+ autofilled: url,
+ completed: url,
+ });
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_heuristic_cancel.js b/browser/components/urlbar/tests/unit/test_heuristic_cancel.js
new file mode 100644
index 0000000000..8806fe0601
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_heuristic_cancel.js
@@ -0,0 +1,136 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests that old results from UrlbarProviderAutofill do not overwrite results
+ * from UrlbarProviderHeuristicFallback after the autofillable query is
+ * cancelled. See bug 1653436.
+ */
+
+const { setTimeout } = ChromeUtils.import("resource://gre/modules/Timer.jsm");
+
+/**
+ * A test provider that waits before returning results to simulate a slow DB
+ * lookup.
+ */
+class SlowHeuristicProvider extends TestProvider {
+ get type() {
+ return UrlbarUtils.PROVIDER_TYPE.HEURISTIC;
+ }
+
+ async startQuery(context, add) {
+ this._context = context;
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 300));
+ for (let result of this._results) {
+ add(this, result);
+ }
+ }
+}
+
+/**
+ * A fast provider that alterts the test when it has added its results.
+ */
+class FastHeuristicProvider extends TestProvider {
+ get type() {
+ return UrlbarUtils.PROVIDER_TYPE.HEURISTIC;
+ }
+
+ async startQuery(context, add) {
+ this._context = context;
+ for (let result of this._results) {
+ add(this, result);
+ }
+ Services.obs.notifyObservers(null, "results-added");
+ }
+}
+
+add_task(async function setup() {
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+});
+
+add_task(async function() {
+ let context = createContext("m", { isPrivate: false });
+ await PlacesTestUtils.promiseAsyncUpdates();
+ info("Manually set up query and then overwrite it.");
+ // slowProvider is a stand-in for a slow UnifiedComplete returning a
+ // non-heuristic result.
+ let slowProvider = new SlowHeuristicProvider({
+ results: [
+ makeVisitResult(context, {
+ uri: `http://mozilla.org/`,
+ title: `mozilla.org/`,
+ }),
+ ],
+ });
+ UrlbarProvidersManager.registerProvider(slowProvider);
+
+ // fastProvider is a stand-in for a fast Autofill returning a heuristic
+ // result.
+ let fastProvider = new FastHeuristicProvider({
+ results: [
+ makeVisitResult(context, {
+ uri: `http://mozilla.com/`,
+ title: `mozilla.com/`,
+ heuristic: true,
+ }),
+ ],
+ });
+ UrlbarProvidersManager.registerProvider(fastProvider);
+ let firstContext = createContext("m", {
+ providers: [slowProvider.name, fastProvider.name],
+ });
+ let secondContext = createContext("ma", {
+ providers: [slowProvider.name, fastProvider.name],
+ });
+
+ let controller = UrlbarTestUtils.newMockController();
+ let queryRecieved, queryCancelled;
+ const controllerListener = {
+ onQueryResults(queryContext) {
+ console.trace(`finished query. context: ${JSON.stringify(queryContext)}`);
+ Assert.equal(
+ queryContext,
+ secondContext,
+ "Only the second query should finish."
+ );
+ queryRecieved = true;
+ },
+ onQueryCancelled(queryContext) {
+ Assert.equal(
+ queryContext,
+ firstContext,
+ "The first query should be cancelled."
+ );
+ Assert.ok(!queryCancelled, "No more than one query should be cancelled.");
+ queryCancelled = true;
+ },
+ };
+ controller.addQueryListener(controllerListener);
+
+ // Wait until FastProvider sends its results to the providers manager.
+ // Then they will be queued up in a _heuristicProvidersTimer, waiting for
+ // the results from SlowProvider.
+ let resultsAddedPromise = new Promise(resolve => {
+ let observe = async (subject, topic, data) => {
+ Services.obs.removeObserver(observe, "results-added");
+ // Fire the second query to cancel the first.
+ await controller.startQuery(secondContext);
+ resolve();
+ };
+
+ Services.obs.addObserver(observe, "results-added");
+ });
+
+ controller.startQuery(firstContext);
+ await resultsAddedPromise;
+
+ Assert.ok(queryCancelled, "At least one query was cancelled.");
+ Assert.ok(queryRecieved, "At least one query finished.");
+ controller.removeQueryListener(controllerListener);
+});
diff --git a/browser/components/urlbar/tests/unit/test_keywords.js b/browser/components/urlbar/tests/unit/test_keywords.js
new file mode 100644
index 0000000000..04984662d3
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_keywords.js
@@ -0,0 +1,207 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const ENGINE_NAME = "engine-suggestions.xml";
+
+testEngine_setup();
+
+add_task(async function test_non_keyword() {
+ info("Searching for non-keyworded entry should autoFill it");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ });
+ let context = createContext("moz", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.org/",
+ completed: "http://mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/",
+ title: "mozilla.org",
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://mozilla.org/test/",
+ title: "A bookmark",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_keyword() {
+ info("Searching for keyworded entry should not autoFill it");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ keyword: "moz",
+ });
+ let context = createContext("moz", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://mozilla.org/test/",
+ keyword: "moz",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_more_than_keyword() {
+ info("Searching for more than keyworded entry should autoFill it");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ keyword: "moz",
+ });
+ let context = createContext("mozi", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.org/",
+ completed: "http://mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/",
+ title: "mozilla.org",
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://mozilla.org/test/",
+ title: "A bookmark",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_less_than_keyword() {
+ info("Searching for less than keyworded entry should autoFill it");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ keyword: "moz",
+ });
+ let context = createContext("mo", { isPrivate: false });
+ await check_results({
+ context,
+ search: "mo",
+ autofilled: "mozilla.org/",
+ completed: "http://mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/",
+ title: "mozilla.org",
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://mozilla.org/test/",
+ title: "A bookmark",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_keyword_casing() {
+ info("Searching for keyworded entry is case-insensitive");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ keyword: "moz",
+ });
+ let context = createContext("MoZ", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://mozilla.org/test/",
+ keyword: "MoZ",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_less_then_equal_than_keyword_bug_1124238() {
+ info("Searching for less than keyworded entry should autoFill it");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ });
+ await PlacesTestUtils.addVisits("http://mozilla.com/");
+ PlacesTestUtils.addBookmarkWithDetails({
+ uri: Services.io.newURI("http://mozilla.com/"),
+ keyword: "moz",
+ });
+
+ let context = createContext("mo", { isPrivate: false });
+ await check_results({
+ context,
+ search: "mo",
+ autofilled: "mozilla.com/",
+ completed: "http://mozilla.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.com/",
+ title: "mozilla.com",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/test/",
+ title: "test visit for http://mozilla.org/test/",
+ }),
+ ],
+ });
+
+ // Search with an additional character. As the input matches a keyword, the
+ // completion should equal the keyword and not the URI as before.
+ context = createContext("moz", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://mozilla.com/",
+ keyword: "moz",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // Search with an additional character. The input doesn't match a keyword
+ // anymore, it should be autofilled.
+ context = createContext("mozi", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.com/",
+ completed: "http://mozilla.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.com/",
+ title: "mozilla.com",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/test/",
+ title: "test visit for http://mozilla.org/test/",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_muxer.js b/browser/components/urlbar/tests/unit/test_muxer.js
new file mode 100644
index 0000000000..b714ee50c1
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_muxer.js
@@ -0,0 +1,246 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_muxer() {
+ Assert.throws(
+ () => UrlbarProvidersManager.registerMuxer(),
+ /invalid muxer/,
+ "Should throw with no arguments"
+ );
+ Assert.throws(
+ () => UrlbarProvidersManager.registerMuxer({}),
+ /invalid muxer/,
+ "Should throw with empty object"
+ );
+ Assert.throws(
+ () =>
+ UrlbarProvidersManager.registerMuxer({
+ name: "",
+ }),
+ /invalid muxer/,
+ "Should throw with empty name"
+ );
+ Assert.throws(
+ () =>
+ UrlbarProvidersManager.registerMuxer({
+ name: "test",
+ sort: "no",
+ }),
+ /invalid muxer/,
+ "Should throw with invalid sort"
+ );
+
+ let matches = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: "http://mozilla.org/tab/" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ { url: "http://mozilla.org/bookmark/" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/history/" }
+ ),
+ ];
+
+ let providerName = registerBasicTestProvider(matches);
+ let context = createContext(undefined, { providers: [providerName] });
+ let controller = UrlbarTestUtils.newMockController();
+ /**
+ * A test muxer.
+ */
+ class TestMuxer extends UrlbarMuxer {
+ get name() {
+ return "TestMuxer";
+ }
+ sort(queryContext) {
+ queryContext.results.sort((a, b) => {
+ if (b.source == UrlbarUtils.RESULT_SOURCE.TABS) {
+ return -1;
+ }
+ if (b.source == UrlbarUtils.RESULT_SOURCE.BOOKMARKS) {
+ return 1;
+ }
+ return a.source == UrlbarUtils.RESULT_SOURCE.BOOKMARKS ? -1 : 1;
+ });
+ }
+ }
+ let muxer = new TestMuxer();
+
+ UrlbarProvidersManager.registerMuxer(muxer);
+ context.muxer = "TestMuxer";
+
+ info("Check results, the order should be: bookmark, history, tab");
+ await UrlbarProvidersManager.startQuery(context, controller);
+ Assert.deepEqual(context.results, [matches[1], matches[2], matches[0]]);
+
+ // Sanity check, should not throw.
+ UrlbarProvidersManager.unregisterMuxer(muxer);
+ UrlbarProvidersManager.unregisterMuxer("TestMuxer"); // no-op.
+});
+
+add_task(async function test_preselectedHeuristic_singleProvider() {
+ let matches = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/a" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/b" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/c" }
+ ),
+ ];
+ matches[1].heuristic = true;
+
+ let providerName = registerBasicTestProvider(matches);
+ let context = createContext(undefined, {
+ providers: [providerName],
+ });
+ let controller = UrlbarTestUtils.newMockController();
+
+ info("Check results, the order should be: b (heuristic), a, c");
+ await UrlbarProvidersManager.startQuery(context, controller);
+ Assert.deepEqual(context.results, [matches[1], matches[0], matches[2]]);
+});
+
+add_task(async function test_preselectedHeuristic_multiProviders() {
+ let matches1 = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/a" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/b" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/c" }
+ ),
+ ];
+
+ let matches2 = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/d" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/e" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/f" }
+ ),
+ ];
+ matches2[1].heuristic = true;
+
+ let provider1Name = registerBasicTestProvider(matches1);
+ let provider2Name = registerBasicTestProvider(matches2);
+
+ let context = createContext(undefined, {
+ providers: [provider1Name, provider2Name],
+ });
+ let controller = UrlbarTestUtils.newMockController();
+
+ info("Check results, the order should be: e (heuristic), a, b, c, d, f");
+ await UrlbarProvidersManager.startQuery(context, controller);
+ Assert.deepEqual(context.results, [
+ matches2[1],
+ ...matches1,
+ matches2[0],
+ matches2[2],
+ ]);
+});
+
+add_task(async function test_suggestions() {
+ Services.prefs.setIntPref("browser.urlbar.maxHistoricalSearchSuggestions", 1);
+
+ let matches = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/a" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/b" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ {
+ engine: "mozSearch",
+ query: "moz",
+ suggestion: "mozzarella",
+ lowerCaseSuggestion: "mozzarella",
+ }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ {
+ engine: "mozSearch",
+ query: "moz",
+ suggestion: "mozilla",
+ lowerCaseSuggestion: "mozilla",
+ }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ {
+ engine: "mozSearch",
+ query: "moz",
+ providesSearchMode: true,
+ keyword: "@moz",
+ }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/c" }
+ ),
+ ];
+
+ let providerName = registerBasicTestProvider(matches);
+
+ let context = createContext(undefined, {
+ providers: [providerName],
+ });
+ let controller = UrlbarTestUtils.newMockController();
+
+ info("Check results, the order should be: mozzarella, moz, a, b, @moz, c");
+ await UrlbarProvidersManager.startQuery(context, controller);
+ Assert.deepEqual(context.results, [
+ matches[2],
+ matches[3],
+ matches[0],
+ matches[1],
+ matches[4],
+ matches[5],
+ ]);
+
+ Services.prefs.clearUserPref("browser.urlbar.maxHistoricalSearchSuggestions");
+});
diff --git a/browser/components/urlbar/tests/unit/test_providerHeuristicFallback.js b/browser/components/urlbar/tests/unit/test_providerHeuristicFallback.js
new file mode 100644
index 0000000000..ce323a5027
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providerHeuristicFallback.js
@@ -0,0 +1,613 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that visit-url and search engine heuristic results are returned by
+ * UrlbarProviderHeuristicFallback.
+ */
+
+const ENGINE_NAME = "engine-suggestions.xml";
+const SUGGEST_PREF = "browser.urlbar.suggest.searches";
+const SUGGEST_ENABLED_PREF = "browser.search.suggest.enabled";
+const PRIVATE_SEARCH_PREF = "browser.search.separatePrivateDefault.ui.enabled";
+
+// We make sure that restriction tokens and search terms are correctly
+// recognized when they are separated by each of these different types of spaces
+// and combinations of spaces. U+3000 is the ideographic space in CJK and is
+// commonly used by CJK speakers.
+const TEST_SPACES = [" ", "\u3000", " \u3000", "\u3000 "];
+
+add_task(async function setup() {
+ // Install a test engine so we're sure of ENGINE_NAME.
+ let engine = await addTestSuggestionsEngine();
+
+ // Install the test engine.
+ let oldDefaultEngine = await Services.search.getDefault();
+ registerCleanupFunction(async () => {
+ Services.search.setDefault(oldDefaultEngine);
+ Services.prefs.clearUserPref(SUGGEST_PREF);
+ Services.prefs.clearUserPref(SUGGEST_ENABLED_PREF);
+ Services.prefs.clearUserPref(PRIVATE_SEARCH_PREF);
+ Services.prefs.clearUserPref("keyword.enabled");
+ });
+ Services.search.setDefault(engine);
+ Services.prefs.setBoolPref(SUGGEST_PREF, false);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, false);
+ Services.prefs.setBoolPref(PRIVATE_SEARCH_PREF, false);
+});
+
+add_task(async function() {
+ info("visit url, no protocol");
+ let query = "mozilla.org";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${query}/`,
+ title: `http://${query}/`,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ }),
+ ],
+ });
+
+ info("visit url, no protocol but with 2 dots");
+ query = "www.mozilla.org";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${query}/`,
+ title: `http://${query}/`,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ }),
+ ],
+ });
+
+ info("visit url, no protocol, e-mail like");
+ query = "a@b.com";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${query}/`,
+ title: `http://${query}/`,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ }),
+ ],
+ });
+
+ info("visit url, with protocol but with 2 dots");
+ query = "https://www.mozilla.org";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `${query}/`,
+ title: `${query}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // info("visit url, with protocol but with 3 dots");
+ query = "https://www.mozilla.org.tw";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `${query}/`,
+ title: `${query}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("visit url, with protocol");
+ query = "https://mozilla.org";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `${query}/`,
+ title: `${query}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("visit url, about: protocol (no host)");
+ query = "about:nonexistent";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: query,
+ title: query,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("visit url, with non-standard whitespace");
+ query = "https://mozilla.org";
+ context = createContext(`${query}\u2028`, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `${query}/`,
+ title: `${query}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // This is distinct because of how we predict being able to url autofill via
+ // host lookups.
+ info("visit url, host matching visited host but not visited url");
+ await PlacesTestUtils.addVisits([
+ {
+ uri: Services.io.newURI("http://mozilla.org/wine/"),
+ title: "Mozilla Wine",
+ transition: PlacesUtils.history.TRANSITION_TYPED,
+ },
+ ]);
+ query = "mozilla.org/rum";
+ context = createContext(`${query}\u2028`, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${query}`,
+ title: `http://${query}`,
+ iconUri: "page-icon:http://mozilla.org/",
+ heuristic: true,
+ }),
+ ],
+ });
+ await PlacesUtils.history.clear();
+
+ // And hosts with no dot in them are special, due to requiring safelisting.
+ info("unknown host");
+ query = "firefox";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("string with known host");
+ query = "firefox/get";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ Services.prefs.setBoolPref("browser.fixup.domainwhitelist.firefox", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.fixup.domainwhitelist.firefox");
+ });
+
+ info("known host");
+ query = "firefox";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${query}/`,
+ title: `http://${query}/`,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ }),
+ ],
+ });
+
+ info("url with known host");
+ query = "firefox/get";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${query}`,
+ title: `http://${query}`,
+ iconUri: "page-icon:http://firefox/",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("visit url, host matching visited host but not visited url, known host");
+ Services.prefs.setBoolPref("browser.fixup.domainwhitelist.mozilla", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.fixup.domainwhitelist.mozilla");
+ });
+ query = "mozilla/rum";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${query}`,
+ title: `http://${query}`,
+ iconUri: "page-icon:http://mozilla/",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // ipv4 and ipv6 literal addresses should offer to visit.
+ info("visit url, ipv4 literal");
+ query = "127.0.0.1";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${query}/`,
+ title: `http://${query}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("visit url, ipv6 literal");
+ query = "[2001:db8::1]";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${query}/`,
+ title: `http://${query}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // Setting keyword.enabled to false should always try to visit.
+ let keywordEnabled = Services.prefs.getBoolPref("keyword.enabled");
+ Services.prefs.setBoolPref("keyword.enabled", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("keyword.enabled");
+ });
+ info("visit url, keyword.enabled = false");
+ query = "bacon";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${query}/`,
+ title: `http://${query}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("visit two word query, keyword.enabled = false");
+ query = "bacon lovers";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: query,
+ title: query,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Forced search through a restriction token, keyword.enabled = false");
+ query = "?bacon";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ query: "bacon",
+ }),
+ ],
+ });
+
+ Services.prefs.setBoolPref("keyword.enabled", true);
+ info("visit two word query, keyword.enabled = true");
+ query = "bacon lovers";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ Services.prefs.setBoolPref("keyword.enabled", keywordEnabled);
+
+ info("visit url, scheme+host");
+ query = "http://example";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `${query}/`,
+ title: `${query}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("visit url, scheme+host");
+ query = "ftp://example";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `${query}/`,
+ title: `${query}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("visit url, host+port");
+ query = "example:8080";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${query}/`,
+ title: `http://${query}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("numerical operations that look like urls should search");
+ query = "123/12";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("numerical operations that look like urls should search");
+ query = "123.12/12.1";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ query = "resource:///modules";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: query,
+ title: query,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("access resource://app/modules");
+ query = "resource://app/modules";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: query,
+ title: query,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("protocol with an extra slash");
+ query = "http:///";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("change default engine");
+ let originalTestEngine = Services.search.getEngineByName(ENGINE_NAME);
+ let engine2 = await Services.search.addEngineWithDetails("AliasEngine", {
+ alias: "alias",
+ method: "GET",
+ template: "http://example.com/?q={searchTerms}",
+ });
+ Assert.notEqual(
+ Services.search.defaultEngine,
+ engine2,
+ "New engine shouldn't be the current engine yet"
+ );
+ await Services.search.setDefault(engine2);
+ query = "toronto";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: "AliasEngine",
+ heuristic: true,
+ }),
+ ],
+ });
+ await Services.search.setDefault(originalTestEngine);
+
+ info(
+ "Leading search-mode restriction tokens are removed from the search result."
+ );
+ for (let token of UrlbarTokenizer.SEARCH_MODE_RESTRICT) {
+ for (let spaces of TEST_SPACES) {
+ query = token + spaces + "query";
+ info("Testing: " + JSON.stringify({ query, spaces: codePoints(spaces) }));
+ let expectedQuery = query.substring(1).trimStart();
+ context = createContext(query, { isPrivate: false });
+ info(`Searching for "${query}", expecting "${expectedQuery}"`);
+ let payload = {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ query: expectedQuery,
+ alias: token,
+ };
+ if (token == UrlbarTokenizer.RESTRICT.SEARCH) {
+ payload.source = UrlbarUtils.RESULT_SOURCE.SEARCH;
+ payload.engineName = ENGINE_NAME;
+ }
+ await check_results({
+ context,
+ matches: [makeSearchResult(context, payload)],
+ });
+ }
+ }
+
+ info(
+ "Leading search-mode restriction tokens are removed from the search result with keyword.enabled = false."
+ );
+ Services.prefs.setBoolPref("keyword.enabled", false);
+ for (let token of UrlbarTokenizer.SEARCH_MODE_RESTRICT) {
+ for (let spaces of TEST_SPACES) {
+ query = token + spaces + "query";
+ info("Testing: " + JSON.stringify({ query, spaces: codePoints(spaces) }));
+ let expectedQuery = query.substring(1).trimStart();
+ context = createContext(query, { isPrivate: false });
+ info(`Searching for "${query}", expecting "${expectedQuery}"`);
+ let payload = {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ query: expectedQuery,
+ alias: token,
+ };
+ if (token == UrlbarTokenizer.RESTRICT.SEARCH) {
+ payload.source = UrlbarUtils.RESULT_SOURCE.SEARCH;
+ payload.engineName = ENGINE_NAME;
+ }
+ await check_results({
+ context,
+ matches: [makeSearchResult(context, payload)],
+ });
+ }
+ }
+ Services.prefs.clearUserPref("keyword.enabled");
+
+ info(
+ "Leading non-search-mode restriction tokens are not removed from the search result."
+ );
+ for (let token of Object.values(UrlbarTokenizer.RESTRICT)) {
+ if (UrlbarTokenizer.SEARCH_MODE_RESTRICT.has(token)) {
+ continue;
+ }
+ for (let spaces of TEST_SPACES) {
+ query = token + spaces + "query";
+ info("Testing: " + JSON.stringify({ query, spaces: codePoints(spaces) }));
+ let expectedQuery = query;
+ context = createContext(query, { isPrivate: false });
+ info(`Searching for "${query}", expecting "${expectedQuery}"`);
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ heuristic: true,
+ query: expectedQuery,
+ engineName: ENGINE_NAME,
+ }),
+ ],
+ });
+ }
+ }
+
+ await Services.search.removeEngine(engine2);
+});
+
+/**
+ * Returns an array of code points in the given string. Each code point is
+ * returned as a hexidecimal string.
+ *
+ * @param {string} str
+ * The code points of this string will be returned.
+ * @returns {array}
+ * Array of code points in the string, where each is a hexidecimal string.
+ */
+function codePoints(str) {
+ return str.split("").map(s => s.charCodeAt(0).toString(16));
+}
diff --git a/browser/components/urlbar/tests/unit/test_providerOmnibox.js b/browser/components/urlbar/tests/unit/test_providerOmnibox.js
new file mode 100644
index 0000000000..09fdfdec05
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providerOmnibox.js
@@ -0,0 +1,818 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim:set ts=2 sw=2 sts=2 et:
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { ExtensionSearchHandler } = ChromeUtils.import(
+ "resource://gre/modules/ExtensionSearchHandler.jsm"
+);
+
+let controller = Cc["@mozilla.org/autocomplete/controller;1"].getService(
+ Ci.nsIAutoCompleteController
+);
+
+const ENGINE_NAME = "engine-suggestions.xml";
+const SUGGEST_PREF = "browser.urlbar.suggest.searches";
+const SUGGEST_ENABLED_PREF = "browser.search.suggest.enabled";
+
+async function cleanup() {
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+}
+
+add_task(function setup() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, false);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, false);
+
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref(SUGGEST_PREF);
+ Services.prefs.clearUserPref(SUGGEST_ENABLED_PREF);
+ });
+});
+
+add_task(async function test_correct_errors_are_thrown() {
+ let keyword = "foo";
+ let anotherKeyword = "bar";
+ let unregisteredKeyword = "baz";
+
+ // Register a keyword.
+ ExtensionSearchHandler.registerKeyword(keyword, { emit: () => {} });
+
+ // Try registering the keyword again.
+ Assert.throws(
+ () => ExtensionSearchHandler.registerKeyword(keyword, { emit: () => {} }),
+ /The keyword provided is already registered/
+ );
+
+ // Register a different keyword.
+ ExtensionSearchHandler.registerKeyword(anotherKeyword, { emit: () => {} });
+
+ // Try calling handleSearch for an unregistered keyword.
+ let searchData = {
+ keyword: unregisteredKeyword,
+ text: `${unregisteredKeyword} `,
+ };
+ Assert.throws(
+ () => ExtensionSearchHandler.handleSearch(searchData, () => {}),
+ /The keyword provided is not registered/
+ );
+
+ // Try calling handleSearch without a callback.
+ Assert.throws(
+ () => ExtensionSearchHandler.handleSearch(searchData),
+ /The keyword provided is not registered/
+ );
+
+ // Try getting the description for a keyword which isn't registered.
+ Assert.throws(
+ () => ExtensionSearchHandler.getDescription(unregisteredKeyword),
+ /The keyword provided is not registered/
+ );
+
+ // Try setting the default suggestion for a keyword which isn't registered.
+ Assert.throws(
+ () =>
+ ExtensionSearchHandler.setDefaultSuggestion(
+ unregisteredKeyword,
+ "suggestion"
+ ),
+ /The keyword provided is not registered/
+ );
+
+ // Try calling handleInputCancelled when there is no active input session.
+ Assert.throws(
+ () => ExtensionSearchHandler.handleInputCancelled(),
+ /There is no active input session/
+ );
+
+ // Try calling handleInputEntered when there is no active input session.
+ Assert.throws(
+ () =>
+ ExtensionSearchHandler.handleInputEntered(
+ anotherKeyword,
+ `${anotherKeyword} test`,
+ "tab"
+ ),
+ /There is no active input session/
+ );
+
+ // Start a session by calling handleSearch with the registered keyword.
+ searchData = {
+ keyword,
+ text: `${keyword} test`,
+ };
+ ExtensionSearchHandler.handleSearch(searchData, () => {});
+
+ // Try providing suggestions for an unregistered keyword.
+ Assert.throws(
+ () => ExtensionSearchHandler.addSuggestions(unregisteredKeyword, 0, []),
+ /The keyword provided is not registered/
+ );
+
+ // Try providing suggestions for an inactive keyword.
+ Assert.throws(
+ () => ExtensionSearchHandler.addSuggestions(anotherKeyword, 0, []),
+ /The keyword provided is not apart of an active input session/
+ );
+
+ // Try calling handleSearch for an inactive keyword.
+ searchData = {
+ keyword: anotherKeyword,
+ text: `${anotherKeyword} `,
+ };
+ Assert.throws(
+ () => ExtensionSearchHandler.handleSearch(searchData, () => {}),
+ /A different input session is already ongoing/
+ );
+
+ // Try calling addSuggestions with an old callback ID.
+ Assert.throws(
+ () => ExtensionSearchHandler.addSuggestions(keyword, 0, []),
+ /The callback is no longer active for the keyword provided/
+ );
+
+ // Add suggestions with a valid callback ID.
+ ExtensionSearchHandler.addSuggestions(keyword, 1, []);
+
+ // Add suggestions again with a valid callback ID.
+ ExtensionSearchHandler.addSuggestions(keyword, 1, []);
+
+ // Try calling addSuggestions with a future callback ID.
+ Assert.throws(
+ () => ExtensionSearchHandler.addSuggestions(keyword, 2, []),
+ /The callback is no longer active for the keyword provided/
+ );
+
+ // End the input session by calling handleInputCancelled.
+ ExtensionSearchHandler.handleInputCancelled();
+
+ // Try calling handleInputCancelled after the session has ended.
+ Assert.throws(
+ () => ExtensionSearchHandler.handleInputCancelled(),
+ /There is no active input sessio/
+ );
+
+ // Try calling handleSearch that doesn't have a space after the keyword.
+ searchData = {
+ keyword: anotherKeyword,
+ text: `${anotherKeyword}`,
+ };
+ Assert.throws(
+ () => ExtensionSearchHandler.handleSearch(searchData, () => {}),
+ /The text provided must start with/
+ );
+
+ // Try calling handleSearch with text starting with the wrong keyword.
+ searchData = {
+ keyword: anotherKeyword,
+ text: `${keyword} test`,
+ };
+ Assert.throws(
+ () => ExtensionSearchHandler.handleSearch(searchData, () => {}),
+ /The text provided must start with/
+ );
+
+ // Start a new session by calling handleSearch with a different keyword
+ searchData = {
+ keyword: anotherKeyword,
+ text: `${anotherKeyword} test`,
+ };
+ ExtensionSearchHandler.handleSearch(searchData, () => {});
+
+ // Try adding suggestions again with the same callback ID now that the input session has ended.
+ Assert.throws(
+ () => ExtensionSearchHandler.addSuggestions(keyword, 1, []),
+ /The keyword provided is not apart of an active input session/
+ );
+
+ // Add suggestions with a valid callback ID.
+ ExtensionSearchHandler.addSuggestions(anotherKeyword, 2, []);
+
+ // Try adding suggestions with a valid callback ID but a different keyword.
+ Assert.throws(
+ () => ExtensionSearchHandler.addSuggestions(keyword, 2, []),
+ /The keyword provided is not apart of an active input session/
+ );
+
+ // Try adding suggestions with a valid callback ID but an unregistered keyword.
+ Assert.throws(
+ () => ExtensionSearchHandler.addSuggestions(unregisteredKeyword, 2, []),
+ /The keyword provided is not registered/
+ );
+
+ // Set the default suggestion.
+ ExtensionSearchHandler.setDefaultSuggestion(anotherKeyword, {
+ description: "test result",
+ });
+
+ // Try ending the session using handleInputEntered with a different keyword.
+ Assert.throws(
+ () =>
+ ExtensionSearchHandler.handleInputEntered(
+ keyword,
+ `${keyword} test`,
+ "tab"
+ ),
+ /A different input session is already ongoing/
+ );
+
+ // Try calling handleInputEntered with invalid text.
+ Assert.throws(
+ () =>
+ ExtensionSearchHandler.handleInputEntered(anotherKeyword, ` test`, "tab"),
+ /The text provided must start with/
+ );
+
+ // Try calling handleInputEntered with an invalid disposition.
+ Assert.throws(
+ () =>
+ ExtensionSearchHandler.handleInputEntered(
+ anotherKeyword,
+ `${anotherKeyword} test`,
+ "invalid"
+ ),
+ /Invalid "where" argument/
+ );
+
+ // End the session by calling handleInputEntered.
+ ExtensionSearchHandler.handleInputEntered(
+ anotherKeyword,
+ `${anotherKeyword} test`,
+ "tab"
+ );
+
+ // Try calling handleInputEntered after the session has ended.
+ Assert.throws(
+ () =>
+ ExtensionSearchHandler.handleInputEntered(
+ anotherKeyword,
+ `${anotherKeyword} test`,
+ "tab"
+ ),
+ /There is no active input session/
+ );
+
+ // Unregister the keyword.
+ ExtensionSearchHandler.unregisterKeyword(keyword);
+
+ // Try setting the default suggestion for the unregistered keyword.
+ Assert.throws(
+ () =>
+ ExtensionSearchHandler.setDefaultSuggestion(keyword, {
+ description: "test",
+ }),
+ /The keyword provided is not registered/
+ );
+
+ // Try handling a search with the unregistered keyword.
+ searchData = {
+ keyword,
+ text: `${keyword} test`,
+ };
+ Assert.throws(
+ () => ExtensionSearchHandler.handleSearch(searchData, () => {}),
+ /The keyword provided is not registered/
+ );
+
+ // Try unregistering the keyword again.
+ Assert.throws(
+ () => ExtensionSearchHandler.unregisterKeyword(keyword),
+ /The keyword provided is not registered/
+ );
+
+ // Unregister the other keyword.
+ ExtensionSearchHandler.unregisterKeyword(anotherKeyword);
+
+ // Try unregistering the word which was never registered.
+ Assert.throws(
+ () => ExtensionSearchHandler.unregisterKeyword(unregisteredKeyword),
+ /The keyword provided is not registered/
+ );
+
+ // Try setting the default suggestion for a word that was never registered.
+ Assert.throws(
+ () =>
+ ExtensionSearchHandler.setDefaultSuggestion(unregisteredKeyword, {
+ description: "test",
+ }),
+ /The keyword provided is not registered/
+ );
+
+ await cleanup();
+});
+
+add_task(async function test_extension_private_browsing() {
+ let events = [];
+ let mockExtension = {
+ emit: message => events.push(message),
+ privateBrowsingAllowed: false,
+ };
+
+ let keyword = "foo";
+
+ ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
+
+ let searchData = {
+ keyword,
+ text: `${keyword} test`,
+ inPrivateWindow: true,
+ };
+ let result = await ExtensionSearchHandler.handleSearch(searchData);
+ Assert.equal(result, false, "unable to handle search for private window");
+
+ // Try calling handleInputEntered after the session has ended.
+ Assert.throws(
+ () =>
+ ExtensionSearchHandler.handleInputEntered(
+ keyword,
+ `${keyword} test`,
+ "tab"
+ ),
+ /There is no active input session/
+ );
+
+ ExtensionSearchHandler.unregisterKeyword(keyword);
+ await cleanup();
+});
+
+add_task(async function test_correct_events_are_emitted() {
+ let events = [];
+ function checkEvents(expectedEvents) {
+ Assert.equal(
+ events.length,
+ expectedEvents.length,
+ "The correct number of events fired"
+ );
+ expectedEvents.forEach((e, i) =>
+ Assert.equal(e, events[i], `Expected "${e}" event to fire`)
+ );
+ events = [];
+ }
+
+ let mockExtension = { emit: message => events.push(message) };
+
+ let keyword = "foo";
+ let anotherKeyword = "bar";
+
+ ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
+ ExtensionSearchHandler.registerKeyword(anotherKeyword, mockExtension);
+
+ let searchData = {
+ keyword,
+ text: `${keyword} `,
+ };
+ ExtensionSearchHandler.handleSearch(searchData, () => {});
+ checkEvents([ExtensionSearchHandler.MSG_INPUT_STARTED]);
+
+ searchData.text = `${keyword} f`;
+ ExtensionSearchHandler.handleSearch(searchData, () => {});
+ checkEvents([ExtensionSearchHandler.MSG_INPUT_CHANGED]);
+
+ ExtensionSearchHandler.handleInputEntered(keyword, searchData.text, "tab");
+ checkEvents([ExtensionSearchHandler.MSG_INPUT_ENTERED]);
+
+ ExtensionSearchHandler.handleSearch(searchData, () => {});
+ checkEvents([
+ ExtensionSearchHandler.MSG_INPUT_STARTED,
+ ExtensionSearchHandler.MSG_INPUT_CHANGED,
+ ]);
+
+ ExtensionSearchHandler.handleInputCancelled();
+ checkEvents([ExtensionSearchHandler.MSG_INPUT_CANCELLED]);
+
+ ExtensionSearchHandler.handleSearch(
+ {
+ keyword: anotherKeyword,
+ text: `${anotherKeyword} baz`,
+ },
+ () => {}
+ );
+ checkEvents([
+ ExtensionSearchHandler.MSG_INPUT_STARTED,
+ ExtensionSearchHandler.MSG_INPUT_CHANGED,
+ ]);
+
+ ExtensionSearchHandler.handleInputEntered(
+ anotherKeyword,
+ `${anotherKeyword} baz`,
+ "tab"
+ );
+ checkEvents([ExtensionSearchHandler.MSG_INPUT_ENTERED]);
+
+ ExtensionSearchHandler.unregisterKeyword(keyword);
+});
+
+add_task(async function test_removes_suggestion_if_its_content_is_typed_in() {
+ let keyword = "test";
+ let extensionName = "Foo Bar";
+
+ let mockExtension = {
+ name: extensionName,
+ emit(message, text, id) {
+ if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
+ ExtensionSearchHandler.addSuggestions(keyword, id, [
+ { content: "foo", description: "first suggestion" },
+ { content: "bar", description: "second suggestion" },
+ { content: "baz", description: "third suggestion" },
+ ]);
+ // The API doesn't have a way to notify when addition is complete.
+ do_timeout(1000, () => {
+ controller.stopSearch();
+ });
+ }
+ },
+ };
+
+ ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
+
+ let query = `${keyword} unmatched`;
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeOmniboxResult(context, {
+ heuristic: true,
+ keyword,
+ description: extensionName,
+ content: `${keyword} unmatched`,
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} foo`,
+ description: "first suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} bar`,
+ description: "second suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} baz`,
+ description: "third suggestion",
+ }),
+ ],
+ });
+
+ query = `${keyword} foo`;
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeOmniboxResult(context, {
+ heuristic: true,
+ keyword,
+ description: extensionName,
+ content: `${keyword} foo`,
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} bar`,
+ description: "second suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} baz`,
+ description: "third suggestion",
+ }),
+ ],
+ });
+
+ query = `${keyword} bar`;
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeOmniboxResult(context, {
+ heuristic: true,
+ keyword,
+ description: extensionName,
+ content: `${keyword} bar`,
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} foo`,
+ description: "first suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} baz`,
+ description: "third suggestion",
+ }),
+ ],
+ });
+
+ query = `${keyword} baz`;
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeOmniboxResult(context, {
+ heuristic: true,
+ keyword,
+ description: extensionName,
+ content: `${keyword} baz`,
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} foo`,
+ description: "first suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} bar`,
+ description: "second suggestion",
+ }),
+ ],
+ });
+
+ ExtensionSearchHandler.unregisterKeyword(keyword);
+ await cleanup();
+});
+
+add_task(async function test_extension_results_should_come_first() {
+ let keyword = "test";
+ let extensionName = "Omnibox Example";
+
+ let uri = Services.io.newURI(`http://a.com/b`);
+ await PlacesTestUtils.addVisits([{ uri, title: `${keyword} -` }]);
+
+ let mockExtension = {
+ name: extensionName,
+ emit(message, text, id) {
+ if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
+ ExtensionSearchHandler.addSuggestions(keyword, id, [
+ { content: "foo", description: "first suggestion" },
+ { content: "bar", description: "second suggestion" },
+ { content: "baz", description: "third suggestion" },
+ ]);
+ }
+ },
+ };
+
+ ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
+
+ // Start an input session before testing MSG_INPUT_CHANGED.
+ ExtensionSearchHandler.handleSearch(
+ { keyword, text: `${keyword} ` },
+ () => {}
+ );
+
+ let query = `${keyword} -`;
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeOmniboxResult(context, {
+ heuristic: true,
+ keyword,
+ description: extensionName,
+ content: `${keyword} -`,
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} foo`,
+ description: "first suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} bar`,
+ description: "second suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} baz`,
+ description: "third suggestion",
+ }),
+ makeVisitResult(context, {
+ uri: `http://a.com/b`,
+ title: `${keyword} -`,
+ }),
+ ],
+ });
+
+ ExtensionSearchHandler.unregisterKeyword(keyword);
+ await cleanup();
+});
+
+add_task(async function test_setting_the_default_suggestion() {
+ let keyword = "test";
+ let extensionName = "Omnibox Example";
+
+ let mockExtension = {
+ name: extensionName,
+ emit(message, text, id) {
+ if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
+ ExtensionSearchHandler.addSuggestions(keyword, id, []);
+ // The API doesn't have a way to notify when addition is complete.
+ do_timeout(1000, () => {
+ controller.stopSearch();
+ });
+ }
+ },
+ };
+
+ ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
+
+ ExtensionSearchHandler.setDefaultSuggestion(keyword, {
+ description: "hello world",
+ });
+
+ let query = `${keyword} search query`;
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeOmniboxResult(context, {
+ heuristic: true,
+ keyword,
+ description: "hello world",
+ content: query,
+ }),
+ ],
+ });
+
+ ExtensionSearchHandler.setDefaultSuggestion(keyword, {
+ description: "foo bar",
+ });
+
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ searchParam: "enable-actions",
+ matches: [
+ makeOmniboxResult(context, {
+ heuristic: true,
+ keyword,
+ description: "foo bar",
+ content: query,
+ }),
+ ],
+ });
+
+ ExtensionSearchHandler.unregisterKeyword(keyword);
+ await cleanup();
+});
+
+add_task(async function test_maximum_number_of_suggestions_is_enforced() {
+ let keyword = "test";
+ let extensionName = "Omnibox Example";
+
+ let mockExtension = {
+ name: extensionName,
+ emit(message, text, id) {
+ if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
+ ExtensionSearchHandler.addSuggestions(keyword, id, [
+ { content: "a", description: "first suggestion" },
+ { content: "b", description: "second suggestion" },
+ { content: "c", description: "third suggestion" },
+ { content: "d", description: "fourth suggestion" },
+ { content: "e", description: "fifth suggestion" },
+ { content: "f", description: "sixth suggestion" },
+ { content: "g", description: "seventh suggestion" },
+ { content: "h", description: "eigth suggestion" },
+ { content: "i", description: "ninth suggestion" },
+ { content: "j", description: "tenth suggestion" },
+ ]);
+ // The API doesn't have a way to notify when addition is complete.
+ do_timeout(1000, () => {
+ controller.stopSearch();
+ });
+ }
+ },
+ };
+
+ ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
+
+ // Start an input session before testing MSG_INPUT_CHANGED.
+ ExtensionSearchHandler.handleSearch(
+ { keyword, text: `${keyword} ` },
+ () => {}
+ );
+
+ let query = `${keyword} #`;
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeOmniboxResult(context, {
+ heuristic: true,
+ keyword,
+ description: extensionName,
+ content: `${keyword} #`,
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} a`,
+ description: "first suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} b`,
+ description: "second suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} c`,
+ description: "third suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} d`,
+ description: "fourth suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} e`,
+ description: "fifth suggestion",
+ }),
+ ],
+ });
+
+ ExtensionSearchHandler.unregisterKeyword(keyword);
+ await cleanup();
+});
+
+add_task(async function conflicting_alias() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+
+ let engine = await addTestSuggestionsEngine();
+ let keyword = "test";
+ engine.alias = keyword;
+ let oldDefaultEngine = await Services.search.getDefault();
+ Services.search.setDefault(engine);
+
+ let extensionName = "Omnibox Example";
+
+ let mockExtension = {
+ name: extensionName,
+ emit(message, text, id) {
+ if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
+ ExtensionSearchHandler.addSuggestions(keyword, id, [
+ { content: "foo", description: "first suggestion" },
+ { content: "bar", description: "second suggestion" },
+ { content: "baz", description: "third suggestion" },
+ ]);
+ // The API doesn't have a way to notify when addition is complete.
+ do_timeout(1000, () => {
+ controller.stopSearch();
+ });
+ }
+ },
+ };
+
+ ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
+ let query = `${keyword} unmatched`;
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeOmniboxResult(context, {
+ heuristic: true,
+ keyword,
+ description: extensionName,
+ content: `${keyword} unmatched`,
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} foo`,
+ description: "first suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} bar`,
+ description: "second suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} baz`,
+ description: "third suggestion",
+ }),
+ makeSearchResult(context, {
+ query: "unmatched",
+ engineName: ENGINE_NAME,
+ alias: keyword,
+ suggestion: "unmatched",
+ }),
+ makeSearchResult(context, {
+ query: "unmatched",
+ engineName: ENGINE_NAME,
+ alias: keyword,
+ suggestion: "unmatched foo",
+ }),
+ makeSearchResult(context, {
+ query: "unmatched",
+ engineName: ENGINE_NAME,
+ alias: keyword,
+ suggestion: "unmatched bar",
+ }),
+ ],
+ });
+
+ Services.search.setDefault(oldDefaultEngine);
+ Services.prefs.setBoolPref(SUGGEST_PREF, false);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, false);
+ await cleanup();
+});
diff --git a/browser/components/urlbar/tests/unit/test_providerOpenTabs.js b/browser/components/urlbar/tests/unit/test_providerOpenTabs.js
new file mode 100644
index 0000000000..898bb6885e
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providerOpenTabs.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_openTabs() {
+ const userContextId = 5;
+ const url = "http://foo.mozilla.org/";
+ UrlbarProviderOpenTabs.registerOpenTab(url, userContextId);
+ UrlbarProviderOpenTabs.registerOpenTab(url, userContextId);
+ Assert.equal(
+ UrlbarProviderOpenTabs.openTabs.get(userContextId).length,
+ 2,
+ "Found all the expected tabs"
+ );
+ UrlbarProviderOpenTabs.unregisterOpenTab(url, userContextId);
+ Assert.equal(
+ UrlbarProviderOpenTabs.openTabs.get(userContextId).length,
+ 1,
+ "Found all the expected tabs"
+ );
+
+ let context = createContext();
+ let matchCount = 0;
+ let callback = function(provider, match) {
+ matchCount++;
+ Assert.ok(
+ provider instanceof UrlbarProviderOpenTabs,
+ "Got the expected provider"
+ );
+ Assert.equal(
+ match.type,
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ "Got the expected result type"
+ );
+ Assert.equal(match.payload.url, url, "Got the expected url");
+ Assert.equal(match.payload.title, undefined, "Got the expected title");
+ };
+
+ let provider = new UrlbarProviderOpenTabs();
+ await provider.startQuery(context, callback);
+ Assert.equal(matchCount, 1, "Found the expected number of matches");
+ // Sanity check that this doesn't throw.
+ provider.cancelQuery(context);
+});
diff --git a/browser/components/urlbar/tests/unit/test_providerTabToSearch.js b/browser/components/urlbar/tests/unit/test_providerTabToSearch.js
new file mode 100644
index 0000000000..ca08493579
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providerTabToSearch.js
@@ -0,0 +1,477 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests UrlbarProviderTabToSearch. See also
+ * browser/components/urlbar/tests/browser/browser_tabToSearch.js
+ */
+
+"use strict";
+
+let testEngine;
+
+add_task(async function init() {
+ // Disable search suggestions for a less verbose test.
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", false);
+ // Disable tab-to-search onboarding results. Those are covered in
+ // browser/components/urlbar/tests/browser/browser_tabToSearch.js.
+ Services.prefs.setIntPref(
+ "browser.urlbar.tabToSearch.onboard.interactionsLeft",
+ 0
+ );
+ testEngine = await Services.search.addEngineWithDetails("Test", {
+ template: "https://example.com/?search={searchTerms}",
+ });
+
+ registerCleanupFunction(async () => {
+ await Services.search.removeEngine(testEngine);
+ Services.prefs.clearUserPref(
+ "browser.urlbar.tabToSearch.onboard.interactionsLeft"
+ );
+ Services.prefs.clearUserPref("browser.search.suggest.enabled");
+ });
+});
+
+// Tests that tab-to-search results appear when the engine's result domain is
+// autofilled.
+add_task(async function basic() {
+ await PlacesTestUtils.addVisits(["https://example.com/"]);
+ let context = createContext("examp", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ completed: "https://example.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://example.com/",
+ title: "https://example.com",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ makeSearchResult(context, {
+ engineName: testEngine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(
+ testEngine.getResultDomain()
+ ),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ ],
+ });
+
+ info("Repeat the search but with tab-to-search disabled through pref.");
+ Services.prefs.setBoolPref("browser.urlbar.suggest.engines", false);
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ completed: "https://example.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://example.com/",
+ title: "https://example.com",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ ],
+ });
+ Services.prefs.clearUserPref("browser.urlbar.suggest.engines");
+
+ await cleanupPlaces();
+});
+
+// Tests that tab-to-search results aren't shown when the typed string matches
+// an engine domain but there is no autofill.
+add_task(async function noAutofill() {
+ // Note we are not adding any history visits.
+ let context = createContext("examp", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: Services.search.defaultEngine.name,
+ engineIconUri: Services.search.defaultEngine.iconURI?.spec,
+ heuristic: true,
+ providerName: "HeuristicFallback",
+ }),
+ ],
+ });
+});
+
+// Tests that tab-to-search results are not shown when the typed string matches
+// an engine domain, but something else is being autofilled.
+add_task(async function autofillDoesNotMatchEngine() {
+ await PlacesTestUtils.addVisits(["https://example.test.ca/"]);
+ let context = createContext("example", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.test.ca/",
+ completed: "https://example.test.ca/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://example.test.ca/",
+ title: "https://example.test.ca",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+// Tests that www. is ignored for the purposes of matching autofill to
+// tab-to-search.
+add_task(async function ignoreWww() {
+ // The history result has www., the engine does not.
+ await PlacesTestUtils.addVisits(["https://www.example.com/"]);
+ let context = createContext("www.examp", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "www.example.com/",
+ completed: "https://www.example.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www.example.com/",
+ title: "https://www.example.com",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ makeSearchResult(context, {
+ engineName: testEngine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(
+ testEngine.getResultDomain()
+ ),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+
+ // The engine has www., the history result does not.
+ await PlacesTestUtils.addVisits(["https://foo.bar/"]);
+ let wwwTestEngine = await Services.search.addEngineWithDetails("TestWww", {
+ template: "https://www.foo.bar/?search={searchTerms}",
+ });
+ context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "foo.bar/",
+ completed: "https://foo.bar/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://foo.bar/",
+ title: "https://foo.bar",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ makeSearchResult(context, {
+ engineName: wwwTestEngine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(
+ wwwTestEngine.getResultDomain()
+ ),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+
+ // Both the engine and the history result have www.
+ await PlacesTestUtils.addVisits(["https://www.foo.bar/"]);
+ context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "foo.bar/",
+ completed: "https://www.foo.bar/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www.foo.bar/",
+ title: "https://www.foo.bar",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ makeSearchResult(context, {
+ engineName: wwwTestEngine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(
+ wwwTestEngine.getResultDomain()
+ ),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+
+ await Services.search.removeEngine(wwwTestEngine);
+});
+
+// Tests that when a user's query causes autofill to replace one engine's domain
+// with another, the correct tab-to-search results are shown.
+add_task(async function conflictingEngines() {
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits([
+ "https://foobar.com/",
+ "https://foo.com/",
+ ]);
+ }
+ let fooBarTestEngine = await Services.search.addEngineWithDetails(
+ "TestFooBar",
+ { template: "https://foobar.com/?search={searchTerms}" }
+ );
+ let fooTestEngine = await Services.search.addEngineWithDetails("TestFoo", {
+ template: "https://foo.com/?search={searchTerms}",
+ });
+
+ // Search for "foo", autofilling foo.com. Observe that the foo.com
+ // tab-to-search result is shown, even though the foobar.com engine was added
+ // first (and thus enginesForDomainPrefix puts it earlier in its returned
+ // array.)
+ let context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "foo.com/",
+ completed: "https://foo.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://foo.com/",
+ title: "https://foo.com",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ makeSearchResult(context, {
+ engineName: fooTestEngine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(
+ fooTestEngine.getResultDomain()
+ ),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ makeVisitResult(context, {
+ uri: "https://foobar.com/",
+ title: "test visit for https://foobar.com/",
+ providerName: "UnifiedComplete",
+ }),
+ ],
+ });
+
+ // Search for "foob", autofilling foobar.com. Observe that the foo.com
+ // tab-to-search result is replaced with the foobar.com tab-to-search result.
+ context = createContext("foob", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "foobar.com/",
+ completed: "https://foobar.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://foobar.com/",
+ title: "https://foobar.com",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ makeSearchResult(context, {
+ engineName: fooBarTestEngine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(
+ fooBarTestEngine.getResultDomain()
+ ),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+ await Services.search.removeEngine(fooTestEngine);
+ await Services.search.removeEngine(fooBarTestEngine);
+});
+
+add_task(async function multipleEnginesForHostname() {
+ info(
+ "In case of multiple engines only one tab-to-search result should be returned"
+ );
+ let mapsEngine = await Services.search.addEngineWithDetails("TestMaps", {
+ template: "https://example.com/maps/?search={searchTerms}",
+ });
+ await PlacesTestUtils.addVisits(["https://example.com/"]);
+ let context = createContext("examp", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ completed: "https://example.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://example.com/",
+ title: "https://example.com",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ makeSearchResult(context, {
+ engineName: testEngine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(
+ testEngine.getResultDomain()
+ ),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+ await Services.search.removeEngine(mapsEngine);
+});
+
+add_task(async function test_casing() {
+ info("Tab-to-search results appear also in case of different casing.");
+ await PlacesTestUtils.addVisits(["https://example.com/"]);
+ let context = createContext("eXAm", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "eXAmple.com/",
+ completed: "https://example.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://example.com/",
+ title: "https://example.com",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ makeSearchResult(context, {
+ engineName: testEngine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(
+ testEngine.getResultDomain()
+ ),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_publicSuffix() {
+ info("Tab-to-search results appear also in case of partial host match.");
+ let engine = await Services.search.addEngineWithDetails("MyTest", {
+ template: "https://test.mytest.it/?search={searchTerms}",
+ });
+ await PlacesTestUtils.addVisits(["https://test.mytest.it/"]);
+ let context = createContext("my", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: Services.search.defaultEngine.name,
+ engineIconUri: Services.search.defaultEngine.iconURI?.spec,
+ heuristic: true,
+ providerName: "HeuristicFallback",
+ }),
+ makeSearchResult(context, {
+ engineName: engine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(engine.getResultDomain()),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ satisfiesAutofillThreshold: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://test.mytest.it/",
+ title: "test visit for https://test.mytest.it/",
+ providerName: "UnifiedComplete",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+ await Services.search.removeEngine(engine);
+});
+
+add_task(async function test_publicSuffixIsHost() {
+ info("Tab-to-search results does not appear in case we autofill a suffix.");
+ let suffixEngine = await Services.search.addEngineWithDetails("SuffixTest", {
+ template: "https://somesuffix.com.mx/?search={searchTerms}",
+ });
+ // The top level domain will be autofilled, not the full domain.
+ await PlacesTestUtils.addVisits(["https://com.mx/"]);
+ let context = createContext("co", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "com.mx/",
+ completed: "https://com.mx/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://com.mx/",
+ title: "https://com.mx",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+ await Services.search.removeEngine(suffixEngine);
+});
+
+add_task(async function test_disabledEngine() {
+ info("Tab-to-search results does not appear for a Pref-disabled engine.");
+ let engine = await Services.search.addEngineWithDetails("Disabled", {
+ template: "https://disabled.com/?search={searchTerms}",
+ });
+ await PlacesTestUtils.addVisits(["https://disabled.com/"]);
+ let context = createContext("dis", { isPrivate: false });
+
+ info("Sanity check that the engine would appear.");
+ await check_results({
+ context,
+ autofilled: "disabled.com/",
+ completed: "https://disabled.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://disabled.com/",
+ title: "https://disabled.com",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ makeSearchResult(context, {
+ engineName: engine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(engine.getResultDomain()),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ ],
+ });
+
+ info("Now disable the engine.");
+ Services.prefs.setCharPref("browser.search.hiddenOneOffs", engine.name);
+ await check_results({
+ context,
+ autofilled: "disabled.com/",
+ completed: "https://disabled.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://disabled.com/",
+ title: "https://disabled.com",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ ],
+ });
+ Services.prefs.clearUserPref("browser.search.hiddenOneOffs");
+
+ await cleanupPlaces();
+ await Services.search.removeEngine(engine);
+});
diff --git a/browser/components/urlbar/tests/unit/test_providerTabToSearch_partialHost.js b/browser/components/urlbar/tests/unit/test_providerTabToSearch_partialHost.js
new file mode 100644
index 0000000000..4644117b07
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providerTabToSearch_partialHost.js
@@ -0,0 +1,150 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Search engine origins are autofilled normally when they get over the
+// threshold, though certain origins redirect to localized subdomains, that
+// the user is unlikely to type, for example wikipedia.org => en.wikipedia.org.
+// We should get a tab to search result also for these cases, where a normal
+// autofill wouldn't happen.
+
+"use strict";
+
+add_task(async function setup() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ // Disable tab-to-search onboarding results.
+ Services.prefs.setIntPref(
+ "browser.urlbar.tabToSearch.onboard.interactionsLeft",
+ 0
+ );
+ Services.prefs.setBoolPref(
+ "browser.search.separatePrivateDefault.ui.enabled",
+ false
+ );
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ Services.prefs.clearUserPref(
+ "browser.search.separatePrivateDefault.ui.enabled"
+ );
+ Services.prefs.clearUserPref(
+ "browser.urlbar.tabToSearch.onboard.interactionsLeft"
+ );
+ });
+
+ let url = "https://en.example.com/";
+ let engine = await Services.search.addEngineWithDetails("TestEngine", {
+ method: "GET",
+ template: url,
+ searchGetParams: "q={searchTerms}",
+ });
+ let defaultEngine = await Services.search.getDefault();
+ await Services.search.setDefault(engine);
+ registerCleanupFunction(async () => {
+ await Services.search.setDefault(defaultEngine);
+ await Services.search.removeEngine(engine);
+ });
+ // Make sure the engine domain would be autofilled.
+ await PlacesUtils.bookmarks.insert({
+ url,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "bookmark",
+ });
+
+ info("Test matching cases");
+
+ for (let searchStr of ["ex", "example.c"]) {
+ info("Searching for " + searchStr);
+ let context = createContext(searchStr, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: Services.search.defaultEngine.name,
+ providerName: "HeuristicFallback",
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: engine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED,
+ uri: "en.example.",
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ satisfiesAutofillThreshold: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: url,
+ title: "bookmark",
+ }),
+ ],
+ });
+ }
+
+ info("Test a www engine");
+ let url2 = "https://www.it.mochi.com/";
+ let engine2 = await Services.search.addEngineWithDetails("TestEngine2", {
+ method: "GET",
+ template: url2,
+ searchGetParams: "q={searchTerms}",
+ });
+ registerCleanupFunction(async () => {
+ await Services.search.removeEngine(engine2);
+ });
+ // Make sure the engine domain would be autofilled.
+ await PlacesUtils.bookmarks.insert({
+ url: url2,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "bookmark",
+ });
+
+ for (let searchStr of ["mo", "mochi.c"]) {
+ info("Searching for " + searchStr);
+ let context = createContext(searchStr, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: Services.search.defaultEngine.name,
+ providerName: "HeuristicFallback",
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: engine2.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED,
+ uri: "www.it.mochi.",
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ satisfiesAutofillThreshold: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: url2,
+ title: "bookmark",
+ }),
+ ],
+ });
+ }
+
+ info("Test non-matching cases");
+
+ for (let searchStr of ["www.en", "www.ex", "https://ex"]) {
+ info("Searching for " + searchStr);
+ let context = createContext(searchStr, { isPrivate: false });
+ // We don't want to generate all the possible results here, just check
+ // the heuristic result is not autofill.
+ let controller = UrlbarTestUtils.newMockController();
+ await UrlbarProvidersManager.startQuery(context, controller);
+ Assert.ok(context.results[0].heuristic, "Check heuristic result");
+ Assert.notEqual(context.results[0].providerName, "Autofill");
+ }
+
+ info("Restricting to history should not autofill our bookmark");
+ let context = createContext("ex", {
+ isPrivate: false,
+ sources: [UrlbarUtils.RESULT_SOURCE.HISTORY],
+ });
+ let controller = UrlbarTestUtils.newMockController();
+ await UrlbarProvidersManager.startQuery(context, controller);
+ Assert.ok(context.results[0].heuristic, "Check heuristic result");
+ Assert.notEqual(context.results[0].providerName, "Autofill");
+});
diff --git a/browser/components/urlbar/tests/unit/test_providerUnifiedComplete.js b/browser/components/urlbar/tests/unit/test_providerUnifiedComplete.js
new file mode 100644
index 0000000000..3b78564484
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providerUnifiedComplete.js
@@ -0,0 +1,242 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// This is a simple test to check the UnifiedComplete provider works, it is not
+// intended to check all the edge cases, because that component is already
+// covered by a good amount of tests.
+
+const SUGGEST_PREF = "browser.urlbar.suggest.searches";
+const SUGGEST_ENABLED_PREF = "browser.search.suggest.enabled";
+
+add_task(async function test_unifiedComplete() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+ let engine = await addTestSuggestionsEngine();
+ Services.search.defaultEngine = engine;
+ let oldCurrentEngine = Services.search.defaultEngine;
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(SUGGEST_PREF);
+ Services.prefs.clearUserPref(SUGGEST_ENABLED_PREF);
+ Services.search.defaultEngine = oldCurrentEngine;
+ });
+
+ let controller = UrlbarTestUtils.newMockController();
+ // Also check case insensitivity.
+ let searchString = "MoZ oRg";
+ let context = createContext(searchString, { isPrivate: false });
+
+ // Add entries from multiple sources.
+ await PlacesUtils.bookmarks.insert({
+ url: "https://bookmark.mozilla.org/",
+ title: "Test bookmark",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ PlacesUtils.tagging.tagURI(
+ Services.io.newURI("https://bookmark.mozilla.org/"),
+ ["mozilla", "org", "ham", "moz", "bacon"]
+ );
+ await PlacesTestUtils.addVisits([
+ { uri: "https://history.mozilla.org/", title: "Test history" },
+ { uri: "https://tab.mozilla.org/", title: "Test tab" },
+ ]);
+ UrlbarProviderOpenTabs.registerOpenTab("https://tab.mozilla.org/", 0);
+
+ await controller.startQuery(context);
+
+ info(
+ "Results:\n" +
+ context.results.map(m => `${m.title} - ${m.payload.url}`).join("\n")
+ );
+ Assert.equal(
+ context.results.length,
+ 6,
+ "Found the expected number of matches"
+ );
+
+ Assert.deepEqual(
+ [
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_TYPE.URL,
+ ],
+ context.results.map(m => m.type),
+ "Check result types"
+ );
+
+ Assert.deepEqual(
+ [
+ searchString,
+ searchString + " foo",
+ searchString + " bar",
+ "Test bookmark",
+ "Test tab",
+ "Test history",
+ ],
+ context.results.map(m => m.title),
+ "Check match titles"
+ );
+
+ Assert.deepEqual(
+ context.results[3].payload.tags,
+ ["moz", "mozilla", "org"],
+ "Check tags"
+ );
+
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ UrlbarProviderOpenTabs.unregisterOpenTab("https://tab.mozilla.org/", 0);
+});
+
+add_task(async function test_bookmarkBehaviorDisabled_tagged() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, false);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, false);
+
+ // Disable the bookmark behavior in UnifiedComplete.
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+
+ let controller = UrlbarTestUtils.newMockController();
+ // Also check case insensitivity.
+ let searchString = "MoZ oRg";
+ let context = createContext(searchString, { isPrivate: false });
+
+ // Add a tagged bookmark that's also visited.
+ await PlacesUtils.bookmarks.insert({
+ url: "https://bookmark.mozilla.org/",
+ title: "Test bookmark",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ PlacesUtils.tagging.tagURI(
+ Services.io.newURI("https://bookmark.mozilla.org/"),
+ ["mozilla", "org", "ham", "moz", "bacon"]
+ );
+ await PlacesTestUtils.addVisits("https://bookmark.mozilla.org/");
+
+ await controller.startQuery(context);
+
+ info(
+ "Results:\n" +
+ context.results.map(m => `${m.title} - ${m.payload.url}`).join("\n")
+ );
+ Assert.equal(
+ context.results.length,
+ 2,
+ "Found the expected number of matches"
+ );
+
+ Assert.deepEqual(
+ [UrlbarUtils.RESULT_TYPE.SEARCH, UrlbarUtils.RESULT_TYPE.URL],
+ context.results.map(m => m.type),
+ "Check result types"
+ );
+
+ Assert.deepEqual(
+ [searchString, "Test bookmark"],
+ context.results.map(m => m.title),
+ "Check match titles"
+ );
+
+ Assert.deepEqual(context.results[1].payload.tags, [], "Check tags");
+
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test_bookmarkBehaviorDisabled_untagged() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, false);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, false);
+
+ // Disable the bookmark behavior in UnifiedComplete.
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+
+ let controller = UrlbarTestUtils.newMockController();
+ // Also check case insensitivity.
+ let searchString = "MoZ oRg";
+ let context = createContext(searchString, { isPrivate: false });
+
+ // Add an *untagged* bookmark that's also visited.
+ await PlacesUtils.bookmarks.insert({
+ url: "https://bookmark.mozilla.org/",
+ title: "Test bookmark",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ await PlacesTestUtils.addVisits("https://bookmark.mozilla.org/");
+
+ await controller.startQuery(context);
+
+ info(
+ "Results:\n" +
+ context.results.map(m => `${m.title} - ${m.payload.url}`).join("\n")
+ );
+ Assert.equal(
+ context.results.length,
+ 2,
+ "Found the expected number of matches"
+ );
+
+ Assert.deepEqual(
+ [UrlbarUtils.RESULT_TYPE.SEARCH, UrlbarUtils.RESULT_TYPE.URL],
+ context.results.map(m => m.type),
+ "Check result types"
+ );
+
+ Assert.deepEqual(
+ [searchString, "Test bookmark"],
+ context.results.map(m => m.title),
+ "Check match titles"
+ );
+
+ Assert.deepEqual(context.results[1].payload.tags, [], "Check tags");
+
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test_diacritics() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, false);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, false);
+
+ // Enable the bookmark behavior in UnifiedComplete.
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true);
+
+ let controller = UrlbarTestUtils.newMockController();
+ let searchString = "agui";
+ let context = createContext(searchString, { isPrivate: false });
+
+ await PlacesUtils.bookmarks.insert({
+ url: "https://bookmark.mozilla.org/%C3%A3g%CC%83u%C4%A9",
+ title: "Test bookmark with accents in path",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+
+ await controller.startQuery(context);
+
+ info(
+ "Results:\n" +
+ context.results.map(m => `${m.title} - ${m.payload.url}`).join("\n")
+ );
+ Assert.equal(
+ context.results.length,
+ 2,
+ "Found the expected number of matches"
+ );
+
+ Assert.deepEqual(
+ [UrlbarUtils.RESULT_TYPE.SEARCH, UrlbarUtils.RESULT_TYPE.URL],
+ context.results.map(m => m.type),
+ "Check result types"
+ );
+
+ Assert.deepEqual(
+ [searchString, "Test bookmark with accents in path"],
+ context.results.map(m => m.title),
+ "Check match titles"
+ );
+
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+});
diff --git a/browser/components/urlbar/tests/unit/test_providerUnifiedComplete_duplicate_entries.js b/browser/components/urlbar/tests/unit/test_providerUnifiedComplete_duplicate_entries.js
new file mode 100644
index 0000000000..7533921fc6
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providerUnifiedComplete_duplicate_entries.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_duplicates() {
+ const TEST_URL = "https://history.mozilla.org/";
+ await PlacesTestUtils.addVisits([
+ { uri: TEST_URL, title: "Test history" },
+ { uri: TEST_URL + "?#", title: "Test history" },
+ { uri: TEST_URL + "#", title: "Test history" },
+ ]);
+
+ let controller = UrlbarTestUtils.newMockController();
+ let searchString = "^Hist";
+ let context = createContext(searchString, { isPrivate: false });
+ await controller.startQuery(context);
+
+ // The first result will be a search heuristic, which we don't care about for
+ // this test.
+ info(
+ "Results:\n" +
+ context.results.map(m => `${m.title} - ${m.payload.url}`).join("\n")
+ );
+ Assert.equal(
+ context.results.length,
+ 2,
+ "Found the expected number of matches"
+ );
+ Assert.equal(
+ context.results[1].type,
+ UrlbarUtils.RESULT_TYPE.URL,
+ "Should have a history result"
+ );
+ Assert.equal(
+ context.results[1].payload.url,
+ TEST_URL + "#",
+ "Check result URL"
+ );
+
+ await PlacesUtils.history.clear();
+});
diff --git a/browser/components/urlbar/tests/unit/test_providersManager.js b/browser/components/urlbar/tests/unit/test_providersManager.js
new file mode 100644
index 0000000000..57598448ea
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providersManager.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_providers() {
+ Assert.throws(
+ () => UrlbarProvidersManager.registerProvider(),
+ /invalid provider/,
+ "Should throw with no arguments"
+ );
+ Assert.throws(
+ () => UrlbarProvidersManager.registerProvider({}),
+ /invalid provider/,
+ "Should throw with empty object"
+ );
+ Assert.throws(
+ () =>
+ UrlbarProvidersManager.registerProvider({
+ name: "",
+ }),
+ /invalid provider/,
+ "Should throw with empty name"
+ );
+ Assert.throws(
+ () =>
+ UrlbarProvidersManager.registerProvider({
+ name: "test",
+ startQuery: "no",
+ }),
+ /invalid provider/,
+ "Should throw with invalid startQuery"
+ );
+ Assert.throws(
+ () =>
+ UrlbarProvidersManager.registerProvider({
+ name: "test",
+ startQuery: () => {},
+ cancelQuery: "no",
+ }),
+ /invalid provider/,
+ "Should throw with invalid cancelQuery"
+ );
+
+ let match = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: "http://mozilla.org/foo/" }
+ );
+
+ let providerName = registerBasicTestProvider([match]);
+ let context = createContext(undefined, { providers: [providerName] });
+ let controller = UrlbarTestUtils.newMockController();
+ let resultsPromise = promiseControllerNotification(
+ controller,
+ "onQueryResults"
+ );
+
+ await UrlbarProvidersManager.startQuery(context, controller);
+ // Sanity check that this doesn't throw. It should be a no-op since we await
+ // for startQuery.
+ UrlbarProvidersManager.cancelQuery(context);
+
+ let params = await resultsPromise;
+ Assert.deepEqual(params[0].results, [match]);
+});
+
+add_task(async function test_criticalSection() {
+ // Just a sanity check, this shouldn't throw.
+ await UrlbarProvidersManager.runInCriticalSection(async () => {
+ let db = await PlacesUtils.promiseLargeCacheDBConnection();
+ await db.execute(`PRAGMA page_cache`);
+ });
+});
diff --git a/browser/components/urlbar/tests/unit/test_providersManager_filtering.js b/browser/components/urlbar/tests/unit/test_providersManager_filtering.js
new file mode 100644
index 0000000000..206dd98896
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providersManager_filtering.js
@@ -0,0 +1,405 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_filtering_disable_only_source() {
+ let match = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: "http://mozilla.org/foo/" }
+ );
+ let providerName = registerBasicTestProvider([match]);
+ let context = createContext(undefined, { providers: [providerName] });
+ let controller = UrlbarTestUtils.newMockController();
+
+ info("Disable the only available source, should get no matches");
+ Services.prefs.setBoolPref("browser.urlbar.suggest.openpage", false);
+ let promise = Promise.race([
+ promiseControllerNotification(controller, "onQueryResults", false),
+ promiseControllerNotification(controller, "onQueryFinished"),
+ ]);
+ await controller.startQuery(context);
+ await promise;
+ Services.prefs.clearUserPref("browser.urlbar.suggest.openpage");
+ UrlbarProvidersManager.unregisterProvider({ name: providerName });
+});
+
+add_task(async function test_filtering_disable_one_source() {
+ let matches = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: "http://mozilla.org/foo/" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/foo/" }
+ ),
+ ];
+ let providerName = registerBasicTestProvider(matches);
+ let context = createContext(undefined, { providers: [providerName] });
+ let controller = UrlbarTestUtils.newMockController();
+
+ info("Disable one of the sources, should get a single match");
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ let promise = Promise.all([
+ promiseControllerNotification(controller, "onQueryResults"),
+ promiseControllerNotification(controller, "onQueryFinished"),
+ ]);
+ await controller.startQuery(context, controller);
+ await promise;
+ Assert.deepEqual(context.results, matches.slice(0, 1));
+ Services.prefs.clearUserPref("browser.urlbar.suggest.history");
+ UrlbarProvidersManager.unregisterProvider({ name: providerName });
+});
+
+add_task(async function test_filtering_restriction_token() {
+ let matches = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: "http://mozilla.org/foo/" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/foo/" }
+ ),
+ ];
+ let providerName = registerBasicTestProvider(matches);
+ let context = createContext(`foo ${UrlbarTokenizer.RESTRICT.OPENPAGE}`, {
+ providers: [providerName],
+ });
+ let controller = UrlbarTestUtils.newMockController();
+
+ info("Use a restriction character, should get a single match");
+ let promise = Promise.all([
+ promiseControllerNotification(controller, "onQueryResults"),
+ promiseControllerNotification(controller, "onQueryFinished"),
+ ]);
+ await controller.startQuery(context, controller);
+ await promise;
+ Assert.deepEqual(context.results, matches.slice(0, 1));
+ UrlbarProvidersManager.unregisterProvider({ name: providerName });
+});
+
+add_task(async function test_filter_javascript() {
+ let match = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: "http://mozilla.org/foo/" }
+ );
+ let jsMatch = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "javascript:foo" }
+ );
+ let providerName = registerBasicTestProvider([match, jsMatch]);
+ let context = createContext(undefined, { providers: [providerName] });
+ let controller = UrlbarTestUtils.newMockController();
+
+ info("By default javascript should be filtered out");
+ let promise = promiseControllerNotification(controller, "onQueryResults");
+ await controller.startQuery(context, controller);
+ await promise;
+ Assert.deepEqual(context.results, [match]);
+
+ info("Except when the user explicitly starts the search with javascript:");
+ context = createContext(`javascript: ${UrlbarTokenizer.RESTRICT.HISTORY}`, {
+ providers: [providerName],
+ });
+ promise = promiseControllerNotification(controller, "onQueryResults");
+ await controller.startQuery(context, controller);
+ await promise;
+ Assert.deepEqual(context.results, [jsMatch]);
+
+ info("Disable javascript filtering");
+ Services.prefs.setBoolPref("browser.urlbar.filter.javascript", false);
+ context = createContext(undefined, { providers: [providerName] });
+ promise = promiseControllerNotification(controller, "onQueryResults");
+ await controller.startQuery(context, controller);
+ await promise;
+ Assert.deepEqual(context.results, [match, jsMatch]);
+ Services.prefs.clearUserPref("browser.urlbar.filter.javascript");
+ UrlbarProvidersManager.unregisterProvider({ name: providerName });
+});
+
+add_task(async function test_filter_isActive() {
+ let goodMatches = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: "http://mozilla.org/foo/" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/foo/" }
+ ),
+ ];
+ let providerName = registerBasicTestProvider(goodMatches);
+
+ let badMatches = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ { url: "http://mozilla.org/foo/" }
+ ),
+ ];
+ /**
+ * A test provider that should not be invoked.
+ */
+ class NoInvokeProvider extends UrlbarProvider {
+ get name() {
+ return "BadProvider";
+ }
+ get type() {
+ return UrlbarUtils.PROVIDER_TYPE.PROFILE;
+ }
+ isActive(context) {
+ info("Acceptable sources: " + context.sources);
+ return context.sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS);
+ }
+ async startQuery(context, add) {
+ Assert.ok(false, "Provider should no be invoked");
+ for (const match of badMatches) {
+ add(this, match);
+ }
+ }
+ }
+ UrlbarProvidersManager.registerProvider(new NoInvokeProvider());
+
+ let context = createContext(undefined, {
+ sources: [UrlbarUtils.RESULT_SOURCE.TABS],
+ providers: [providerName, "BadProvider"],
+ });
+ let controller = UrlbarTestUtils.newMockController();
+
+ info("Only tabs should be returned");
+ let promise = promiseControllerNotification(controller, "onQueryResults");
+ await controller.startQuery(context, controller);
+ await promise;
+ Assert.deepEqual(context.results.length, 1, "Should find only one match");
+ Assert.deepEqual(
+ context.results[0].source,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ "Should find only a tab match"
+ );
+ UrlbarProvidersManager.unregisterProvider({ name: providerName });
+ UrlbarProvidersManager.unregisterProvider({ name: "BadProvider" });
+});
+
+add_task(async function test_filter_queryContext() {
+ let providerName = registerBasicTestProvider();
+
+ /**
+ * A test provider that should not be invoked because of queryContext.providers.
+ */
+ class NoInvokeProvider extends UrlbarProvider {
+ get name() {
+ return "BadProvider";
+ }
+ get type() {
+ return UrlbarUtils.PROVIDER_TYPE.PROFILE;
+ }
+ isActive(context) {
+ return true;
+ }
+ async startQuery(context, add) {
+ Assert.ok(false, "Provider should no be invoked");
+ }
+ }
+ UrlbarProvidersManager.registerProvider(new NoInvokeProvider());
+
+ let context = createContext(undefined, {
+ providers: [providerName],
+ });
+ let controller = UrlbarTestUtils.newMockController();
+
+ await controller.startQuery(context, controller);
+ UrlbarProvidersManager.unregisterProvider({ name: providerName });
+ UrlbarProvidersManager.unregisterProvider({ name: "BadProvider" });
+});
+
+add_task(async function test_nofilter_heuristic() {
+ // Checks that even if a provider returns a result that should be filtered out
+ // it will still be invoked if it's of type heuristic, and only the heuristic
+ // result is returned.
+ let matches = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: "http://mozilla.org/foo/" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: "http://mozilla.org/foo2/" }
+ ),
+ ];
+ matches[0].heuristic = true;
+ let providerName = registerBasicTestProvider(
+ matches,
+ undefined,
+ UrlbarUtils.PROVIDER_TYPE.HEURISTIC
+ );
+
+ let context = createContext(undefined, {
+ sources: [UrlbarUtils.RESULT_SOURCE.SEARCH],
+ providers: [providerName],
+ });
+ let controller = UrlbarTestUtils.newMockController();
+
+ // Disable search matches through prefs.
+ Services.prefs.setBoolPref("browser.urlbar.suggest.openpage", false);
+ info("Only 1 heuristic tab result should be returned");
+ let promise = promiseControllerNotification(controller, "onQueryResults");
+ await controller.startQuery(context, controller);
+ await promise;
+ Services.prefs.clearUserPref("browser.urlbar.suggest.openpage");
+ Assert.deepEqual(context.results.length, 1, "Should find only one match");
+ Assert.deepEqual(
+ context.results[0].source,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ "Should find only a tab match"
+ );
+ UrlbarProvidersManager.unregisterProvider({ name: providerName });
+});
+
+add_task(async function test_nofilter_restrict() {
+ // Checks that even if a pref is disabled, we still return results on a
+ // restriction token.
+ let matches = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: "http://mozilla.org/foo_tab/" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ { url: "http://mozilla.org/foo_bookmark/" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/foo_history/" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ { engine: "noengine" }
+ ),
+ ];
+ /**
+ * A test provider.
+ */
+ class TestProvider extends UrlbarProvider {
+ get name() {
+ return "MyProvider";
+ }
+ get type() {
+ return UrlbarUtils.PROVIDER_TYPE.PROFILE;
+ }
+ isActive(context) {
+ Assert.equal(context.sources.length, 1, "Check acceptable sources");
+ return true;
+ }
+ async startQuery(context, add) {
+ Assert.ok(true, "expected provider was invoked");
+ for (let match of matches) {
+ add(this, match);
+ }
+ }
+ }
+ let provider = new TestProvider();
+ UrlbarProvidersManager.registerProvider(provider);
+
+ let typeToPropertiesMap = new Map([
+ ["HISTORY", { source: "HISTORY", pref: "history" }],
+ ["BOOKMARK", { source: "BOOKMARKS", pref: "bookmark" }],
+ ["OPENPAGE", { source: "TABS", pref: "openpage" }],
+ ["SEARCH", { source: "SEARCH", pref: "searches" }],
+ ]);
+ for (let [type, token] of Object.entries(UrlbarTokenizer.RESTRICT)) {
+ let properties = typeToPropertiesMap.get(type);
+ if (!properties) {
+ continue;
+ }
+ info("Restricting on " + type);
+ let context = createContext(token + " foo", {
+ providers: ["MyProvider"],
+ });
+ let controller = UrlbarTestUtils.newMockController();
+ // Disable the corresponding pref.
+ const pref = "browser.urlbar.suggest." + properties.pref;
+ info("Disabling " + pref);
+ Services.prefs.setBoolPref(pref, false);
+ await controller.startQuery(context, controller);
+ Assert.equal(context.results.length, 1, "Should find one result");
+ Assert.equal(
+ context.results[0].source,
+ UrlbarUtils.RESULT_SOURCE[properties.source],
+ "Check result source"
+ );
+ Services.prefs.clearUserPref(pref);
+ }
+ UrlbarProvidersManager.unregisterProvider(provider);
+});
+
+add_task(async function test_filter_priority() {
+ /**
+ * A test provider.
+ */
+ class TestProvider extends UrlbarTestUtils.TestProvider {
+ constructor(priority, shouldBeInvoked, namePart = "") {
+ super();
+ this._priority = priority;
+ this._name = `${priority}` + namePart;
+ this._shouldBeInvoked = shouldBeInvoked;
+ }
+ async startQuery(context, add) {
+ Assert.ok(this._shouldBeInvoked, `${this.name} was invoked`);
+ }
+ }
+
+ // Test all possible orderings of the providers to make sure the logic that
+ // finds the highest priority providers is correct.
+ let providerPerms = permute([
+ new TestProvider(0, false),
+ new TestProvider(1, false),
+ new TestProvider(2, true, "a"),
+ new TestProvider(2, true, "b"),
+ ]);
+ for (let providers of providerPerms) {
+ for (let provider of providers) {
+ UrlbarProvidersManager.registerProvider(provider);
+ }
+ let providerNames = providers.map(p => p.name);
+ let context = createContext(undefined, { providers: providerNames });
+ let controller = UrlbarTestUtils.newMockController();
+ await controller.startQuery(context, controller);
+ for (let name of providerNames) {
+ UrlbarProvidersManager.unregisterProvider({ name });
+ }
+ }
+});
+
+function permute(objects) {
+ if (objects.length <= 1) {
+ return [objects];
+ }
+ let perms = [];
+ for (let i = 0; i < objects.length; i++) {
+ let otherObjects = objects.slice();
+ otherObjects.splice(i, 1);
+ let otherPerms = permute(otherObjects);
+ for (let perm of otherPerms) {
+ perm.unshift(objects[i]);
+ }
+ perms = perms.concat(otherPerms);
+ }
+ return perms;
+}
diff --git a/browser/components/urlbar/tests/unit/test_providersManager_maxResults.js b/browser/components/urlbar/tests/unit/test_providersManager_maxResults.js
new file mode 100644
index 0000000000..c3f6e5c2e3
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providersManager_maxResults.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_maxResults() {
+ const MATCHES_LENGTH = 20;
+ let matches = [];
+ for (let i = 0; i < MATCHES_LENGTH; i++) {
+ matches.push(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: `http://mozilla.org/foo/${i}` }
+ )
+ );
+ }
+ let providerName = registerBasicTestProvider(matches);
+ let context = createContext(undefined, { providers: [providerName] });
+ let controller = UrlbarTestUtils.newMockController();
+
+ async function test_count(count) {
+ let promise = promiseControllerNotification(controller, "onQueryFinished");
+ context.maxResults = count;
+ await controller.startQuery(context);
+ await promise;
+ Assert.equal(
+ context.results.length,
+ Math.min(MATCHES_LENGTH, count),
+ "Check count"
+ );
+ Assert.deepEqual(context.results, matches.slice(0, count), "Check results");
+ }
+ await test_count(10);
+ await test_count(1);
+ await test_count(30);
+});
diff --git a/browser/components/urlbar/tests/unit/test_queryScorer.js b/browser/components/urlbar/tests/unit/test_queryScorer.js
new file mode 100644
index 0000000000..4af1b26112
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_queryScorer.js
@@ -0,0 +1,405 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ QueryScorer: "resource:///modules/UrlbarProviderInterventions.jsm",
+});
+
+const DISTANCE_THRESHOLD = 1;
+
+const DOCUMENTS = {
+ clear: [
+ "cache firefox",
+ "clear cache firefox",
+ "clear cache in firefox",
+ "clear cookies firefox",
+ "clear firefox cache",
+ "clear history firefox",
+ "cookies firefox",
+ "delete cookies firefox",
+ "delete history firefox",
+ "firefox cache",
+ "firefox clear cache",
+ "firefox clear cookies",
+ "firefox clear history",
+ "firefox cookie",
+ "firefox cookies",
+ "firefox delete cookies",
+ "firefox delete history",
+ "firefox history",
+ "firefox not loading pages",
+ "history firefox",
+ "how to clear cache",
+ "how to clear history",
+ ],
+ refresh: [
+ "firefox crashing",
+ "firefox keeps crashing",
+ "firefox not responding",
+ "firefox not working",
+ "firefox refresh",
+ "firefox slow",
+ "how to reset firefox",
+ "refresh firefox",
+ "reset firefox",
+ ],
+ update: [
+ "download firefox",
+ "download mozilla",
+ "firefox browser",
+ "firefox download",
+ "firefox for mac",
+ "firefox for windows",
+ "firefox free download",
+ "firefox install",
+ "firefox installer",
+ "firefox latest version",
+ "firefox mac",
+ "firefox quantum",
+ "firefox update",
+ "firefox version",
+ "firefox windows",
+ "get firefox",
+ "how to update firefox",
+ "install firefox",
+ "mozilla download",
+ "mozilla firefox 2019",
+ "mozilla firefox 2020",
+ "mozilla firefox download",
+ "mozilla firefox for mac",
+ "mozilla firefox for windows",
+ "mozilla firefox free download",
+ "mozilla firefox mac",
+ "mozilla firefox update",
+ "mozilla firefox windows",
+ "mozilla update",
+ "update firefox",
+ "update mozilla",
+ "www.firefox.com",
+ ],
+};
+
+const VARIATIONS = new Map([["firefox", ["fire fox", "fox fire", "foxfire"]]]);
+
+let tests = [
+ {
+ query: "firefox",
+ matches: [
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "bogus",
+ matches: [
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "no match",
+ matches: [
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+
+ // clear
+ {
+ query: "firefox histo",
+ matches: [
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefox histor",
+ matches: [
+ { id: "clear", score: 1 },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefox history",
+ matches: [
+ { id: "clear", score: 0 },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefox history we'll keep matching once we match",
+ matches: [
+ { id: "clear", score: 0 },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+
+ {
+ query: "firef history",
+ matches: [
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefo history",
+ matches: [
+ { id: "clear", score: 1 },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefo histor",
+ matches: [
+ { id: "clear", score: 2 },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefo histor we'll keep matching once we match",
+ matches: [
+ { id: "clear", score: 2 },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+
+ {
+ query: "fire fox history",
+ matches: [
+ { id: "clear", score: 0 },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "fox fire history",
+ matches: [
+ { id: "clear", score: 0 },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "foxfire history",
+ matches: [
+ { id: "clear", score: 0 },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+
+ // refresh
+ {
+ query: "firefox sl",
+ matches: [
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefox slo",
+ matches: [
+ { id: "refresh", score: 1 },
+ { id: "clear", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefox slow",
+ matches: [
+ { id: "refresh", score: 0 },
+ { id: "clear", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefox slow we'll keep matching once we match",
+ matches: [
+ { id: "refresh", score: 0 },
+ { id: "clear", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+
+ {
+ query: "firef slow",
+ matches: [
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefo slow",
+ matches: [
+ { id: "refresh", score: 1 },
+ { id: "clear", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefo slo",
+ matches: [
+ { id: "refresh", score: 2 },
+ { id: "clear", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefo slo we'll keep matching once we match",
+ matches: [
+ { id: "refresh", score: 2 },
+ { id: "clear", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+
+ {
+ query: "fire fox slow",
+ matches: [
+ { id: "refresh", score: 0 },
+ { id: "clear", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "fox fire slow",
+ matches: [
+ { id: "refresh", score: 0 },
+ { id: "clear", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "foxfire slow",
+ matches: [
+ { id: "refresh", score: 0 },
+ { id: "clear", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+
+ // update
+ {
+ query: "firefox upda",
+ matches: [
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefox updat",
+ matches: [
+ { id: "update", score: 1 },
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ ],
+ },
+ {
+ query: "firefox update",
+ matches: [
+ { id: "update", score: 0 },
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ ],
+ },
+ {
+ query: "firefox update we'll keep matching once we match",
+ matches: [
+ { id: "update", score: 0 },
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ ],
+ },
+
+ {
+ query: "firef update",
+ matches: [
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefo update",
+ matches: [
+ { id: "update", score: 1 },
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ ],
+ },
+ {
+ query: "firefo updat",
+ matches: [
+ { id: "update", score: 2 },
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ ],
+ },
+ {
+ query: "firefo updat we'll keep matching once we match",
+ matches: [
+ { id: "update", score: 2 },
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ ],
+ },
+
+ {
+ query: "fire fox update",
+ matches: [
+ { id: "update", score: 0 },
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ ],
+ },
+ {
+ query: "fox fire update",
+ matches: [
+ { id: "update", score: 0 },
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ ],
+ },
+ {
+ query: "foxfire update",
+ matches: [
+ { id: "update", score: 0 },
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ ],
+ },
+];
+
+add_task(async function test() {
+ let qs = new QueryScorer({
+ distanceThreshold: DISTANCE_THRESHOLD,
+ variations: VARIATIONS,
+ });
+
+ for (let [id, phrases] of Object.entries(DOCUMENTS)) {
+ qs.addDocument({ id, phrases });
+ }
+
+ for (let { query, matches } of tests) {
+ let actual = qs
+ .score(query)
+ .map(result => ({ id: result.document.id, score: result.score }));
+ Assert.deepEqual(actual, matches, `Query: "${query}"`);
+ }
+});
diff --git a/browser/components/urlbar/tests/unit/test_query_url.js b/browser/components/urlbar/tests/unit/test_query_url.js
new file mode 100644
index 0000000000..464cae1064
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_query_url.js
@@ -0,0 +1,123 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const ENGINE_NAME = "engine-suggestions.xml";
+const HEURISTIC_FALLBACK_PROVIDERNAME = "HeuristicFallback";
+const UNIFIEDCOMPLETE_PROVIDERNAME = "UnifiedComplete";
+
+testEngine_setup();
+
+add_task(async function test_no_slash() {
+ info("Searching for host match without slash should match host");
+ await PlacesTestUtils.addVisits([
+ { uri: "http://file.org/test/" },
+ { uri: "file:///c:/test.html" },
+ ]);
+ let context = createContext("file", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "file.org/",
+ completed: "http://file.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://file.org/",
+ title: "file.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "file:///c:/test.html",
+ title: "test visit for file:///c:/test.html",
+ iconUri: UrlbarUtils.ICON.DEFAULT,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://file.org/test/",
+ title: "test visit for http://file.org/test/",
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_w_slash() {
+ info("Searching match with slash at the end should match url");
+ await PlacesTestUtils.addVisits(
+ {
+ uri: Services.io.newURI("http://file.org/test/"),
+ },
+ {
+ uri: Services.io.newURI("file:///c:/test.html"),
+ }
+ );
+ let context = createContext("file.org/", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "file.org/",
+ completed: "http://file.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://file.org/",
+ title: "file.org/",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://file.org/test/",
+ title: "test visit for http://file.org/test/",
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_middle() {
+ info("Searching match with slash in the middle should match url");
+ await PlacesTestUtils.addVisits(
+ {
+ uri: Services.io.newURI("http://file.org/test/"),
+ },
+ {
+ uri: Services.io.newURI("file:///c:/test.html"),
+ }
+ );
+ let context = createContext("file.org/t", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "file.org/test/",
+ completed: "http://file.org/test/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://file.org/test/",
+ title: "file.org/test/",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_nonhost() {
+ info("Searching for non-host match without slash should not match url");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("file:///c:/test.html"),
+ });
+ let context = createContext("file", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "file:///c:/test.html",
+ title: "test visit for file:///c:/test.html",
+ iconUri: UrlbarUtils.ICON.DEFAULT,
+ providerName: UNIFIEDCOMPLETE_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_search_engine_host.js b/browser/components/urlbar/tests/unit/test_search_engine_host.js
new file mode 100644
index 0000000000..73c455e9c4
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_search_engine_host.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let engine;
+
+add_task(async function test_searchEngine_autoFill() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ await Services.search.addEngineWithDetails("MySearchEngine", {
+ method: "GET",
+ template: "http://my.search.com/",
+ });
+ engine = Services.search.getEngineByName("MySearchEngine");
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.autoFill.searchEngines");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ Services.search.removeEngine(engine);
+ });
+
+ // Add an uri that matches the search string with high frecency.
+ let uri = Services.io.newURI("http://www.example.com/my/");
+ let visits = [];
+ for (let i = 0; i < 100; ++i) {
+ visits.push({ uri, title: "Terms - SearchEngine Search" });
+ }
+ await PlacesTestUtils.addVisits(visits);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri,
+ title: "Example bookmark",
+ });
+ await PlacesTestUtils.promiseAsyncUpdates();
+ ok(
+ frecencyForUrl(uri) > 10000,
+ "Added URI should have expected high frecency"
+ );
+
+ info(
+ "Check search domain is autoFilled even if there's an higher frecency match"
+ );
+ let context = createContext("my", { isPrivate: false });
+ await check_results({
+ search: "my",
+ autofilled: "my.search.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: "MySearchEngine",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_searchEngine_noautoFill() {
+ Services.prefs.setIntPref(
+ "browser.urlbar.tabToSearch.onboard.interactionsLeft",
+ 0
+ );
+ await PlacesTestUtils.addVisits(
+ Services.io.newURI("http://my.search.com/samplepage/")
+ );
+
+ info("Check search domain is not autoFilled if it matches a visited domain");
+ let context = createContext("my", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "my.search.com/",
+ completed: "http://my.search.com/",
+ matches: [
+ // Note this result is a normal Autofill result and not a priority engine.
+ makeVisitResult(context, {
+ uri: "http://my.search.com/",
+ title: "my.search.com",
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: engine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(engine.getResultDomain()),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ makeVisitResult(context, {
+ uri: "http://my.search.com/samplepage/",
+ title: "test visit for http://my.search.com/samplepage/",
+ providerName: "UnifiedComplete",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+ Services.prefs.clearUserPref(
+ "browser.urlbar.tabToSearch.onboard.interactionsLeft"
+ );
+});
diff --git a/browser/components/urlbar/tests/unit/test_search_suggestions.js b/browser/components/urlbar/tests/unit/test_search_suggestions.js
new file mode 100644
index 0000000000..0b1180959a
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_search_suggestions.js
@@ -0,0 +1,1695 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that search engine suggestions are returned by
+ * UrlbarProviderSearchSuggestions.
+ */
+
+const { FormHistory } = ChromeUtils.import(
+ "resource://gre/modules/FormHistory.jsm"
+);
+
+const ENGINE_NAME = "engine-suggestions.xml";
+// This is fixed to match the port number in engine-suggestions.xml.
+const SERVER_PORT = 9000;
+const SUGGEST_PREF = "browser.urlbar.suggest.searches";
+const SUGGEST_ENABLED_PREF = "browser.search.suggest.enabled";
+const PRIVATE_ENABLED_PREF = "browser.search.suggest.enabled.private";
+const PRIVATE_SEARCH_PREF = "browser.search.separatePrivateDefault.ui.enabled";
+const MAX_RICH_RESULTS_PREF = "browser.urlbar.maxRichResults";
+const MAX_FORM_HISTORY_PREF = "browser.urlbar.maxHistoricalSearchSuggestions";
+const SEARCH_STRING = "hello";
+const MATCH_BUCKETS_VALUE = "general:5,suggestion:Infinity";
+
+var suggestionsFn;
+var previousSuggestionsFn;
+
+/**
+ * Set the current suggestion funciton.
+ * @param {function} fn
+ * A function that that a search string and returns an array of strings that
+ * will be used as search suggestions.
+ * Note: `fn` should return > 0 suggestions in most cases. Otherwise, you may
+ * encounter unexpected behaviour with UrlbarProviderSuggestion's
+ * _lastLowResultsSearchSuggestion safeguard.
+ */
+function setSuggestionsFn(fn) {
+ previousSuggestionsFn = suggestionsFn;
+ suggestionsFn = fn;
+}
+
+async function cleanup() {
+ Services.prefs.clearUserPref("browser.urlbar.autoFill");
+ Services.prefs.clearUserPref("browser.urlbar.autoFill.searchEngines");
+ Services.prefs.clearUserPref(SUGGEST_PREF);
+ Services.prefs.clearUserPref(SUGGEST_ENABLED_PREF);
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+}
+
+async function cleanUpSuggestions() {
+ await cleanup();
+ if (previousSuggestionsFn) {
+ suggestionsFn = previousSuggestionsFn;
+ previousSuggestionsFn = null;
+ }
+}
+
+function makeExpectedFormHistoryResults(context, minCount = 0) {
+ let count = Math.max(
+ minCount,
+ Services.prefs.getIntPref(MAX_FORM_HISTORY_PREF, 0)
+ );
+ let results = [];
+ for (let i = 0; i < count; i++) {
+ results.push(
+ makeFormHistoryResult(context, {
+ suggestion: `${SEARCH_STRING} world Form History ${i}`,
+ engineName: ENGINE_NAME,
+ })
+ );
+ }
+ return results;
+}
+
+function makeExpectedRemoteSuggestionResults(
+ context,
+ { suggestionPrefix = SEARCH_STRING, query = undefined } = {}
+) {
+ return [
+ makeSearchResult(context, {
+ query,
+ engineName: ENGINE_NAME,
+ suggestion: suggestionPrefix + " foo",
+ }),
+ makeSearchResult(context, {
+ query,
+ engineName: ENGINE_NAME,
+ suggestion: suggestionPrefix + " bar",
+ }),
+ ];
+}
+
+function makeExpectedSuggestionResults(
+ context,
+ { suggestionPrefix = SEARCH_STRING, query = undefined } = {}
+) {
+ return [
+ ...makeExpectedFormHistoryResults(context),
+ ...makeExpectedRemoteSuggestionResults(context, {
+ suggestionPrefix,
+ query,
+ }),
+ ];
+}
+
+add_task(async function setup() {
+ Services.prefs.setCharPref(
+ "browser.urlbar.matchBuckets",
+ MATCH_BUCKETS_VALUE
+ );
+
+ let engine = await addTestSuggestionsEngine(searchStr => {
+ return suggestionsFn(searchStr);
+ });
+ setSuggestionsFn(searchStr => {
+ let suffixes = ["foo", "bar"];
+ return [searchStr].concat(suffixes.map(s => searchStr + " " + s));
+ });
+
+ // Install the test engine.
+ let oldDefaultEngine = await Services.search.getDefault();
+ registerCleanupFunction(async () => {
+ Services.search.setDefault(oldDefaultEngine);
+ Services.prefs.clearUserPref(PRIVATE_SEARCH_PREF);
+ });
+ Services.search.setDefault(engine);
+ Services.prefs.setBoolPref(PRIVATE_SEARCH_PREF, false);
+
+ // Add some form history.
+ let context = createContext(SEARCH_STRING, { isPrivate: false });
+ let entries = makeExpectedFormHistoryResults(context, 2).map(r => ({
+ value: r.payload.suggestion,
+ source: ENGINE_NAME,
+ }));
+ await UrlbarTestUtils.formHistory.add(entries);
+});
+
+add_task(async function disabled_urlbarSuggestions() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, false);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+ let context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+ await cleanUpSuggestions();
+});
+
+add_task(async function disabled_allSuggestions() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, false);
+ let context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+ await cleanUpSuggestions();
+});
+
+add_task(async function disabled_privateWindow() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+ Services.prefs.setBoolPref(PRIVATE_ENABLED_PREF, false);
+ let context = createContext(SEARCH_STRING, { isPrivate: true });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+ await cleanUpSuggestions();
+});
+
+add_task(async function disabled_urlbarSuggestions_withRestrictionToken() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, false);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+ let context = createContext(
+ `${UrlbarTokenizer.RESTRICT.SEARCH} ${SEARCH_STRING}`,
+ { isPrivate: false }
+ );
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ query: SEARCH_STRING,
+ alias: UrlbarTokenizer.RESTRICT.SEARCH,
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeExpectedSuggestionResults(context, {
+ query: SEARCH_STRING,
+ }),
+ ],
+ });
+ await cleanUpSuggestions();
+});
+
+add_task(
+ async function disabled_urlbarSuggestions_withRestrictionToken_private() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, false);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+ Services.prefs.setBoolPref(PRIVATE_ENABLED_PREF, false);
+ let context = createContext(
+ `${UrlbarTokenizer.RESTRICT.SEARCH} ${SEARCH_STRING}`,
+ { isPrivate: true }
+ );
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ query: SEARCH_STRING,
+ alias: UrlbarTokenizer.RESTRICT.SEARCH,
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanUpSuggestions();
+ }
+);
+
+add_task(
+ async function disabled_urlbarSuggestions_withRestrictionToken_private_enabled() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, false);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+ Services.prefs.setBoolPref(PRIVATE_ENABLED_PREF, true);
+ let context = createContext(
+ `${UrlbarTokenizer.RESTRICT.SEARCH} ${SEARCH_STRING}`,
+ { isPrivate: true }
+ );
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ query: SEARCH_STRING,
+ alias: UrlbarTokenizer.RESTRICT.SEARCH,
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeExpectedSuggestionResults(context, {
+ query: SEARCH_STRING,
+ }),
+ ],
+ });
+ await cleanUpSuggestions();
+ }
+);
+
+add_task(async function enabled_by_pref_privateWindow() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+ Services.prefs.setBoolPref(PRIVATE_ENABLED_PREF, true);
+ let context = createContext(SEARCH_STRING, { isPrivate: true });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ...makeExpectedSuggestionResults(context),
+ ],
+ });
+ await cleanUpSuggestions();
+
+ Services.prefs.clearUserPref(PRIVATE_ENABLED_PREF);
+});
+
+add_task(async function singleWordQuery() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+ let context = createContext(SEARCH_STRING, { isPrivate: false });
+
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ...makeExpectedSuggestionResults(context),
+ ],
+ });
+
+ await cleanUpSuggestions();
+});
+
+add_task(async function multiWordQuery() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+ const query = `${SEARCH_STRING} world`;
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ...makeExpectedSuggestionResults(context, { suggestionPrefix: query }),
+ ],
+ });
+
+ await cleanUpSuggestions();
+});
+
+add_task(async function suffixMatch() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+
+ setSuggestionsFn(searchStr => {
+ let prefixes = ["baz", "quux"];
+ return prefixes.map(p => p + " " + searchStr);
+ });
+
+ let context = createContext(SEARCH_STRING, { isPrivate: false });
+
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ...makeExpectedFormHistoryResults(context),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "baz " + SEARCH_STRING,
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "quux " + SEARCH_STRING,
+ }),
+ ],
+ });
+
+ await cleanUpSuggestions();
+});
+
+add_task(async function queryIsNotASubstring() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+
+ setSuggestionsFn(searchStr => {
+ return ["aaa", "bbb"];
+ });
+
+ let context = createContext(SEARCH_STRING, { isPrivate: false });
+
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ...makeExpectedFormHistoryResults(context),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "aaa",
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "bbb",
+ }),
+ ],
+ });
+
+ await cleanUpSuggestions();
+});
+
+add_task(async function restrictToken() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+
+ // Add a visit and a bookmark. Actually, make the bookmark visited too so
+ // that it's guaranteed, with its higher frecency, to appear above the search
+ // suggestions.
+ await PlacesTestUtils.addVisits([
+ {
+ uri: Services.io.newURI(`http://example.com/${SEARCH_STRING}-visit`),
+ title: `${SEARCH_STRING} visit`,
+ },
+ {
+ uri: Services.io.newURI(`http://example.com/${SEARCH_STRING}-bookmark`),
+ title: `${SEARCH_STRING} bookmark`,
+ },
+ ]);
+
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: Services.io.newURI(`http://example.com/${SEARCH_STRING}-bookmark`),
+ title: `${SEARCH_STRING} bookmark`,
+ });
+
+ let context = createContext(SEARCH_STRING, { isPrivate: false });
+
+ // Do an unrestricted search to make sure everything appears in it, including
+ // the visit and bookmark.
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeBookmarkResult(context, {
+ uri: `http://example.com/${SEARCH_STRING}-bookmark`,
+ title: `${SEARCH_STRING} bookmark`,
+ }),
+ makeVisitResult(context, {
+ uri: `http://example.com/${SEARCH_STRING}-visit`,
+ title: `${SEARCH_STRING} visit`,
+ }),
+ ...makeExpectedSuggestionResults(context),
+ ],
+ });
+
+ // Now do a restricted search to make sure only suggestions appear.
+ context = createContext(
+ `${UrlbarTokenizer.RESTRICT.SEARCH} ${SEARCH_STRING}`,
+ {
+ isPrivate: false,
+ }
+ );
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ alias: UrlbarTokenizer.RESTRICT.SEARCH,
+ query: SEARCH_STRING,
+ heuristic: true,
+ }),
+ ...makeExpectedSuggestionResults(context, {
+ suggestionPrefix: SEARCH_STRING,
+ query: SEARCH_STRING,
+ }),
+ ],
+ });
+
+ // Typing the search restriction char shows the Search Engine entry and local
+ // results.
+ context = createContext(UrlbarTokenizer.RESTRICT.SEARCH, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ query: "",
+ heuristic: true,
+ }),
+ ...makeExpectedFormHistoryResults(context),
+ ],
+ });
+
+ // Also if followed by multiple spaces.
+ context = createContext(`${UrlbarTokenizer.RESTRICT.SEARCH} `, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ alias: UrlbarTokenizer.RESTRICT.SEARCH,
+ query: "",
+ heuristic: true,
+ }),
+ ...makeExpectedFormHistoryResults(context),
+ ],
+ });
+
+ // If followed by any char we should fetch suggestions.
+ // Note this uses "h" to match form history.
+ context = createContext(`${UrlbarTokenizer.RESTRICT.SEARCH}h`, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ query: "h",
+ heuristic: true,
+ }),
+ ...makeExpectedSuggestionResults(context, {
+ suggestionPrefix: "h",
+ query: "h",
+ }),
+ ],
+ });
+
+ // Also if followed by a space and single char.
+ context = createContext(`${UrlbarTokenizer.RESTRICT.SEARCH} h`, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ alias: UrlbarTokenizer.RESTRICT.SEARCH,
+ query: "h",
+ heuristic: true,
+ }),
+ ...makeExpectedSuggestionResults(context, {
+ suggestionPrefix: "h",
+ query: "h",
+ }),
+ ],
+ });
+
+ // Leading search-mode restriction tokens are removed.
+ context = createContext(
+ `${UrlbarTokenizer.RESTRICT.BOOKMARK} ${SEARCH_STRING}`,
+ { isPrivate: false }
+ );
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ query: SEARCH_STRING,
+ alias: UrlbarTokenizer.RESTRICT.BOOKMARK,
+ }),
+ makeBookmarkResult(context, {
+ uri: `http://example.com/${SEARCH_STRING}-bookmark`,
+ title: `${SEARCH_STRING} bookmark`,
+ }),
+ ],
+ });
+
+ // Non-search-mode restriction tokens remain in the query and heuristic search
+ // result.
+ let token;
+ for (let t of Object.values(UrlbarTokenizer.RESTRICT)) {
+ if (!UrlbarTokenizer.SEARCH_MODE_RESTRICT.has(t)) {
+ token = t;
+ break;
+ }
+ }
+ Assert.ok(
+ token,
+ "Non-search-mode restrict token exists -- if not, you can probably remove me!"
+ );
+ context = createContext(token, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ await cleanUpSuggestions();
+});
+
+add_task(async function mixup_frecency() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ // At most, we should have 14 results in this subtest. We set this to 20 to
+ // make we're not cutting off any results and we are actually getting 12.
+ Services.prefs.setIntPref(MAX_RICH_RESULTS_PREF, 20);
+
+ // Add a visit and a bookmark. Actually, make the bookmark visited too so
+ // that it's guaranteed, with its higher frecency, to appear above the search
+ // suggestions.
+ await PlacesTestUtils.addVisits([
+ {
+ uri: Services.io.newURI("http://example.com/lo0"),
+ title: `${SEARCH_STRING} low frecency 0`,
+ },
+ {
+ uri: Services.io.newURI("http://example.com/lo1"),
+ title: `${SEARCH_STRING} low frecency 1`,
+ },
+ {
+ uri: Services.io.newURI("http://example.com/lo2"),
+ title: `${SEARCH_STRING} low frecency 2`,
+ },
+ {
+ uri: Services.io.newURI("http://example.com/lo3"),
+ title: `${SEARCH_STRING} low frecency 3`,
+ },
+ {
+ uri: Services.io.newURI("http://example.com/lo4"),
+ title: `${SEARCH_STRING} low frecency 4`,
+ },
+ ]);
+
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: Services.io.newURI("http://example.com/hi0"),
+ title: `${SEARCH_STRING} high frecency 0`,
+ transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ },
+ {
+ uri: Services.io.newURI("http://example.com/hi1"),
+ title: `${SEARCH_STRING} high frecency 1`,
+ transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ },
+ {
+ uri: Services.io.newURI("http://example.com/hi2"),
+ title: `${SEARCH_STRING} high frecency 2`,
+ transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ },
+ {
+ uri: Services.io.newURI("http://example.com/hi3"),
+ title: `${SEARCH_STRING} high frecency 3`,
+ transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ },
+ ]);
+ }
+
+ for (let i = 0; i < 4; i++) {
+ let href = `http://example.com/hi${i}`;
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: href,
+ title: `${SEARCH_STRING} high frecency ${i}`,
+ });
+ }
+
+ // Do an unrestricted search to make sure everything appears in it, including
+ // the visit and bookmark.
+ let context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeBookmarkResult(context, {
+ uri: "http://example.com/hi3",
+ title: `${SEARCH_STRING} high frecency 3`,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://example.com/hi2",
+ title: `${SEARCH_STRING} high frecency 2`,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://example.com/hi1",
+ title: `${SEARCH_STRING} high frecency 1`,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://example.com/hi0",
+ title: `${SEARCH_STRING} high frecency 0`,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo4",
+ title: `${SEARCH_STRING} low frecency 4`,
+ }),
+ ...makeExpectedSuggestionResults(context),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo3",
+ title: `${SEARCH_STRING} low frecency 3`,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo2",
+ title: `${SEARCH_STRING} low frecency 2`,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo1",
+ title: `${SEARCH_STRING} low frecency 1`,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo0",
+ title: `${SEARCH_STRING} low frecency 0`,
+ }),
+ ],
+ });
+
+ // Change the "general" context mixup.
+ Services.prefs.setCharPref(
+ "browser.urlbar.matchBuckets",
+ "suggestion:1,general:5,suggestion:1"
+ );
+
+ // Do an unrestricted search to make sure everything appears in it, including
+ // the visits and bookmarks.
+ context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ...makeExpectedSuggestionResults(context).slice(0, 1),
+ makeBookmarkResult(context, {
+ uri: "http://example.com/hi3",
+ title: `${SEARCH_STRING} high frecency 3`,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://example.com/hi2",
+ title: `${SEARCH_STRING} high frecency 2`,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://example.com/hi1",
+ title: `${SEARCH_STRING} high frecency 1`,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://example.com/hi0",
+ title: `${SEARCH_STRING} high frecency 0`,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo4",
+ title: `${SEARCH_STRING} low frecency 4`,
+ }),
+ ...makeExpectedSuggestionResults(context).slice(1),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo3",
+ title: `${SEARCH_STRING} low frecency 3`,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo2",
+ title: `${SEARCH_STRING} low frecency 2`,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo1",
+ title: `${SEARCH_STRING} low frecency 1`,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo0",
+ title: `${SEARCH_STRING} low frecency 0`,
+ }),
+ ],
+ });
+
+ // Change the "search" context mixup.
+ Services.prefs.setCharPref(
+ "browser.urlbar.matchBucketsSearch",
+ "suggestion:2,general:4"
+ );
+
+ context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ...makeExpectedSuggestionResults(context).slice(0, 2),
+ makeBookmarkResult(context, {
+ uri: "http://example.com/hi3",
+ title: `${SEARCH_STRING} high frecency 3`,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://example.com/hi2",
+ title: `${SEARCH_STRING} high frecency 2`,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://example.com/hi1",
+ title: `${SEARCH_STRING} high frecency 1`,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://example.com/hi0",
+ title: `${SEARCH_STRING} high frecency 0`,
+ }),
+ ...makeExpectedSuggestionResults(context).slice(2),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo4",
+ title: `${SEARCH_STRING} low frecency 4`,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo3",
+ title: `${SEARCH_STRING} low frecency 3`,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo2",
+ title: `${SEARCH_STRING} low frecency 2`,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo1",
+ title: `${SEARCH_STRING} low frecency 1`,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo0",
+ title: `${SEARCH_STRING} low frecency 0`,
+ }),
+ ],
+ });
+
+ Services.prefs.setCharPref(
+ "browser.urlbar.matchBuckets",
+ MATCH_BUCKETS_VALUE
+ );
+ Services.prefs.clearUserPref("browser.urlbar.matchBucketsSearch");
+ Services.prefs.clearUserPref(MAX_RICH_RESULTS_PREF);
+ await cleanUpSuggestions();
+});
+
+add_task(async function prohibit_suggestions() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(
+ `browser.fixup.domainwhitelist.${SEARCH_STRING}`,
+ false
+ );
+
+ let context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ...makeExpectedSuggestionResults(context),
+ ],
+ });
+
+ Services.prefs.setBoolPref(
+ `browser.fixup.domainwhitelist.${SEARCH_STRING}`,
+ true
+ );
+ registerCleanupFunction(() => {
+ Services.prefs.setBoolPref(
+ `browser.fixup.domainwhitelist.${SEARCH_STRING}`,
+ false
+ );
+ });
+ context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${SEARCH_STRING}/`,
+ title: `http://${SEARCH_STRING}/`,
+ iconUri: "",
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: false,
+ }),
+ ...makeExpectedFormHistoryResults(context),
+ ],
+ });
+
+ // When using multiple words, we should still get suggestions:
+ let query = `${SEARCH_STRING} world`;
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ...makeExpectedSuggestionResults(context, { suggestionPrefix: query }),
+ ],
+ });
+
+ // Clear the whitelist for SEARCH_STRING and try preferring DNS for any single
+ // word instead:
+ Services.prefs.setBoolPref(
+ `browser.fixup.domainwhitelist.${SEARCH_STRING}`,
+ false
+ );
+ Services.prefs.setBoolPref("browser.fixup.dns_first_for_single_words", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.fixup.dns_first_for_single_words");
+ });
+
+ context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${SEARCH_STRING}/`,
+ title: `http://${SEARCH_STRING}/`,
+ iconUri: "",
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: false,
+ }),
+ ...makeExpectedFormHistoryResults(context),
+ ],
+ });
+
+ context = createContext("somethingelse", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://somethingelse/",
+ title: "http://somethingelse/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: false,
+ }),
+ ],
+ });
+
+ // When using multiple words, we should still get suggestions:
+ query = `${SEARCH_STRING} world`;
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ...makeExpectedSuggestionResults(context, { suggestionPrefix: query }),
+ ],
+ });
+
+ Services.prefs.clearUserPref("browser.fixup.dns_first_for_single_words");
+
+ context = createContext("http://1.2.3.4/", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://1.2.3.4/",
+ title: "http://1.2.3.4/",
+ iconUri: "page-icon:http://1.2.3.4/",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("[2001::1]:30", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://[2001::1]:30/",
+ title: "http://[2001::1]:30/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("user:pass@test", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://user:pass@test/",
+ title: "http://user:pass@test/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("data:text/plain,Content", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "data:text/plain,Content",
+ title: "data:text/plain,Content",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("a", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ await cleanUpSuggestions();
+});
+
+add_task(async function uri_like_queries() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+
+ // We should not fetch any suggestions for an actual URL.
+ let query = "mozilla.org";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ title: `http://${query}/`,
+ uri: `http://${query}/`,
+ iconUri: "",
+ heuristic: true,
+ }),
+ makeSearchResult(context, { query, engineName: ENGINE_NAME }),
+ ],
+ });
+
+ // We should also not fetch suggestions for a partially-typed URL.
+ query = "mozilla.o";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ // Now trying queries that could be confused for URLs. They should return
+ // results.
+ const uriLikeQueries = [
+ "mozilla.org is a great website",
+ "I like mozilla.org",
+ "a/b testing",
+ "he/him",
+ "Google vs.",
+ "5.8 cm",
+ ];
+ for (query of uriLikeQueries) {
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ...makeExpectedRemoteSuggestionResults(context, {
+ suggestionPrefix: query,
+ }),
+ ],
+ });
+ }
+
+ await cleanUpSuggestions();
+});
+
+add_task(async function avoid_remote_url_suggestions_1() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setIntPref(MAX_FORM_HISTORY_PREF, 1);
+
+ setSuggestionsFn(searchStr => {
+ let suffixes = [".com", "/test", ":1]", "@test", ". com"];
+ return suffixes.map(s => searchStr + s);
+ });
+
+ const query = "test";
+
+ await UrlbarTestUtils.formHistory.add([`${query}.com`]);
+
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeFormHistoryResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: `${query}.com`,
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: `${query}. com`,
+ }),
+ ],
+ });
+
+ await cleanUpSuggestions();
+ await UrlbarTestUtils.formHistory.remove([`${query}.com`]);
+ Services.prefs.clearUserPref(MAX_FORM_HISTORY_PREF);
+});
+
+add_task(async function avoid_remote_url_suggestions_2() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
+
+ setSuggestionsFn(searchStr => {
+ let suffixes = ["ed", "eds"];
+ return suffixes.map(s => searchStr + s);
+ });
+
+ let context = createContext("htt", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "htted",
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "htteds",
+ }),
+ ],
+ });
+
+ context = createContext("ftp", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "ftped",
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "ftpeds",
+ }),
+ ],
+ });
+
+ context = createContext("http", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "httped",
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "httpeds",
+ }),
+ ],
+ });
+
+ context = createContext("http:", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ context = createContext("https", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "httpsed",
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "httpseds",
+ }),
+ ],
+ });
+
+ context = createContext("https:", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ context = createContext("httpd", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "httpded",
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "httpdeds",
+ }),
+ ],
+ });
+
+ // Check FTP enabled
+ Services.prefs.setBoolPref("network.ftp.enabled", true);
+ registerCleanupFunction(() =>
+ Services.prefs.clearUserPref("network.ftp.enabled")
+ );
+
+ context = createContext("ftp:", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ context = createContext("ftp:/", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ context = createContext("ftp://", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ context = createContext("ftp://test", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "ftp://test/",
+ title: "ftp://test/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // Check FTP disabled
+ Services.prefs.setBoolPref("network.ftp.enabled", false);
+ context = createContext("ftp:", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ context = createContext("ftp:/", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ context = createContext("ftp://", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ context = createContext("ftp://test", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "ftp://test/",
+ title: "ftp://test/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("http:/", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ context = createContext("https:/", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ context = createContext("http://", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ context = createContext("https://", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ context = createContext("http://www", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://www/",
+ title: "http://www/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("https://www", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "https://www/",
+ title: "https://www/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("http://test", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://test/",
+ title: "http://test/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("https://test", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "https://test/",
+ title: "https://test/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("http://www.test", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://www.test/",
+ title: "http://www.test/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("http://www.test.com", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://www.test.com/",
+ title: "http://www.test.com/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("file", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "fileed",
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "fileeds",
+ }),
+ ],
+ });
+
+ context = createContext("file:", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ context = createContext("file:///Users", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "file:///Users",
+ title: "file:///Users",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("moz-test://", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ context = createContext("moz+test://", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ context = createContext("about", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "abouted",
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "abouteds",
+ }),
+ ],
+ });
+
+ context = createContext("about:", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ await cleanUpSuggestions();
+});
+
+add_task(async function restrict_remote_suggestions_after_no_results() {
+ // We don't fetch remote suggestions if a query with a length over
+ // maxCharsForSearchSuggestions returns 0 results. We set it to 4 here to
+ // avoid constructing a 100+ character string.
+ Services.prefs.setIntPref("browser.urlbar.maxCharsForSearchSuggestions", 4);
+ setSuggestionsFn(searchStr => {
+ return [];
+ });
+
+ const query = SEARCH_STRING.substring(0, SEARCH_STRING.length - 1);
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ...makeExpectedFormHistoryResults(context),
+ ],
+ });
+
+ context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ...makeExpectedFormHistoryResults(context),
+ // Because the previous search returned no suggestions, we will not fetch
+ // remote suggestions for this query that is just a longer version of the
+ // previous query.
+ ],
+ });
+
+ // Do one more search before resetting maxCharsForSearchSuggestions to reset
+ // the search suggestion provider's _lastLowResultsSearchSuggestion property.
+ // Otherwise it will be stuck at SEARCH_STRING, which interferes with
+ // subsequent tests.
+ context = createContext("not the search string", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ Services.prefs.clearUserPref("browser.urlbar.maxCharsForSearchSuggestions");
+
+ await cleanUpSuggestions();
+});
+
+add_task(async function formHistory() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+
+ // Setting maxHistoricalSearchSuggestions = 0 is special and indicates that
+ // the user has opted out of form history, so we should include form history
+ // neither before the expected remote results nor after, unlike the other
+ // checks below, where remaining form history is included after the expected
+ // remote results.
+ Services.prefs.setIntPref(MAX_FORM_HISTORY_PREF, 0);
+ let context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ...makeExpectedRemoteSuggestionResults(context),
+ ],
+ });
+
+ Services.prefs.setIntPref(MAX_FORM_HISTORY_PREF, 1);
+ context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ...makeExpectedFormHistoryResults(context, 2).slice(0, 1),
+ ...makeExpectedRemoteSuggestionResults(context),
+ ...makeExpectedFormHistoryResults(context, 2).slice(1),
+ ],
+ });
+
+ Services.prefs.setIntPref(MAX_FORM_HISTORY_PREF, 2);
+ context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ...makeExpectedFormHistoryResults(context, 2),
+ ...makeExpectedRemoteSuggestionResults(context),
+ ],
+ });
+
+ // Do a search for exactly the suggestion of the first form history result.
+ // The heuristic's query should be the suggestion; the first form history
+ // result should not be included since it dupes the heuristic; the second form
+ // history result should not be included since it doesn't match; and both
+ // remote suggestions should be included.
+ let firstSuggestion = makeExpectedFormHistoryResults(context)[0].payload
+ .suggestion;
+ context = createContext(firstSuggestion, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ...makeExpectedRemoteSuggestionResults(context, {
+ suggestionPrefix: firstSuggestion,
+ }),
+ ],
+ });
+
+ // Add these form history strings to use below.
+ let formHistoryStrings = ["foo", "foobar", "fooquux"];
+ await UrlbarTestUtils.formHistory.add(formHistoryStrings);
+
+ // Search for "foo". "foo" shouldn't be included since it dupes the
+ // heuristic. Both "foobar" and "fooquux" should be included even though the
+ // max form history count is only two and there are three matching form
+ // history results (including "foo").
+ context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeFormHistoryResult(context, {
+ suggestion: "foobar",
+ engineName: ENGINE_NAME,
+ }),
+ ...makeExpectedRemoteSuggestionResults(context, {
+ suggestionPrefix: "foo",
+ }),
+ // Note that the second form history result appears after the remote
+ // suggestions. This isn't ideal because it should appear right after the
+ // first form history result, but it doesn't because the actual first form
+ // history result duped the heuristic, so the muxer discarded it.
+ makeFormHistoryResult(context, {
+ suggestion: "fooquux",
+ engineName: ENGINE_NAME,
+ }),
+ ],
+ });
+
+ // Add a visit that matches "foo" and will autofill so that the heuristic is
+ // not a search result. Now the "foo" and "foobar" form history should be
+ // included.
+ await PlacesTestUtils.addVisits("http://foo.example.com/");
+ context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ uri: "http://foo.example.com/",
+ title: "foo.example.com",
+ heuristic: true,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "foo",
+ engineName: ENGINE_NAME,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "foobar",
+ engineName: ENGINE_NAME,
+ }),
+ ...makeExpectedRemoteSuggestionResults(context, {
+ suggestionPrefix: "foo",
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "fooquux",
+ engineName: ENGINE_NAME,
+ }),
+ ],
+ });
+ await PlacesUtils.history.clear();
+
+ // Add SERPs for "foobar" and "food" and search for "foo". The "foo" form
+ // history should be excluded since it dupes the heuristic; the "foobar" and
+ // "fooquux" form history should be included; the "food" SERP should be
+ // included since it doesn't dupe either form history result; and the "foobar"
+ // SERP depends on the match buckets, see below.
+ let engine = await Services.search.getDefault();
+ let [serpURL1] = UrlbarUtils.getSearchQueryUrl(engine, "foobar");
+ let [serpURL2] = UrlbarUtils.getSearchQueryUrl(engine, "food");
+ await PlacesTestUtils.addVisits([serpURL1, serpURL2]);
+
+ // First, use the MATCH_BUCKETS_VALUE that the test set above. General
+ // results appear before suggestions, which means that the muxer visits the
+ // "foobar" SERP before visiting the "foobar" form history, and so it doesn't
+ // see that the SERP dupes the form history. The "foobar" SERP is therefore
+ // included.
+ context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeVisitResult(context, {
+ uri: "http://localhost:9000/search?terms=food",
+ title: "test visit for http://localhost:9000/search?terms=food",
+ }),
+ makeVisitResult(context, {
+ uri: "http://localhost:9000/search?terms=foobar",
+ title: "test visit for http://localhost:9000/search?terms=foobar",
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "foobar",
+ engineName: ENGINE_NAME,
+ }),
+ ...makeExpectedRemoteSuggestionResults(context, {
+ suggestionPrefix: "foo",
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "fooquux",
+ engineName: ENGINE_NAME,
+ }),
+ ],
+ });
+
+ // Now use Firefox's default match buckets, where suggestions appear before
+ // general results. Now the muxer will see that the "foobar" SERP dupes the
+ // "foobar" form history, so it will exclude the SERP.
+ Services.prefs.setCharPref(
+ "browser.urlbar.matchBuckets",
+ "suggestion:4,general:Infinity"
+ );
+ context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ // Note that the remote suggestions appear in between the two form history
+ // results. Ideally the form history would appear together before the
+ // remote suggestions, but they don't because the actual first form
+ // history result duped the heuristic, so the muxer discarded it.
+ makeFormHistoryResult(context, {
+ suggestion: "foobar",
+ engineName: ENGINE_NAME,
+ }),
+ ...makeExpectedRemoteSuggestionResults(context, {
+ suggestionPrefix: "foo",
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "fooquux",
+ engineName: ENGINE_NAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://localhost:9000/search?terms=food",
+ title: "test visit for http://localhost:9000/search?terms=food",
+ }),
+ ],
+ });
+ Services.prefs.setCharPref(
+ "browser.urlbar.matchBuckets",
+ MATCH_BUCKETS_VALUE
+ );
+
+ await PlacesUtils.history.clear();
+
+ await UrlbarTestUtils.formHistory.remove(formHistoryStrings);
+
+ await cleanUpSuggestions();
+ await PlacesUtils.history.clear();
+ Services.prefs.clearUserPref(MAX_FORM_HISTORY_PREF);
+});
diff --git a/browser/components/urlbar/tests/unit/test_search_suggestions_aliases.js b/browser/components/urlbar/tests/unit/test_search_suggestions_aliases.js
new file mode 100644
index 0000000000..09a671b635
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_search_suggestions_aliases.js
@@ -0,0 +1,359 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that an engine with suggestions works with our alias autocomplete
+ * behavior.
+ */
+
+const DEFAULT_ENGINE_NAME = "TestDefaultEngine";
+const SUGGESTIONS_ENGINE_NAME = "engine-suggestions.xml";
+const SUGGEST_PREF = "browser.urlbar.suggest.searches";
+const SUGGEST_ENABLED_PREF = "browser.search.suggest.enabled";
+const HISTORY_TITLE = "fire";
+
+// We make sure that aliases and search terms are correctly recognized when they
+// are separated by each of these different types of spaces and combinations of
+// spaces. U+3000 is the ideographic space in CJK and is commonly used by CJK
+// speakers.
+const TEST_SPACES = [" ", "\u3000", " \u3000", "\u3000 "];
+
+let engine;
+
+add_task(async function setup() {
+ engine = await addTestSuggestionsEngine();
+
+ // Set a mock engine as the default so we don't hit the network below when we
+ // do searches that return the default engine heuristic result.
+ Services.search.defaultEngine = await Services.search.addEngineWithDetails(
+ DEFAULT_ENGINE_NAME,
+ { template: "http://example.com/?s=%S" }
+ );
+
+ // History matches should not appear with @aliases, so this visit should not
+ // appear when searching with @aliases below.
+ await PlacesTestUtils.addVisits({
+ uri: engine.searchForm,
+ title: HISTORY_TITLE,
+ });
+});
+
+// A non-token alias without a trailing space shouldn't be recognized as a
+// keyword. It should be treated as part of the search string.
+add_task(async function nonTokenAlias_noTrailingSpace() {
+ Services.prefs.setBoolPref(
+ "browser.search.separatePrivateDefault.ui.enabled",
+ false
+ );
+
+ let alias = "moz";
+ engine.alias = alias;
+ Assert.equal(engine.alias, alias);
+ let context = createContext(alias, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: DEFAULT_ENGINE_NAME,
+ query: alias,
+ heuristic: true,
+ }),
+ ],
+ });
+ Services.prefs.clearUserPref(
+ "browser.search.separatePrivateDefault.ui.enabled"
+ );
+});
+
+// A non-token alias with a trailing space should be recognized as a keyword,
+// and the history result should be included.
+add_task(async function nonTokenAlias_trailingSpace() {
+ let alias = "moz";
+ engine.alias = alias;
+ Assert.equal(engine.alias, alias);
+ for (let isPrivate of [false, true]) {
+ for (let spaces of TEST_SPACES) {
+ info(
+ "Testing: " + JSON.stringify({ isPrivate, spaces: codePoints(spaces) })
+ );
+ let context = createContext(alias + spaces, { isPrivate });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: "",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://localhost:9000/search?terms=",
+ title: HISTORY_TITLE,
+ }),
+ ],
+ });
+ }
+ }
+});
+
+// Search for "alias HISTORY_TITLE" with a non-token alias in a non-private
+// context. The remote suggestions and history result should be shown.
+add_task(async function nonTokenAlias_history_nonPrivate() {
+ let alias = "moz";
+ engine.alias = alias;
+ Assert.equal(engine.alias, alias);
+ for (let spaces of TEST_SPACES) {
+ info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) }));
+ let context = createContext(alias + spaces + HISTORY_TITLE, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: HISTORY_TITLE,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: HISTORY_TITLE,
+ suggestion: `${HISTORY_TITLE} foo`,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: HISTORY_TITLE,
+ suggestion: `${HISTORY_TITLE} bar`,
+ }),
+ makeVisitResult(context, {
+ uri: "http://localhost:9000/search?terms=",
+ title: HISTORY_TITLE,
+ }),
+ ],
+ });
+ }
+});
+
+// Search for "alias HISTORY_TITLE" with a non-token alias in a private context.
+// The history result should be shown, but not the remote suggestions.
+add_task(async function nonTokenAlias_history_private() {
+ let alias = "moz";
+ engine.alias = alias;
+ Assert.equal(engine.alias, alias);
+ for (let spaces of TEST_SPACES) {
+ info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) }));
+ let context = createContext(alias + spaces + HISTORY_TITLE, {
+ isPrivate: true,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: HISTORY_TITLE,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://localhost:9000/search?terms=",
+ title: HISTORY_TITLE,
+ }),
+ ],
+ });
+ }
+});
+
+// A token alias without a trailing space should be autofilled with a trailing
+// space and recognized as a keyword with a keyword offer.
+add_task(async function tokenAlias_noTrailingSpace() {
+ let alias = "@moz";
+ engine.alias = alias;
+ Assert.equal(engine.alias, alias);
+ for (let isPrivate of [false, true]) {
+ let context = createContext(alias, { isPrivate });
+ await check_results({
+ context,
+ autofilled: alias + " ",
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ providesSearchMode: true,
+ query: "",
+ heuristic: false,
+ }),
+ ],
+ });
+ }
+});
+
+// A token alias with a trailing space should be recognized as a keyword without
+// a keyword offer.
+add_task(async function tokenAlias_trailingSpace() {
+ let alias = "@moz";
+ engine.alias = alias;
+ Assert.equal(engine.alias, alias);
+ for (let isPrivate of [false, true]) {
+ for (let spaces of TEST_SPACES) {
+ info(
+ "Testing: " + JSON.stringify({ isPrivate, spaces: codePoints(spaces) })
+ );
+ let context = createContext(alias + spaces, { isPrivate });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: "",
+ heuristic: true,
+ }),
+ ],
+ });
+ }
+ }
+});
+
+// Search for "alias HISTORY_TITLE" with a token alias in a non-private context.
+// The remote suggestions should be shown, but not the history result.
+add_task(async function tokenAlias_history_nonPrivate() {
+ let alias = "@moz";
+ engine.alias = alias;
+ Assert.equal(engine.alias, alias);
+ for (let spaces of TEST_SPACES) {
+ info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) }));
+ let context = createContext(alias + spaces + HISTORY_TITLE, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: HISTORY_TITLE,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: HISTORY_TITLE,
+ suggestion: `${HISTORY_TITLE} foo`,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: HISTORY_TITLE,
+ suggestion: `${HISTORY_TITLE} bar`,
+ }),
+ ],
+ });
+ }
+});
+
+// Search for "alias HISTORY_TITLE" with a token alias in a private context.
+// Neither the history result nor the remote suggestions should be shown.
+add_task(async function tokenAlias_history_private() {
+ let alias = "@moz";
+ engine.alias = alias;
+ Assert.equal(engine.alias, alias);
+ for (let spaces of TEST_SPACES) {
+ info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) }));
+ let context = createContext(alias + spaces + HISTORY_TITLE, {
+ isPrivate: true,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: HISTORY_TITLE,
+ heuristic: true,
+ }),
+ ],
+ });
+ }
+});
+
+// Even when they're disabled, suggestions should still be returned when using a
+// token alias in a non-private context.
+add_task(async function suggestionsDisabled_nonPrivate() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, false);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+ let alias = "@moz";
+ engine.alias = alias;
+ Assert.equal(engine.alias, alias);
+ for (let spaces of TEST_SPACES) {
+ info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) }));
+ let context = createContext(alias + spaces + "term", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: "term",
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: "term",
+ suggestion: "term foo",
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: "term",
+ suggestion: "term bar",
+ }),
+ ],
+ });
+ }
+ Services.prefs.clearUserPref(SUGGEST_PREF);
+ Services.prefs.clearUserPref(SUGGEST_ENABLED_PREF);
+});
+
+// Suggestions should not be returned when using a token alias in a private
+// context.
+add_task(async function suggestionsDisabled_private() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, false);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+ let alias = "@moz";
+ engine.alias = alias;
+ Assert.equal(engine.alias, alias);
+ for (let spaces of TEST_SPACES) {
+ info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) }));
+ let context = createContext(alias + spaces + "term", { isPrivate: true });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: "term",
+ heuristic: true,
+ }),
+ ],
+ });
+ Services.prefs.clearUserPref(SUGGEST_PREF);
+ Services.prefs.clearUserPref(SUGGEST_ENABLED_PREF);
+ }
+});
+
+/**
+ * Returns an array of code points in the given string. Each code point is
+ * returned as a hexidecimal string.
+ *
+ * @param {string} str
+ * The code points of this string will be returned.
+ * @returns {array}
+ * Array of code points in the string, where each is a hexidecimal string.
+ */
+function codePoints(str) {
+ return str.split("").map(s => s.charCodeAt(0).toString(16));
+}
diff --git a/browser/components/urlbar/tests/unit/test_search_suggestions_tail.js b/browser/components/urlbar/tests/unit/test_search_suggestions_tail.js
new file mode 100644
index 0000000000..6d690c1b99
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_search_suggestions_tail.js
@@ -0,0 +1,358 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that tailed search engine suggestions are returned by
+ * UrlbarProviderSearchSuggestions when available.
+ */
+
+const { FormHistory } = ChromeUtils.import(
+ "resource://gre/modules/FormHistory.jsm"
+);
+
+const ENGINE_NAME = "engine-tail-suggestions.xml";
+const SUGGEST_PREF = "browser.urlbar.suggest.searches";
+const SUGGEST_ENABLED_PREF = "browser.search.suggest.enabled";
+const PRIVATE_SEARCH_PREF = "browser.search.separatePrivateDefault.ui.enabled";
+const TAIL_SUGGESTIONS_PREF = "browser.urlbar.richSuggestions.tail";
+
+var suggestionsFn;
+var previousSuggestionsFn;
+
+/**
+ * Set the current suggestion funciton.
+ * @param {function} fn
+ * A function that that a search string and returns an array of strings that
+ * will be used as search suggestions.
+ * Note: `fn` should return > 1 suggestion in most cases. Otherwise, you may
+ * encounter unexceptede behaviour with UrlbarProviderSuggestion's
+ * _lastLowResultsSearchSuggestion safeguard.
+ */
+function setSuggestionsFn(fn) {
+ previousSuggestionsFn = suggestionsFn;
+ suggestionsFn = fn;
+}
+
+async function cleanup() {
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+}
+
+async function cleanUpSuggestions() {
+ await cleanup();
+ if (previousSuggestionsFn) {
+ suggestionsFn = previousSuggestionsFn;
+ previousSuggestionsFn = null;
+ }
+}
+
+add_task(async function setup() {
+ Services.prefs.setCharPref(
+ "browser.urlbar.matchBuckets",
+ "general:5,suggestion:Infinity"
+ );
+
+ let engine = await addTestTailSuggestionsEngine(searchStr => {
+ return suggestionsFn(searchStr);
+ });
+ setSuggestionsFn(searchStr => {
+ let suffixes = ["toronto", "tunisia"];
+ return [
+ "what time is it in t",
+ suffixes.map(s => searchStr + s.slice(1)),
+ [],
+ {
+ "google:irrelevantparameter": [],
+ "google:suggestdetail": suffixes.map(s => ({
+ mp: "… ",
+ t: s,
+ })),
+ },
+ ];
+ });
+
+ // Install the test engine.
+ let oldDefaultEngine = await Services.search.getDefault();
+ registerCleanupFunction(async () => {
+ Services.search.setDefault(oldDefaultEngine);
+ Services.prefs.clearUserPref(PRIVATE_SEARCH_PREF);
+ Services.prefs.clearUserPref(TAIL_SUGGESTIONS_PREF);
+ Services.prefs.clearUserPref(SUGGEST_ENABLED_PREF);
+ });
+ Services.search.setDefault(engine);
+ Services.prefs.setBoolPref(PRIVATE_SEARCH_PREF, false);
+ Services.prefs.setBoolPref(TAIL_SUGGESTIONS_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+});
+
+/**
+ * Tests that non-tail suggestion providers still return results correctly when
+ * the tailSuggestions pref is enabled.
+ */
+add_task(async function normal_suggestions_provider() {
+ let engine = await addTestSuggestionsEngine();
+ let tailEngine = await Services.search.getDefault();
+ Services.search.setDefault(engine);
+
+ const query = "hello world";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: "engine-suggestions.xml",
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: "engine-suggestions.xml",
+ suggestion: query + " foo",
+ }),
+ makeSearchResult(context, {
+ engineName: "engine-suggestions.xml",
+ suggestion: query + " bar",
+ }),
+ ],
+ });
+
+ Services.search.setDefault(tailEngine);
+ await cleanUpSuggestions();
+});
+
+/**
+ * Tests a suggestions provider that returns only tail suggestions.
+ */
+add_task(async function basic_tail() {
+ const query = "what time is it in t";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: query + "oronto",
+ tail: "toronto",
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: query + "unisia",
+ tail: "tunisia",
+ }),
+ ],
+ });
+ await cleanUpSuggestions();
+});
+
+/**
+ * Tests a suggestions provider that returns both normal and tail suggestions.
+ * Only normal results should be shown.
+ */
+add_task(async function mixed_suggestions() {
+ // When normal suggestions are mixed with tail suggestions, they appear at the
+ // correct position in the google:suggestdetail array as empty objects.
+ setSuggestionsFn(searchStr => {
+ let suffixes = ["toronto", "tunisia"];
+ return [
+ "what time is it in t",
+ ["what is the time today texas"].concat(
+ suffixes.map(s => searchStr + s.slice(1))
+ ),
+ [],
+ {
+ "google:irrelevantparameter": [],
+ "google:suggestdetail": [{}].concat(
+ suffixes.map(s => ({
+ mp: "… ",
+ t: s,
+ }))
+ ),
+ },
+ ];
+ });
+
+ const query = "what time is it in t";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "what is the time today texas",
+ tail: undefined,
+ }),
+ ],
+ });
+ await cleanUpSuggestions();
+});
+
+/**
+ * Tests a suggestions provider that returns both normal and tail suggestions,
+ * with tail suggestions listed before normal suggestions. In the real world
+ * we don't expect that to happen, but we should handle it by showing only the
+ * normal suggestions.
+ */
+add_task(async function mixed_suggestions_tail_first() {
+ setSuggestionsFn(searchStr => {
+ let suffixes = ["toronto", "tunisia"];
+ return [
+ "what time is it in t",
+ suffixes
+ .map(s => searchStr + s.slice(1))
+ .concat(["what is the time today texas"]),
+ [],
+ {
+ "google:irrelevantparameter": [],
+ "google:suggestdetail": suffixes
+ .map(s => ({
+ mp: "… ",
+ t: s,
+ }))
+ .concat([{}]),
+ },
+ ];
+ });
+
+ const query = "what time is it in t";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: "what is the time today texas",
+ tail: undefined,
+ }),
+ ],
+ });
+ await cleanUpSuggestions();
+});
+
+/**
+ * Tests a search that returns history results, bookmark results and tail
+ * suggestions. Only the history and bookmark results should be shown.
+ */
+add_task(async function mixed_results() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: Services.io.newURI("http://example.com/1"),
+ title: "what time is",
+ },
+ ]);
+
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.com/2",
+ title: "what time is",
+ });
+
+ // Tail suggestions should not be shown.
+ const query = "what time is";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeBookmarkResult(context, {
+ uri: "http://example.com/2",
+ title: "what time is",
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/1",
+ title: "what time is",
+ }),
+ ],
+ });
+
+ // Once we make the query specific enough to exclude the history and bookmark
+ // results, we should show tail suggestions.
+ const tQuery = "what time is it in t";
+ context = createContext(tQuery, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: tQuery + "oronto",
+ tail: "toronto",
+ }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: tQuery + "unisia",
+ tail: "tunisia",
+ }),
+ ],
+ });
+
+ await cleanUpSuggestions();
+});
+
+/**
+ * Tests that tail suggestions are deduped if their full-text form is a dupe of
+ * a local search suggestion. Remaining tail suggestions should also not be
+ * shown since we do not mix tail and non-tail suggestions.
+ */
+add_task(async function dedupe_local() {
+ Services.prefs.setIntPref("browser.urlbar.maxHistoricalSearchSuggestions", 1);
+ await UrlbarTestUtils.formHistory.add(["what time is it in toronto"]);
+
+ const query = "what time is it in t";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeFormHistoryResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: query + "oronto",
+ }),
+ ],
+ });
+
+ Services.prefs.clearUserPref("browser.urlbar.maxHistoricalSearchSuggestions");
+ await cleanUpSuggestions();
+});
+
+/**
+ * Tests that the correct number of suggestion results are displayed if
+ * maxResults is limited, even when tail suggestions are returned.
+ */
+add_task(async function limit_results() {
+ await UrlbarTestUtils.formHistory.clear();
+ const query = "what time is it in t";
+ let context = createContext(query, { isPrivate: false });
+ context.maxResults = 2;
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ makeSearchResult(context, {
+ engineName: ENGINE_NAME,
+ suggestion: query + "oronto",
+ tail: "toronto",
+ }),
+ ],
+ });
+ await cleanUpSuggestions();
+});
+
+/**
+ * Tests that tail suggestions are hidden if the pref is disabled.
+ */
+add_task(async function disable_pref() {
+ let oldPrefValue = Services.prefs.getBoolPref(TAIL_SUGGESTIONS_PREF);
+ Services.prefs.setBoolPref(TAIL_SUGGESTIONS_PREF, false);
+ const query = "what time is it in t";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ Services.prefs.setBoolPref(TAIL_SUGGESTIONS_PREF, oldPrefValue);
+ await cleanUpSuggestions();
+});
diff --git a/browser/components/urlbar/tests/unit/test_tokenizer.js b/browser/components/urlbar/tests/unit/test_tokenizer.js
new file mode 100644
index 0000000000..22dc629a46
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_tokenizer.js
@@ -0,0 +1,450 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function test_tokenizer() {
+ let testContexts = [
+ { desc: "Empty string", searchString: "", expectedTokens: [] },
+ { desc: "Spaces string", searchString: " ", expectedTokens: [] },
+ {
+ desc: "Single word string",
+ searchString: "test",
+ expectedTokens: [{ value: "test", type: UrlbarTokenizer.TYPE.TEXT }],
+ },
+ {
+ desc: "Multi word string with mixed whitespace types",
+ searchString: " test1 test2\u1680test3\u2004test4\u1680",
+ expectedTokens: [
+ { value: "test1", type: UrlbarTokenizer.TYPE.TEXT },
+ { value: "test2", type: UrlbarTokenizer.TYPE.TEXT },
+ { value: "test3", type: UrlbarTokenizer.TYPE.TEXT },
+ { value: "test4", type: UrlbarTokenizer.TYPE.TEXT },
+ ],
+ },
+ {
+ desc: "separate restriction char at beginning",
+ searchString: `${UrlbarTokenizer.RESTRICT.BOOKMARK} test`,
+ expectedTokens: [
+ {
+ value: UrlbarTokenizer.RESTRICT.BOOKMARK,
+ type: UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK,
+ },
+ { value: "test", type: UrlbarTokenizer.TYPE.TEXT },
+ ],
+ },
+ {
+ desc: "separate restriction char at end",
+ searchString: `test ${UrlbarTokenizer.RESTRICT.BOOKMARK}`,
+ expectedTokens: [
+ { value: "test", type: UrlbarTokenizer.TYPE.TEXT },
+ {
+ value: UrlbarTokenizer.RESTRICT.BOOKMARK,
+ type: UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK,
+ },
+ ],
+ },
+ {
+ desc: "boundary restriction char at end",
+ searchString: `test${UrlbarTokenizer.RESTRICT.BOOKMARK}`,
+ expectedTokens: [
+ {
+ value: `test${UrlbarTokenizer.RESTRICT.BOOKMARK}`,
+ type: UrlbarTokenizer.TYPE.TEXT,
+ },
+ ],
+ },
+ {
+ desc: "boundary search restriction char at end",
+ searchString: `test${UrlbarTokenizer.RESTRICT.SEARCH}`,
+ expectedTokens: [
+ { value: "test", type: UrlbarTokenizer.TYPE.TEXT },
+ {
+ value: UrlbarTokenizer.RESTRICT.SEARCH,
+ type: UrlbarTokenizer.TYPE.RESTRICT_SEARCH,
+ },
+ ],
+ },
+ {
+ desc: "separate restriction char in the middle",
+ searchString: `test ${UrlbarTokenizer.RESTRICT.BOOKMARK} test`,
+ expectedTokens: [
+ { value: "test", type: UrlbarTokenizer.TYPE.TEXT },
+ {
+ value: UrlbarTokenizer.RESTRICT.BOOKMARK,
+ type: UrlbarTokenizer.TYPE.TEXT,
+ },
+ { value: "test", type: UrlbarTokenizer.TYPE.TEXT },
+ ],
+ },
+ {
+ desc: "restriction char in the middle",
+ searchString: `test${UrlbarTokenizer.RESTRICT.BOOKMARK}test`,
+ expectedTokens: [
+ {
+ value: `test${UrlbarTokenizer.RESTRICT.BOOKMARK}test`,
+ type: UrlbarTokenizer.TYPE.TEXT,
+ },
+ ],
+ },
+ {
+ desc: "restriction char in the middle 2",
+ searchString: `test${UrlbarTokenizer.RESTRICT.BOOKMARK} test`,
+ expectedTokens: [
+ {
+ value: `test${UrlbarTokenizer.RESTRICT.BOOKMARK}`,
+ type: UrlbarTokenizer.TYPE.TEXT,
+ },
+ { value: `test`, type: UrlbarTokenizer.TYPE.TEXT },
+ ],
+ },
+ {
+ desc: "double boundary restriction char",
+ searchString: `${UrlbarTokenizer.RESTRICT.BOOKMARK}test${UrlbarTokenizer.RESTRICT.TITLE}`,
+ expectedTokens: [
+ {
+ value: UrlbarTokenizer.RESTRICT.BOOKMARK,
+ type: UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK,
+ },
+ {
+ value: `test${UrlbarTokenizer.RESTRICT.TITLE}`,
+ type: UrlbarTokenizer.TYPE.TEXT,
+ },
+ ],
+ },
+ {
+ desc: "double non-combinable restriction char, single char string",
+ searchString: `t${UrlbarTokenizer.RESTRICT.BOOKMARK}${UrlbarTokenizer.RESTRICT.SEARCH}`,
+ expectedTokens: [
+ {
+ value: `t${UrlbarTokenizer.RESTRICT.BOOKMARK}`,
+ type: UrlbarTokenizer.TYPE.TEXT,
+ },
+ {
+ value: UrlbarTokenizer.RESTRICT.SEARCH,
+ type: UrlbarTokenizer.TYPE.RESTRICT_SEARCH,
+ },
+ ],
+ },
+ {
+ desc: "only boundary restriction chars",
+ searchString: `${UrlbarTokenizer.RESTRICT.BOOKMARK}${UrlbarTokenizer.RESTRICT.TITLE}`,
+ expectedTokens: [
+ {
+ value: UrlbarTokenizer.RESTRICT.BOOKMARK,
+ type: UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK,
+ },
+ {
+ value: UrlbarTokenizer.RESTRICT.TITLE,
+ type: UrlbarTokenizer.TYPE.RESTRICT_TITLE,
+ },
+ ],
+ },
+ {
+ desc: "only the boundary restriction char",
+ searchString: UrlbarTokenizer.RESTRICT.BOOKMARK,
+ expectedTokens: [
+ {
+ value: UrlbarTokenizer.RESTRICT.BOOKMARK,
+ type: UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK,
+ },
+ ],
+ },
+ // Some restriction chars may be # or ?, that are also valid path parts.
+ // The next 2 tests will check we consider those as part of url paths.
+ {
+ desc: "boundary # char on path",
+ searchString: "test/#",
+ expectedTokens: [
+ { value: "test/#", type: UrlbarTokenizer.TYPE.POSSIBLE_URL },
+ ],
+ },
+ {
+ desc: "boundary ? char on path",
+ searchString: "test/?",
+ expectedTokens: [
+ { value: "test/?", type: UrlbarTokenizer.TYPE.POSSIBLE_URL },
+ ],
+ },
+ {
+ desc: "multiple boundary restriction chars suffix",
+ searchString: `test ${UrlbarTokenizer.RESTRICT.HISTORY} ${UrlbarTokenizer.RESTRICT.TAG}`,
+ expectedTokens: [
+ { value: "test", type: UrlbarTokenizer.TYPE.TEXT },
+ {
+ value: UrlbarTokenizer.RESTRICT.HISTORY,
+ type: UrlbarTokenizer.TYPE.TEXT,
+ },
+ {
+ value: UrlbarTokenizer.RESTRICT.TAG,
+ type: UrlbarTokenizer.TYPE.RESTRICT_TAG,
+ },
+ ],
+ },
+ {
+ desc: "multiple boundary restriction chars prefix",
+ searchString: `${UrlbarTokenizer.RESTRICT.HISTORY} ${UrlbarTokenizer.RESTRICT.TAG} test`,
+ expectedTokens: [
+ {
+ value: UrlbarTokenizer.RESTRICT.HISTORY,
+ type: UrlbarTokenizer.TYPE.RESTRICT_HISTORY,
+ },
+ {
+ value: UrlbarTokenizer.RESTRICT.TAG,
+ type: UrlbarTokenizer.TYPE.TEXT,
+ },
+ { value: "test", type: UrlbarTokenizer.TYPE.TEXT },
+ ],
+ },
+ {
+ desc: "Math with division",
+ searchString: "3.6/1.2",
+ expectedTokens: [{ value: "3.6/1.2", type: UrlbarTokenizer.TYPE.TEXT }],
+ },
+ {
+ desc: "ipv4 in bookmarks",
+ searchString: `${UrlbarTokenizer.RESTRICT.BOOKMARK} 192.168.1.1:8`,
+ expectedTokens: [
+ {
+ value: UrlbarTokenizer.RESTRICT.BOOKMARK,
+ type: UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK,
+ },
+ { value: "192.168.1.1:8", type: UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN },
+ ],
+ },
+ {
+ desc: "email",
+ searchString: "test@mozilla.com",
+ expectedTokens: [
+ { value: "test@mozilla.com", type: UrlbarTokenizer.TYPE.TEXT },
+ ],
+ },
+ {
+ desc: "email2",
+ searchString: "test.test@mozilla.co.uk",
+ expectedTokens: [
+ { value: "test.test@mozilla.co.uk", type: UrlbarTokenizer.TYPE.TEXT },
+ ],
+ },
+ {
+ desc: "protocol",
+ searchString: "http://test",
+ expectedTokens: [
+ { value: "http://test", type: UrlbarTokenizer.TYPE.POSSIBLE_URL },
+ ],
+ },
+ {
+ desc:
+ "bogus protocol with host (we allow visits to http://///example.com)",
+ searchString: "http:///test",
+ expectedTokens: [
+ { value: "http:///test", type: UrlbarTokenizer.TYPE.POSSIBLE_URL },
+ ],
+ },
+ {
+ desc: "file protocol with path",
+ searchString: "file:///home",
+ expectedTokens: [
+ { value: "file:///home", type: UrlbarTokenizer.TYPE.POSSIBLE_URL },
+ ],
+ },
+ {
+ desc: "almost a protocol",
+ searchString: "http:",
+ expectedTokens: [
+ { value: "http:", type: UrlbarTokenizer.TYPE.POSSIBLE_URL },
+ ],
+ },
+ {
+ desc: "almost a protocol 2",
+ searchString: "http:/",
+ expectedTokens: [
+ { value: "http:/", type: UrlbarTokenizer.TYPE.POSSIBLE_URL },
+ ],
+ },
+ {
+ desc: "bogus protocol (we allow visits to http://///example.com)",
+ searchString: "http:///",
+ expectedTokens: [
+ { value: "http:///", type: UrlbarTokenizer.TYPE.POSSIBLE_URL },
+ ],
+ },
+ {
+ desc: "file protocol",
+ searchString: "file:///",
+ expectedTokens: [
+ { value: "file:///", type: UrlbarTokenizer.TYPE.POSSIBLE_URL },
+ ],
+ },
+ {
+ desc: "userinfo",
+ searchString: "user:pass@test",
+ expectedTokens: [
+ { value: "user:pass@test", type: UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN },
+ ],
+ },
+ {
+ desc: "domain",
+ searchString: "www.mozilla.org",
+ expectedTokens: [
+ {
+ value: "www.mozilla.org",
+ type: UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN,
+ },
+ ],
+ },
+ {
+ desc: "data uri",
+ searchString: "data:text/plain,Content",
+ expectedTokens: [
+ {
+ value: "data:text/plain,Content",
+ type: UrlbarTokenizer.TYPE.POSSIBLE_URL,
+ },
+ ],
+ },
+ {
+ desc: "ipv6",
+ searchString: "[2001:db8::1]",
+ expectedTokens: [
+ { value: "[2001:db8::1]", type: UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN },
+ ],
+ },
+ {
+ desc: "numeric domain",
+ searchString: "test1001.com",
+ expectedTokens: [
+ { value: "test1001.com", type: UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN },
+ ],
+ },
+ {
+ desc: "invalid ip",
+ searchString: "192.2134.1.2",
+ expectedTokens: [
+ { value: "192.2134.1.2", type: UrlbarTokenizer.TYPE.TEXT },
+ ],
+ },
+ {
+ desc: "ipv4",
+ searchString: "1.2.3.4",
+ expectedTokens: [
+ { value: "1.2.3.4", type: UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN },
+ ],
+ },
+ {
+ desc: "host/path",
+ searchString: "test/test",
+ expectedTokens: [
+ { value: "test/test", type: UrlbarTokenizer.TYPE.POSSIBLE_URL },
+ ],
+ },
+ {
+ desc: "percent encoded string",
+ searchString: "%E6%97%A5%E6%9C%AC",
+ expectedTokens: [
+ { value: "%E6%97%A5%E6%9C%AC", type: UrlbarTokenizer.TYPE.TEXT },
+ ],
+ },
+ {
+ desc: "Uppercase",
+ searchString: "TEST",
+ expectedTokens: [{ value: "TEST", type: UrlbarTokenizer.TYPE.TEXT }],
+ },
+ {
+ desc: "Mixed case 1",
+ searchString: "TeSt",
+ expectedTokens: [{ value: "TeSt", type: UrlbarTokenizer.TYPE.TEXT }],
+ },
+ {
+ desc: "Mixed case 2",
+ searchString: "tEsT",
+ expectedTokens: [{ value: "tEsT", type: UrlbarTokenizer.TYPE.TEXT }],
+ },
+ {
+ desc: "Uppercase with spaces",
+ searchString: "TEST EXAMPLE",
+ expectedTokens: [
+ { value: "TEST", type: UrlbarTokenizer.TYPE.TEXT },
+ { value: "EXAMPLE", type: UrlbarTokenizer.TYPE.TEXT },
+ ],
+ },
+ {
+ desc: "Mixed case with spaces",
+ searchString: "TeSt eXaMpLe",
+ expectedTokens: [
+ { value: "TeSt", type: UrlbarTokenizer.TYPE.TEXT },
+ { value: "eXaMpLe", type: UrlbarTokenizer.TYPE.TEXT },
+ ],
+ },
+ {
+ desc: "plain number",
+ searchString: "1001",
+ expectedTokens: [{ value: "1001", type: UrlbarTokenizer.TYPE.TEXT }],
+ },
+ {
+ desc: "data uri with spaces",
+ searchString: "data:text/html,oh hi?",
+ expectedTokens: [
+ {
+ value: "data:text/html,oh hi?",
+ type: UrlbarTokenizer.TYPE.POSSIBLE_URL,
+ },
+ ],
+ },
+ {
+ desc: "data uri with spaces ignored with other tokens",
+ searchString: "hi data:text/html,oh hi?",
+ expectedTokens: [
+ {
+ value: "hi",
+ type: UrlbarTokenizer.TYPE.TEXT,
+ },
+ {
+ value: "data:text/html,oh",
+ type: UrlbarTokenizer.TYPE.POSSIBLE_URL,
+ },
+ {
+ value: "hi",
+ type: UrlbarTokenizer.TYPE.TEXT,
+ },
+ {
+ value: UrlbarTokenizer.RESTRICT.SEARCH,
+ type: UrlbarTokenizer.TYPE.RESTRICT_SEARCH,
+ },
+ ],
+ },
+ {
+ desc: "whitelisted host",
+ searchString: "test whitelisted",
+ expectedTokens: [
+ {
+ value: "test",
+ type: UrlbarTokenizer.TYPE.TEXT,
+ },
+ {
+ value: "whitelisted",
+ type: UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN,
+ },
+ ],
+ },
+ ];
+
+ Services.prefs.setBoolPref("browser.fixup.domainwhitelist.whitelisted", true);
+
+ for (let queryContext of testContexts) {
+ info(queryContext.desc);
+ queryContext.trimmedSearchString = queryContext.searchString.trim();
+ for (let token of queryContext.expectedTokens) {
+ token.lowerCaseValue = token.value.toLocaleLowerCase();
+ }
+ let newQueryContext = UrlbarTokenizer.tokenize(queryContext);
+ Assert.equal(
+ queryContext,
+ newQueryContext,
+ "The queryContext object is the same"
+ );
+ Assert.deepEqual(
+ queryContext.tokens,
+ queryContext.expectedTokens,
+ "Check the expected tokens"
+ );
+ }
+});
diff --git a/browser/components/urlbar/tests/unit/test_trimming.js b/browser/components/urlbar/tests/unit/test_trimming.js
new file mode 100644
index 0000000000..1d275fef3a
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_trimming.js
@@ -0,0 +1,222 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+add_task(async function setup() {
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+});
+
+add_task(async function test_untrimmed_secure_www() {
+ info("Searching for untrimmed https://www entry");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("https://www.mozilla.org/test/"),
+ });
+ let context = createContext("mo", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.org/",
+ completed: "https://www.mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www.mozilla.org/",
+ title: "https://www.mozilla.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://www.mozilla.org/test/",
+ title: "test visit for https://www.mozilla.org/test/",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_untrimmed_secure_www_path() {
+ info("Searching for untrimmed https://www entry with path");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("https://www.mozilla.org/test/"),
+ });
+ let context = createContext("mozilla.org/t", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.org/test/",
+ completed: "https://www.mozilla.org/test/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www.mozilla.org/test/",
+ title: "https://www.mozilla.org/test/",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_untrimmed_secure() {
+ info("Searching for untrimmed https:// entry");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("https://mozilla.org/test/"),
+ });
+ let context = createContext("mo", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.org/",
+ completed: "https://mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://mozilla.org/",
+ title: "https://mozilla.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://mozilla.org/test/",
+ title: "test visit for https://mozilla.org/test/",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_untrimmed_secure_path() {
+ info("Searching for untrimmed https:// entry with path");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("https://mozilla.org/test/"),
+ });
+ let context = createContext("mozilla.org/t", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.org/test/",
+ completed: "https://mozilla.org/test/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://mozilla.org/test/",
+ title: "https://mozilla.org/test/",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_untrimmed_www() {
+ info("Searching for untrimmed http://www entry");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://www.mozilla.org/test/"),
+ });
+ let context = createContext("mo", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.org/",
+ completed: "http://www.mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://www.mozilla.org/",
+ title: "www.mozilla.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://www.mozilla.org/test/",
+ title: "test visit for http://www.mozilla.org/test/",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_untrimmed_www_path() {
+ info("Searching for untrimmed http://www entry with path");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://www.mozilla.org/test/"),
+ });
+ let context = createContext("mozilla.org/t", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.org/test/",
+ completed: "http://www.mozilla.org/test/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://www.mozilla.org/test/",
+ title: "www.mozilla.org/test/",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_untrimmed_ftp() {
+ info("Searching for untrimmed ftp:// entry");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("ftp://mozilla.org/test/"),
+ });
+ let context = createContext("mo", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.org/",
+ completed: "ftp://mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "ftp://mozilla.org/",
+ title: "ftp://mozilla.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "ftp://mozilla.org/test/",
+ title: "test visit for ftp://mozilla.org/test/",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_untrimmed_ftp_path() {
+ info("Searching for untrimmed ftp:// entry with path");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("ftp://mozilla.org/test/"),
+ });
+ let context = createContext("mozilla.org/t", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.org/test/",
+ completed: "ftp://mozilla.org/test/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "ftp://mozilla.org/test/",
+ title: "ftp://mozilla.org/test/",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_escaped_chars() {
+ info("Searching for URL with characters that are normally escaped");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("https://www.mozilla.org/啊-test"),
+ });
+ let context = createContext("https://www.mozilla.org/啊-test", {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "https://www.mozilla.org/%E5%95%8A-test",
+ title: "https://www.mozilla.org/啊-test",
+ iconUri: "page-icon:https://www.mozilla.org/",
+ heuristic: true,
+ }),
+ // UnifiedComplete escapes this character.
+ makeVisitResult(context, {
+ uri: "https://www.mozilla.org/%E5%95%8A-test",
+ title: "test visit for https://www.mozilla.org/%E5%95%8A-test",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/xpcshell.ini b/browser/components/urlbar/tests/unit/xpcshell.ini
new file mode 100644
index 0000000000..555f8ac7ce
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/xpcshell.ini
@@ -0,0 +1,54 @@
+[DEFAULT]
+head = head.js
+firefox-appdir = browser
+support-files =
+ data/engine-suggestions.xml
+ data/engine-tail-suggestions.xml
+
+[test_autofill_about_urls.js]
+[test_autofill_bookmarked.js]
+[test_autofill_functional.js]
+[test_autofill_origins.js]
+[test_autofill_originsAndQueries.js]
+[test_autofill_prefix_fallback.js]
+[test_autofill_search_engines.js]
+[test_autofill_search_engine_aliases.js]
+[test_autofill_urls.js]
+[test_avoid_middle_complete.js]
+[test_avoid_stripping_to_empty_tokens.js]
+[test_casing.js]
+[test_dedupe_prefix.js]
+[test_dupe_urls.js]
+[test_encoded_urls.js]
+[test_heuristic_cancel.js]
+[test_keywords.js]
+skip-if = os == 'linux' # bug 1474616
+[test_muxer.js]
+[test_providerHeuristicFallback.js]
+[test_providerOmnibox.js]
+[test_providerOpenTabs.js]
+[test_providersManager.js]
+[test_providersManager_filtering.js]
+[test_providersManager_maxResults.js]
+[test_providerTabToSearch.js]
+[test_providerTabToSearch_partialHost.js]
+[test_providerUnifiedComplete.js]
+[test_providerUnifiedComplete_duplicate_entries.js]
+[test_query_url.js]
+[test_queryScorer.js]
+[test_search_engine_host.js]
+[test_search_suggestions.js]
+[test_search_suggestions_aliases.js]
+[test_search_suggestions_tail.js]
+[test_tokenizer.js]
+[test_trimming.js]
+[test_UrlbarController_integration.js]
+[test_UrlbarController_telemetry.js]
+[test_UrlbarController_unit.js]
+[test_UrlbarPrefs.js]
+[test_UrlbarQueryContext.js]
+[test_UrlbarQueryContext_restrictSource.js]
+[test_UrlbarSearchUtils.jsm]
+[test_UrlbarUtils_addToUrlbarHistory.js]
+[test_UrlbarUtils_getShortcutOrURIAndPostData.js]
+[test_UrlbarUtils_getTokenMatches.js]