summaryrefslogtreecommitdiffstats
path: root/browser/components/urlbar/tests
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /browser/components/urlbar/tests
parentInitial commit. (diff)
downloadfirefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz
firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/urlbar/tests')
-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
256 files changed, 51909 insertions, 0 deletions
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]