From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- .../urlbar/tests/UrlbarTestUtils.sys.mjs | 1432 +++++++ .../urlbar/tests/browser-tips/README.txt | 7 + .../urlbar/tests/browser-tips/browser.ini | 26 + .../tests/browser-tips/browser_interventions.js | 271 ++ .../urlbar/tests/browser-tips/browser_picks.js | 223 ++ .../tests/browser-tips/browser_searchTips.js | 657 ++++ .../browser-tips/browser_searchTips_interaction.js | 837 +++++ .../urlbar/tests/browser-tips/browser_selection.js | 269 ++ .../urlbar/tests/browser-tips/browser_updateAsk.js | 74 + .../tests/browser-tips/browser_updateRefresh.js | 54 + .../tests/browser-tips/browser_updateRestart.js | 48 + .../urlbar/tests/browser-tips/browser_updateWeb.js | 52 + .../components/urlbar/tests/browser-tips/head.js | 771 ++++ .../urlbar/tests/browser-tips/slow-page.html | 7 + .../urlbar/tests/browser-updateResults/browser.ini | 17 + .../browser_appendSpanCount.js | 183 + .../browser_suggestedIndex_10_search_10_url.js | 1102 ++++++ .../browser_suggestedIndex_10_search_5_url.js | 661 ++++ .../browser_suggestedIndex_10_url_10_search.js | 1185 ++++++ .../browser_suggestedIndex_10_url_5_search.js | 707 ++++ .../browser_suggestedIndex_5_search_10_url.js | 1015 +++++ .../browser_suggestedIndex_5_search_5_url.js | 1131 ++++++ .../browser_suggestedIndex_5_url_10_search.js | 1057 ++++++ .../browser_suggestedIndex_5_url_5_search.js | 1203 ++++++ .../urlbar/tests/browser-updateResults/head.js | 550 +++ .../urlbar/tests/browser/POSTSearchEngine.xml | 6 + .../urlbar/tests/browser/add_search_engine_0.xml | 7 + .../urlbar/tests/browser/add_search_engine_1.xml | 7 + .../urlbar/tests/browser/add_search_engine_2.xml | 7 + .../urlbar/tests/browser/add_search_engine_3.xml | 7 + .../tests/browser/add_search_engine_invalid.html | 11 + .../tests/browser/add_search_engine_many.html | 24 + .../tests/browser/add_search_engine_one.html | 12 + .../browser/add_search_engine_same_names.html | 15 + .../tests/browser/add_search_engine_two.html | 16 + .../urlbar/tests/browser/authenticate.sjs | 218 ++ .../components/urlbar/tests/browser/browser.ini | 434 +++ .../browser/browser_UrlbarInput_formatValue.js | 178 + .../browser_UrlbarInput_formatValue_detachedTab.js | 76 + .../browser/browser_UrlbarInput_hiddenFocus.js | 21 + .../tests/browser/browser_UrlbarInput_overflow.js | 156 + .../browser/browser_UrlbarInput_overflow_resize.js | 58 + .../browser/browser_UrlbarInput_privateFeature.js | 74 + .../browser/browser_UrlbarInput_searchTerms.js | 306 ++ ...owser_UrlbarInput_searchTerms_backgroundTabs.js | 63 + .../browser_UrlbarInput_searchTerms_modifiedUrl.js | 100 + .../browser_UrlbarInput_searchTerms_moveTab.js | 133 + .../browser_UrlbarInput_searchTerms_popup.js | 145 + .../browser_UrlbarInput_searchTerms_revert.js | 170 + .../browser_UrlbarInput_searchTerms_searchBar.js | 104 + .../browser_UrlbarInput_searchTerms_searchMode.js | 81 + .../browser_UrlbarInput_searchTerms_switch_tab.js | 139 + .../browser_UrlbarInput_searchTerms_telemetry.js | 378 ++ .../tests/browser/browser_UrlbarInput_setURI.js | 128 + .../tests/browser/browser_UrlbarInput_tooltip.js | 83 + .../tests/browser/browser_UrlbarInput_trimURLs.js | 127 + .../tests/browser/browser_aboutHomeLoading.js | 196 + .../browser_acknowledgeFeedbackAndDismissal.js | 304 ++ .../tests/browser/browser_action_searchengine.js | 125 + .../browser/browser_action_searchengine_alias.js | 63 + .../tests/browser/browser_add_search_engine.js | 325 ++ .../tests/browser/browser_autoFill_backspaced.js | 268 ++ .../tests/browser/browser_autoFill_canonize.js | 62 + .../browser/browser_autoFill_caretNotAtEnd.js | 34 + .../tests/browser/browser_autoFill_firstResult.js | 200 + .../urlbar/tests/browser/browser_autoFill_paste.js | 38 + .../tests/browser/browser_autoFill_placeholder.js | 1017 +++++ .../tests/browser/browser_autoFill_preserve.js | 257 ++ .../tests/browser/browser_autoFill_trimURLs.js | 183 + .../urlbar/tests/browser/browser_autoFill_typed.js | 172 + .../urlbar/tests/browser/browser_autoFill_undo.js | 50 + .../urlbar/tests/browser/browser_autoOpen.js | 93 + .../browser/browser_autocomplete_a11y_label.js | 185 + .../browser/browser_autocomplete_autoselect.js | 122 + .../tests/browser/browser_autocomplete_cursor.js | 37 + .../browser/browser_autocomplete_edit_completed.js | 75 + .../browser/browser_autocomplete_enter_race.js | 193 + .../tests/browser/browser_autocomplete_no_title.js | 34 + .../browser_autocomplete_readline_navigation.js | 71 + .../browser_autocomplete_tag_star_visibility.js | 167 + .../urlbar/tests/browser/browser_bestMatch.js | 229 ++ .../urlbar/tests/browser/browser_blanking.js | 54 + .../browser/browser_bufferer_onQueryResults.js | 82 + .../urlbar/tests/browser/browser_calculator.js | 33 + .../urlbar/tests/browser/browser_canonizeURL.js | 277 ++ .../urlbar/tests/browser/browser_caret_position.js | 359 ++ .../tests/browser/browser_click_row_border.js | 36 + .../tests/browser/browser_closePanelOnClick.js | 34 + .../urlbar/tests/browser/browser_content_opener.js | 23 + .../tests/browser/browser_contextualsearch.js | 119 + .../tests/browser/browser_copy_during_load.js | 51 + .../urlbar/tests/browser/browser_copying.js | 416 +++ .../urlbar/tests/browser/browser_customizeMode.js | 73 + .../urlbar/tests/browser/browser_cutting.js | 17 + .../urlbar/tests/browser/browser_decode.js | 144 + .../urlbar/tests/browser/browser_delete.js | 51 + .../urlbar/tests/browser/browser_deleteAllText.js | 100 + .../browser_display_selectedAction_Extensions.js | 57 + .../browser/browser_dns_first_for_single_words.js | 52 + .../tests/browser/browser_downArrowKeySearch.js | 83 + .../urlbar/tests/browser/browser_dragdropURL.js | 106 + .../urlbar/tests/browser/browser_dynamicResults.js | 799 ++++ .../tests/browser/browser_edit_invalid_url.js | 91 + .../urlbar/tests/browser/browser_engagement.js | 206 ++ .../urlbar/tests/browser/browser_enter.js | 331 ++ .../tests/browser/browser_enterAfterMouseOver.js | 97 + .../urlbar/tests/browser/browser_focusedCmdK.js | 15 + .../urlbar/tests/browser/browser_groupLabels.js | 629 ++++ .../browser/browser_handleCommand_fallback.js | 142 + .../tests/browser/browser_hashChangeProxyState.js | 151 + .../urlbar/tests/browser/browser_helpUrl.js | 428 +++ .../browser/browser_heuristicNotAddedFirst.js | 159 + .../urlbar/tests/browser/browser_hideHeuristic.js | 513 +++ .../tests/browser/browser_ime_composition.js | 327 ++ .../urlbar/tests/browser/browser_inputHistory.js | 548 +++ .../tests/browser/browser_inputHistory_autofill.js | 207 ++ .../browser/browser_inputHistory_emptystring.js | 94 + .../browser/browser_keepStateAcrossTabSwitches.js | 224 ++ .../urlbar/tests/browser/browser_keyword.js | 238 ++ .../tests/browser/browser_keywordBookmarklets.js | 133 + .../urlbar/tests/browser/browser_keywordSearch.js | 57 + .../browser/browser_keywordSearch_postData.js | 74 + .../tests/browser/browser_keyword_override.js | 61 + .../browser/browser_keyword_select_and_type.js | 97 + .../urlbar/tests/browser/browser_loadRace.js | 90 + .../tests/browser/browser_locationBarCommand.js | 291 ++ .../browser/browser_locationBarExternalLoad.js | 94 + .../browser_locationchange_urlbar_edit_dos.js | 67 + .../urlbar/tests/browser/browser_middleClick.js | 255 ++ .../tests/browser/browser_new_tab_urlbar_reset.js | 39 + .../urlbar/tests/browser/browser_oneOffs.js | 980 +++++ .../tests/browser/browser_oneOffs_contextMenu.js | 80 + .../browser/browser_oneOffs_heuristicRestyle.js | 516 +++ .../tests/browser/browser_oneOffs_keyModifiers.js | 392 ++ .../browser/browser_oneOffs_searchSuggestions.js | 358 ++ .../tests/browser/browser_oneOffs_settings.js | 89 + .../urlbar/tests/browser/browser_pasteAndGo.js | 80 + .../tests/browser/browser_paste_multi_lines.js | 239 ++ .../tests/browser/browser_paste_then_focus.js | 60 + .../tests/browser/browser_paste_then_switch_tab.js | 73 + .../tests/browser/browser_percent_encoded.js | 59 + .../urlbar/tests/browser/browser_placeholder.js | 412 +++ .../browser/browser_populateAfterPushState.js | 32 + .../browser_primary_selection_safe_on_new_tab.js | 73 + .../browser/browser_privateBrowsingWindowChange.js | 51 + .../tests/browser/browser_queryContextCache.js | 482 +++ .../urlbar/tests/browser/browser_quickactions.js | 783 ++++ .../tests/browser/browser_quickactions_devtools.js | 176 + .../browser/browser_quickactions_tab_refocus.js | 194 + .../urlbar/tests/browser/browser_raceWithTabs.js | 86 + .../urlbar/tests/browser/browser_redirect_error.js | 137 + .../tests/browser/browser_remoteness_switch.js | 56 + .../urlbar/tests/browser/browser_remotetab.js | 111 + ...browser_removeUnsafeProtocolsFromURLBarPaste.js | 95 + .../urlbar/tests/browser/browser_remove_match.js | 297 ++ .../tests/browser/browser_restoreEmptyInput.js | 64 + .../urlbar/tests/browser/browser_resultSpan.js | 254 ++ .../urlbar/tests/browser/browser_result_menu.js | 266 ++ .../tests/browser/browser_result_onSelection.js | 67 + .../browser/browser_retainedResultsOnFocus.js | 435 +++ .../urlbar/tests/browser/browser_revert.js | 33 + .../urlbar/tests/browser/browser_searchFunction.js | 278 ++ .../tests/browser/browser_searchHistoryLimit.js | 87 + .../browser_searchMode_alias_replacement.js | 274 ++ .../tests/browser/browser_searchMode_autofill.js | 133 + .../tests/browser/browser_searchMode_clickLink.js | 94 + .../browser/browser_searchMode_engineRemoval.js | 109 + .../browser/browser_searchMode_excludeResults.js | 217 ++ .../tests/browser/browser_searchMode_heuristic.js | 221 ++ .../tests/browser/browser_searchMode_indicator.js | 377 ++ .../browser_searchMode_indicator_clickthrough.js | 100 + .../browser_searchMode_localOneOffs_actionText.js | 459 +++ .../tests/browser/browser_searchMode_newWindow.js | 40 + .../tests/browser/browser_searchMode_no_results.js | 290 ++ .../browser/browser_searchMode_oneOffButton.js | 108 + .../tests/browser/browser_searchMode_pickResult.js | 89 + .../tests/browser/browser_searchMode_preview.js | 489 +++ .../browser/browser_searchMode_sessionStore.js | 332 ++ .../tests/browser/browser_searchMode_setURI.js | 119 + .../browser/browser_searchMode_suggestions.js | 579 +++ .../tests/browser/browser_searchMode_switchTabs.js | 317 ++ .../urlbar/tests/browser/browser_searchSettings.js | 30 + .../browser_searchSingleWordNotification.js | 372 ++ .../tests/browser/browser_searchSuggestions.js | 341 ++ .../tests/browser/browser_searchTelemetry.js | 220 ++ ...browser_search_bookmarks_from_bookmarks_menu.js | 55 + .../browser_search_history_from_history_panel.js | 97 + .../tests/browser/browser_selectStaleResults.js | 311 ++ .../browser/browser_selectionKeyNavigation.js | 200 + .../browser/browser_separatePrivateDefault.js | 223 ++ ...owser_separatePrivateDefault_differentEngine.js | 354 ++ .../browser/browser_shortcuts_add_search_engine.js | 243 ++ .../tests/browser/browser_speculative_connect.js | 199 + ...ser_speculative_connect_not_with_client_cert.js | 236 ++ .../urlbar/tests/browser/browser_stop.js | 75 + .../tests/browser/browser_stopSearchOnSelection.js | 113 + .../urlbar/tests/browser/browser_stop_pending.js | 459 +++ .../urlbar/tests/browser/browser_strip_on_share.js | 125 + .../urlbar/tests/browser/browser_suggestedIndex.js | 120 + .../tests/browser/browser_suppressFocusBorder.js | 391 ++ .../browser/browser_switchTab_closesUrlbarPopup.js | 42 + .../tests/browser/browser_switchTab_currentTab.js | 41 + .../tests/browser/browser_switchTab_decodeuri.js | 51 + .../browser/browser_switchTab_inputHistory.js | 91 + .../tests/browser/browser_switchTab_override.js | 100 + .../browser_switchToTabHavingURI_aOpenParams.js | 217 ++ .../tests/browser/browser_switchToTab_chiclet.js | 110 + .../browser/browser_switchToTab_closes_newtab.js | 63 + .../browser_switchToTab_fullUrl_repeatedKeydown.js | 60 + .../urlbar/tests/browser/browser_tabKeyBehavior.js | 378 ++ .../browser/browser_tabMatchesInAwesomebar.js | 224 ++ .../browser_tabMatchesInAwesomebar_perwindowpb.js | 120 + .../urlbar/tests/browser/browser_tabToSearch.js | 641 ++++ .../urlbar/tests/browser/browser_textruns.js | 55 + .../urlbar/tests/browser/browser_tokenAlias.js | 861 +++++ .../urlbar/tests/browser/browser_top_sites.js | 481 +++ .../tests/browser/browser_top_sites_private.js | 174 + .../urlbar/tests/browser/browser_typed_value.js | 69 + .../urlbar/tests/browser/browser_unitConversion.js | 88 + .../browser/browser_updateForDomainCompletion.js | 51 + .../tests/browser/browser_urlbar_annotation.js | 333 ++ .../browser_urlbar_event_telemetry_abandonment.js | 357 ++ .../browser_urlbar_event_telemetry_engagement.js | 1340 +++++++ .../browser_urlbar_event_telemetry_noEvent.js | 81 + .../tests/browser/browser_urlbar_selection.js | 307 ++ .../tests/browser/browser_urlbar_telemetry.js | 1218 ++++++ .../browser/browser_urlbar_telemetry_autofill.js | 733 ++++ .../browser/browser_urlbar_telemetry_dynamic.js | 136 + .../browser/browser_urlbar_telemetry_extension.js | 155 + .../browser/browser_urlbar_telemetry_handoff.js | 182 + .../browser/browser_urlbar_telemetry_persisted.js | 270 ++ .../browser/browser_urlbar_telemetry_places.js | 270 ++ .../browser_urlbar_telemetry_quickactions.js | 133 + .../browser/browser_urlbar_telemetry_remotetab.js | 185 + .../browser/browser_urlbar_telemetry_searchmode.js | 592 +++ .../browser_urlbar_telemetry_sponsored_topsites.js | 181 + .../browser_urlbar_telemetry_tabtosearch.js | 416 +++ .../tests/browser/browser_urlbar_telemetry_tip.js | 130 + .../browser/browser_urlbar_telemetry_topsite.js | 136 + .../browser/browser_urlbar_telemetry_zeroPrefix.js | 266 ++ .../urlbar/tests/browser/browser_userTypedValue.js | 46 + .../tests/browser/browser_valueOnTabSwitch.js | 166 + .../tests/browser/browser_view_emptyResultSet.js | 40 + .../tests/browser/browser_view_resultDisplay.js | 354 ++ .../browser/browser_view_resultTypes_display.js | 317 ++ .../tests/browser/browser_view_selectionByMouse.js | 607 +++ .../tests/browser/browser_waitForLoadOrTimeout.js | 37 + .../urlbar/tests/browser/browser_whereToOpen.js | 192 + .../urlbar/tests/browser/dummy_page.html | 9 + .../urlbar/tests/browser/dynamicResult0.css | 50 + .../urlbar/tests/browser/dynamicResult1.css | 50 + .../tests/browser/file_blank_but_not_blank.html | 2 + .../urlbar/tests/browser/file_urlbar_edit_dos.html | 18 + .../urlbar/tests/browser/file_userTypedValue.html | 1 + .../components/urlbar/tests/browser/head-common.js | 156 + browser/components/urlbar/tests/browser/head.js | 125 + browser/components/urlbar/tests/browser/moz.png | Bin 0 -> 580 bytes .../urlbar/tests/browser/print_postdata.sjs | 25 + .../urlbar/tests/browser/redirect_error.sjs | 16 + .../urlbar/tests/browser/redirect_to.sjs | 9 + .../tests/browser/searchSuggestionEngine.sjs | 57 + .../tests/browser/searchSuggestionEngine.xml | 11 + .../tests/browser/searchSuggestionEngine2.xml | 13 + .../tests/browser/searchSuggestionEngineMany.xml | 11 + .../tests/browser/searchSuggestionEngineSlow.xml | 11 + .../components/urlbar/tests/browser/slow-page.sjs | 23 + .../browser/urlbarTelemetrySearchSuggestions.sjs | 9 + .../browser/urlbarTelemetrySearchSuggestions.xml | 6 + .../tests/browser/urlbarTelemetryUrlbarDynamic.css | 45 + .../tests/engagementTelemetry/browser/browser.ini | 52 + .../browser_glean_telemetry_abandonment_groups.js | 202 + ...wser_glean_telemetry_abandonment_interaction.js | 58 + ..._interaction_persisted_search_terms_disabled.js | 48 + ...t_interaction_persisted_search_terms_enabled.js | 53 + ..._glean_telemetry_abandonment_n_chars_n_words.js | 36 + .../browser_glean_telemetry_abandonment_sap.js | 39 + ...wser_glean_telemetry_abandonment_search_mode.js | 54 + .../browser_glean_telemetry_abandonment_tips.js | 88 + ...rowser_glean_telemetry_engagement_edge_cases.js | 218 ++ .../browser_glean_telemetry_engagement_groups.js | 259 ++ ...owser_glean_telemetry_engagement_interaction.js | 87 + ..._interaction_persisted_search_terms_disabled.js | 61 + ...t_interaction_persisted_search_terms_enabled.js | 60 + ...r_glean_telemetry_engagement_n_chars_n_words.js | 36 + .../browser_glean_telemetry_engagement_sap.js | 33 + ...owser_glean_telemetry_engagement_search_mode.js | 63 + ...r_glean_telemetry_engagement_selected_result.js | 920 +++++ .../browser_glean_telemetry_engagement_tips.js | 175 + .../browser_glean_telemetry_engagement_type.js | 151 + .../browser/browser_glean_telemetry_exposure.js | 107 + .../browser_glean_telemetry_impression_groups.js | 223 ++ ...owser_glean_telemetry_impression_interaction.js | 63 + ..._interaction_persisted_search_terms_disabled.js | 57 + ...n_interaction_persisted_search_terms_enabled.js | 61 + ...r_glean_telemetry_impression_n_chars_n_words.js | 40 + ...owser_glean_telemetry_impression_preferences.js | 41 + .../browser_glean_telemetry_impression_sap.js | 38 + ...owser_glean_telemetry_impression_search_mode.js | 72 + .../browser_glean_telemetry_impression_timing.js | 91 + .../browser_glean_telemetry_record_preferences.js | 54 + .../engagementTelemetry/browser/head-exposure.js | 47 + .../engagementTelemetry/browser/head-groups.js | 294 ++ .../browser/head-interaction.js | 238 ++ .../browser/head-n_chars_n_words.js | 56 + .../tests/engagementTelemetry/browser/head-sap.js | 66 + .../browser/head-search_mode.js | 93 + .../tests/engagementTelemetry/browser/head.js | 458 +++ browser/components/urlbar/tests/ext/api.js | 260 ++ .../urlbar/tests/ext/browser/.eslintrc.js | 7 + .../urlbar/tests/ext/browser/browser.ini | 18 + .../browser/browser_ext_urlbar_attributionURL.js | 16 + .../ext/browser/browser_ext_urlbar_clearInput.js | 31 + .../browser/browser_ext_urlbar_dynamicResult.js | 137 + .../browser_ext_urlbar_engagementTelemetry.js | 18 + .../browser/browser_ext_urlbar_extensionTimeout.js | 16 + .../urlbar/tests/ext/browser/dynamicResult.css | 36 + .../components/urlbar/tests/ext/browser/head.js | 253 ++ browser/components/urlbar/tests/ext/schema.json | 113 + .../tests/quicksuggest/MerinoTestUtils.sys.mjs | 765 ++++ .../quicksuggest/QuickSuggestTestUtils.sys.mjs | 1017 +++++ .../urlbar/tests/quicksuggest/browser/browser.ini | 37 + .../quicksuggest/browser/browser_quicksuggest.js | 88 + .../browser/browser_quicksuggest_addons.js | 560 +++ .../browser/browser_quicksuggest_block.js | 445 +++ .../browser/browser_quicksuggest_configuration.js | 2101 +++++++++++ .../browser/browser_quicksuggest_indexes.js | 425 +++ .../browser/browser_quicksuggest_merinoSessions.js | 142 + .../browser_quicksuggest_onboardingDialog.js | 1596 ++++++++ .../browser/browser_telemetry_dynamicWikipedia.js | 114 + .../browser_telemetry_impressionEdgeCases.js | 477 +++ .../browser_telemetry_navigationalSuggestions.js | 353 ++ .../browser/browser_telemetry_nonsponsored.js | 368 ++ .../browser/browser_telemetry_other.js | 409 ++ .../browser/browser_telemetry_sponsored.js | 367 ++ .../browser/browser_telemetry_weather.js | 152 + .../tests/quicksuggest/browser/browser_weather.js | 379 ++ .../urlbar/tests/quicksuggest/browser/head.js | 569 +++ .../browser/searchSuggestionEngine.sjs | 57 + .../browser/searchSuggestionEngine.xml | 11 + .../tests/quicksuggest/browser/subdialog.xhtml | 14 + .../urlbar/tests/quicksuggest/unit/head.js | 227 ++ .../tests/quicksuggest/unit/test_merinoClient.js | 647 ++++ .../unit/test_merinoClient_sessions.js | 402 ++ .../tests/quicksuggest/unit/test_quicksuggest.js | 1341 +++++++ .../quicksuggest/unit/test_quicksuggest_addons.js | 728 ++++ .../unit/test_quicksuggest_bestMatch.js | 463 +++ .../unit/test_quicksuggest_dynamicWikipedia.js | 95 + .../unit/test_quicksuggest_impressionCaps.js | 3888 ++++++++++++++++++++ .../quicksuggest/unit/test_quicksuggest_merino.js | 681 ++++ .../unit/test_quicksuggest_merinoSessions.js | 174 + .../unit/test_quicksuggest_migrate_v1.js | 490 +++ .../unit/test_quicksuggest_migrate_v2.js | 1355 +++++++ .../unit/test_quicksuggest_nonUniqueKeywords.js | 282 ++ .../unit/test_quicksuggest_offlineDefault.js | 127 + .../test_quicksuggest_positionInSuggestions.js | 487 +++ .../unit/test_quicksuggest_topPicks.js | 284 ++ .../tests/quicksuggest/unit/test_suggestionsMap.js | 217 ++ .../urlbar/tests/quicksuggest/unit/test_weather.js | 1394 +++++++ .../quicksuggest/unit/test_weather_keywords.js | 1395 +++++++ .../urlbar/tests/quicksuggest/unit/xpcshell.ini | 23 + .../components/urlbar/tests/unit/data/engine.xml | 10 + browser/components/urlbar/tests/unit/head.js | 1127 ++++++ .../urlbar/tests/unit/test_000_frecency.js | 245 ++ .../unit/test_UrlbarController_integration.js | 83 + .../tests/unit/test_UrlbarController_telemetry.js | 256 ++ .../tests/unit/test_UrlbarController_unit.js | 389 ++ .../urlbar/tests/unit/test_UrlbarPrefs.js | 449 +++ .../urlbar/tests/unit/test_UrlbarQueryContext.js | 73 + .../unit/test_UrlbarQueryContext_restrictSource.js | 132 + .../urlbar/tests/unit/test_UrlbarSearchUtils.js | 462 +++ .../unit/test_UrlbarUtils_addToUrlbarHistory.js | 63 + ...test_UrlbarUtils_getShortcutOrURIAndPostData.js | 249 ++ .../tests/unit/test_UrlbarUtils_getTokenMatches.js | 294 ++ .../unit/test_UrlbarUtils_unEscapeURIForUI.js | 36 + .../urlbar/tests/unit/test_about_urls.js | 176 + .../tests/unit/test_autofill_adaptiveHistory.js | 1441 ++++++++ .../urlbar/tests/unit/test_autofill_bookmarked.js | 150 + .../urlbar/tests/unit/test_autofill_do_not_trim.js | 140 + .../urlbar/tests/unit/test_autofill_functional.js | 115 + .../urlbar/tests/unit/test_autofill_origins.js | 1041 ++++++ .../tests/unit/test_autofill_originsAndQueries.js | 2471 +++++++++++++ .../unit/test_autofill_origins_alt_frecency.js | 243 ++ .../tests/unit/test_autofill_prefix_fallback.js | 76 + .../unit/test_autofill_search_engine_aliases.js | 85 + .../tests/unit/test_autofill_search_engines.js | 246 ++ .../urlbar/tests/unit/test_autofill_urls.js | 881 +++++ .../tests/unit/test_avoid_middle_complete.js | 270 ++ .../unit/test_avoid_stripping_to_empty_tokens.js | 119 + .../urlbar/tests/unit/test_calculator.js | 46 + .../components/urlbar/tests/unit/test_casing.js | 370 ++ .../urlbar/tests/unit/test_dedupe_prefix.js | 277 ++ .../urlbar/tests/unit/test_dedupe_switchTab.js | 34 + .../tests/unit/test_download_embed_bookmarks.js | 137 + .../urlbar/tests/unit/test_empty_search.js | 181 + .../urlbar/tests/unit/test_encoded_urls.js | 97 + .../tests/unit/test_escaping_badEscapedURI.js | 37 + .../urlbar/tests/unit/test_escaping_escapeSelf.js | 62 + .../components/urlbar/tests/unit/test_exposure.js | 195 + .../components/urlbar/tests/unit/test_frecency.js | 403 ++ .../tests/unit/test_frecency_alternative_nimbus.js | 50 + .../urlbar/tests/unit/test_heuristic_cancel.js | 238 ++ .../urlbar/tests/unit/test_hideSponsoredHistory.js | 104 + .../components/urlbar/tests/unit/test_keywords.js | 212 ++ .../components/urlbar/tests/unit/test_l10nCache.js | 685 ++++ .../urlbar/tests/unit/test_local_suggest_prefs.js | 126 + .../urlbar/tests/unit/test_match_javascript.js | 155 + .../urlbar/tests/unit/test_multi_word_search.js | 126 + browser/components/urlbar/tests/unit/test_muxer.js | 731 ++++ .../urlbar/tests/unit/test_protocol_ignore.js | 42 + .../urlbar/tests/unit/test_protocol_swap.js | 303 ++ .../urlbar/tests/unit/test_providerAliasEngines.js | 114 + .../tests/unit/test_providerHeuristicFallback.js | 691 ++++ .../tests/unit/test_providerHistoryUrlHeuristic.js | 197 + .../urlbar/tests/unit/test_providerKeywords.js | 360 ++ .../urlbar/tests/unit/test_providerOmnibox.js | 887 +++++ .../urlbar/tests/unit/test_providerOpenTabs.js | 45 + .../urlbar/tests/unit/test_providerPlaces.js | 250 ++ .../unit/test_providerPlaces_duplicate_entries.js | 42 + .../tests/unit/test_providerPlaces_nonEnglish.js | 43 + .../urlbar/tests/unit/test_providerPreloaded.js | 578 +++ .../urlbar/tests/unit/test_providerTabToSearch.js | 535 +++ .../unit/test_providerTabToSearch_partialHost.js | 214 ++ .../urlbar/tests/unit/test_providersManager.js | 74 + .../tests/unit/test_providersManager_filtering.js | 407 ++ .../tests/unit/test_providersManager_maxResults.js | 37 + .../urlbar/tests/unit/test_queryScorer.js | 405 ++ .../components/urlbar/tests/unit/test_query_url.js | 121 + .../urlbar/tests/unit/test_quickactions.js | 126 + .../urlbar/tests/unit/test_remote_tabs.js | 695 ++++ .../urlbar/tests/unit/test_resultGroups.js | 1576 ++++++++ .../urlbar/tests/unit/test_search_engine_host.js | 98 + .../tests/unit/test_search_engine_restyle.js | 124 + .../urlbar/tests/unit/test_search_suggestions.js | 2078 +++++++++++ .../tests/unit/test_search_suggestions_aliases.js | 364 ++ .../tests/unit/test_search_suggestions_tail.js | 379 ++ .../urlbar/tests/unit/test_special_search.js | 543 +++ .../urlbar/tests/unit/test_suggestedIndex.js | 562 +++ .../unit/test_suggestedIndexRelativeToGroup.js | 596 +++ .../urlbar/tests/unit/test_tab_matches.js | 354 ++ .../tests/unit/test_tags_caseInsensitivity.js | 137 + .../urlbar/tests/unit/test_tags_extendedUnicode.js | 66 + .../urlbar/tests/unit/test_tags_general.js | 207 ++ .../tests/unit/test_tags_matchBookmarkTitles.js | 42 + .../tests/unit/test_tags_returnedInSearches.js | 125 + .../components/urlbar/tests/unit/test_tokenizer.js | 449 +++ .../components/urlbar/tests/unit/test_trimming.js | 171 + .../urlbar/tests/unit/test_unitConversion.js | 504 +++ .../urlbar/tests/unit/test_word_boundary_search.js | 403 ++ browser/components/urlbar/tests/unit/xpcshell.ini | 99 + 449 files changed, 121071 insertions(+) create mode 100644 browser/components/urlbar/tests/UrlbarTestUtils.sys.mjs create mode 100644 browser/components/urlbar/tests/browser-tips/README.txt create mode 100644 browser/components/urlbar/tests/browser-tips/browser.ini create mode 100644 browser/components/urlbar/tests/browser-tips/browser_interventions.js create mode 100644 browser/components/urlbar/tests/browser-tips/browser_picks.js create mode 100644 browser/components/urlbar/tests/browser-tips/browser_searchTips.js create mode 100644 browser/components/urlbar/tests/browser-tips/browser_searchTips_interaction.js create mode 100644 browser/components/urlbar/tests/browser-tips/browser_selection.js create mode 100644 browser/components/urlbar/tests/browser-tips/browser_updateAsk.js create mode 100644 browser/components/urlbar/tests/browser-tips/browser_updateRefresh.js create mode 100644 browser/components/urlbar/tests/browser-tips/browser_updateRestart.js create mode 100644 browser/components/urlbar/tests/browser-tips/browser_updateWeb.js create mode 100644 browser/components/urlbar/tests/browser-tips/head.js create mode 100644 browser/components/urlbar/tests/browser-tips/slow-page.html create mode 100644 browser/components/urlbar/tests/browser-updateResults/browser.ini create mode 100644 browser/components/urlbar/tests/browser-updateResults/browser_appendSpanCount.js create mode 100644 browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_10_search_10_url.js create mode 100644 browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_10_search_5_url.js create mode 100644 browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_10_url_10_search.js create mode 100644 browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_10_url_5_search.js create mode 100644 browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_5_search_10_url.js create mode 100644 browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_5_search_5_url.js create mode 100644 browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_5_url_10_search.js create mode 100644 browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_5_url_5_search.js create mode 100644 browser/components/urlbar/tests/browser-updateResults/head.js create mode 100644 browser/components/urlbar/tests/browser/POSTSearchEngine.xml create mode 100644 browser/components/urlbar/tests/browser/add_search_engine_0.xml create mode 100644 browser/components/urlbar/tests/browser/add_search_engine_1.xml create mode 100644 browser/components/urlbar/tests/browser/add_search_engine_2.xml create mode 100644 browser/components/urlbar/tests/browser/add_search_engine_3.xml create mode 100644 browser/components/urlbar/tests/browser/add_search_engine_invalid.html create mode 100644 browser/components/urlbar/tests/browser/add_search_engine_many.html create mode 100644 browser/components/urlbar/tests/browser/add_search_engine_one.html create mode 100644 browser/components/urlbar/tests/browser/add_search_engine_same_names.html create mode 100644 browser/components/urlbar/tests/browser/add_search_engine_two.html create mode 100644 browser/components/urlbar/tests/browser/authenticate.sjs create mode 100644 browser/components/urlbar/tests/browser/browser.ini create mode 100644 browser/components/urlbar/tests/browser/browser_UrlbarInput_formatValue.js create mode 100644 browser/components/urlbar/tests/browser/browser_UrlbarInput_formatValue_detachedTab.js create mode 100644 browser/components/urlbar/tests/browser/browser_UrlbarInput_hiddenFocus.js create mode 100644 browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow.js create mode 100644 browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow_resize.js create mode 100644 browser/components/urlbar/tests/browser/browser_UrlbarInput_privateFeature.js create mode 100644 browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms.js create mode 100644 browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_backgroundTabs.js create mode 100644 browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_modifiedUrl.js create mode 100644 browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_moveTab.js create mode 100644 browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_popup.js create mode 100644 browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_revert.js create mode 100644 browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_searchBar.js create mode 100644 browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_searchMode.js create mode 100644 browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_switch_tab.js create mode 100644 browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_telemetry.js create mode 100644 browser/components/urlbar/tests/browser/browser_UrlbarInput_setURI.js create mode 100644 browser/components/urlbar/tests/browser/browser_UrlbarInput_tooltip.js create mode 100644 browser/components/urlbar/tests/browser/browser_UrlbarInput_trimURLs.js create mode 100644 browser/components/urlbar/tests/browser/browser_aboutHomeLoading.js create mode 100644 browser/components/urlbar/tests/browser/browser_acknowledgeFeedbackAndDismissal.js create mode 100644 browser/components/urlbar/tests/browser/browser_action_searchengine.js create mode 100644 browser/components/urlbar/tests/browser/browser_action_searchengine_alias.js create mode 100644 browser/components/urlbar/tests/browser/browser_add_search_engine.js create mode 100644 browser/components/urlbar/tests/browser/browser_autoFill_backspaced.js create mode 100644 browser/components/urlbar/tests/browser/browser_autoFill_canonize.js create mode 100644 browser/components/urlbar/tests/browser/browser_autoFill_caretNotAtEnd.js create mode 100644 browser/components/urlbar/tests/browser/browser_autoFill_firstResult.js create mode 100644 browser/components/urlbar/tests/browser/browser_autoFill_paste.js create mode 100644 browser/components/urlbar/tests/browser/browser_autoFill_placeholder.js create mode 100644 browser/components/urlbar/tests/browser/browser_autoFill_preserve.js create mode 100644 browser/components/urlbar/tests/browser/browser_autoFill_trimURLs.js create mode 100644 browser/components/urlbar/tests/browser/browser_autoFill_typed.js create mode 100644 browser/components/urlbar/tests/browser/browser_autoFill_undo.js create mode 100644 browser/components/urlbar/tests/browser/browser_autoOpen.js create mode 100644 browser/components/urlbar/tests/browser/browser_autocomplete_a11y_label.js create mode 100644 browser/components/urlbar/tests/browser/browser_autocomplete_autoselect.js create mode 100644 browser/components/urlbar/tests/browser/browser_autocomplete_cursor.js create mode 100644 browser/components/urlbar/tests/browser/browser_autocomplete_edit_completed.js create mode 100644 browser/components/urlbar/tests/browser/browser_autocomplete_enter_race.js create mode 100644 browser/components/urlbar/tests/browser/browser_autocomplete_no_title.js create mode 100644 browser/components/urlbar/tests/browser/browser_autocomplete_readline_navigation.js create mode 100644 browser/components/urlbar/tests/browser/browser_autocomplete_tag_star_visibility.js create mode 100644 browser/components/urlbar/tests/browser/browser_bestMatch.js create mode 100644 browser/components/urlbar/tests/browser/browser_blanking.js create mode 100644 browser/components/urlbar/tests/browser/browser_bufferer_onQueryResults.js create mode 100644 browser/components/urlbar/tests/browser/browser_calculator.js create mode 100644 browser/components/urlbar/tests/browser/browser_canonizeURL.js create mode 100644 browser/components/urlbar/tests/browser/browser_caret_position.js create mode 100644 browser/components/urlbar/tests/browser/browser_click_row_border.js create mode 100644 browser/components/urlbar/tests/browser/browser_closePanelOnClick.js create mode 100644 browser/components/urlbar/tests/browser/browser_content_opener.js create mode 100644 browser/components/urlbar/tests/browser/browser_contextualsearch.js create mode 100644 browser/components/urlbar/tests/browser/browser_copy_during_load.js create mode 100644 browser/components/urlbar/tests/browser/browser_copying.js create mode 100644 browser/components/urlbar/tests/browser/browser_customizeMode.js create mode 100644 browser/components/urlbar/tests/browser/browser_cutting.js create mode 100644 browser/components/urlbar/tests/browser/browser_decode.js create mode 100644 browser/components/urlbar/tests/browser/browser_delete.js create mode 100644 browser/components/urlbar/tests/browser/browser_deleteAllText.js create mode 100644 browser/components/urlbar/tests/browser/browser_display_selectedAction_Extensions.js create mode 100644 browser/components/urlbar/tests/browser/browser_dns_first_for_single_words.js create mode 100644 browser/components/urlbar/tests/browser/browser_downArrowKeySearch.js create mode 100644 browser/components/urlbar/tests/browser/browser_dragdropURL.js create mode 100644 browser/components/urlbar/tests/browser/browser_dynamicResults.js create mode 100644 browser/components/urlbar/tests/browser/browser_edit_invalid_url.js create mode 100644 browser/components/urlbar/tests/browser/browser_engagement.js create mode 100644 browser/components/urlbar/tests/browser/browser_enter.js create mode 100644 browser/components/urlbar/tests/browser/browser_enterAfterMouseOver.js create mode 100644 browser/components/urlbar/tests/browser/browser_focusedCmdK.js create mode 100644 browser/components/urlbar/tests/browser/browser_groupLabels.js create mode 100644 browser/components/urlbar/tests/browser/browser_handleCommand_fallback.js create mode 100644 browser/components/urlbar/tests/browser/browser_hashChangeProxyState.js create mode 100644 browser/components/urlbar/tests/browser/browser_helpUrl.js create mode 100644 browser/components/urlbar/tests/browser/browser_heuristicNotAddedFirst.js create mode 100644 browser/components/urlbar/tests/browser/browser_hideHeuristic.js create mode 100644 browser/components/urlbar/tests/browser/browser_ime_composition.js create mode 100644 browser/components/urlbar/tests/browser/browser_inputHistory.js create mode 100644 browser/components/urlbar/tests/browser/browser_inputHistory_autofill.js create mode 100644 browser/components/urlbar/tests/browser/browser_inputHistory_emptystring.js create mode 100644 browser/components/urlbar/tests/browser/browser_keepStateAcrossTabSwitches.js create mode 100644 browser/components/urlbar/tests/browser/browser_keyword.js create mode 100644 browser/components/urlbar/tests/browser/browser_keywordBookmarklets.js create mode 100644 browser/components/urlbar/tests/browser/browser_keywordSearch.js create mode 100644 browser/components/urlbar/tests/browser/browser_keywordSearch_postData.js create mode 100644 browser/components/urlbar/tests/browser/browser_keyword_override.js create mode 100644 browser/components/urlbar/tests/browser/browser_keyword_select_and_type.js create mode 100644 browser/components/urlbar/tests/browser/browser_loadRace.js create mode 100644 browser/components/urlbar/tests/browser/browser_locationBarCommand.js create mode 100644 browser/components/urlbar/tests/browser/browser_locationBarExternalLoad.js create mode 100644 browser/components/urlbar/tests/browser/browser_locationchange_urlbar_edit_dos.js create mode 100644 browser/components/urlbar/tests/browser/browser_middleClick.js create mode 100644 browser/components/urlbar/tests/browser/browser_new_tab_urlbar_reset.js create mode 100644 browser/components/urlbar/tests/browser/browser_oneOffs.js create mode 100644 browser/components/urlbar/tests/browser/browser_oneOffs_contextMenu.js create mode 100644 browser/components/urlbar/tests/browser/browser_oneOffs_heuristicRestyle.js create mode 100644 browser/components/urlbar/tests/browser/browser_oneOffs_keyModifiers.js create mode 100644 browser/components/urlbar/tests/browser/browser_oneOffs_searchSuggestions.js create mode 100644 browser/components/urlbar/tests/browser/browser_oneOffs_settings.js create mode 100644 browser/components/urlbar/tests/browser/browser_pasteAndGo.js create mode 100644 browser/components/urlbar/tests/browser/browser_paste_multi_lines.js create mode 100644 browser/components/urlbar/tests/browser/browser_paste_then_focus.js create mode 100644 browser/components/urlbar/tests/browser/browser_paste_then_switch_tab.js create mode 100644 browser/components/urlbar/tests/browser/browser_percent_encoded.js create mode 100644 browser/components/urlbar/tests/browser/browser_placeholder.js create mode 100644 browser/components/urlbar/tests/browser/browser_populateAfterPushState.js create mode 100644 browser/components/urlbar/tests/browser/browser_primary_selection_safe_on_new_tab.js create mode 100644 browser/components/urlbar/tests/browser/browser_privateBrowsingWindowChange.js create mode 100644 browser/components/urlbar/tests/browser/browser_queryContextCache.js create mode 100644 browser/components/urlbar/tests/browser/browser_quickactions.js create mode 100644 browser/components/urlbar/tests/browser/browser_quickactions_devtools.js create mode 100644 browser/components/urlbar/tests/browser/browser_quickactions_tab_refocus.js create mode 100644 browser/components/urlbar/tests/browser/browser_raceWithTabs.js create mode 100644 browser/components/urlbar/tests/browser/browser_redirect_error.js create mode 100644 browser/components/urlbar/tests/browser/browser_remoteness_switch.js create mode 100644 browser/components/urlbar/tests/browser/browser_remotetab.js create mode 100644 browser/components/urlbar/tests/browser/browser_removeUnsafeProtocolsFromURLBarPaste.js create mode 100644 browser/components/urlbar/tests/browser/browser_remove_match.js create mode 100644 browser/components/urlbar/tests/browser/browser_restoreEmptyInput.js create mode 100644 browser/components/urlbar/tests/browser/browser_resultSpan.js create mode 100644 browser/components/urlbar/tests/browser/browser_result_menu.js create mode 100644 browser/components/urlbar/tests/browser/browser_result_onSelection.js create mode 100644 browser/components/urlbar/tests/browser/browser_retainedResultsOnFocus.js create mode 100644 browser/components/urlbar/tests/browser/browser_revert.js create mode 100644 browser/components/urlbar/tests/browser/browser_searchFunction.js create mode 100644 browser/components/urlbar/tests/browser/browser_searchHistoryLimit.js create mode 100644 browser/components/urlbar/tests/browser/browser_searchMode_alias_replacement.js create mode 100644 browser/components/urlbar/tests/browser/browser_searchMode_autofill.js create mode 100644 browser/components/urlbar/tests/browser/browser_searchMode_clickLink.js create mode 100644 browser/components/urlbar/tests/browser/browser_searchMode_engineRemoval.js create mode 100644 browser/components/urlbar/tests/browser/browser_searchMode_excludeResults.js create mode 100644 browser/components/urlbar/tests/browser/browser_searchMode_heuristic.js create mode 100644 browser/components/urlbar/tests/browser/browser_searchMode_indicator.js create mode 100644 browser/components/urlbar/tests/browser/browser_searchMode_indicator_clickthrough.js create mode 100644 browser/components/urlbar/tests/browser/browser_searchMode_localOneOffs_actionText.js create mode 100644 browser/components/urlbar/tests/browser/browser_searchMode_newWindow.js create mode 100644 browser/components/urlbar/tests/browser/browser_searchMode_no_results.js create mode 100644 browser/components/urlbar/tests/browser/browser_searchMode_oneOffButton.js create mode 100644 browser/components/urlbar/tests/browser/browser_searchMode_pickResult.js create mode 100644 browser/components/urlbar/tests/browser/browser_searchMode_preview.js create mode 100644 browser/components/urlbar/tests/browser/browser_searchMode_sessionStore.js create mode 100644 browser/components/urlbar/tests/browser/browser_searchMode_setURI.js create mode 100644 browser/components/urlbar/tests/browser/browser_searchMode_suggestions.js create mode 100644 browser/components/urlbar/tests/browser/browser_searchMode_switchTabs.js create mode 100644 browser/components/urlbar/tests/browser/browser_searchSettings.js create mode 100644 browser/components/urlbar/tests/browser/browser_searchSingleWordNotification.js create mode 100644 browser/components/urlbar/tests/browser/browser_searchSuggestions.js create mode 100644 browser/components/urlbar/tests/browser/browser_searchTelemetry.js create mode 100644 browser/components/urlbar/tests/browser/browser_search_bookmarks_from_bookmarks_menu.js create mode 100644 browser/components/urlbar/tests/browser/browser_search_history_from_history_panel.js create mode 100644 browser/components/urlbar/tests/browser/browser_selectStaleResults.js create mode 100644 browser/components/urlbar/tests/browser/browser_selectionKeyNavigation.js create mode 100644 browser/components/urlbar/tests/browser/browser_separatePrivateDefault.js create mode 100644 browser/components/urlbar/tests/browser/browser_separatePrivateDefault_differentEngine.js create mode 100644 browser/components/urlbar/tests/browser/browser_shortcuts_add_search_engine.js create mode 100644 browser/components/urlbar/tests/browser/browser_speculative_connect.js create mode 100644 browser/components/urlbar/tests/browser/browser_speculative_connect_not_with_client_cert.js create mode 100644 browser/components/urlbar/tests/browser/browser_stop.js create mode 100644 browser/components/urlbar/tests/browser/browser_stopSearchOnSelection.js create mode 100644 browser/components/urlbar/tests/browser/browser_stop_pending.js create mode 100644 browser/components/urlbar/tests/browser/browser_strip_on_share.js create mode 100644 browser/components/urlbar/tests/browser/browser_suggestedIndex.js create mode 100644 browser/components/urlbar/tests/browser/browser_suppressFocusBorder.js create mode 100644 browser/components/urlbar/tests/browser/browser_switchTab_closesUrlbarPopup.js create mode 100644 browser/components/urlbar/tests/browser/browser_switchTab_currentTab.js create mode 100644 browser/components/urlbar/tests/browser/browser_switchTab_decodeuri.js create mode 100644 browser/components/urlbar/tests/browser/browser_switchTab_inputHistory.js create mode 100644 browser/components/urlbar/tests/browser/browser_switchTab_override.js create mode 100644 browser/components/urlbar/tests/browser/browser_switchToTabHavingURI_aOpenParams.js create mode 100644 browser/components/urlbar/tests/browser/browser_switchToTab_chiclet.js create mode 100644 browser/components/urlbar/tests/browser/browser_switchToTab_closes_newtab.js create mode 100644 browser/components/urlbar/tests/browser/browser_switchToTab_fullUrl_repeatedKeydown.js create mode 100644 browser/components/urlbar/tests/browser/browser_tabKeyBehavior.js create mode 100644 browser/components/urlbar/tests/browser/browser_tabMatchesInAwesomebar.js create mode 100644 browser/components/urlbar/tests/browser/browser_tabMatchesInAwesomebar_perwindowpb.js create mode 100644 browser/components/urlbar/tests/browser/browser_tabToSearch.js create mode 100644 browser/components/urlbar/tests/browser/browser_textruns.js create mode 100644 browser/components/urlbar/tests/browser/browser_tokenAlias.js create mode 100644 browser/components/urlbar/tests/browser/browser_top_sites.js create mode 100644 browser/components/urlbar/tests/browser/browser_top_sites_private.js create mode 100644 browser/components/urlbar/tests/browser/browser_typed_value.js create mode 100644 browser/components/urlbar/tests/browser/browser_unitConversion.js create mode 100644 browser/components/urlbar/tests/browser/browser_updateForDomainCompletion.js create mode 100644 browser/components/urlbar/tests/browser/browser_urlbar_annotation.js create mode 100644 browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry_abandonment.js create mode 100644 browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry_engagement.js create mode 100644 browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry_noEvent.js create mode 100644 browser/components/urlbar/tests/browser/browser_urlbar_selection.js create mode 100644 browser/components/urlbar/tests/browser/browser_urlbar_telemetry.js create mode 100644 browser/components/urlbar/tests/browser/browser_urlbar_telemetry_autofill.js create mode 100644 browser/components/urlbar/tests/browser/browser_urlbar_telemetry_dynamic.js create mode 100644 browser/components/urlbar/tests/browser/browser_urlbar_telemetry_extension.js create mode 100644 browser/components/urlbar/tests/browser/browser_urlbar_telemetry_handoff.js create mode 100644 browser/components/urlbar/tests/browser/browser_urlbar_telemetry_persisted.js create mode 100644 browser/components/urlbar/tests/browser/browser_urlbar_telemetry_places.js create mode 100644 browser/components/urlbar/tests/browser/browser_urlbar_telemetry_quickactions.js create mode 100644 browser/components/urlbar/tests/browser/browser_urlbar_telemetry_remotetab.js create mode 100644 browser/components/urlbar/tests/browser/browser_urlbar_telemetry_searchmode.js create mode 100644 browser/components/urlbar/tests/browser/browser_urlbar_telemetry_sponsored_topsites.js create mode 100644 browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tabtosearch.js create mode 100644 browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tip.js create mode 100644 browser/components/urlbar/tests/browser/browser_urlbar_telemetry_topsite.js create mode 100644 browser/components/urlbar/tests/browser/browser_urlbar_telemetry_zeroPrefix.js create mode 100644 browser/components/urlbar/tests/browser/browser_userTypedValue.js create mode 100644 browser/components/urlbar/tests/browser/browser_valueOnTabSwitch.js create mode 100644 browser/components/urlbar/tests/browser/browser_view_emptyResultSet.js create mode 100644 browser/components/urlbar/tests/browser/browser_view_resultDisplay.js create mode 100644 browser/components/urlbar/tests/browser/browser_view_resultTypes_display.js create mode 100644 browser/components/urlbar/tests/browser/browser_view_selectionByMouse.js create mode 100644 browser/components/urlbar/tests/browser/browser_waitForLoadOrTimeout.js create mode 100644 browser/components/urlbar/tests/browser/browser_whereToOpen.js create mode 100644 browser/components/urlbar/tests/browser/dummy_page.html create mode 100644 browser/components/urlbar/tests/browser/dynamicResult0.css create mode 100644 browser/components/urlbar/tests/browser/dynamicResult1.css create mode 100644 browser/components/urlbar/tests/browser/file_blank_but_not_blank.html create mode 100644 browser/components/urlbar/tests/browser/file_urlbar_edit_dos.html create mode 100644 browser/components/urlbar/tests/browser/file_userTypedValue.html create mode 100644 browser/components/urlbar/tests/browser/head-common.js create mode 100644 browser/components/urlbar/tests/browser/head.js create mode 100644 browser/components/urlbar/tests/browser/moz.png create mode 100644 browser/components/urlbar/tests/browser/print_postdata.sjs create mode 100644 browser/components/urlbar/tests/browser/redirect_error.sjs create mode 100644 browser/components/urlbar/tests/browser/redirect_to.sjs create mode 100644 browser/components/urlbar/tests/browser/searchSuggestionEngine.sjs create mode 100644 browser/components/urlbar/tests/browser/searchSuggestionEngine.xml create mode 100644 browser/components/urlbar/tests/browser/searchSuggestionEngine2.xml create mode 100644 browser/components/urlbar/tests/browser/searchSuggestionEngineMany.xml create mode 100644 browser/components/urlbar/tests/browser/searchSuggestionEngineSlow.xml create mode 100644 browser/components/urlbar/tests/browser/slow-page.sjs create mode 100644 browser/components/urlbar/tests/browser/urlbarTelemetrySearchSuggestions.sjs create mode 100644 browser/components/urlbar/tests/browser/urlbarTelemetrySearchSuggestions.xml create mode 100644 browser/components/urlbar/tests/browser/urlbarTelemetryUrlbarDynamic.css create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/browser.ini create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_groups.js create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_interaction.js create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_interaction_persisted_search_terms_disabled.js create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_interaction_persisted_search_terms_enabled.js create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_n_chars_n_words.js create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_sap.js create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_search_mode.js create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_tips.js create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_edge_cases.js create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_groups.js create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_interaction.js create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_interaction_persisted_search_terms_disabled.js create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_interaction_persisted_search_terms_enabled.js create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_n_chars_n_words.js create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_sap.js create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_search_mode.js create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_selected_result.js create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_tips.js create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_type.js create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_exposure.js create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_groups.js create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_interaction.js create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_interaction_persisted_search_terms_disabled.js create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_interaction_persisted_search_terms_enabled.js create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_n_chars_n_words.js create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_preferences.js create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_sap.js create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_search_mode.js create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_timing.js create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_record_preferences.js create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/head-exposure.js create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/head-groups.js create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/head-interaction.js create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/head-n_chars_n_words.js create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/head-sap.js create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/head-search_mode.js create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/head.js create mode 100644 browser/components/urlbar/tests/ext/api.js create mode 100644 browser/components/urlbar/tests/ext/browser/.eslintrc.js create mode 100644 browser/components/urlbar/tests/ext/browser/browser.ini create mode 100644 browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_attributionURL.js create mode 100644 browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_clearInput.js create mode 100644 browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_dynamicResult.js create mode 100644 browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_engagementTelemetry.js create mode 100644 browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_extensionTimeout.js create mode 100644 browser/components/urlbar/tests/ext/browser/dynamicResult.css create mode 100644 browser/components/urlbar/tests/ext/browser/head.js create mode 100644 browser/components/urlbar/tests/ext/schema.json create mode 100644 browser/components/urlbar/tests/quicksuggest/MerinoTestUtils.sys.mjs create mode 100644 browser/components/urlbar/tests/quicksuggest/QuickSuggestTestUtils.sys.mjs create mode 100644 browser/components/urlbar/tests/quicksuggest/browser/browser.ini create mode 100644 browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest.js create mode 100644 browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_addons.js create mode 100644 browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_block.js create mode 100644 browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_configuration.js create mode 100644 browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_indexes.js create mode 100644 browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_merinoSessions.js create mode 100644 browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_onboardingDialog.js create mode 100644 browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_dynamicWikipedia.js create mode 100644 browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_impressionEdgeCases.js create mode 100644 browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_navigationalSuggestions.js create mode 100644 browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_nonsponsored.js create mode 100644 browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_other.js create mode 100644 browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_sponsored.js create mode 100644 browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_weather.js create mode 100644 browser/components/urlbar/tests/quicksuggest/browser/browser_weather.js create mode 100644 browser/components/urlbar/tests/quicksuggest/browser/head.js create mode 100644 browser/components/urlbar/tests/quicksuggest/browser/searchSuggestionEngine.sjs create mode 100644 browser/components/urlbar/tests/quicksuggest/browser/searchSuggestionEngine.xml create mode 100644 browser/components/urlbar/tests/quicksuggest/browser/subdialog.xhtml create mode 100644 browser/components/urlbar/tests/quicksuggest/unit/head.js create mode 100644 browser/components/urlbar/tests/quicksuggest/unit/test_merinoClient.js create mode 100644 browser/components/urlbar/tests/quicksuggest/unit/test_merinoClient_sessions.js create mode 100644 browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest.js create mode 100644 browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_addons.js create mode 100644 browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_bestMatch.js create mode 100644 browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_dynamicWikipedia.js create mode 100644 browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_impressionCaps.js create mode 100644 browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_merino.js create mode 100644 browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_merinoSessions.js create mode 100644 browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_migrate_v1.js create mode 100644 browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_migrate_v2.js create mode 100644 browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_nonUniqueKeywords.js create mode 100644 browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_offlineDefault.js create mode 100644 browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_positionInSuggestions.js create mode 100644 browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_topPicks.js create mode 100644 browser/components/urlbar/tests/quicksuggest/unit/test_suggestionsMap.js create mode 100644 browser/components/urlbar/tests/quicksuggest/unit/test_weather.js create mode 100644 browser/components/urlbar/tests/quicksuggest/unit/test_weather_keywords.js create mode 100644 browser/components/urlbar/tests/quicksuggest/unit/xpcshell.ini create mode 100644 browser/components/urlbar/tests/unit/data/engine.xml create mode 100644 browser/components/urlbar/tests/unit/head.js create mode 100644 browser/components/urlbar/tests/unit/test_000_frecency.js create mode 100644 browser/components/urlbar/tests/unit/test_UrlbarController_integration.js create mode 100644 browser/components/urlbar/tests/unit/test_UrlbarController_telemetry.js create mode 100644 browser/components/urlbar/tests/unit/test_UrlbarController_unit.js create mode 100644 browser/components/urlbar/tests/unit/test_UrlbarPrefs.js create mode 100644 browser/components/urlbar/tests/unit/test_UrlbarQueryContext.js create mode 100644 browser/components/urlbar/tests/unit/test_UrlbarQueryContext_restrictSource.js create mode 100644 browser/components/urlbar/tests/unit/test_UrlbarSearchUtils.js create mode 100644 browser/components/urlbar/tests/unit/test_UrlbarUtils_addToUrlbarHistory.js create mode 100644 browser/components/urlbar/tests/unit/test_UrlbarUtils_getShortcutOrURIAndPostData.js create mode 100644 browser/components/urlbar/tests/unit/test_UrlbarUtils_getTokenMatches.js create mode 100644 browser/components/urlbar/tests/unit/test_UrlbarUtils_unEscapeURIForUI.js create mode 100644 browser/components/urlbar/tests/unit/test_about_urls.js create mode 100644 browser/components/urlbar/tests/unit/test_autofill_adaptiveHistory.js create mode 100644 browser/components/urlbar/tests/unit/test_autofill_bookmarked.js create mode 100644 browser/components/urlbar/tests/unit/test_autofill_do_not_trim.js create mode 100644 browser/components/urlbar/tests/unit/test_autofill_functional.js create mode 100644 browser/components/urlbar/tests/unit/test_autofill_origins.js create mode 100644 browser/components/urlbar/tests/unit/test_autofill_originsAndQueries.js create mode 100644 browser/components/urlbar/tests/unit/test_autofill_origins_alt_frecency.js create mode 100644 browser/components/urlbar/tests/unit/test_autofill_prefix_fallback.js create mode 100644 browser/components/urlbar/tests/unit/test_autofill_search_engine_aliases.js create mode 100644 browser/components/urlbar/tests/unit/test_autofill_search_engines.js create mode 100644 browser/components/urlbar/tests/unit/test_autofill_urls.js create mode 100644 browser/components/urlbar/tests/unit/test_avoid_middle_complete.js create mode 100644 browser/components/urlbar/tests/unit/test_avoid_stripping_to_empty_tokens.js create mode 100644 browser/components/urlbar/tests/unit/test_calculator.js create mode 100644 browser/components/urlbar/tests/unit/test_casing.js create mode 100644 browser/components/urlbar/tests/unit/test_dedupe_prefix.js create mode 100644 browser/components/urlbar/tests/unit/test_dedupe_switchTab.js create mode 100644 browser/components/urlbar/tests/unit/test_download_embed_bookmarks.js create mode 100644 browser/components/urlbar/tests/unit/test_empty_search.js create mode 100644 browser/components/urlbar/tests/unit/test_encoded_urls.js create mode 100644 browser/components/urlbar/tests/unit/test_escaping_badEscapedURI.js create mode 100644 browser/components/urlbar/tests/unit/test_escaping_escapeSelf.js create mode 100644 browser/components/urlbar/tests/unit/test_exposure.js create mode 100644 browser/components/urlbar/tests/unit/test_frecency.js create mode 100644 browser/components/urlbar/tests/unit/test_frecency_alternative_nimbus.js create mode 100644 browser/components/urlbar/tests/unit/test_heuristic_cancel.js create mode 100644 browser/components/urlbar/tests/unit/test_hideSponsoredHistory.js create mode 100644 browser/components/urlbar/tests/unit/test_keywords.js create mode 100644 browser/components/urlbar/tests/unit/test_l10nCache.js create mode 100644 browser/components/urlbar/tests/unit/test_local_suggest_prefs.js create mode 100644 browser/components/urlbar/tests/unit/test_match_javascript.js create mode 100644 browser/components/urlbar/tests/unit/test_multi_word_search.js create mode 100644 browser/components/urlbar/tests/unit/test_muxer.js create mode 100644 browser/components/urlbar/tests/unit/test_protocol_ignore.js create mode 100644 browser/components/urlbar/tests/unit/test_protocol_swap.js create mode 100644 browser/components/urlbar/tests/unit/test_providerAliasEngines.js create mode 100644 browser/components/urlbar/tests/unit/test_providerHeuristicFallback.js create mode 100644 browser/components/urlbar/tests/unit/test_providerHistoryUrlHeuristic.js create mode 100644 browser/components/urlbar/tests/unit/test_providerKeywords.js create mode 100644 browser/components/urlbar/tests/unit/test_providerOmnibox.js create mode 100644 browser/components/urlbar/tests/unit/test_providerOpenTabs.js create mode 100644 browser/components/urlbar/tests/unit/test_providerPlaces.js create mode 100644 browser/components/urlbar/tests/unit/test_providerPlaces_duplicate_entries.js create mode 100644 browser/components/urlbar/tests/unit/test_providerPlaces_nonEnglish.js create mode 100644 browser/components/urlbar/tests/unit/test_providerPreloaded.js create mode 100644 browser/components/urlbar/tests/unit/test_providerTabToSearch.js create mode 100644 browser/components/urlbar/tests/unit/test_providerTabToSearch_partialHost.js create mode 100644 browser/components/urlbar/tests/unit/test_providersManager.js create mode 100644 browser/components/urlbar/tests/unit/test_providersManager_filtering.js create mode 100644 browser/components/urlbar/tests/unit/test_providersManager_maxResults.js create mode 100644 browser/components/urlbar/tests/unit/test_queryScorer.js create mode 100644 browser/components/urlbar/tests/unit/test_query_url.js create mode 100644 browser/components/urlbar/tests/unit/test_quickactions.js create mode 100644 browser/components/urlbar/tests/unit/test_remote_tabs.js create mode 100644 browser/components/urlbar/tests/unit/test_resultGroups.js create mode 100644 browser/components/urlbar/tests/unit/test_search_engine_host.js create mode 100644 browser/components/urlbar/tests/unit/test_search_engine_restyle.js create mode 100644 browser/components/urlbar/tests/unit/test_search_suggestions.js create mode 100644 browser/components/urlbar/tests/unit/test_search_suggestions_aliases.js create mode 100644 browser/components/urlbar/tests/unit/test_search_suggestions_tail.js create mode 100644 browser/components/urlbar/tests/unit/test_special_search.js create mode 100644 browser/components/urlbar/tests/unit/test_suggestedIndex.js create mode 100644 browser/components/urlbar/tests/unit/test_suggestedIndexRelativeToGroup.js create mode 100644 browser/components/urlbar/tests/unit/test_tab_matches.js create mode 100644 browser/components/urlbar/tests/unit/test_tags_caseInsensitivity.js create mode 100644 browser/components/urlbar/tests/unit/test_tags_extendedUnicode.js create mode 100644 browser/components/urlbar/tests/unit/test_tags_general.js create mode 100644 browser/components/urlbar/tests/unit/test_tags_matchBookmarkTitles.js create mode 100644 browser/components/urlbar/tests/unit/test_tags_returnedInSearches.js create mode 100644 browser/components/urlbar/tests/unit/test_tokenizer.js create mode 100644 browser/components/urlbar/tests/unit/test_trimming.js create mode 100644 browser/components/urlbar/tests/unit/test_unitConversion.js create mode 100644 browser/components/urlbar/tests/unit/test_word_boundary_search.js create mode 100644 browser/components/urlbar/tests/unit/xpcshell.ini (limited to 'browser/components/urlbar/tests') diff --git a/browser/components/urlbar/tests/UrlbarTestUtils.sys.mjs b/browser/components/urlbar/tests/UrlbarTestUtils.sys.mjs new file mode 100644 index 0000000000..399fb7cc11 --- /dev/null +++ b/browser/components/urlbar/tests/UrlbarTestUtils.sys.mjs @@ -0,0 +1,1432 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; + +import { + UrlbarProvider, + UrlbarUtils, +} from "resource:///modules/UrlbarUtils.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + AddonTestUtils: "resource://testing-common/AddonTestUtils.sys.mjs", + BrowserTestUtils: "resource://testing-common/BrowserTestUtils.sys.mjs", + ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs", + ExperimentFakes: "resource://testing-common/NimbusTestUtils.sys.mjs", + ExperimentManager: "resource://nimbus/lib/ExperimentManager.sys.mjs", + + FormHistoryTestUtils: + "resource://testing-common/FormHistoryTestUtils.sys.mjs", + + NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", + TestUtils: "resource://testing-common/TestUtils.sys.mjs", + UrlbarController: "resource:///modules/UrlbarController.sys.mjs", + UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs", + UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.sys.mjs", + setTimeout: "resource://gre/modules/Timer.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + BrowserUIUtils: "resource:///modules/BrowserUIUtils.jsm", + BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm", +}); + +export 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, + }, + + // Fallback to the console. + info: console.log, + + /** + * 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 the properties set here and provide a fallback path. + * + * @param {object} scope The global scope where tests are being run. + */ + init(scope) { + if (!scope) { + throw new Error("Must initialize UrlbarTestUtils with a test scope"); + } + // If you add other properties to `this`, null them in uninit(). + this.Assert = scope.Assert; + this.info = scope.info; + this.registerCleanupFunction = scope.registerCleanupFunction; + + if (Services.env.exists("XPCSHELL_TEST_PROFILE_DIR")) { + this.initXPCShellDependencies(); + } else { + // xpcshell doesn't support EventUtils. + this.EventUtils = scope.EventUtils; + this.SimpleTest = scope.SimpleTest; + } + + this.registerCleanupFunction(() => { + this.Assert = null; + this.info = console.log; + this.registerCleanupFunction = null; + this.EventUtils = null; + this.SimpleTest = 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 The options object. + * @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 = true, + selectionStart = -1, + selectionEnd = -1, + } = {}) { + if (this.SimpleTest) { + await this.SimpleTest.promiseFocus(window); + } else { + await new Promise(resolve => waitForFocus(resolve, window)); + } + + const setup = () => { + window.gURLBar.inputField.focus(); + // Using the value setter in some cases may trim and fetch unexpected + // results, then pick an alternate path. + if ( + lazy.UrlbarPrefs.get("trimURLs") && + value != lazy.BrowserUIUtils.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(); + } + }; + setup(); + + // In Linux TV test, as there is case that the input field lost the focus + // until showing popup, timeout failure happens since the expected poup + // never be shown. To avoid this, if losing the focus, retry setup to open + // popup. + const blurListener = () => { + setup(); + }; + window.gURLBar.inputField.addEventListener("blur", blurListener, { + once: true, + }); + const result = await this.promiseSearchComplete(window); + window.gURLBar.inputField.removeEventListener("blur", blurListener); + return result; + }, + + /** + * 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); + let container = this.getResultsContainer(win); + if (index >= container.children.length) { + throw new Error("Not enough results"); + } + return container.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 a specific button of a result. + * + * @param {object} win The window containing the urlbar + * @param {string} buttonName The name of the button, e.g. "menu", "0", etc. + * @param {number} resultIndex The index of the result + * @returns {HtmlElement} The button + */ + getButtonForResultIndex(win, buttonName, resultIndex) { + return this.getRowAt(win, resultIndex).querySelector( + `.urlbarView-button-${buttonName}` + ); + }, + + /** + * Show the result menu button regardless of the result being hovered or + + selected. + * + * @param {object} win The window containing the urlbar + */ + disableResultMenuAutohide(win) { + let container = this.getResultsContainer(win); + let attr = "disable-resultmenu-autohide"; + container.toggleAttribute(attr, true); + this.registerCleanupFunction?.(() => { + container.toggleAttribute(attr, false); + }); + }, + + /** + * Opens the result menu of a specific result. + * + * @param {object} win The window containing the urlbar + * @param {object} [options] The options object. + * @param {number} [options.resultIndex] The index of the result. Defaults + * to the current selected index. + * @param {boolean} [options.byMouse] Whether to open the menu by mouse or + * keyboard. + * @param {string} [options.activationKey] Key to activate the button with, + * defaults to KEY_Enter. + */ + async openResultMenu( + win, + { + resultIndex = win.gURLBar.view.selectedRowIndex, + byMouse = false, + activationKey = "KEY_Enter", + } = {} + ) { + this.Assert?.ok(win.gURLBar.view.isOpen, "view should be open"); + let menuButton = this.getButtonForResultIndex(win, "menu", resultIndex); + this.Assert?.ok( + menuButton, + `found the menu button at result index ${resultIndex}` + ); + let promiseMenuOpen = lazy.BrowserTestUtils.waitForEvent( + win.gURLBar.view.resultMenu, + "popupshown" + ); + if (byMouse) { + this.info( + `synthesizing mousemove on row to make the menu button visible` + ); + await this.EventUtils.promiseElementReadyForUserInput( + menuButton.closest(".urlbarView-row"), + win, + this.info + ); + this.info(`got mousemove, now clicking the menu button`); + this.EventUtils.synthesizeMouseAtCenter(menuButton, {}, win); + this.info(`waiting for the menu popup to open via mouse`); + } else { + this.info(`selecting the result at index ${resultIndex}`); + while (win.gURLBar.view.selectedRowIndex != resultIndex) { + this.EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + } + if (this.getSelectedElement(win) != menuButton) { + this.EventUtils.synthesizeKey("KEY_Tab", {}, win); + } + this.Assert?.equal( + this.getSelectedElement(win), + menuButton, + `selected the menu button at result index ${resultIndex}` + ); + this.EventUtils.synthesizeKey(activationKey, {}, win); + this.info(`waiting for ${activationKey} to open the menu popup`); + } + await promiseMenuOpen; + this.Assert?.equal( + win.gURLBar.view.resultMenu.state, + "open", + "Checking popup state" + ); + }, + + /** + * Opens the result menu of a specific result and gets a menu item by either + * accesskey or command name. Either `accesskey` or `command` must be given. + * + * @param {object} options + * The options object. + * @param {object} options.window + * The window containing the urlbar. + * @param {string} options.accesskey + * The access key of the menu item to return. + * @param {string} options.command + * The command name of the menu item to return. + * @param {number} options.resultIndex + * The index of the result. Defaults to the current selected index. + * @param {boolean} options.openByMouse + * Whether to open the menu by mouse or keyboard. + * @param {Array} options.submenuSelectors + * If the command is in the top-level result menu, leave this as an empty + * array. If it's in a submenu, set this to an array where each element i is + * a selector that can be used to get the i'th menu item that opens a + * submenu. + */ + async openResultMenuAndGetItem({ + window, + accesskey, + command, + resultIndex = window.gURLBar.view.selectedRowIndex, + openByMouse = false, + submenuSelectors = [], + }) { + await this.openResultMenu(window, { resultIndex, byMouse: openByMouse }); + + // Open the sequence of submenus that contains the item. + for (let selector of submenuSelectors) { + let menuitem = window.gURLBar.view.resultMenu.querySelector(selector); + if (!menuitem) { + throw new Error("Submenu item not found for selector: " + selector); + } + + let promisePopup = lazy.BrowserTestUtils.waitForEvent( + window.gURLBar.view.resultMenu, + "popupshown" + ); + + if (AppConstants.platform == "macosx") { + // Synthesized clicks don't work in the native Mac menu. + this.info( + "Calling openMenu() on submenu item with selector: " + selector + ); + menuitem.openMenu(true); + } else { + this.info("Clicking submenu item with selector: " + selector); + this.EventUtils.synthesizeMouseAtCenter(menuitem, {}, window); + } + + this.info("Waiting for submenu popupshown event"); + await promisePopup; + this.info("Got the submenu popupshown event"); + } + + // Now get the item. + let menuitem; + if (accesskey) { + await lazy.BrowserTestUtils.waitForCondition(() => { + menuitem = window.gURLBar.view.resultMenu.querySelector( + `menuitem[accesskey=${accesskey}]` + ); + return menuitem; + }, "Waiting for strings to load"); + } else if (command) { + menuitem = window.gURLBar.view.resultMenu.querySelector( + `menuitem[data-command=${command}]` + ); + } else { + throw new Error("accesskey or command must be specified"); + } + + return menuitem; + }, + + /** + * Opens the result menu of a specific result and presses an access key to + * activate a menu item. + * + * @param {object} win The window containing the urlbar + * @param {string} accesskey The access key to press once the menu is open + * @param {object} [options] The options object. + * @param {number} [options.resultIndex] The index of the result. Defaults + * to the current selected index. + * @param {boolean} [options.openByMouse] Whether to open the menu by mouse + * or keyboard. + */ + async openResultMenuAndPressAccesskey( + win, + accesskey, + { + resultIndex = win.gURLBar.view.selectedRowIndex, + openByMouse = false, + } = {} + ) { + let menuitem = await this.openResultMenuAndGetItem({ + accesskey, + resultIndex, + openByMouse, + window: win, + }); + if (!menuitem) { + throw new Error("Menu item not found for accesskey: " + accesskey); + } + + let promiseCommand = lazy.BrowserTestUtils.waitForEvent( + win.gURLBar.view.resultMenu, + "command" + ); + + if (AppConstants.platform == "macosx") { + // The native Mac menu doesn't support access keys. + this.info("calling doCommand() to activate menu item"); + menuitem.doCommand(); + win.gURLBar.view.resultMenu.hidePopup(true); + } else { + this.info(`pressing access key (${accesskey}) to activate menu item`); + this.EventUtils.synthesizeKey(accesskey, {}, win); + } + + this.info("waiting for command event"); + await promiseCommand; + this.info("got the command event"); + }, + + /** + * Opens the result menu of a specific result and clicks a menu item with a + * specified command name. + * + * @param {object} win + * The window containing the urlbar. + * @param {string|Array} commandOrArray + * If the command is in the top-level result menu, set this to the command + * name. If it's in a submenu, set this to an array where each element i is + * a selector that can be used to click the i'th menu item that opens a + * submenu, and the last element is the command name. + * @param {object} options + * The options object. + * @param {number} options.resultIndex + * The index of the result. Defaults to the current selected index. + * @param {boolean} options.openByMouse + * Whether to open the menu by mouse or keyboard. + */ + async openResultMenuAndClickItem( + win, + commandOrArray, + { + resultIndex = win.gURLBar.view.selectedRowIndex, + openByMouse = false, + } = {} + ) { + let submenuSelectors = Array.isArray(commandOrArray) + ? commandOrArray + : [commandOrArray]; + let command = submenuSelectors.pop(); + + let menuitem = await this.openResultMenuAndGetItem({ + resultIndex, + openByMouse, + command, + submenuSelectors, + window: win, + }); + if (!menuitem) { + throw new Error("Menu item not found for command: " + command); + } + + let promiseCommand = lazy.BrowserTestUtils.waitForEvent( + win.gURLBar.view.resultMenu, + "command" + ); + + if (AppConstants.platform == "macosx") { + // Synthesized clicks don't work in the native Mac menu. + this.info("calling doCommand() to activate menu item"); + menuitem.doCommand(); + win.gURLBar.view.resultMenu.hidePopup(true); + } else { + this.info("Clicking menu item with command: " + command); + this.EventUtils.synthesizeMouseAtCenter(menuitem, {}, win); + } + + this.info("Waiting for command event"); + await promiseCommand; + this.info("Got the command event"); + }, + + /** + * 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; + details.result = 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 : []; + details.isSponsored = result.payload.isSponsored; + 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; + } else if (details.type == UrlbarUtils.RESULT_TYPE.DYNAMIC) { + details.dynamicType = result.payload.dynamicType; + } + 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 row at a specific index. + * + * @param {object} win The window containing the urlbar. + * @param {number} index The index to look for. + * @returns {HTMLElement|XulElement} The selected row. + */ + getRowAt(win, index) { + return this.getResultsContainer(win).children.item(index); + }, + + /** + * 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 this.getRowAt(win, this.getSelectedRowIndex(win)); + }, + + /** + * 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; + }, + + getResultsContainer(win) { + return win.gURLBar.view.panel.querySelector(".urlbarView-results"); + }, + + /** + * 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 this.getResultsContainer(win).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 lazy.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; + } + this.info("Awaiting for the urlbar panel to open"); + await new Promise(resolve => { + win.gURLBar.controller.addQueryListener({ + onViewOpen() { + win.gURLBar.controller.removeQueryListener(this); + resolve(); + }, + }); + }); + this.info("Urlbar panel opened"); + }, + + /** + * 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; + } + this.info("Awaiting for the urlbar panel to close"); + await new Promise(resolve => { + win.gURLBar.controller.addQueryListener({ + onViewClose() { + win.gURLBar.controller.removeQueryListener(this); + resolve(); + }, + }); + }); + this.info("Urlbar panel closed"); + }, + + /** + * Open the input field context menu and run a task on it. + * + * @param {nsIWindow} win the current window + * @param {Function} task a task function to run, gets the contextmenu popup + * as argument. + */ + async withContextMenu(win, task) { + let textBox = win.gURLBar.querySelector("moz-input-box"); + let cxmenu = textBox.menupopup; + let openPromise = lazy.BrowserTestUtils.waitForEvent(cxmenu, "popupshown"); + this.EventUtils.synthesizeMouseAtCenter( + win.gURLBar.inputField, + { + type: "contextmenu", + button: 2, + }, + win + ); + await openPromise; + // On Mac sometimes the menuitems are not ready. + await new Promise(win.requestAnimationFrame); + try { + await task(cxmenu); + } finally { + // Close the context menu if the task didn't pick anything. + if (cxmenu.state == "open" || cxmenu.state == "showing") { + let closePromise = lazy.BrowserTestUtils.waitForEvent( + cxmenu, + "popuphidden" + ); + cxmenu.hidePopup(); + await closePromise; + } + } + }, + + /** + * @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. Can + * only be used if UrlbarTestUtils has been initialized with init(). + * + * @param {Window} window + * The browser window. + * @param {object} expectedSearchMode + * The expected search mode object. + */ + 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 ( + window.gURLBar.searchMode?.source && + window.gURLBar.searchMode.source !== UrlbarUtils.RESULT_SOURCE.SEARCH + ) { + this.Assert.equal( + window.gURLBar.getAttribute("searchmodesource"), + UrlbarUtils.getResultSourceName(window.gURLBar.searchMode.source), + "gURLBar has proper searchmodesource attribute" + ); + } else { + this.Assert.ok( + !window.gURLBar.hasAttribute("searchmodesource"), + "gURLBar does not have searchmodesource attribute" + ); + } + + if (!expectedSearchMode) { + // Check the input's placeholder. + const prefName = + "browser.urlbar.placeholderName" + + (lazy.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; + } + + let isGeneralPurposeEngine = false; + if (expectedSearchMode.engineName) { + let engine = Services.search.getEngineByName( + expectedSearchMode.engineName + ); + isGeneralPurposeEngine = engine.isGeneralPurposeEngine; + expectedSearchMode.isGeneralPurposeEngine = isGeneralPurposeEngine; + } + + // 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", "telemetryLabel"]; + for (let prop of ignoreProperties) { + if (prop in expectedSearchMode && !(prop in window.gURLBar.searchMode)) { + this.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: isGeneralPurposeEngine + ? "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 = + lazy.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. Can only be used if UrlbarTestUtils has been + * initialized with init(). + * + * @param {object} window + * The window to operate on. + * @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. + */ + async enterSearchMode(window, searchMode = null) { + this.info(`Enter Search Mode ${JSON.stringify(searchMode)}`); + + // 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 lazy.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 }; + let engine = Services.search.getEngineByName(searchMode.engineName); + if (engine.isGeneralPurposeEngine) { + 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. If neither `backspace` nor `clickClose` is given, we'll + * default to backspacing. Can only be used if UrlbarTestUtils has been + * initialized with init(). + * + * @param {object} window + * The window to operate on. + * @param {object} options + * Options object + * @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} [options.waitForSearch] + * Whether the test should wait for a search after exiting search mode. + * Defaults to true. + */ + 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. Its value is 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 lazy.UrlbarController( + Object.assign( + { + input: { + isPrivate: false, + onFirstResult() { + return false; + }, + getSearchSource() { + return "dummy-search-source"; + }, + 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 lazy.AddonTestUtils.promiseStartupManager(); + } catch (error) { + if (!error.message.includes("already started")) { + throw error; + } + } + }, + + /** + * Enrolls in a mock Nimbus feature. + * + * If you call UrlbarPrefs.updateFirefoxSuggestScenario() from an xpcshell + * test, you must call this first to intialize the Nimbus urlbar feature. + * + * @param {object} value + * Define any desired Nimbus variables in this object. + * @param {string} [feature] + * The feature to init. + * @param {string} [enrollmentType] + * The enrollment type, either "rollout" (default) or "config". + * @returns {Function} + * A cleanup function that will unenroll the feature, returns a promise. + */ + async initNimbusFeature( + value = {}, + feature = "urlbar", + enrollmentType = "rollout" + ) { + this.info("initNimbusFeature awaiting ExperimentManager.onStartup"); + await lazy.ExperimentManager.onStartup(); + + this.info("initNimbusFeature awaiting ExperimentAPI.ready"); + await lazy.ExperimentAPI.ready(); + + let method = + enrollmentType == "rollout" + ? "enrollWithRollout" + : "enrollWithFeatureConfig"; + this.info(`initNimbusFeature awaiting ExperimentFakes.${method}`); + let doCleanup = await lazy.ExperimentFakes[method]({ + featureId: lazy.NimbusFeatures[feature].featureId, + value: { enabled: true, ...value }, + }); + + this.info("initNimbusFeature done"); + + this.registerCleanupFunction?.(async () => { + // If `doCleanup()` has already been called (i.e., by the caller), it will + // throw an error here. + try { + await doCleanup(); + } catch (error) {} + }); + + return doCleanup; + }, + + /** + * Simulate that user clicks URLBar and inputs text into it. + * + * @param {object} win + * The browser window containing target gURLBar. + * @param {string} text + * The text to be input. + */ + async inputIntoURLBar(win, text) { + this.EventUtils.synthesizeMouseAtCenter(win.gURLBar.inputField, {}, win); + await lazy.BrowserTestUtils.waitForCondition( + () => win.document.activeElement === win.gURLBar.inputField + ); + this.EventUtils.sendString(text, win); + }, +}; + +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 = lazy.BrowserWindowTracker.getTopWindow()) { + let fieldname = this.getFormHistoryName(window); + return lazy.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 = lazy.BrowserWindowTracker.getTopWindow()) { + let fieldname = this.getFormHistoryName(window); + return lazy.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 = lazy.BrowserWindowTracker.getTopWindow()) { + let fieldname = this.getFormHistoryName(window); + return lazy.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 = lazy.BrowserWindowTracker.getTopWindow()) { + let fieldname = this.getFormHistoryName(window); + return lazy.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 lazy.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 = lazy.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 {object} options + * Constructor options + * @param {Array} options.results + * An array of UrlbarResult objects that will be the provider's results. + * @param {string} [options.name] + * The provider's name. Provider names should be unique. + * @param {UrlbarUtils.PROVIDER_TYPE} [options.type] + * The provider's type. + * @param {number} [options.priority] + * The provider's priority. Built-in providers have a priority of zero. + * @param {number} [options.addTimeout] + * If non-zero, each result will be added on this timeout. If zero, all + * results will be added immediately and synchronously. + * @param {Function} [options.onCancel] + * If given, a function that will be called when the provider's cancelQuery + * method is called. + * @param {Function} [options.onSelection] + * If given, a function that will be called when + * {@link UrlbarView.#selectElement} method is called. + * @param {Function} [options.onEngagement] + * If given, a function that will be called when engagement. + */ + constructor({ + results, + name = "TestProvider" + Services.uuid.generateUUID(), + type = UrlbarUtils.PROVIDER_TYPE.PROFILE, + priority = 0, + addTimeout = 0, + onCancel = null, + onSelection = null, + onEngagement = null, + } = {}) { + super(); + this._results = results; + this._name = name; + this._type = type; + this._priority = priority; + this._addTimeout = addTimeout; + this._onCancel = onCancel; + this._onSelection = onSelection; + this._onEngagement = onEngagement; + } + get name() { + return 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 => { + lazy.setTimeout(() => { + addCallback(this, result); + resolve(); + }, this._addTimeout); + }); + } + } + } + cancelQuery(context) { + if (this._onCancel) { + this._onCancel(); + } + } + + onSelection(result, element) { + if (this._onSelection) { + this._onSelection(result, element); + } + } + + onEngagement(isPrivate, state, queryContext, details) { + if (this._onEngagement) { + this._onEngagement(isPrivate, state, queryContext, details); + } + } +} + +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..d7674161dc --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/browser.ini @@ -0,0 +1,26 @@ +# 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.js] +support-files = + ../browser/slow-page.sjs + slow-page.html +https_first_disabled = true +[browser_searchTips_interaction.js] +https_first_disabled = true +[browser_selection.js] +[browser_updateAsk.js] +skip-if = os == 'win' && msix # Updater is disabled in MSIX builds +[browser_updateRefresh.js] +skip-if = os == 'win' && msix # Updater is disabled in MSIX builds +[browser_updateRestart.js] +skip-if = os == 'win' && msix # Updater is disabled in MSIX builds +[browser_updateWeb.js] +skip-if = os == 'win' && msix # Updater is disabled in MSIX builds 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..ebac90ad85 --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/browser_interventions.js @@ -0,0 +1,271 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarProviderInterventions: + "resource:///modules/UrlbarProviderInterventions.sys.mjs", +}); + +add_setup(async function () { + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); + 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 BrowserTestUtils.promiseAlertDialog( + "cancel", + "chrome://global/content/resetProfile.xhtml", + { isSubDialog: true } + ); + }, + }); +}); + +// 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 BrowserTestUtils.promiseAlertDialog( + "cancel", + "chrome://browser/content/sanitize.xhtml", + { + isSubDialog: true, + } + ); + }, + }); +}); + +// 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 + ); +}); + +// Test the result of UrlbarProviderInterventions.isActive() +// and whether or not the function calucates the score. +add_task(async function testIsActive() { + const testData = [ + { + description: "Test for search string that activates the intervention", + searchString: "firefox slow", + expectedActive: true, + expectedScoreCalculated: true, + }, + { + description: + "Test for search string that does not activate the intervention", + searchString: "example slow", + expectedActive: false, + expectedScoreCalculated: true, + }, + { + description: "Test for empty search string", + searchString: "", + expectedActive: false, + expectedScoreCalculated: false, + }, + { + description: "Test for an URL", + searchString: "https://firefox/slow", + expectedActive: false, + expectedScoreCalculated: false, + }, + { + description: "Test for a data URL", + searchString: "data:text/html,
firefox slow
", + expectedActive: false, + expectedScoreCalculated: false, + }, + { + description: "Test for string like URL", + searchString: "firefox://slow", + expectedActive: false, + expectedScoreCalculated: false, + }, + ]; + + for (const { + description, + searchString, + expectedActive, + expectedScoreCalculated, + } of testData) { + info(description); + + // Set null to currentTip to know whether or not UrlbarProviderInterventions + // calculated the score. + UrlbarProviderInterventions.currentTip = null; + + const isActive = UrlbarProviderInterventions.isActive({ searchString }); + Assert.equal(isActive, expectedActive, "Result of isAcitive is correct"); + const isScoreCalculated = UrlbarProviderInterventions.currentTip !== null; + Assert.equal( + isScoreCalculated, + expectedScoreCalculated, + "The score is calculated correctly" + ); + } +}); + +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"]; + + let cleanup = async () => { + let reloadPromise = + SearchTestUtils.promiseSearchNotification("engines-reloaded"); + Services.locale.requestedLocales = originalRequested; + Services.locale.availableLocales = originalAvailable; + await reloadPromise; + cleanup = null; + }; + registerCleanupFunction(() => cleanup?.()); + + 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()); + + await cleanup(); +}); + +// Tests the help command (using the clear intervention). It should open the +// help page and it should not trigger the primary intervention behavior. +add_task(async function pickHelp() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + // Do a search that triggers the clear tip. + let [result] = await awaitTip(SEARCH_STRINGS.CLEAR); + Assert.strictEqual( + result.payload.type, + UrlbarProviderInterventions.TIP_TYPE.CLEAR + ); + + // Click the help command and wait for the help page to load. + Assert.ok( + !!result.payload.helpUrl, + "The result's helpUrl should be defined and non-empty: " + + JSON.stringify(result.payload.helpUrl) + ); + let loadPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + result.payload.helpUrl + ); + await UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "h", { + openByMouse: true, + resultIndex: 1, + }); + info("Waiting for help URL to load in the current tab"); + await loadPromise; + + // Wait a bit and make sure the clear recent history dialog did not open. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(r => setTimeout(r, 2000)); + Assert.strictEqual(gDialogBox.isOpen, false, "No dialog should be open"); + + // Check telemetry. + 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..60a7676668 --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/browser_picks.js @@ -0,0 +1,223 @@ +/* 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_setup(async function () { + 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_help() { + 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_help() { + await doTest({ click: true, helpUrl: HELP_URL }); +}); + +// Clicks inside a tip but not on any button. +add_task(async function mouse_insideTipButNotOnButtons() { + let results = [makeTipResult({ buttonUrl: TIP_URL, helpUrl: HELP_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._buttons.get("0"), + "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._buttons.get("0"), + "The main button element should remain selected" + ); + + await UrlbarTestUtils.promisePopupClose(window); + UrlbarProvidersManager.unregisterProvider(provider); +}); + +/** + * Runs this test's main checks. + * + * @param {object} options + * Options for the test. + * @param {boolean} options.click + * Pass true to trigger a click, false to trigger an enter key. + * @param {string} [options.buttonUrl] + * Pass a URL if picking the main button should open a URL. Pass nothing if + * a URL shouldn't be opened or if you want to pick the help button instead of + * the main button. + * @param {string} [options.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: [makeTipResult({ buttonUrl, helpUrl })], + priority: 1, + }); + UrlbarProvidersManager.registerProvider(provider); + + let onEngagementPromise = new Promise( + resolve => (provider.onEngagement = resolve) + ); + + // 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._buttons.get("0"); + let target = helpUrl + ? row._buttons.get(UrlbarPrefs.get("resultMenu") ? "menu" : "help") + : mainButton; + + // If we're picking the tip with the keyboard, TAB to select the proper + // target. + if (!click) { + EventUtils.synthesizeKey("KEY_Tab", { repeat: helpUrl ? 2 : 1 }); + Assert.equal( + UrlbarTestUtils.getSelectedElement(window), + target, + `${target.className} should be selected.` + ); + } + + // Now pick the target and wait for provider.onEngagement to be called and + // the URL to load if necessary. + let loadPromise; + if (buttonUrl || helpUrl) { + loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + } + await UrlbarTestUtils.promisePopupClose(window, () => { + if (helpUrl && UrlbarPrefs.get("resultMenu")) { + UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "h", { + openByMouse: click, + resultIndex: 0, + }); + } else if (click) { + EventUtils.synthesizeMouseAtCenter(target, {}); + } else { + EventUtils.synthesizeKey("KEY_Enter"); + } + }); + await onEngagementPromise; + await loadPromise; + + // 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 && !(helpUrl && UrlbarPrefs.get("resultMenu")) + ? "click" + : "enter", + value: "typed", + }, + ], + { category: "urlbar" } + ); + + // Done. + UrlbarProvidersManager.unregisterProvider(provider); + if (tab) { + BrowserTestUtils.removeTab(tab); + } +} + +function makeTipResult({ buttonUrl, helpUrl }) { + return new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TIP, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + type: "test", + titleL10n: { id: "urlbar-search-tips-confirm" }, + buttons: [ + { + url: buttonUrl, + l10n: { id: "urlbar-search-tips-confirm" }, + }, + ], + helpUrl, + helpL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-tip-get-help" + : "urlbar-tip-help-icon", + }, + } + ); +} 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..ff592bc831 --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/browser_searchTips.js @@ -0,0 +1,657 @@ +/* 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"; + +ChromeUtils.defineESModuleGetters(this, { + AppMenuNotifications: "resource://gre/modules/AppMenuNotifications.sys.mjs", + ProfileAge: "resource://gre/modules/ProfileAge.sys.mjs", + PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs", + UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs", + UrlbarProviderSearchTips: + "resource:///modules/UrlbarProviderSearchTips.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(this, { + HttpServer: "resource://testing-common/httpd.js", +}); + +// 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", +]; + +// In order for the persist tip to appear, the scheme of the +// search engine has to be the same as the scheme of the SERP url. +// withDNSRedirect() loads an http: url while the searchform +// of the default engine uses https. To enable the search term +// to be shown, we use the Example engine because it doesn't require +// a redirect. +const SEARCH_SERP_URL = "https://example.com/?q=chocolate"; + +add_setup(async function () { + 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.PERSIST}`, + 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."); + + // Add a mock engine so we don't hit the network loading the SERP. + await SearchTestUtils.installSearchExtension(); + + 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 + ); +}); + +// The persist tip should show on default SERPs. +// This test also has an implied check that the SERP +// is receiving an originalURI. +// This is because the page the test is attempting to load +// will differ from the page that's actually loaded due to +// the DNS redirect. +add_task(async function persistTipOnDefault() { + await setDefaultEngine("Example"); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", true]], + }); + await checkTab( + window, + SEARCH_SERP_URL, + UrlbarProviderSearchTips.TIP_TYPE.PERSIST + ); + await SpecialPowers.popPrefEnv(); +}); + +// The persist tip should not show on non-default SERPs. +add_task(async function noPersistTipOnNonDefault() { + await setDefaultEngine("DuckDuckGo"); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", true]], + }); + await checkTab( + window, + SEARCH_SERP_URL, + UrlbarProviderSearchTips.TIP_TYPE.NONE + ); + await SpecialPowers.popPrefEnv(); +}); + +// The persist tip should only show up once a session. +add_task(async function persistTipOnceOnDefaultSerp() { + await setDefaultEngine("Example"); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", true]], + }); + await checkTab( + window, + SEARCH_SERP_URL, + UrlbarProviderSearchTips.TIP_TYPE.PERSIST + ); + await checkTab( + window, + SEARCH_SERP_URL, + UrlbarProviderSearchTips.TIP_TYPE.NONE + ); + await SpecialPowers.popPrefEnv(); +}); + +// The persist tip should not show in a window +// with a selected tab containing a non-SERP url. +add_task(async function noPersistTipInWindowWithNonSerpTab() { + await setDefaultEngine("Example"); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", true]], + }); + + // Create a new window for the SERP to be loaded into. + let newWindow = await BrowserTestUtils.openNewBrowserWindow(); + + // Focus on the original window. + window.focus(); + await waitForBrowserWindowActive(window); + + // Load the SERP in the new window to initiate a background load. + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + newWindow.gBrowser.selectedBrowser, + false, + SEARCH_SERP_URL + ); + BrowserTestUtils.loadURIString( + newWindow.gBrowser.selectedBrowser, + SEARCH_SERP_URL + ); + await browserLoadedPromise; + + // Wait longer than the persist tip delay to check that the search tip + // doesn't show on the non-SERP tab. + await new Promise(resolve => + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + setTimeout(resolve, UrlbarProviderSearchTips.SHOW_PERSIST_TIP_DELAY_MS * 2) + ); + Assert.ok(!window.gURLBar.view.isOpen); + + // Clean up. + await BrowserTestUtils.closeWindow(newWindow); + await SpecialPowers.popPrefEnv(); + resetSearchTipsProvider(); +}); + +// 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); + }); + await setDefaultEngine("Example"); + await checkTab( + window, + SEARCH_SERP_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 + ); +}); + +// Don't show the persist search tip when the browser loads +// a different page from the page the tip was supposed to show on. +add_task(async function noSearchTipWhileAnotherPageLoads() { + await setDefaultEngine("Example"); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", true]], + }); + + // Create a slow endpoint. + const SLOW_PAGE = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://www.example.com" + ) + "slow-page.sjs"; + + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: SEARCH_SERP_URL, + }); + + // Load a slow URI to cause an onStateChange event but + // not an onLocationChange event. + BrowserTestUtils.loadURIString(tab.linkedBrowser, SLOW_PAGE); + + // Wait roughly for the amount of time it would take for the + // persist search tip to show. + await new Promise(resolve => + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + setTimeout(resolve, UrlbarProviderSearchTips.SHOW_PERSIST_TIP_DELAY_MS * 2) + ); + + // Check the search tip didn't show while the page was loading. + Assert.equal( + UrlbarPrefs.get( + `tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.PERSIST}` + ), + 0, + "The shownCount pref should be 0." + ); + + Assert.equal(false, window.gURLBar.view.isOpen, "Urlbar should be closed."); + + // Clean up. + await SpecialPowers.popPrefEnv(); + resetSearchTipsProvider(); + BrowserTestUtils.removeTab(tab); +}); + +// Show the persist search tip when the browser is still loading +// resources from the page the tip is supposed to show on. +add_task(async function searchTipWhilePageLoads() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", true]], + }); + + // Create a search engine endpoint that will still + // be loading resources on the page load. + const SLOW_PAGE = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://www.example.com" + ) + "slow-page.html"; + + await SearchTestUtils.installSearchExtension({ + name: "Slow Engine", + search_url: SLOW_PAGE, + search_url_get_params: "search={searchTerms}", + }); + await setDefaultEngine("Slow Engine"); + + let engine = Services.search.getEngineByName("Slow Engine"); + let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl(engine, "chocolate"); + + // Load a slow SERP. + await checkTab( + window, + expectedSearchUrl, + UrlbarProviderSearchTips.TIP_TYPE.PERSIST + ); + + // Clean up. + await SpecialPowers.popPrefEnv(); + resetSearchTipsProvider(); +}); + +// Search tips modify the userTypedValue of a tab. The next time +// the pageproxystate is updated, the existence of the userTypedValue +// can change the pageproxystate. In the case of the Persist Search Tip, +// we don't want to change the pageproxystate while the Urlbar is non-focused, +// so check that when an event causes the pageproxystate to update +// (e.g. a SERP pushing state), the pageproxystate remains the same. +add_task(async function persistSearchTipAfterPushState() { + await setDefaultEngine("Example"); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", true]], + }); + + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: SEARCH_SERP_URL, + }); + + // Ensure the search tip is visible. + await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.PERSIST, false); + Assert.equal( + gURLBar.getAttribute("pageproxystate"), + "valid", + "Urlbar is should be in a valid pageproxystate." + ); + + // Mock the default SERP using the History API on an exising website. + // This is to trigger another call to setURI. + await SpecialPowers.spawn(tab.linkedBrowser, [SEARCH_SERP_URL], async url => { + content.history.pushState({}, "", url); + }); + + Assert.equal( + gURLBar.getAttribute("pageproxystate"), + "valid", + "Urlbar is should be in a valid pageproxystate." + ); + + // Clean up. + await SpecialPowers.popPrefEnv(); + resetSearchTipsProvider(); + BrowserTestUtils.removeTab(tab); +}); + +// Ensure a the Persist Search Tip is non-visible when a PopupNotification +// is already visible. +add_task(async function persistSearchTipBeforePopupShown() { + await setDefaultEngine("Example"); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", true]], + }); + + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: SEARCH_SERP_URL, + }); + + let promisePopupShown = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + PopupNotifications.show( + gBrowser.selectedBrowser, + "test-notification", + "This is a sample popup.", + "geo-notification-icon" + ); + await promisePopupShown; + + // Wait roughly for the amount of time it would take for the + // persist search tip to show. + await new Promise(resolve => + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + setTimeout(resolve, UrlbarProviderSearchTips.SHOW_PERSIST_TIP_DELAY_MS * 2) + ); + + // Check the search tip didn't show while the page was loading. + Assert.equal( + UrlbarPrefs.get( + `tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.PERSIST}` + ), + 0, + "The shownCount pref should be 0." + ); + Assert.equal(false, window.gURLBar.view.isOpen, "Urlbar should be closed."); + + // Clean up. + await SpecialPowers.popPrefEnv(); + resetSearchTipsProvider(); + BrowserTestUtils.removeTab(tab); +}); + +// The Persist Search Tip should be hidden when a PopupNotification appears. +add_task(async function persistSearchTipAfterPopupShown() { + await setDefaultEngine("Example"); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", true]], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: SEARCH_SERP_URL, + }); + + // Ensure the search tip is visible. + await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.PERSIST, false); + + // Show a popup after the search tip is shown. + let promisePopupShown = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + PopupNotifications.show( + gBrowser.selectedBrowser, + "test-notification", + "This is a sample popup.", + "geo-notification-icon" + ); + await promisePopupShown; + + // The search tip should not be visible. + Assert.equal(false, window.gURLBar.view.isOpen, "Urlbar should be closed."); + Assert.equal( + gURLBar.getAttribute("pageproxystate"), + "valid", + "Urlbar is should be in a valid pageproxystate." + ); + + // Clean up. + await SpecialPowers.popPrefEnv(); + resetSearchTipsProvider(); + BrowserTestUtils.removeTab(tab); +}); + +function waitForBrowserWindowActive(win) { + return new Promise(resolve => { + if (Services.focus.activeWindow == win) { + resolve(); + } else { + win.addEventListener( + "activate", + () => { + resolve(); + }, + { once: true } + ); + } + }); +} 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..76281dfaf2 --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/browser_searchTips_interaction.js @@ -0,0 +1,837 @@ +/* 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"; + +ChromeUtils.defineESModuleGetters(this, { + AppMenuNotifications: "resource://gre/modules/AppMenuNotifications.sys.mjs", + ProfileAge: "resource://gre/modules/ProfileAge.sys.mjs", + UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs", + UrlbarProviderSearchTips: + "resource:///modules/UrlbarProviderSearchTips.sys.mjs", + UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(this, { + HttpServer: "resource://testing-common/httpd.js", +}); + +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", +]; + +// In order for the persist tip to appear, the scheme of the +// search engine has to be the same as the scheme of the SERP url. +// withDNSRedirect() loads an http: url while the searchform +// of the default engine uses https. To enable the search term +// to be shown, we use the Example engine because it doesn't require +// a redirect. +const SEARCH_TERM = "chocolate"; +const SEARCH_SERP_URL = `https://example.com/?q=${SEARCH_TERM}`; + +add_setup(async function () { + 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.PERSIST}`, + 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."); + + // Add a mock engine so we don't hit the network loading the SERP. + await SearchTestUtils.installSearchExtension(); + + 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._buttons.get("0"); + 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.loadURIString(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._buttons.get("0"); + 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(); +}); + +// Picking the tip's button should cause the Urlbar to keep its current +// value and the tip to be not to be shown again in any session. +// Telemetry should be updated. +add_task(async function pickButton_persist() { + UrlbarProviderSearchTips.disableTipsForCurrentSession = false; + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.eventTelemetry.enabled", true], + ["browser.urlbar.showSearchTerms.featureGate", true], + ], + }); + Services.telemetry.clearEvents(); + + await setDefaultEngine("Example"); + + await BrowserTestUtils.withNewTab("about:blank", async () => { + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + SEARCH_SERP_URL + ); + BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, SEARCH_SERP_URL); + await browserLoadedPromise; + await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.PERSIST, false); + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + let button = result.element.row._buttons.get("0"); + + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeMouseAtCenter(button, {}); + }); + gURLBar.blur(); + + Assert.equal( + gURLBar.value, + SEARCH_TERM, + "The Urlbar should keep its existing value." + ); + }); + + // Check telemetry. + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "urlbar.tips", + `${UrlbarProviderSearchTips.TIP_TYPE.PERSIST}-picked`, + 1 + ); + TelemetryTestUtils.assertEvents( + [ + { + category: "urlbar", + method: "engagement", + object: "click", + value: "typed", + }, + ], + { category: "urlbar" } + ); + + Assert.equal( + UrlbarPrefs.get( + `tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.PERSIST}` + ), + MAX_SHOWN_COUNT, + "Persist 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(); + + await setDefaultEngine("Google"); + 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(); + + await setDefaultEngine("Google"); + 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.loadURIString(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(); +}); + +// Clicking in the input while the persist tip is showing should have the same +// effect as picking the tip. +add_task(async function clickInInput_persist() { + UrlbarProviderSearchTips.disableTipsForCurrentSession = false; + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.eventTelemetry.enabled", true], + ["browser.urlbar.showSearchTerms.featureGate", true], + ], + }); + Services.telemetry.clearEvents(); + + await setDefaultEngine("Example"); + await BrowserTestUtils.withNewTab("about:blank", async () => { + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + SEARCH_SERP_URL + ); + BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, SEARCH_SERP_URL); + await browserLoadedPromise; + await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.PERSIST, false); + + // Click in the input. + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.textbox.parentNode, {}); + }); + gURLBar.blur(); + Assert.equal( + gURLBar.value, + SEARCH_TERM, + "The Urlbar should keep its existing value." + ); + }); + + // Check telemetry. + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "urlbar.tips", + `${UrlbarProviderSearchTips.TIP_TYPE.PERSIST}-picked`, + 1 + ); + TelemetryTestUtils.assertEvents( + [ + { + category: "urlbar", + method: "engagement", + object: "click", + value: "typed", + }, + ], + { category: "urlbar" } + ); + + Assert.equal( + UrlbarPrefs.get( + `tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.PERSIST}` + ), + MAX_SHOWN_COUNT, + "Persist 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.loadURIString(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(); +}); + +// Pressing Ctrl+L (the open location command) while the persist tip is showing +// should have the same effect as picking the tip. +add_task(async function openLocation_persist() { + UrlbarProviderSearchTips.disableTipsForCurrentSession = false; + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.eventTelemetry.enabled", true], + ["browser.urlbar.showSearchTerms.featureGate", true], + ], + }); + Services.telemetry.clearEvents(); + + await setDefaultEngine("Example"); + await BrowserTestUtils.withNewTab("about:blank", async () => { + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + SEARCH_SERP_URL + ); + BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, SEARCH_SERP_URL); + await browserLoadedPromise; + await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.PERSIST, false); + + // Trigger the open location command. + await UrlbarTestUtils.promisePopupClose(window, () => { + document.getElementById("Browser:OpenLocation").doCommand(); + }); + gURLBar.blur(); + Assert.equal( + gURLBar.value, + SEARCH_TERM, + "The Urlbar should keep its existing value." + ); + }); + + // Check telemetry. + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "urlbar.tips", + `${UrlbarProviderSearchTips.TIP_TYPE.PERSIST}-picked`, + 1 + ); + TelemetryTestUtils.assertEvents( + [ + { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + }, + ], + { category: "urlbar" } + ); + + Assert.equal( + UrlbarPrefs.get( + `tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.PERSIST}` + ), + MAX_SHOWN_COUNT, + "Persist 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; + await setDefaultEngine("Google"); + 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._buttons.get("0"); + 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("urlbar-test", { + label: "Test", + priority: box.PRIORITY_INFO_HIGH, + }); + // 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.loadURIString(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.loadURIString(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() { + await setDefaultEngine("Example"); + await doPasteAndGoTest( + "pasteAndGo_nonURL", + "https://example.com/?q=pasteAndGo_nonURL" + ); + await setDefaultEngine("Google"); +}); + +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 + ); + cxmenu.activateItem(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..89206b123e --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/browser_selection.js @@ -0,0 +1,269 @@ +/* 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" } + ), + makeTipResult({ buttonUrl: TIP_URL, helpUrl: HELP_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-button-0" + ), + "The selected element should be the tip button." + ); + Assert.equal( + UrlbarTestUtils.getSelectedElementIndex(window), + UrlbarPrefs.get("resultMenu") ? 2 : 1, + "Selected element index" + ); + + EventUtils.synthesizeKey("KEY_Tab"); + Assert.ok( + UrlbarTestUtils.getSelectedElement(window).classList.contains( + UrlbarPrefs.get("resultMenu") + ? "urlbarView-button-menu" + : "urlbarView-button-help" + ), + UrlbarPrefs.get("resultMenu") + ? "The selected element should be the tip menu button." + : "The selected element should be the tip help button." + ); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 1, + UrlbarPrefs.get("resultMenu") + ? "getSelectedRowIndex should return 1 even though the menu button is selected." + : "getSelectedRowIndex should return 1 even though the help button is selected." + ); + Assert.equal( + UrlbarTestUtils.getSelectedElementIndex(window), + UrlbarPrefs.get("resultMenu") ? 3 : 2, + "Selected element index" + ); + + // 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_Tab", { shiftKey: true }); + Assert.ok( + UrlbarTestUtils.getSelectedElement(window).classList.contains( + UrlbarPrefs.get("resultMenu") + ? "urlbarView-button-menu" + : "urlbarView-button-help" + ), + UrlbarPrefs.get("resultMenu") + ? "The selected element should be the tip menu button." + : "The selected element should be the tip help button." + ); + + gURLBar.view.close(); + UrlbarProvidersManager.unregisterProvider(provider); +}); + +add_task(async function tipIsOnlyResult() { + let results = [makeTipResult({ buttonUrl: TIP_URL, helpUrl: HELP_URL })]; + + 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_Tab"); + Assert.ok( + UrlbarTestUtils.getSelectedElement(window).classList.contains( + "urlbarView-button-0" + ), + "The selected element should be the tip button." + ); + Assert.equal( + UrlbarTestUtils.getSelectedElementIndex(window), + 0, + "The first element should be selected." + ); + + EventUtils.synthesizeKey("KEY_Tab"); + Assert.ok( + UrlbarTestUtils.getSelectedElement(window).classList.contains( + UrlbarPrefs.get("resultMenu") + ? "urlbarView-button-menu" + : "urlbarView-button-help" + ), + UrlbarPrefs.get("resultMenu") + ? "The selected element should be the tip menu button." + : "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_Tab", { shiftKey: true }); + Assert.ok( + UrlbarTestUtils.getSelectedElement(window).classList.contains( + UrlbarPrefs.get("resultMenu") + ? "urlbarView-button-menu" + : "urlbarView-button-help" + ), + UrlbarPrefs.get("resultMenu") + ? "The selected element should be the tip menu button." + : "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" } + ), + makeTipResult({ 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-button-0" + ), + "The selected element should be the tip button." + ); + Assert.equal( + UrlbarTestUtils.getSelectedElementIndex(window), + UrlbarPrefs.get("resultMenu") ? 2 : 1, + "Selected element index" + ); + + 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-button-0" + ), + "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..3f2014d8c0 --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/browser_updateAsk.js @@ -0,0 +1,74 @@ +/* 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() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.quickactions", false]], + }); + + // 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..5e94298996 --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/browser_updateRefresh.js @@ -0,0 +1,54 @@ +/* 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() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.quickactions", false]], + }); + + 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", + { isSubDialog: true } + ); + }, + }); +}); 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..75e92910f0 --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/browser_updateRestart.js @@ -0,0 +1,48 @@ +/* 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], + ["browser.urlbar.suggest.quickactions", false], + ], + }); + + // 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..daca12fea4 --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/browser_updateWeb.js @@ -0,0 +1,52 @@ +/* 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() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.quickactions", false]], + }); + + // 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..d6797f5338 --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/head.js @@ -0,0 +1,771 @@ +/* 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"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/mozapps/update/tests/browser/head.js", + this +); + +ChromeUtils.defineESModuleGetters(this, { + ResetProfile: "resource://gre/modules/ResetProfile.sys.mjs", + TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs", + UrlbarProviderInterventions: + "resource:///modules/UrlbarProviderInterventions.sys.mjs", + UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs", + UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(this, { + HttpServer: "resource://testing-common/httpd.js", +}); + +XPCOMUtils.defineLazyGetter(this, "UrlbarTestUtils", () => { + const { UrlbarTestUtils: module } = ChromeUtils.importESModule( + "resource://testing-common/UrlbarTestUtils.sys.mjs" + ); + module.init(this); + return module; +}); + +XPCOMUtils.defineLazyGetter(this, "SearchTestUtils", () => { + const { SearchTestUtils: module } = ChromeUtils.importESModule( + "resource://testing-common/SearchTestUtils.sys.mjs" + ); + 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", +}; + +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 = Services.env.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) { + Services.env.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 ( + panelId == "downloading" && + gAUS.currentState == Ci.nsIApplicationUpdateService.STATE_IDLE + ) { + // Now that `AUS.downloadUpdate` is async, we start showing the + // downloading panel while `AUS.downloadUpdate` is still resolving. + // But the below checks assume that this resolution has already + // happened. So we need to wait for things to actually resolve. + await gAUS.stateTransition; + } + + 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 {object} options + * Options for the test + * @param {string} options.searchString + * The search string. + * @param {string} options.tip + * The expected tip type. + * @param {string | RegExp} options.title + * The expected tip title. + * @param {string | RegExp} options.button + * The expected button title. + * @param {Function} options.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._buttons.get("0").textContent; + if (typeof button == "string") { + Assert.equal(actualButton, button, "Button string"); + } else { + // regexp + Assert.ok(button.test(actualButton), "Button regexp"); + } + + if (UrlbarPrefs.get("resultMenu")) { + Assert.ok(element._buttons.has("menu"), "Tip has a menu button"); + } else { + Assert.ok(element._buttons.has("help"), "Tip has a help button"); + } + + // 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._buttons.get("0"); + 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 {object} options + * Options for the test + * @param {string} options.searchString + * The search string. + * @param {TIPS} options.tip + * The expected tip type. + * @param {string} options.title + * The expected tip title. + * @param {string} options.button + * The expected button title. + * @param {Function} options.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._buttons.get("0").textContent; + if (typeof button == "string") { + Assert.equal(actualButton, button, "Button string"); + } else { + // regexp + Assert.ok(button.test(actualButton), "Button regexp"); + } + + if (UrlbarPrefs.get("resultMenu")) { + let menuButton = element._buttons.get("menu"); + Assert.ok(menuButton, "Menu button exists"); + Assert.ok( + BrowserTestUtils.is_visible(menuButton), + "Menu button is visible" + ); + } else { + let helpButton = element._buttons.get("help"); + Assert.ok(helpButton, "Help button exists"); + Assert.ok( + BrowserTestUtils.is_visible(helpButton), + "Help button is visible" + ); + } + + 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 + * The host window. + */ +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); + } +} + +/** + * 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; + case UrlbarProviderSearchTips.TIP_TYPE.PERSIST: + heuristic = false; + title = + "Searching just got simpler." + + " Try making your search more specific here in the address bar." + + " To show the URL instead, visit Search, in settings."; + break; + } + Assert.equal(result.heuristic, heuristic); + Assert.equal(result.displayed.title, title); + Assert.equal( + result.element.row._buttons.get("0").textContent, + expectedTip == UrlbarProviderSearchTips.TIP_TYPE.PERSIST + ? `Got it` + : `Okay, Got It` + ); + Assert.ok(!result.element.row._buttons.has("help")); + + 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); + } +} + +function makeTipResult({ buttonUrl, helpUrl = undefined }) { + return new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TIP, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + helpUrl, + type: "test", + titleL10n: { id: "urlbar-search-tips-confirm" }, + buttons: [ + { + url: buttonUrl, + l10n: { id: "urlbar-search-tips-confirm" }, + }, + ], + } + ); +} + +/** + * 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(); +} + +function resetSearchTipsProvider() { + Services.prefs.clearUserPref( + `browser.urlbar.tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.ONBOARD}` + ); + Services.prefs.clearUserPref( + `browser.urlbar.tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.PERSIST}` + ); + 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, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); +} diff --git a/browser/components/urlbar/tests/browser-tips/slow-page.html b/browser/components/urlbar/tests/browser-tips/slow-page.html new file mode 100644 index 0000000000..f58a44dc62 --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/slow-page.html @@ -0,0 +1,7 @@ + + + +

Search Engine Results Page that is loading a slow resource.

+ + + diff --git a/browser/components/urlbar/tests/browser-updateResults/browser.ini b/browser/components/urlbar/tests/browser-updateResults/browser.ini new file mode 100644 index 0000000000..f20fed7e13 --- /dev/null +++ b/browser/components/urlbar/tests/browser-updateResults/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_appendSpanCount.js] +[browser_suggestedIndex_10_search_10_url.js] +[browser_suggestedIndex_10_search_5_url.js] +[browser_suggestedIndex_10_url_10_search.js] +[browser_suggestedIndex_10_url_5_search.js] +[browser_suggestedIndex_5_search_10_url.js] +[browser_suggestedIndex_5_search_5_url.js] +[browser_suggestedIndex_5_url_10_search.js] +[browser_suggestedIndex_5_url_5_search.js] diff --git a/browser/components/urlbar/tests/browser-updateResults/browser_appendSpanCount.js b/browser/components/urlbar/tests/browser-updateResults/browser_appendSpanCount.js new file mode 100644 index 0000000000..bcf9609597 --- /dev/null +++ b/browser/components/urlbar/tests/browser-updateResults/browser_appendSpanCount.js @@ -0,0 +1,183 @@ +/* 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 when the view updates itself and appends new rows, +// the new rows start out hidden when they exceed the current visible result +// span count. It includes a tip result so that it tests a row with > 1 result +// span. + +"use strict"; + +add_task(async function viewUpdateAppendHidden() { + // We'll use this test provider to test specific results. We assume that + // history and bookmarks have been cleared (by init() above). + let provider = new DelayingTestProvider(); + UrlbarProvidersManager.registerProvider(provider); + registerCleanupFunction(() => { + UrlbarProvidersManager.unregisterProvider(provider); + }); + + // We do two searches below without closing the panel. Use "firefox cach" as + // the first query and "firefox cache" as the second so that (1) an + // intervention tip is triggered both times but also so that (2) the queries + // are different each time. + let baseQuery = "firefox cache"; + let queries = [baseQuery.substring(0, baseQuery.length - 1), baseQuery]; + let maxResults = UrlbarPrefs.get("maxRichResults"); + + let queryStrings = []; + for (let i = 0; i < maxResults; i++) { + queryStrings.push(`${baseQuery} ${i}`); + } + + // First search: Trigger the intervention tip and a view full of search + // suggestions. + provider._results = queryStrings.map( + suggestion => + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.SEARCH, + UrlbarUtils.RESULT_SOURCE.SEARCH, + { + query: queries[0], + suggestion, + lowerCaseSuggestion: suggestion.toLocaleLowerCase(), + engine: Services.search.defaultEngine.name, + } + ) + ); + provider.finishQueryPromise = Promise.resolve(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: queries[0], + }); + + // Sanity check the tip result and row count. + let tipResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal( + tipResult.type, + UrlbarUtils.RESULT_TYPE.TIP, + "Result at index 1 is a tip" + ); + let tipResultSpan = UrlbarUtils.getSpanForResult( + tipResult.element.row.result + ); + Assert.greater(tipResultSpan, 1, "Sanity check: Tip has large result span"); + let expectedRowCount = maxResults - tipResultSpan + 1; + Assert.equal( + UrlbarTestUtils.getResultCount(window), + expectedRowCount, + "Sanity check: Initial row count takes tip result span into account" + ); + + // Second search: Change the provider's results so that it has enough history + // to fill up the view. Search suggestion rows cannot be updated to history + // results, so the view will append the history results as new rows. + provider._results = queryStrings.map(title => { + let url = "http://example.com/" + title; + return new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { + title, + url, + displayUrl: "http://example.com/" + title, + } + ); + }); + + // Don't allow the search to finish until we check the updated rows. We'll + // accomplish that by adding a mutation observer on the rows and delaying + // resolving the provider's finishQueryPromise. When all new rows have been + // added, we expect the new row count to be: + // + // expectedRowCount // the original row count + // + (expectedRowCount - 2) // the newly added history row count (hidden) + // -------------------------- + // (2 * expectedRowCount) - 2 + // + // The `- 2` subtracts the heuristic and tip result. + let newExpectedRowCount = 2 * expectedRowCount - 2; + let mutationPromise = new Promise(resolve => { + let observer = new MutationObserver(mutations => { + let childCount = UrlbarTestUtils.getResultCount(window); + info(`Rows mutation observer called, childCount now ${childCount}`); + if (newExpectedRowCount <= childCount) { + observer.disconnect(); + resolve(); + } + }); + observer.observe(UrlbarTestUtils.getResultsContainer(window), { + childList: true, + }); + }); + + // Now do the second search but don't wait for it to finish. + let resolveQuery; + provider.finishQueryPromise = new Promise( + resolve => (resolveQuery = resolve) + ); + let queryPromise = UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: queries[1], + }); + + // Wait for the history rows to be added. + await mutationPromise; + + // Check the rows. We can't use UrlbarTestUtils.getDetailsOfResultAt() here + // because it waits for the query to finish. + Assert.equal( + UrlbarTestUtils.getResultCount(window), + newExpectedRowCount, + "New expected row count" + ); + // stale search rows + let rows = UrlbarTestUtils.getResultsContainer(window).children; + for (let i = 2; i < expectedRowCount; i++) { + let row = rows[i]; + Assert.equal( + row.result.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + `Result at index ${i} is a search result` + ); + Assert.ok( + BrowserTestUtils.is_visible(row), + `Search result at index ${i} is visible` + ); + Assert.equal( + row.getAttribute("stale"), + "true", + `Search result at index ${i} is stale` + ); + } + // new hidden history rows + for (let i = expectedRowCount; i < newExpectedRowCount; i++) { + let row = rows[i]; + Assert.equal( + row.result.type, + UrlbarUtils.RESULT_TYPE.URL, + `Result at index ${i} is a URL result` + ); + Assert.ok( + !BrowserTestUtils.is_visible(row), + `URL result at index ${i} is hidden` + ); + Assert.ok( + !row.hasAttribute("stale"), + `URL result at index ${i} is not stale` + ); + } + + // Finish the query, and we're done. + resolveQuery(); + await queryPromise; + + await UrlbarTestUtils.promisePopupClose(window); + gURLBar.handleRevert(); + + // We unregister the provider above in a cleanup function so we don't + // accidentally interfere with later tests, but do it here too in case we add + // more tasks to this test. It's harmless to call more than once. + UrlbarProvidersManager.unregisterProvider(provider); +}); diff --git a/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_10_search_10_url.js b/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_10_search_10_url.js new file mode 100644 index 0000000000..755c453850 --- /dev/null +++ b/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_10_search_10_url.js @@ -0,0 +1,1102 @@ +/* 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 checks row visibility during view updates when rows with suggested +// indexes are added and removed. Each task performs two searches: Search 1 +// returns 10 results with search suggestions, and search 2 returns 10 results +// with URL results. + +"use strict"; + +// Search 1: +// 10 results, no suggestedIndex +// Search 2: +// 10 results including suggestedIndex = 1 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 10 results, no suggestedIndex +// Search 2: +// 10 results including suggestedIndex = 2 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 2, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 2, + hidden: true, + }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 10 results, no suggestedIndex +// Search 2: +// 10 results including suggestedIndex = 9 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results, no suggestedIndex +// Search 2: +// 10 results including suggestedIndex = -1 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results, no suggestedIndex +// Search 2: +// 10 results including suggestedIndex = -2 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + hidden: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 10 results, no suggestedIndex +// Search 2: +// 10 results including suggestedIndex = -9 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -9, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -9, + hidden: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = 1 +// Search 2: +// 10 results including suggestedIndex = 1 +// Expected visible rows during update: +// 10 original rows with no changes (because the original search results can't +// be replaced with URL results) +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = 1 +// Search 2: +// 10 results including suggestedIndex = 2 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 2, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + stale: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 2, + hidden: true, + }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = 1 +// Search 2: +// 10 results including suggestedIndex = 9 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + stale: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = 1 +// Search 2: +// 10 results including suggestedIndex = -1 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + stale: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = 1 +// Search 2: +// 10 results including suggestedIndex = -2 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + stale: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + hidden: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = 1 +// Search 2: +// 10 results including suggestedIndex = -9 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -9, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + stale: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -9, + hidden: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = 9 +// Search 2: +// 10 results including suggestedIndex = 1 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 9, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = 9 +// Search 2: +// 10 results including suggestedIndex = 9 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 9, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + stale: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = 9 +// Search 2: +// 10 results including suggestedIndex = -1 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 9, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + stale: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = 9 +// Search 2: +// 10 results including suggestedIndex = -9 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 9, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -9, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -9, + hidden: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = -1 +// Search 2: +// 10 results including suggestedIndex = 1 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -1, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = -1 +// Search 2: +// 10 results including suggestedIndex = 9 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -1, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + stale: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = -1 +// Search 2: +// 10 results including suggestedIndex = -1 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -1, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + stale: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = -1 +// Search 2: +// 10 results including suggestedIndex = -9 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -1, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -9, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -9, + hidden: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = -9 +// Search 2: +// 10 results including suggestedIndex = 1 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -9, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -9, + stale: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = -9 +// Search 2: +// 10 results including suggestedIndex = 9 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -9, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -9, + stale: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = -9 +// Search 2: +// 10 results including suggestedIndex = -1 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -9, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -9, + stale: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = -9 +// Search 2: +// 10 results including suggestedIndex = -9 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -9, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -9, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -9, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 10 results, no suggestedIndex +// Search 2: +// 10 results including suggestedIndex = 1 and suggestedIndex = -1 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndexes: [1, -1], + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = 1 +// Search 2: +// 10 results including suggestedIndex = 1 and suggestedIndex = -1 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndexes: [1, -1], + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = -1 +// Search 2: +// 10 results including suggestedIndex = 1 and suggestedIndex = -1 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -1, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndexes: [1, -1], + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results, no suggestedIndex +// Search 2: +// 9 results including suggestedIndex = 1 with resultSpan = 2 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + resultSpan: 2, + viewCount: 9, + }, + duringUpdate: [ + { count: 1 }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + resultSpan: 2, + hidden: true, + }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 10 results, no suggestedIndex +// Search 2: +// 9 results including: +// suggestedIndex = 1 with resultSpan = 2 +// suggestedIndex = -1 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndexes: [[1, 2], -1], + viewCount: 9, + }, + duringUpdate: [ + { count: 1 }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + resultSpan: 2, + hidden: true, + }, + { count: 6, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 9 results including suggestedIndex = 1 with resultSpan = 2 +// Search 2: +// 9 results including: +// suggestedIndex = 1 with resultSpan = 2 +// suggestedIndex = -1 +// Expected visible rows during update: +// 9 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndexes: [[1, 2]], + viewCount: 9, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndexes: [[1, 2], -1], + viewCount: 9, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + resultSpan: 2, + }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 6, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); diff --git a/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_10_search_5_url.js b/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_10_search_5_url.js new file mode 100644 index 0000000000..bd707dd422 --- /dev/null +++ b/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_10_search_5_url.js @@ -0,0 +1,661 @@ +/* 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 checks row visibility during view updates when rows with suggested +// indexes are added and removed. Each task performs two searches: Search 1 +// returns 10 results with search suggestions, and search 2 returns 5 results +// with URL results. + +"use strict"; + +// Search 1: +// 10 results, no suggestedIndex +// Search 2: +// 5 results including suggestedIndex = 1 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + viewCount: 10, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 10 results, no suggestedIndex +// Search 2: +// 5 results including suggestedIndex = 2 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + viewCount: 10, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 2, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 2, + hidden: true, + }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 10 results, no suggestedIndex +// Search 2: +// 5 results including suggestedIndex = 9 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + viewCount: 10, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results, no suggestedIndex +// Search 2: +// 5 results including suggestedIndex = -1 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + viewCount: 10, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results, no suggestedIndex +// Search 2: +// 5 results including suggestedIndex = -2 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + viewCount: 10, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + hidden: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = 1 +// Search 2: +// 5 results including suggestedIndex = 1 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 10, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = 1 +// Search 2: +// 5 results including suggestedIndex = 9 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 10, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + stale: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = 1 +// Search 2: +// 5 results including suggestedIndex = -1 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 10, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + stale: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = 1 +// Search 2: +// 5 results including suggestedIndex = -3 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 10, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -3, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + stale: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -3, + hidden: true, + }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = 9 +// Search 2: +// 5 results including suggestedIndex = 1 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 9, + viewCount: 10, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = 9 +// Search 2: +// 5 results including suggestedIndex = 9 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 9, + viewCount: 10, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + stale: true, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = -1 +// Search 2: +// 5 results including suggestedIndex = 1 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -1, + viewCount: 10, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = -1 +// Search 2: +// 5 results including suggestedIndex = 9 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -1, + viewCount: 10, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + stale: true, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = -1 +// Search 2: +// 5 results including suggestedIndex = -1 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -1, + viewCount: 10, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + stale: true, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results, no suggestedIndex +// Search 2: +// 5 results including suggestedIndex = 1 and suggestedIndex = -1 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + viewCount: 10, + }, + search2: { + otherCount: 2, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndexes: [1, -1], + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = 1 +// Search 2: +// 5 results including suggestedIndex = 1 and suggestedIndex = -1 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 10, + }, + search2: { + otherCount: 2, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndexes: [1, -1], + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = -1 +// Search 2: +// 5 results including suggestedIndex = 1 and suggestedIndex = -1 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -1, + viewCount: 10, + }, + search2: { + otherCount: 2, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndexes: [1, -1], + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 9 results including suggestedIndex = 1 with resultSpan = 2 +// Search 2: +// 5 results including: +// suggestedIndex = 1 with resultSpan = 2 +// suggestedIndex = -1 +// Expected visible rows during update: +// 9 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndexes: [[1, 2]], + viewCount: 9, + }, + search2: { + otherCount: 2, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndexes: [[1, 2], -1], + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + resultSpan: 2, + }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); diff --git a/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_10_url_10_search.js b/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_10_url_10_search.js new file mode 100644 index 0000000000..57f172adba --- /dev/null +++ b/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_10_url_10_search.js @@ -0,0 +1,1185 @@ +/* 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 checks row visibility during view updates when rows with suggested +// indexes are added and removed. Each task performs two searches: Search 1 +// returns 10 results where the first result is a search suggestion and the +// remainder are URL results, and search 2 returns 10 results with search +// suggestions. This tests the view-update logic that allows search suggestions +// to replace other results once an existing suggestion row is encountered. + +"use strict"; + +// Search 1: +// 10 results, no suggestedIndex +// Search 2: +// 10 results including suggestedIndex = 1 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 10 results, no suggestedIndex +// Search 2: +// 10 results including suggestedIndex = 2 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 2, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 2, + hidden: true, + }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 10 results, no suggestedIndex +// Search 2: +// 10 results including suggestedIndex = 9 +// Expected visible rows during update: +// Indexes 2-8 replaced with search suggestions +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 9, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results, no suggestedIndex +// Search 2: +// 10 results including suggestedIndex = -1 +// Expected visible rows during update: +// Indexes 2-8 replaced with search suggestions +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -1, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results, no suggestedIndex +// Search 2: +// 10 results including suggestedIndex = -2 +// Expected visible rows during update: +// Indexes 2-7 replaced with search suggestions +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -2, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + hidden: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 10 results, no suggestedIndex +// Search 2: +// 10 results including suggestedIndex = -9 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -9, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -9, + hidden: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = 1 +// Search 2: +// 10 results including suggestedIndex = 1 +// Expected visible rows during update: +// All search-2 rows +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 1, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { + count: 7, + type: UrlbarPrefs.get("resultMenu") + ? UrlbarUtils.RESULT_TYPE.URL + : UrlbarUtils.RESULT_TYPE.SEARCH, + }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = 1 +// Search 2: +// 10 results including suggestedIndex = 2 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 1, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 2, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + stale: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 2, + hidden: true, + }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = 1 +// Search 2: +// 10 results including suggestedIndex = 9 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 1, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 9, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + stale: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = 1 +// Search 2: +// 10 results including suggestedIndex = -1 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 1, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -1, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + stale: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = 1 +// Search 2: +// 10 results including suggestedIndex = -2 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 1, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -2, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + stale: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + hidden: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = 1 +// Search 2: +// 10 results including suggestedIndex = -9 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 1, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -9, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + stale: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -9, + hidden: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = 9 +// Search 2: +// 10 results including suggestedIndex = 1 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 9, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = 9 +// Search 2: +// 10 results including suggestedIndex = 9 +// Expected visible rows during update: +// All search-2 rows +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 9, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 9, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { + count: 7, + type: UrlbarPrefs.get("resultMenu") + ? UrlbarUtils.RESULT_TYPE.URL + : UrlbarUtils.RESULT_TYPE.SEARCH, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = 9 +// Search 2: +// 10 results including suggestedIndex = -1 +// Expected visible rows during update: +// Indexes 2-8 replaced with search suggestions +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 9, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -1, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = 9 +// Search 2: +// 10 results including suggestedIndex = -9 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 9, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -9, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -9, + hidden: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = -1 +// Search 2: +// 10 results including suggestedIndex = 1 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: -1, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = -1 +// Search 2: +// 10 results including suggestedIndex = 9 +// Expected visible rows during update: +// Indexes 2-8 replaced with search suggestions +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: -1, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 9, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = -1 +// Search 2: +// 10 results including suggestedIndex = -1 +// Expected visible rows during update: +// All search-2 rows +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: -1, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -1, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { + count: 7, + type: UrlbarPrefs.get("resultMenu") + ? UrlbarUtils.RESULT_TYPE.URL + : UrlbarUtils.RESULT_TYPE.SEARCH, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = -1 +// Search 2: +// 10 results including suggestedIndex = -9 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: -1, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -9, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -9, + hidden: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = -9 +// Search 2: +// 10 results including suggestedIndex = 1 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: -9, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -9, + stale: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = -9 +// Search 2: +// 10 results including suggestedIndex = 9 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: -9, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 9, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -9, + stale: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = -9 +// Search 2: +// 10 results including suggestedIndex = -1 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: -9, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -1, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -9, + stale: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = -9 +// Search 2: +// 10 results including suggestedIndex = -9 +// Expected visible rows during update: +// All search-2 rows +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: -9, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -9, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -9, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { + count: 7, + type: UrlbarPrefs.get("resultMenu") + ? UrlbarUtils.RESULT_TYPE.URL + : UrlbarUtils.RESULT_TYPE.SEARCH, + }, + ], +}); + +// Search 1: +// 10 results, no suggestedIndex +// Search 2: +// 10 results including suggestedIndex = 1 and suggestedIndex = -1 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndexes: [1, -1], + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = 1 +// Search 2: +// 10 results including suggestedIndex = 1 and suggestedIndex = -1 +// Expected visible rows during update: +// Indexes 2-8 replaced with search suggestions +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 1, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndexes: [1, -1], + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = -1 +// Search 2: +// 10 results including suggestedIndex = 1 and suggestedIndex = -1 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: -1, + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndexes: [1, -1], + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results, no suggestedIndex +// Search 2: +// 9 results including suggestedIndex = 1 with resultSpan = 2 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + resultSpan: 2, + viewCount: 9, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + resultSpan: 2, + hidden: true, + }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 10 results, no suggestedIndex +// Search 2: +// 9 results including: +// suggestedIndex = 1 with resultSpan = 2 +// suggestedIndex = -1 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + viewCount: 10, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndexes: [[1, 2], -1], + viewCount: 9, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + resultSpan: 2, + hidden: true, + }, + { count: 6, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 9 results including suggestedIndex = 1 with resultSpan = 2 +// Search 2: +// 9 results including: +// suggestedIndex = 1 with resultSpan = 2 +// suggestedIndex = -1 +// Expected visible rows during update: +// Indexes 2-8 replaced with search suggestions +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndexes: [[1, 2]], + viewCount: 9, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndexes: [[1, 2], -1], + viewCount: 9, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + resultSpan: 2, + }, + { count: 6, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); diff --git a/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_10_url_5_search.js b/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_10_url_5_search.js new file mode 100644 index 0000000000..03d6f158f4 --- /dev/null +++ b/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_10_url_5_search.js @@ -0,0 +1,707 @@ +/* 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 checks row visibility during view updates when rows with suggested +// indexes are added and removed. Each task performs two searches: Search 1 +// returns 10 results where the first result is a search suggestion and the +// remainder are URL results, and search 2 returns 5 results with search +// suggestions. This tests the view-update logic that allows search suggestions +// to replace other results once an existing suggestion row is encountered. + +"use strict"; + +// Search 1: +// 10 results, no suggestedIndex +// Search 2: +// 5 results including suggestedIndex = 1 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + viewCount: 10, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 10 results, no suggestedIndex +// Search 2: +// 5 results including suggestedIndex = 2 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + viewCount: 10, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 2, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 2, + hidden: true, + }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 10 results, no suggestedIndex +// Search 2: +// 5 results including suggestedIndex = 9 +// Expected visible rows during update: +// Indexes 2 and 3 replaced with search suggestions, no other changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + viewCount: 10, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 9, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 6, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results, no suggestedIndex +// Search 2: +// 5 results including suggestedIndex = -1 +// Expected visible rows during update: +// Indexes 2 and 3 replaced with search suggestions, no other changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + viewCount: 10, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -1, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 6, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results, no suggestedIndex +// Search 2: +// 5 results including suggestedIndex = -2 +// Expected visible rows during update: +// Indexes 1 and 2 replaced with search suggestions, no other changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + viewCount: 10, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -2, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + hidden: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = 1 +// Search 2: +// 5 results including suggestedIndex = 1 +// Expected visible rows during update: +// Indexes 3 and 4 replaced with search suggestions, no other changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 1, + viewCount: 10, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 5, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = 1 +// Search 2: +// 5 results including suggestedIndex = 9 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 1, + viewCount: 10, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 9, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + stale: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = 1 +// Search 2: +// 5 results including suggestedIndex = -1 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 1, + viewCount: 10, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -1, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + stale: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = 1 +// Search 2: +// 5 results including suggestedIndex = -3 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 1, + viewCount: 10, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -3, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + stale: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -3, + hidden: true, + }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = 9 +// Search 2: +// 5 results including suggestedIndex = 1 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 9, + viewCount: 10, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = 9 +// Search 2: +// 5 results including suggestedIndex = 9 +// Expected visible rows during update: +// Indexes 2 and 3 replaced with search suggestions, no other changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 9, + viewCount: 10, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 9, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 5, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = -1 +// Search 2: +// 5 results including suggestedIndex = 1 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: -1, + viewCount: 10, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = -1 +// Search 2: +// 5 results including suggestedIndex = 9 +// Expected visible rows during update: +// Indexes 2 and 3 replaced with search suggestions, no other changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: -1, + viewCount: 10, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 9, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 5, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = -1 +// Search 2: +// 5 results including suggestedIndex = -1 +// Expected visible rows during update: +// Indexes 2 and 3 replaced with search suggestions, no other changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: -1, + viewCount: 10, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -1, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 5, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results, no suggestedIndex +// Search 2: +// 5 results including suggestedIndex = 1 and suggestedIndex = -1 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + viewCount: 10, + }, + search2: { + otherCount: 2, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndexes: [1, -1], + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = 1 +// Search 2: +// 5 results including suggestedIndex = 1 and suggestedIndex = -1 +// Expected visible rows during update: +// Index 3 replaced with search suggestion, no other changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 1, + viewCount: 10, + }, + search2: { + otherCount: 2, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndexes: [1, -1], + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 6, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 10 results including suggestedIndex = -1 +// Search 2: +// 5 results including suggestedIndex = 1 and suggestedIndex = -1 +// Expected visible rows during update: +// 10 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: -1, + viewCount: 10, + }, + search2: { + otherCount: 2, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndexes: [1, -1], + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 9 results including suggestedIndex = 1 with resultSpan = 2 +// Search 2: +// 5 results including: +// suggestedIndex = 1 with resultSpan = 2 +// suggestedIndex = -1 +// Expected visible rows during update: +// Index 3 replaced with search suggestion, no other changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 9, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndexes: [[1, 2]], + viewCount: 9, + }, + search2: { + otherCount: 2, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndexes: [[1, 2], -1], + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + resultSpan: 2, + }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 5, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); diff --git a/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_5_search_10_url.js b/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_5_search_10_url.js new file mode 100644 index 0000000000..dfd626d701 --- /dev/null +++ b/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_5_search_10_url.js @@ -0,0 +1,1015 @@ +/* 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 checks row visibility during view updates when rows with suggested +// indexes are added and removed. Each task performs two searches: Search 1 +// returns 5 results with search suggestions, and search 2 returns 10 results +// with URL results. + +"use strict"; + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 10 results including suggestedIndex = 1 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 4, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 10 results including suggestedIndex = 2 +// Expected visible rows during update: +// 5 search-1 rows + 1 search-2 row (the one before the suggestedIndex row) +add_suggestedIndex_task({ + search1: { + otherCount: 4, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 2, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 2, + hidden: true, + }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 10 results including suggestedIndex = 4 +// Expected visible rows during update: +// 5 search-1 rows + 3 search-2 rows (the ones before the suggestedIndex row) +add_suggestedIndex_task({ + search1: { + otherCount: 4, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 4, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 4, + hidden: true, + }, + { count: 5, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 10 results including suggestedIndex = 6 +// Expected visible rows during update: +// 5 search-1 rows + 5 search-2 rows (the ones before the suggestedIndex row) +add_suggestedIndex_task({ + search1: { + otherCount: 4, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 6, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 5, type: UrlbarUtils.RESULT_TYPE.URL }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 6, + hidden: true, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 10 results including suggestedIndex = 8 +// Expected visible rows during update: +// 5 search-1 rows + 5 search-2 rows (some of the ones before the +// suggestedIndex) +add_suggestedIndex_task({ + search1: { + otherCount: 4, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 8, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 5, type: UrlbarUtils.RESULT_TYPE.URL }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 8, + hidden: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 10 results including suggestedIndex = 9 +// Expected visible rows during update: +// 5 search-1 rows + 5 search-2 rows (some of the ones before the +// suggestedIndex) +add_suggestedIndex_task({ + search1: { + otherCount: 4, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 5, type: UrlbarUtils.RESULT_TYPE.URL }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 10 results including suggestedIndex = -1 +// Expected visible rows during update: +// 5 search-1 rows + 5 search-2 rows +add_suggestedIndex_task({ + search1: { + otherCount: 4, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 5, type: UrlbarUtils.RESULT_TYPE.URL }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 10 results including suggestedIndex = -2 +// Expected visible rows during update: +// 5 search-1 rows + 5 search-2 rows +add_suggestedIndex_task({ + search1: { + otherCount: 4, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 5, type: UrlbarUtils.RESULT_TYPE.URL }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + hidden: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 10 results including suggestedIndex = -4 +// Expected visible rows during update: +// 5 search-1 rows + 5 search-2 rows +add_suggestedIndex_task({ + search1: { + otherCount: 4, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -4, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 5, type: UrlbarUtils.RESULT_TYPE.URL }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -4, + hidden: true, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 10 results including suggestedIndex = -6 +// Expected visible rows during update: +// 5 search-1 rows + 3 search-2 rows (the ones before the suggestedIndex row) +add_suggestedIndex_task({ + search1: { + otherCount: 4, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -6, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -6, + hidden: true, + }, + { count: 5, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 10 results including suggestedIndex = -8 +// Expected visible rows during update: +// 5 search-1 rows + 1 search-2 row (the one before the suggestedIndex row) +add_suggestedIndex_task({ + search1: { + otherCount: 4, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -8, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -8, + hidden: true, + }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 1 +// Search 2: +// 10 results including suggestedIndex = 1 +// Expected visible rows during update: +// 5 search-1 rows + 5 search-2 rows +add_suggestedIndex_task({ + search1: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 5, type: UrlbarUtils.RESULT_TYPE.URL }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 1 +// Search 2: +// 10 results including suggestedIndex = 2 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 2, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + stale: true, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 2, + hidden: true, + }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 1 +// Search 2: +// 10 results including suggestedIndex = 9 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + stale: true, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 1 +// Search 2: +// 10 results including suggestedIndex = -1 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + stale: true, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 1 +// Search 2: +// 10 results including suggestedIndex = -2 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + stale: true, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + hidden: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 1 +// Search 2: +// 10 results including suggestedIndex = -9 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -9, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + stale: true, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -9, + hidden: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 9 +// Search 2: +// 10 results including suggestedIndex = 1 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 9, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 9 +// Search 2: +// 10 results including suggestedIndex = 3 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 9, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 3, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + stale: true, + }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 3, + hidden: true, + }, + { count: 6, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 9 +// Search 2: +// 10 results including suggestedIndex = 9 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 9, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + stale: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 9 +// Search 2: +// 10 results including suggestedIndex = -1 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 9, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + stale: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 9 +// Search 2: +// 10 results including suggestedIndex = -7 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 9, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -7, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + stale: true, + }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -7, + hidden: true, + }, + { count: 6, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 10 results including suggestedIndex = 1 and suggestedIndex = -1 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 4, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndexes: [1, -1], + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 1 +// Search 2: +// 10 results including suggestedIndex = 1 and suggestedIndex = -1 +// Expected visible rows during update: +// 5 search-1 rows + 5 search-2 rows (some of the ones before the +// suggestedIndex) +add_suggestedIndex_task({ + search1: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndexes: [1, -1], + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 5, type: UrlbarUtils.RESULT_TYPE.URL }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = -1 +// Search 2: +// 10 results including suggestedIndex = 1 and suggestedIndex = -1 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -1, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndexes: [1, -1], + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 9 results including suggestedIndex = 1 with resultSpan = 2 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 4, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + resultSpan: 2, + viewCount: 9, + }, + duringUpdate: [ + { count: 1 }, + { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + resultSpan: 2, + hidden: true, + }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 9 results including: +// suggestedIndex = 1 with resultSpan = 2 +// suggestedIndex = -1 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 4, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndexes: [[1, 2], -1], + viewCount: 9, + }, + duringUpdate: [ + { count: 1 }, + { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + resultSpan: 2, + hidden: true, + }, + { count: 6, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 1 with resultSpan = 2 +// Search 2: +// 9 results including: +// suggestedIndex = 1 with resultSpan = 2 +// suggestedIndex = -1 +// Expected visible rows during update: +// 5 search-1 rows + 4 search-2 rows +add_suggestedIndex_task({ + search1: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndexes: [[1, 2]], + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndexes: [[1, 2], -1], + viewCount: 9, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + resultSpan: 2, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 4, type: UrlbarUtils.RESULT_TYPE.URL }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); diff --git a/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_5_search_5_url.js b/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_5_search_5_url.js new file mode 100644 index 0000000000..49d6ac0663 --- /dev/null +++ b/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_5_search_5_url.js @@ -0,0 +1,1131 @@ +/* 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 checks row visibility during view updates when rows with suggested +// indexes are added and removed. Each task performs two searches: Search 1 +// returns 5 results with search suggestions, and search 2 returns 5 results +// with URL results. + +"use strict"; + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 5 results including suggestedIndex = 1 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 4, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 5 results including suggestedIndex = 2 +// Expected visible rows during update: +// 5 search-1 rows + 1 search-2 row (the one before the suggestedIndex row) +add_suggestedIndex_task({ + search1: { + otherCount: 4, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 2, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 2, + hidden: true, + }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 5 results including suggestedIndex = 9 +// Expected visible rows during update: +// 5 search-1 rows + 3 search-2 rows (the ones before the suggestedIndex) +add_suggestedIndex_task({ + search1: { + otherCount: 4, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 5 results including suggestedIndex = -1 +// Expected visible rows during update: +// 5 search-1 rows + 3 search-2 rows (the ones before the suggestedIndex row) +add_suggestedIndex_task({ + search1: { + otherCount: 4, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 5 results including suggestedIndex = -2 +// Expected visible rows during update: +// 5 search-1 rows + 2 search-2 rows (the one before the suggestedIndex row) +add_suggestedIndex_task({ + search1: { + otherCount: 4, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + hidden: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 1 +// Search 2: +// 5 results including suggestedIndex = 1 +// Expected visible rows during update: +// 5 search-1 rows + 3 search-2 rows (i.e., all rows from both searches) +add_suggestedIndex_task({ + search1: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 1 +// Search 2: +// 5 results including suggestedIndex = 2 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 2, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + stale: true, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 2, + hidden: true, + }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 1 +// Search 2: +// 5 results including suggestedIndex = 9 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + stale: true, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 1 +// Search 2: +// 5 results including suggestedIndex = -1 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + stale: true, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 1 +// Search 2: +// 5 results including suggestedIndex = -2 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + stale: true, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + hidden: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 1 +// Search 2: +// 5 results including suggestedIndex = -3 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -3, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + stale: true, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -3, + hidden: true, + }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 9 +// Search 2: +// 5 results including suggestedIndex = 1 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 9, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 9 +// Search 2: +// 5 results including suggestedIndex = 2 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 9, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 2, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + stale: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 2, + hidden: true, + }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 9 +// Search 2: +// 5 results including suggestedIndex = 9 +// Expected visible rows during update: +// 5 original rows with no changes (because the original search results can't +// be replaced with URL results) +add_suggestedIndex_task({ + search1: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 9, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + stale: true, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 9 +// Search 2: +// 5 results including suggestedIndex = -1 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 9, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + stale: true, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 9 +// Search 2: +// 5 results including suggestedIndex = -2 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 9, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + stale: true, + }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + hidden: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 9 +// Search 2: +// 5 results including suggestedIndex = -3 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 9, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -3, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + stale: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -3, + hidden: true, + }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = -1 +// Search 2: +// 5 results including suggestedIndex = 1 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -1, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = -1 +// Search 2: +// 5 results including suggestedIndex = 2 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -1, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 2, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + stale: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 2, + hidden: true, + }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = -1 +// Search 2: +// 5 results including suggestedIndex = 9 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -1, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + stale: true, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = -1 +// Search 2: +// 5 results including suggestedIndex = -1 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -1, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + stale: true, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = -1 +// Search 2: +// 5 results including suggestedIndex = -2 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -1, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + stale: true, + }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + hidden: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = -2 +// Search 2: +// 5 results including suggestedIndex = 1 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -2, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + stale: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = -2 +// Search 2: +// 5 results including suggestedIndex = 2 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -2, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 2, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + stale: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 2, + hidden: true, + }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = -2 +// Search 2: +// 5 results including suggestedIndex = 9 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -2, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + stale: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = -2 +// Search 2: +// 5 results including suggestedIndex = -1 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -2, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + stale: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = -2 +// Search 2: +// 5 results including suggestedIndex = -2 +// Expected visible rows during update: +// 5 original rows with no changes (because the original search results can't +// be replaced with URL results) +add_suggestedIndex_task({ + search1: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -2, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + stale: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + hidden: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + ], +}); + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 5 results including suggestedIndex = 1 and suggestedIndex = -1 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 4, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + viewCount: 5, + }, + search2: { + otherCount: 2, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndexes: [1, -1], + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 1 +// Search 2: +// 5 results including suggestedIndex = 1 and suggestedIndex = -1 +// Expected visible rows during update: +// 5 search-1 rows + 2 search-2 rows (the ones before the suggestedIndex row) +add_suggestedIndex_task({ + search1: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 5, + }, + search2: { + otherCount: 2, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndexes: [1, -1], + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = -1 +// Search 2: +// 5 results including suggestedIndex = 1 and suggestedIndex = -1 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -1, + viewCount: 5, + }, + search2: { + otherCount: 2, + otherType: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndexes: [1, -1], + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); diff --git a/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_5_url_10_search.js b/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_5_url_10_search.js new file mode 100644 index 0000000000..4306efbeff --- /dev/null +++ b/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_5_url_10_search.js @@ -0,0 +1,1057 @@ +/* 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 checks row visibility during view updates when rows with suggested +// indexes are added and removed. Each task performs two searches: Search 1 +// returns 5 results where the first result is a search suggestion and the +// remainder are URL results, and search 2 returns 10 results with search +// suggestions. This tests the view-update logic that allows search suggestions +// to replace other results once an existing suggestion row is encountered. + +"use strict"; + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 10 results including suggestedIndex = 1 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 10 results including suggestedIndex = 2 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 2, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 2, + hidden: true, + }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 10 results including suggestedIndex = 4 +// Expected visible rows during update: +// Indexes 2 and 3 replaced with search suggestions, no other changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 4, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 4, + hidden: true, + }, + { count: 5, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 10 results including suggestedIndex = 6 +// Expected visible rows during update: +// All search-2 rows +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 6, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 5, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 6, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + ], +}); + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 10 results including suggestedIndex = 8 +// Expected visible rows during update: +// All search-2 rows +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 8, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 8, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + ], +}); + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 10 results including suggestedIndex = 9 +// Expected visible rows during update: +// All search-2 rows +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 9, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + }, + ], +}); + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 10 results including suggestedIndex = -1 +// Expected visible rows during update: +// All search-2 rows +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -1, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + }, + ], +}); + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 10 results including suggestedIndex = -2 +// Expected visible rows during update: +// All search-2 rows +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -2, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + ], +}); + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 10 results including suggestedIndex = -4 +// Expected visible rows during update: +// All search-2 rows +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -4, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 5, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -4, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + ], +}); + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 10 results including suggestedIndex = -6 +// Expected visible rows during update: +// Indexes 2 and 3 replaced with search suggestions, no other changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -6, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -6, + hidden: true, + }, + { count: 5, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 10 results including suggestedIndex = -8 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -8, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -8, + hidden: true, + }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 1 +// Search 2: +// 10 results including suggestedIndex = 1 +// Expected visible rows during update: +// All search-2 rows +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 1, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 1 +// Search 2: +// 10 results including suggestedIndex = 2 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 1, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 2, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + stale: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 2, + hidden: true, + }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 1 +// Search 2: +// 10 results including suggestedIndex = 9 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 1, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 9, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + stale: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 1 +// Search 2: +// 10 results including suggestedIndex = -1 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 1, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -1, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + stale: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 1 +// Search 2: +// 10 results including suggestedIndex = -2 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 1, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -2, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + stale: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + hidden: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 1 +// Search 2: +// 10 results including suggestedIndex = -9 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 1, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -9, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + stale: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -9, + hidden: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 9 +// Search 2: +// 10 results including suggestedIndex = 1 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 9, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 9 +// Search 2: +// 10 results including suggestedIndex = 3 +// Expected visible rows during update: +// Index 2 replaced with search suggestion, no other changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 9, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 3, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 3, + hidden: true, + }, + { count: 6, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 9 +// Search 2: +// 10 results including suggestedIndex = 9 +// Expected visible rows during update: +// Indexes 2 and 3 replaced with search suggestions, no other changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 9, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 9, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + stale: true, + }, + { count: 5, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 9 +// Search 2: +// 10 results including suggestedIndex = -1 +// Expected visible rows during update: +// Indexes 2 and 3 replaced with search suggestions, no other changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 9, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -1, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + stale: true, + }, + { count: 5, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 9 +// Search 2: +// 10 results including suggestedIndex = -7 +// Expected visible rows during update: +// Index 2 replaced with search suggestion, no other changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 9, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -7, + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -7, + hidden: true, + }, + { count: 6, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 10 results including suggestedIndex = 1 and suggestedIndex = -1 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndexes: [1, -1], + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 1 +// Search 2: +// 10 results including suggestedIndex = 1 and suggestedIndex = -1 +// Expected visible rows during update: +// All search-2 rows +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 1, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndexes: [1, -1], + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = -1 +// Search 2: +// 10 results including suggestedIndex = 1 and suggestedIndex = -1 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: -1, + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndexes: [1, -1], + viewCount: 10, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 9 results including suggestedIndex = 1 with resultSpan = 2 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + resultSpan: 2, + viewCount: 9, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + resultSpan: 2, + hidden: true, + }, + { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 9 results including: +// suggestedIndex = 1 with resultSpan = 2 +// suggestedIndex = -1 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndexes: [[1, 2], -1], + viewCount: 9, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + resultSpan: 2, + hidden: true, + }, + { count: 6, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 1 with resultSpan = 2 +// Search 2: +// 9 results including: +// suggestedIndex = 1 with resultSpan = 2 +// suggestedIndex = -1 +// Expected visible rows during update: +// All search-2 rows +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndexes: [[1, 2]], + viewCount: 5, + }, + search2: { + otherCount: 10, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndexes: [[1, 2], -1], + viewCount: 9, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + resultSpan: 2, + }, + { count: 6, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + }, + ], +}); diff --git a/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_5_url_5_search.js b/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_5_url_5_search.js new file mode 100644 index 0000000000..44078c2251 --- /dev/null +++ b/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_5_url_5_search.js @@ -0,0 +1,1203 @@ +/* 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 checks row visibility during view updates when rows with suggested +// indexes are added and removed. Each task performs two searches: Search 1 +// returns 5 results where the first result is a search suggestion and the +// remainder are URL results, and search 2 returns 5 results with search +// suggestions. This tests the view-update logic that allows search suggestions +// to replace other results once an existing suggestion row is encountered. + +"use strict"; + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 5 results including suggestedIndex = 1 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 5 results including suggestedIndex = 2 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 2, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 2, + hidden: true, + }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 5 results including suggestedIndex = 9 +// Expected visible rows during update: +// Indexes 2 and 3 replaced with search suggestions, no other changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 9, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 5 results including suggestedIndex = -1 +// Expected visible rows during update: +// Indexes 2 and 3 replaced with search suggestions, no other changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -1, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 5 results including suggestedIndex = -2 +// Expected visible rows during update: +// Index 2 replaced with search suggestion, no other changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -2, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + hidden: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 1 +// Search 2: +// 5 results including suggestedIndex = 1 +// Expected visible rows during update: +// All search-2 rows +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 1, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { + count: 2, + type: UrlbarPrefs.get("resultMenu") + ? UrlbarUtils.RESULT_TYPE.URL + : UrlbarUtils.RESULT_TYPE.SEARCH, + }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 1 +// Search 2: +// 5 results including suggestedIndex = 2 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 1, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 2, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + stale: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 2, + hidden: true, + }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 1 +// Search 2: +// 5 results including suggestedIndex = 9 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 1, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 9, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + stale: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 1 +// Search 2: +// 5 results including suggestedIndex = -1 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 1, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -1, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + stale: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 1 +// Search 2: +// 5 results including suggestedIndex = -2 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 1, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -2, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + stale: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + hidden: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 1 +// Search 2: +// 5 results including suggestedIndex = -3 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 1, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -3, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + stale: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -3, + hidden: true, + }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 9 +// Search 2: +// 5 results including suggestedIndex = 1 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 9, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 9 +// Search 2: +// 5 results including suggestedIndex = 2 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 9, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 2, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 2, + hidden: true, + }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 9 +// Search 2: +// 5 results including suggestedIndex = 9 +// Expected visible rows during update: +// All search-2 rows +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 9, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 9, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { + count: 2, + type: UrlbarPrefs.get("resultMenu") + ? UrlbarUtils.RESULT_TYPE.URL + : UrlbarUtils.RESULT_TYPE.SEARCH, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 9 +// Search 2: +// 5 results including suggestedIndex = -1 +// Expected visible rows during update: +// Indexes 2 and 3 replaced with search suggestions, no other changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 9, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -1, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 9 +// Search 2: +// 5 results including suggestedIndex = -2 +// Expected visible rows during update: +// Index 2 replaced with search suggestion, no other changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 9, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -2, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + hidden: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 9 +// Search 2: +// 5 results including suggestedIndex = -3 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 9, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -3, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -3, + hidden: true, + }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = -1 +// Search 2: +// 5 results including suggestedIndex = 1 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: -1, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = -1 +// Search 2: +// 5 results including suggestedIndex = 2 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: -1, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 2, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 2, + hidden: true, + }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = -1 +// Search 2: +// 5 results including suggestedIndex = 9 +// Expected visible rows during update: +// Indexes 2 and 3 replaced with search suggestions, no other changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: -1, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 9, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = -1 +// Search 2: +// 5 results including suggestedIndex = -1 +// Expected visible rows during update: +// All search-2 rows +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: -1, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -1, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { + count: 2, + type: UrlbarPrefs.get("resultMenu") + ? UrlbarUtils.RESULT_TYPE.URL + : UrlbarUtils.RESULT_TYPE.SEARCH, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = -1 +// Search 2: +// 5 results including suggestedIndex = -2 +// Expected visible rows during update: +// Index 2 replaced with search suggestion, no other changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: -1, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -2, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + hidden: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = -2 +// Search 2: +// 5 results including suggestedIndex = 1 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: -2, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 1, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + stale: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = -2 +// Search 2: +// 5 results including suggestedIndex = 2 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: -2, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 2, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + stale: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 2, + hidden: true, + }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = -2 +// Search 2: +// 5 results including suggestedIndex = 9 +// Expected visible rows during update: +// Index 2 replaced with search suggestion, no other changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: -2, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: 9, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + stale: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 9, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = -2 +// Search 2: +// 5 results including suggestedIndex = -1 +// Expected visible rows during update: +// Index 2 replaced with search suggestion, no other changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: -2, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -1, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + stale: true, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = -2 +// Search 2: +// 5 results including suggestedIndex = -2 +// Expected visible rows during update: +// All search-2 rows +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: -2, + viewCount: 5, + }, + search2: { + otherCount: 3, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndex: -2, + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { + count: 1, + type: UrlbarPrefs.get("resultMenu") + ? UrlbarUtils.RESULT_TYPE.URL + : UrlbarUtils.RESULT_TYPE.SEARCH, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + }, + { + count: 1, + type: UrlbarPrefs.get("resultMenu") + ? UrlbarUtils.RESULT_TYPE.URL + : UrlbarUtils.RESULT_TYPE.SEARCH, + }, + ], +}); + +// Search 1: +// 5 results, no suggestedIndex +// Search 2: +// 5 results including suggestedIndex = 1 and suggestedIndex = -1 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + viewCount: 5, + }, + search2: { + otherCount: 2, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndexes: [1, -1], + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = 1 +// Search 2: +// 5 results including suggestedIndex = 1 and suggestedIndex = -1 +// Expected visible rows during update: +// Index 3 replaced with search suggestion, no other changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: 1, + viewCount: 5, + }, + search2: { + otherCount: 2, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndexes: [1, -1], + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); + +// Search 1: +// 5 results including suggestedIndex = -1 +// Search 2: +// 5 results including suggestedIndex = 1 and suggestedIndex = -1 +// Expected visible rows during update: +// 5 original rows with no changes +add_suggestedIndex_task({ + search1: { + other: [ + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL }, + ], + suggestedIndex: -1, + viewCount: 5, + }, + search2: { + otherCount: 2, + otherType: UrlbarUtils.RESULT_TYPE.SEARCH, + suggestedIndexes: [1, -1], + viewCount: 5, + }, + duringUpdate: [ + { count: 1 }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + stale: true, + }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: 1, + hidden: true, + }, + { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -1, + hidden: true, + }, + ], +}); diff --git a/browser/components/urlbar/tests/browser-updateResults/head.js b/browser/components/urlbar/tests/browser-updateResults/head.js new file mode 100644 index 0000000000..7bdb1bcb1f --- /dev/null +++ b/browser/components/urlbar/tests/browser-updateResults/head.js @@ -0,0 +1,550 @@ +/* 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 files in this directory test UrlbarView.#updateResults(). + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs", + UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs", + UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs", + UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs", + UrlbarView: "resource:///modules/UrlbarView.sys.mjs", +}); + +XPCOMUtils.defineLazyGetter(this, "UrlbarTestUtils", () => { + const { UrlbarTestUtils: module } = ChromeUtils.importESModule( + "resource://testing-common/UrlbarTestUtils.sys.mjs" + ); + module.init(this); + return module; +}); + +add_setup(async function headInit() { + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + + await SpecialPowers.pushPrefEnv({ + set: [ + // Make absolutely sure the panel stays open during the test. There are + // spurious blurs on WebRender TV tests as the test starts that cause the + // panel to close and the query to be canceled, resulting in intermittent + // failures without this. + ["ui.popup.disable_autohide", true], + + // Make sure maxRichResults is 10 for sanity. + ["browser.urlbar.maxRichResults", 10], + ], + }); + + // Increase the timeout of the remove-stale-rows timer so that it doesn't + // interfere with the tests. + let originalRemoveStaleRowsTimeout = UrlbarView.removeStaleRowsTimeout; + UrlbarView.removeStaleRowsTimeout = 30000; + registerCleanupFunction(() => { + UrlbarView.removeStaleRowsTimeout = originalRemoveStaleRowsTimeout; + }); +}); + +/** + * A test provider that doesn't finish startQuery() until `finishQueryPromise` + * is resolved. + */ +class DelayingTestProvider extends UrlbarTestUtils.TestProvider { + finishQueryPromise = null; + async startQuery(context, addCallback) { + for (let result of this._results) { + addCallback(this, result); + } + await this.finishQueryPromise; + } +} + +/** + * Makes a result with a suggested index. + * + * @param {number} suggestedIndex + * The preferred index of the result. + * @param {number} resultSpan + * The result will have this span. + * @returns {UrlbarResult} + */ +function makeSuggestedIndexResult(suggestedIndex, resultSpan = 1) { + return Object.assign( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { + url: "http://example.com/si", + displayUrl: "http://example.com/si", + title: "suggested index", + } + ), + { suggestedIndex, resultSpan } + ); +} + +/** + * Makes an array of results for the suggestedIndex tests. The array will + * include a heuristic followed by the specified results. + * + * @param {object} options + * The options object + * @param {number} [options.count] + * The number of results to return other than the heuristic. This and + * `type` must be given together. + * @param {UrlbarUtils.RESULT_TYPE} [options.type] + * The type of results to return other than the heuristic. This and `count` + * must be given together. + * @param {Array} [options.specs] + * If you want a mix of result types instead of only one type, then use this + * param instead of `count` and `type`. Each item in this array must be an + * object with the following properties: + * {number} count + * The number of results to return for the given `type`. + * {UrlbarUtils.RESULT_TYPE} type + * The type of results. + * @returns {Array} + * An array of results. + */ +function makeProviderResults({ count = 0, type = undefined, specs = [] }) { + if (count) { + specs.push({ count, type }); + } + + let query = "test"; + let results = [ + Object.assign( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.SEARCH, + UrlbarUtils.RESULT_SOURCE.SEARCH, + { + query, + engine: Services.search.defaultEngine.name, + } + ), + { heuristic: true } + ), + ]; + + for (let { count: specCount, type: specType } of specs) { + for (let i = 0; i < specCount; i++) { + let str = `${query} ${results.length}`; + switch (specType) { + case UrlbarUtils.RESULT_TYPE.SEARCH: + results.push( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.SEARCH, + UrlbarUtils.RESULT_SOURCE.SEARCH, + { + query, + suggestion: str, + lowerCaseSuggestion: str.toLowerCase(), + engine: Services.search.defaultEngine.name, + } + ) + ); + break; + case UrlbarUtils.RESULT_TYPE.URL: + results.push( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { + url: "http://example.com/" + i, + displayUrl: "http://example.com/" + i, + title: str, + } + ) + ); + break; + default: + throw new Error(`Unsupported makeProviderResults type: ${specType}`); + } + } + } + + return results; +} + +let gSuggestedIndexTaskIndex = 0; + +/** + * Adds a suggestedIndex test task. See doSuggestedIndexTest() for params. + * + * @param {object} options + * See doSuggestedIndexTest(). + */ +function add_suggestedIndex_task(options) { + if (!gSuggestedIndexTaskIndex) { + initSuggestedIndexTest(); + } + let testIndex = gSuggestedIndexTaskIndex++; + let testName = "test_" + testIndex; + let testDesc = JSON.stringify(options); + let func = async () => { + info(`Running task at index ${testIndex}: ${testDesc}`); + await doSuggestedIndexTest(options); + }; + Object.defineProperty(func, "name", { value: testName }); + add_task(func); +} + +/** + * Initializes suggestedIndex tests. You don't normally need to call this from + * your test because add_suggestedIndex_task() calls it automatically. + */ +function initSuggestedIndexTest() { + // These tests can time out on Mac TV WebRender just because they do so much, + // so request a longer timeout. + if (AppConstants.platform == "macosx") { + requestLongerTimeout(3); + } + registerCleanupFunction(() => { + gSuggestedIndexTaskIndex = 0; + }); +} + +/** + * @typedef {object} SuggestedIndexTestOptions + * @property {number} [otherCount] + * The number of results other than the heuristic and suggestedIndex results + * that the provider should return for search 1. This and `otherType` must be + * given together. + * @property {UrlbarUtils.RESULT_TYPE} [otherType] + * The type of results other than the heuristic and suggestedIndex results + * that the provider should return for search 1. This and `otherCount` must be + * given together. + * @property {Array} [other] + * If you want the provider to return a mix of result types instead of only + * one type, then use this param instead of `otherCount` and `otherType`. Each + * item in this array must be an object with the following properties: + * {number} count + * The number of results to return for the given `type`. + * {UrlbarUtils.RESULT_TYPE} type + * The type of results. + * @property {number} viewCount + * The total number of results expected in the view after search 1 finishes, + * including the heuristic and suggestedIndex results. + * @param {number} [suggestedIndex] + * If given, the provider will return a result with this suggested index for + * search 1. + * @property {number} [resultSpan] + * If this and `search1.suggestedIndex` are given, then the suggestedIndex + * result for search 1 will have this resultSpan. + * @property {Array} [suggestedIndexes] + * If you want the provider to return more than one suggestedIndex result for + * search 1, then use this instead of `search1.suggestedIndex`. Each item in + * this array must be one of the following: + * suggestedIndex value + * [suggestedIndex, resultSpan] tuple + */ + +/** + * Runs a suggestedIndex test. Performs two searches and checks the results just + * after the view update and after the second search finishes. The caller is + * responsible for passing in a description of what the rows should look like + * just after the view update finishes but before the second search finishes, + * i.e., before stale rows are removed and hidden rows are shown -- this is the + * `duringUpdate` param. The important thing this checks is that the rows with + * suggested indexes don't move around or appear in the wrong places. + * + * @param {object} options + * The options object + * @param {SuggestedIndexTestOptions} options.search1 + * The first search options object + * @param {SuggestedIndexTestOptions} options.search2 + * This object has the same properties as the `search1` object but it applies + * to the second search. + * @param {Array<{ count: number, type: UrlbarUtils.RESULT_TYPE, suggestedIndex: ?number, stale: ?boolean, hidden: ?boolean }>} options.duringUpdate + * An array of expected row states during the view update. Each item in the + * array must be an object with the following properties: + * {number} count + * The number of rows in the view to which this row state object applies. + * {UrlbarUtils.RESULT_TYPE} type + * The expected type of the rows. + * {number} [suggestedIndex] + * The expected suggestedIndex of the row. + * {boolean} [stale] + * Whether the rows are expected to be stale. Defaults to false. + * {boolean} [hidden] + * Whether the rows are expected to be hidden. Defaults to false. + */ +async function doSuggestedIndexTest({ search1, search2, duringUpdate }) { + // We use this test provider to test specific results. It has an Infinity + // priority so that it provides all results in our test searches, including + // the heuristic. That lets us avoid any potential races with the built-in + // providers; testing them is not important here. + let provider = new DelayingTestProvider({ priority: Infinity }); + UrlbarProvidersManager.registerProvider(provider); + registerCleanupFunction(() => { + UrlbarProvidersManager.unregisterProvider(provider); + }); + + // Set up the first search. First, add the non-suggestedIndex results to the + // provider. + provider._results = makeProviderResults({ + specs: search1.other, + count: search1.otherCount, + type: search1.otherType, + }); + + // Set up `suggestedIndexes`. It's an array with [suggestedIndex, resultSpan] + // tuples. + if (!search1.suggestedIndexes) { + search1.suggestedIndexes = []; + } + search1.suggestedIndexes = search1.suggestedIndexes.map(value => + typeof value == "number" ? [value, 1] : value + ); + if (typeof search1.suggestedIndex == "number") { + search1.suggestedIndexes.push([ + search1.suggestedIndex, + search1.resultSpan || 1, + ]); + } + + // Add the suggestedIndex results to the provider. + for (let [suggestedIndex, resultSpan] of search1.suggestedIndexes) { + provider._results.push( + makeSuggestedIndexResult(suggestedIndex, resultSpan) + ); + } + + // Do the first search. + provider.finishQueryPromise = Promise.resolve(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + + // Sanity check the results. + Assert.equal( + UrlbarTestUtils.getResultCount(window), + search1.viewCount, + "Row count after first search" + ); + for (let [suggestedIndex, resultSpan] of search1.suggestedIndexes) { + let index = + suggestedIndex >= 0 + ? Math.min(search1.viewCount - 1, suggestedIndex) + : Math.max(0, search1.viewCount + suggestedIndex); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index); + Assert.equal( + result.element.row.result.suggestedIndex, + suggestedIndex, + "suggestedIndex after first search" + ); + Assert.equal( + UrlbarUtils.getSpanForResult(result.element.row.result), + resultSpan, + "resultSpan after first search" + ); + } + + // Set up the second search. First, add the non-suggestedIndex results to the + // provider. + provider._results = makeProviderResults({ + specs: search2.other, + count: search2.otherCount, + type: search2.otherType, + }); + + // Set up `suggestedIndexes`. It's an array with [suggestedIndex, resultSpan] + // tuples. + if (!search2.suggestedIndexes) { + search2.suggestedIndexes = []; + } + search2.suggestedIndexes = search2.suggestedIndexes.map(value => + typeof value == "number" ? [value, 1] : value + ); + if (typeof search2.suggestedIndex == "number") { + search2.suggestedIndexes.push([ + search2.suggestedIndex, + search2.resultSpan || 1, + ]); + } + + // Add the suggestedIndex results to the provider. + for (let [suggestedIndex, resultSpan] of search2.suggestedIndexes) { + provider._results.push( + makeSuggestedIndexResult(suggestedIndex, resultSpan) + ); + } + + let rowCountDuringUpdate = duringUpdate.reduce( + (count, rowState) => count + rowState.count, + 0 + ); + + // Don't allow the search to finish until we check the updated rows. We'll + // accomplish that by adding a mutation observer to observe completion of the + // update and delaying resolving the provider's finishQueryPromise. + let mutationPromise = new Promise(resolve => { + let lastRowState = duringUpdate[duringUpdate.length - 1]; + let observer = new MutationObserver(mutations => { + observer.disconnect(); + resolve(); + }); + if (lastRowState.stale) { + // The last row during the update is expected to become stale. Wait for + // the stale attribute to be set on it. We'll actually just wait for any + // attribute. + let { children } = UrlbarTestUtils.getResultsContainer(window); + observer.observe(children[children.length - 1], { attributes: true }); + } else if (search1.viewCount == rowCountDuringUpdate) { + // No rows are expected to be added during the view update, so it must be + // the case that some rows will be updated for results in the the second + // search. Wait for any change to an existing row. + observer.observe(UrlbarTestUtils.getResultsContainer(window), { + subtree: true, + attributes: true, + characterData: true, + }); + } else { + // Rows are expected to be added during the update. Wait for them. + observer.observe(UrlbarTestUtils.getResultsContainer(window), { + childList: true, + }); + } + }); + + // Now do the second search but don't wait for it to finish. + let resolveQuery; + provider.finishQueryPromise = new Promise( + resolve => (resolveQuery = resolve) + ); + let queryPromise = UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + + // Wait for the update to finish. + await mutationPromise; + + // Check the rows. We can't use UrlbarTestUtils.getDetailsOfResultAt() here + // because it waits for the search to finish. + Assert.equal( + UrlbarTestUtils.getResultCount(window), + rowCountDuringUpdate, + "Row count during update" + ); + let rows = UrlbarTestUtils.getResultsContainer(window).children; + let rowIndex = 0; + for (let rowState of duringUpdate) { + for (let i = 0; i < rowState.count; i++) { + let row = rows[rowIndex]; + + // type + if ("type" in rowState) { + Assert.equal( + row.result.type, + rowState.type, + `Type at index ${rowIndex} during update` + ); + } + + // suggestedIndex + if ("suggestedIndex" in rowState) { + Assert.ok( + row.result.hasSuggestedIndex, + `Row at index ${rowIndex} has suggestedIndex during update` + ); + Assert.equal( + row.result.suggestedIndex, + rowState.suggestedIndex, + `suggestedIndex at index ${rowIndex} during update` + ); + } else { + Assert.ok( + !row.result.hasSuggestedIndex, + `Row at index ${rowIndex} does not have suggestedIndex during update` + ); + } + + // resultSpan + Assert.equal( + UrlbarUtils.getSpanForResult(row.result), + rowState.resultSpan || 1, + `resultSpan at index ${rowIndex} during update` + ); + + // stale + if (rowState.stale) { + Assert.equal( + row.getAttribute("stale"), + "true", + `Row at index ${rowIndex} is stale during update` + ); + } else { + Assert.ok( + !row.hasAttribute("stale"), + `Row at index ${rowIndex} is not stale during update` + ); + } + + // visible + Assert.equal( + BrowserTestUtils.is_visible(row), + !rowState.hidden, + `Visible at index ${rowIndex} during update` + ); + + rowIndex++; + } + } + + // Finish the search. + resolveQuery(); + await queryPromise; + + // Check the rows now that the second search is done. First, build a map from + // real indexes to suggested index. e.g., if a suggestedIndex = -1, then the + // real index = the result count - 1. + let suggestedIndexesByRealIndex = new Map(); + for (let [suggestedIndex, resultSpan] of search2.suggestedIndexes) { + let realIndex = + suggestedIndex >= 0 + ? Math.min(suggestedIndex, search2.viewCount - 1) + : Math.max(0, search2.viewCount + suggestedIndex); + suggestedIndexesByRealIndex.set(realIndex, [suggestedIndex, resultSpan]); + } + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + search2.viewCount, + "Row count after update" + ); + for (let i = 0; i < search2.viewCount; i++) { + let result = rows[i].result; + let tuple = suggestedIndexesByRealIndex.get(i); + if (tuple) { + let [suggestedIndex, resultSpan] = tuple; + Assert.ok( + result.hasSuggestedIndex, + `Row at index ${i} has suggestedIndex after update` + ); + Assert.equal( + result.suggestedIndex, + suggestedIndex, + `suggestedIndex at index ${i} after update` + ); + Assert.equal( + UrlbarUtils.getSpanForResult(result), + resultSpan, + `resultSpan at index ${i} after update` + ); + } else { + Assert.ok( + !result.hasSuggestedIndex, + `Row at index ${i} does not have suggestedIndex after update` + ); + } + } + + await UrlbarTestUtils.promisePopupClose(window); + gURLBar.handleRevert(); + UrlbarProvidersManager.unregisterProvider(provider); +} 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 @@ + + POST Search + + + + diff --git a/browser/components/urlbar/tests/browser/add_search_engine_0.xml b/browser/components/urlbar/tests/browser/add_search_engine_0.xml new file mode 100644 index 0000000000..a24348deb7 --- /dev/null +++ b/browser/components/urlbar/tests/browser/add_search_engine_0.xml @@ -0,0 +1,7 @@ + + +add_search_engine_0 + + + + diff --git a/browser/components/urlbar/tests/browser/add_search_engine_1.xml b/browser/components/urlbar/tests/browser/add_search_engine_1.xml new file mode 100644 index 0000000000..61092247a9 --- /dev/null +++ b/browser/components/urlbar/tests/browser/add_search_engine_1.xml @@ -0,0 +1,7 @@ + + +add_search_engine_1 + + + + diff --git a/browser/components/urlbar/tests/browser/add_search_engine_2.xml b/browser/components/urlbar/tests/browser/add_search_engine_2.xml new file mode 100644 index 0000000000..3f5c2f0037 --- /dev/null +++ b/browser/components/urlbar/tests/browser/add_search_engine_2.xml @@ -0,0 +1,7 @@ + + +add_search_engine_2 + + + + diff --git a/browser/components/urlbar/tests/browser/add_search_engine_3.xml b/browser/components/urlbar/tests/browser/add_search_engine_3.xml new file mode 100644 index 0000000000..bacfffa3e2 --- /dev/null +++ b/browser/components/urlbar/tests/browser/add_search_engine_3.xml @@ -0,0 +1,7 @@ + + +add_search_engine_3 + + + + diff --git a/browser/components/urlbar/tests/browser/add_search_engine_invalid.html b/browser/components/urlbar/tests/browser/add_search_engine_invalid.html new file mode 100644 index 0000000000..ea5baa93d5 --- /dev/null +++ b/browser/components/urlbar/tests/browser/add_search_engine_invalid.html @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/browser/components/urlbar/tests/browser/add_search_engine_many.html b/browser/components/urlbar/tests/browser/add_search_engine_many.html new file mode 100644 index 0000000000..d75bccc890 --- /dev/null +++ b/browser/components/urlbar/tests/browser/add_search_engine_many.html @@ -0,0 +1,24 @@ + + + + + + + + + + + + diff --git a/browser/components/urlbar/tests/browser/add_search_engine_one.html b/browser/components/urlbar/tests/browser/add_search_engine_one.html new file mode 100644 index 0000000000..c11409604f --- /dev/null +++ b/browser/components/urlbar/tests/browser/add_search_engine_one.html @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/browser/components/urlbar/tests/browser/add_search_engine_same_names.html b/browser/components/urlbar/tests/browser/add_search_engine_same_names.html new file mode 100644 index 0000000000..7112905a75 --- /dev/null +++ b/browser/components/urlbar/tests/browser/add_search_engine_same_names.html @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/browser/components/urlbar/tests/browser/add_search_engine_two.html b/browser/components/urlbar/tests/browser/add_search_engine_two.html new file mode 100644 index 0000000000..c1d33860a4 --- /dev/null +++ b/browser/components/urlbar/tests/browser/add_search_engine_two.html @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/browser/components/urlbar/tests/browser/authenticate.sjs b/browser/components/urlbar/tests/browser/authenticate.sjs new file mode 100644 index 0000000000..8218a46eb6 --- /dev/null +++ b/browser/components/urlbar/tests/browser/authenticate.sjs @@ -0,0 +1,218 @@ +"use strict"; + +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) { + let match; + let 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. + let query = "?" + request.queryString; + + let expected_user = "", + expected_pass = "", + realm = "mochitest"; + let proxy_expected_user = "", + proxy_expected_pass = "", + proxy_realm = "mochi-proxy"; + let huge = false, + plugin = false, + anonymous = false; + let 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. + + let 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 Error("Couldn't parse auth header: " + authHeader); + } + + let userpass = atob(match[1]); + match = /(.*):(.*)/.exec(userpass); + if (match.length != 3) { + throw Error("Couldn't decode auth header: " + userpass); + } + actual_user = match[1]; + actual_pass = match[2]; + } + + let 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 Error("Couldn't parse auth header: " + authHeader); + } + + let userpass = atob(match[1]); + match = /(.*):(.*)/.exec(userpass); + if (match.length != 3) { + throw Error("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 (let 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 (let 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(""); + response.write( + "

Login: " + + (requestAuth ? "FAIL" : "PASS") + + "

\n" + ); + response.write( + "

Proxy: " + + (requestProxyAuth ? "FAIL" : "PASS") + + "

\n" + ); + response.write("

Auth: " + authHeader + "

\n"); + response.write("

User: " + actual_user + "

\n"); + response.write("

Pass: " + actual_pass + "

\n"); + + if (huge) { + response.write("
"); + for (let i = 0; i < 100000; i++) { + response.write("123456789\n"); + } + response.write("
"); + response.write( + "This is a footnote after the huge content fill" + ); + } + + if (plugin) { + response.write( + "\n" + ); + } + + response.write(""); +} diff --git a/browser/components/urlbar/tests/browser/browser.ini b/browser/components/urlbar/tests/browser/browser.ini new file mode 100644 index 0000000000..64290236d3 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser.ini @@ -0,0 +1,434 @@ +# 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 +skip-if = + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure + +prefs = + browser.urlbar.trending.featureGate=false + extensions.screenshots.disabled=false + screenshots.browser.component.enabled=true + +[browser_UrlbarInput_formatValue.js] +[browser_UrlbarInput_formatValue_detachedTab.js] +skip-if = + apple_catalina # Bug 1756585 + os == 'win' # Bug 1756585 +[browser_UrlbarInput_hiddenFocus.js] +[browser_UrlbarInput_overflow.js] +[browser_UrlbarInput_overflow_resize.js] +[browser_UrlbarInput_privateFeature.js] +[browser_UrlbarInput_searchTerms.js] +[browser_UrlbarInput_searchTerms_backgroundTabs.js] +[browser_UrlbarInput_searchTerms_modifiedUrl.js] +[browser_UrlbarInput_searchTerms_moveTab.js] +[browser_UrlbarInput_searchTerms_popup.js] +[browser_UrlbarInput_searchTerms_revert.js] +[browser_UrlbarInput_searchTerms_searchBar.js] +[browser_UrlbarInput_searchTerms_searchMode.js] +[browser_UrlbarInput_searchTerms_switch_tab.js] +[browser_UrlbarInput_searchTerms_telemetry.js] +[browser_UrlbarInput_setURI.js] +https_first_disabled = true +skip-if = + apple_catalina && debug # Bug 1773790 +[browser_UrlbarInput_tooltip.js] +[browser_UrlbarInput_trimURLs.js] +https_first_disabled = true +[browser_aboutHomeLoading.js] +skip-if = + tsan # Intermittently times out, see 1622698 (frequent on TSan). + os == 'linux' && bits == 64 && !debug # Bug 1622698 +[browser_acknowledgeFeedbackAndDismissal.js] +[browser_action_searchengine.js] +[browser_action_searchengine_alias.js] +[browser_add_search_engine.js] +support-files = + add_search_engine_0.xml + add_search_engine_1.xml + add_search_engine_2.xml + add_search_engine_3.xml + add_search_engine_invalid.html + add_search_engine_one.html + add_search_engine_many.html + add_search_engine_same_names.html + add_search_engine_two.html +[browser_autoFill_backspaced.js] +[browser_autoFill_canonize.js] +https_first_disabled = true +[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_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] +https_first_disabled = true +[browser_autocomplete_no_title.js] +[browser_autocomplete_readline_navigation.js] +skip-if = os != "mac" # Mac only feature +[browser_autocomplete_tag_star_visibility.js] +[browser_bestMatch.js] +[browser_blanking.js] +support-files = + file_blank_but_not_blank.html +[browser_bufferer_onQueryResults.js] +[browser_calculator.js] +[browser_canonizeURL.js] +https_first_disabled = true +support-files = + searchSuggestionEngine.xml + searchSuggestionEngine.sjs +[browser_caret_position.js] +[browser_click_row_border.js] +[browser_closePanelOnClick.js] +[browser_content_opener.js] +[browser_contextualsearch.js] +[browser_copy_during_load.js] +support-files = + slow-page.sjs +[browser_copying.js] +https_first_disabled = true +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] +https_first_disabled = true +[browser_dragdropURL.js] +[browser_dynamicResults.js] +https_first_disabled = true +support-files = + dynamicResult0.css + dynamicResult1.css +[browser_edit_invalid_url.js] +[browser_engagement.js] +[browser_enter.js] +[browser_enterAfterMouseOver.js] +[browser_focusedCmdK.js] +[browser_groupLabels.js] +support-files = + searchSuggestionEngine.xml + searchSuggestionEngine.sjs +[browser_handleCommand_fallback.js] +[browser_hashChangeProxyState.js] +[browser_helpUrl.js] +[browser_heuristicNotAddedFirst.js] +[browser_hideHeuristic.js] +[browser_ime_composition.js] +[browser_inputHistory.js] +https_first_disabled = true +support-files = + searchSuggestionEngine.xml + searchSuggestionEngine.sjs +[browser_inputHistory_autofill.js] +[browser_inputHistory_emptystring.js] +[browser_keepStateAcrossTabSwitches.js] +https_first_disabled = true +[browser_keyword.js] +support-files = + print_postdata.sjs + searchSuggestionEngine.xml + searchSuggestionEngine.sjs +[browser_keywordBookmarklets.js] +[browser_keywordSearch.js] +support-files = + searchSuggestionEngine.xml + searchSuggestionEngine.sjs +[browser_keywordSearch_postData.js] +support-files = + POSTSearchEngine.xml + print_postdata.sjs +[browser_keyword_override.js] +[browser_keyword_select_and_type.js] +[browser_loadRace.js] +[browser_locationBarCommand.js] +https_first_disabled = true +skip-if = + os == 'linux' && bits == 64 && !debug # Bug 1787020 +[browser_locationBarExternalLoad.js] +[browser_locationchange_urlbar_edit_dos.js] +support-files = + file_urlbar_edit_dos.html +[browser_middleClick.js] +[browser_new_tab_urlbar_reset.js] +[browser_oneOffs.js] +support-files = + searchSuggestionEngine.xml + searchSuggestionEngine.sjs +[browser_oneOffs_contextMenu.js] +support-files = + searchSuggestionEngine.xml + searchSuggestionEngine.sjs +[browser_oneOffs_heuristicRestyle.js] +skip-if = + os == "linux" && bits == 64 && !debug # Bug 1775811 +[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_pasteAndGo.js] +https_first_disabled = true +[browser_paste_multi_lines.js] +[browser_paste_then_focus.js] +[browser_paste_then_switch_tab.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_queryContextCache.js] +[browser_quickactions.js] +skip-if = + os == "linux" # Bug 1806090 +[browser_quickactions_devtools.js] +[browser_quickactions_tab_refocus.js] +[browser_raceWithTabs.js] +[browser_redirect_error.js] +support-files = redirect_error.sjs +[browser_remoteness_switch.js] +https_first_disabled = true +[browser_remotetab.js] +[browser_removeUnsafeProtocolsFromURLBarPaste.js] +[browser_remove_match.js] +[browser_restoreEmptyInput.js] +[browser_resultSpan.js] +[browser_result_menu.js] +[browser_result_onSelection.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] +https_first_disabled = true +support-files = + dummy_page.html +[browser_searchMode_engineRemoval.js] +[browser_searchMode_excludeResults.js] +[browser_searchMode_heuristic.js] +https_first_disabled = true +[browser_searchMode_indicator.js] +https_first_disabled = true +support-files = + searchSuggestionEngine.xml + searchSuggestionEngine.sjs +[browser_searchMode_indicator_clickthrough.js] +[browser_searchMode_localOneOffs_actionText.js] +support-files = + searchSuggestionEngine.xml + searchSuggestionEngine.sjs +[browser_searchMode_newWindow.js] +[browser_searchMode_no_results.js] +[browser_searchMode_oneOffButton.js] +[browser_searchMode_pickResult.js] +https_first_disabled = true +[browser_searchMode_preview.js] +[browser_searchMode_sessionStore.js] +https_first_disabled = true +skip-if = os == 'mac' && debug && verify # bug 1671045 +[browser_searchMode_setURI.js] +https_first_disabled = true +[browser_searchMode_suggestions.js] +support-files = + searchSuggestionEngine.xml + searchSuggestionEngine.sjs + searchSuggestionEngineMany.xml +[browser_searchMode_switchTabs.js] +[browser_searchSettings.js] +[browser_searchSingleWordNotification.js] +https_first_disabled = true +skip-if = + os == 'linux' && bits == 64 # Bug 1773830 +[browser_searchSuggestions.js] +support-files = + searchSuggestionEngine.xml + searchSuggestionEngine.sjs +[browser_searchTelemetry.js] +support-files = + searchSuggestionEngine.xml + searchSuggestionEngine.sjs +[browser_search_bookmarks_from_bookmarks_menu.js] +[browser_search_history_from_history_panel.js] +[browser_selectStaleResults.js] +support-files = + searchSuggestionEngineSlow.xml + searchSuggestionEngine.sjs +[browser_selectionKeyNavigation.js] +[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_shortcuts_add_search_engine.js] +support-files = + add_search_engine_many.html + add_search_engine_two.html + add_search_engine_0.xml + add_search_engine_1.xml +[browser_speculative_connect.js] +support-files = + searchSuggestionEngine2.xml + searchSuggestionEngine.sjs +[browser_speculative_connect_not_with_client_cert.js] +[browser_stop.js] +[browser_stopSearchOnSelection.js] +support-files = + searchSuggestionEngineSlow.xml + searchSuggestionEngine.sjs +[browser_stop_pending.js] +https_first_disabled = true +support-files = + slow-page.sjs +[browser_strip_on_share.js] +[browser_suggestedIndex.js] +[browser_suppressFocusBorder.js] +[browser_switchTab_closesUrlbarPopup.js] +[browser_switchTab_decodeuri.js] +[browser_switchTab_inputHistory.js] +[browser_switchTab_override.js] +[browser_switchToTabHavingURI_aOpenParams.js] +[browser_switchToTab_chiclet.js] +[browser_switchToTab_closes_newtab.js] +[browser_switchToTab_fullUrl_repeatedKeydown.js] +[browser_tabKeyBehavior.js] +[browser_tabMatchesInAwesomebar.js] +support-files = + moz.png +[browser_tabMatchesInAwesomebar_perwindowpb.js] +[browser_tabToSearch.js] +[browser_textruns.js] +[browser_tokenAlias.js] +[browser_top_sites.js] +https_first_disabled = true +[browser_top_sites_private.js] +https_first_disabled = true +[browser_typed_value.js] +[browser_unitConversion.js] +[browser_updateForDomainCompletion.js] +https_first_disabled = true +[browser_urlbar_annotation.js] +support-files = + redirect_to.sjs +[browser_urlbar_event_telemetry_abandonment.js] +support-files = + searchSuggestionEngine.xml + searchSuggestionEngine.sjs +skip-if = os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +[browser_urlbar_event_telemetry_engagement.js] +https_first_disabled = true +skip-if = + apple_catalina # Bug 1625690 + apple_silicon # Disabled due to bleedover with other tests when run in regular suites; passes in "failures" jobs + os == 'linux' # Bug 1748986, bug 1775824 + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +support-files = + searchSuggestionEngine.xml + searchSuggestionEngine.sjs +[browser_urlbar_event_telemetry_noEvent.js] +[browser_urlbar_selection.js] +skip-if = (os == 'mac') # bug 1570474 +[browser_urlbar_telemetry.js] +tags = search-telemetry +support-files = + urlbarTelemetrySearchSuggestions.sjs + urlbarTelemetrySearchSuggestions.xml +[browser_urlbar_telemetry_autofill.js] +tags = search-telemetry +[browser_urlbar_telemetry_dynamic.js] +tags = search-telemetry +support-files = + urlbarTelemetryUrlbarDynamic.css +[browser_urlbar_telemetry_extension.js] +tags = search-telemetry +[browser_urlbar_telemetry_handoff.js] +tags = search-telemetry +[browser_urlbar_telemetry_persisted.js] +tags = search-telemetry +[browser_urlbar_telemetry_places.js] +https_first_disabled = true +tags = search-telemetry +[browser_urlbar_telemetry_quickactions.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_sponsored_topsites.js] +https_first_disabled = true +tags = search-telemetry +[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_zeroPrefix.js] +tags = search-telemetry +[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_view_selectionByMouse.js] +skip-if = + os == "linux" && asan # Bug 1789051 +[browser_waitForLoadOrTimeout.js] +https_first_disabled = true +skip-if = + tsan # Bug 1683730 + os == "linux" && bits == 64 && !debug # Bug 1666092 +[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..d4b73603f9 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_formatValue.js @@ -0,0 +1,178 @@ +/* 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("mozilla.org"); + testVal("mözilla.org"); + testVal("mozilla.imaginatory"); + + testVal("mozilla.org"); + testVal("mozilla.org"); + testVal("mozilla.org"); + testVal("mozilla.org"); + testVal("mozilla.org"); + testVal("mozilla.org"); + testVal("mozilla.com"); + testVal("mozilla.com"); + testVal("mozilla.com"); + + testVal("mozilla.org"); + testVal("mozilla.org"); + + testVal("mozilla.org"); + testVal("mozilla.org"); + testVal("mozilla.org"); + testVal("mozilla.org"); + testVal("mozilla.org"); + testVal("mozilla.org"); + + testVal("mozilla.org< >"); + testVal("mozilla.org< >"); + // RTL characters in domain change order of domain and suffix. Domain should + // be highlighted correctly. + testVal("اختبار.اختبار"); + + testVal("mozilla.org"); + testVal("mozilla.org"); + testVal("mozilla.org"); + testVal("mozilla.org"); + testVal("mozilla.org"); + testVal("mozilla.org"); + testVal("foo.bar"); + testVal("foo.bar<#mozilla.org>"); + testVal("foo.bar"); + testVal("foo.bar"); + testVal("foo.bar<#x@mozilla.org>"); + testVal("foo.bar<#@x@mozilla.org>"); + testVal("foo.bar"); + testVal("foo.bar"); + testVal("mozilla.org"); + testVal("mozilla.org"); + testVal("mozilla.org"); + testVal("mozilla.org"); + testVal("mozilla.org"); + testVal( + "foopy:\\blah@somewhere.com//whatever/", + "foopy" + ); + + testVal("mozilla.org<:666/file.ext>"); + testVal("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 + ""); + testVal(IP + "<:666/file.ext>"); + testVal("" + IP); + testVal(`${IP}`); + testVal(`${IP}<:666/file.ext>`); + testVal(`${IP}<:666/file.ext>`); + testVal(`user:\\pass@${IP}/`, `user`); + }); + + 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_formatValue_detachedTab.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_formatValue_detachedTab.js new file mode 100644 index 0000000000..02da2a8e2b --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_formatValue_detachedTab.js @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +// After detaching a tab into a new window, the input value in the new window +// should be formatted. + +add_task(async function detach() { + // Sometimes the value isn't formatted on Mac when running in verify chaos + // mode. The usual, proper front-end code path is hit, and the path that + // removes formatting is not hit, so it seems like some kind of race in the + // editor or selection code in Gecko. Since this has only been observed on Mac + // in chaos mode and doesn't seem to be a problem in urlbar code, skip the + // test in that case. + if (AppConstants.platform == "macosx" && Services.env.get("MOZ_CHAOSMODE")) { + Assert.ok(true, "Skipping test in chaos mode on Mac"); + return; + } + + UrlbarPrefs.clear("formatting.enabled"); + Assert.ok( + UrlbarPrefs.get("formatting.enabled"), + "Formatting is enabled by default" + ); + + info("Waiting for new tab"); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: "https://example.com/detach", + }); + + let winPromise = BrowserTestUtils.waitForNewWindow(); + info("Detaching tab"); + let win = gBrowser.replaceTabWithWindow(tab, {}); + info("Waiting for new window"); + await winPromise; + + // Wait an extra tick for good measure since the code itself also waits for + // `delayedStartupPromise`. + info("Waiting for delayed startup in new window"); + await win.delayedStartupPromise; + info("Waiting for tick"); + await TestUtils.waitForTick(); + + assertValue("example.com", win); + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Asserts formatting in the input is correct. + * + * @param {string} expectedValue + * The URL to test. The parts the are expected to be de-emphasized should be + * wrapped in "<" and ">" chars. + * @param {window} win + * The input in this window will be tested. + */ +function assertValue(expectedValue, win = window) { + let selectionController = win.gURLBar.editor.selectionController; + let selection = selectionController.getSelection( + selectionController.SELECTION_URLSECONDARY + ); + let value = win.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, + expectedValue, + "Correct part of the url is de-emphasized" + ); +} 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..08e5ae97d3 --- /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..b0a3337d84 --- /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..879911d703 --- /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_privateFeature.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_privateFeature.js new file mode 100644 index 0000000000..fb81e9f536 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_privateFeature.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/. */ + +// Tests that _loadURL correctly sets and passes on the `private` window +// attribute (or not) with various arguments. + +add_task(async function privateFeatureSetOnNewWindowImplicitly() { + let privateWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + let newWinOpened = BrowserTestUtils.waitForNewWindow(); + + privateWin.gURLBar._loadURL("about:blank", null, "window", {}); + + let newWin = await newWinOpened; + Assert.equal( + newWin.docShell.treeOwner + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIAppWindow).chromeFlags & + Ci.nsIWebBrowserChrome.CHROME_PRIVATE_WINDOW, + Ci.nsIWebBrowserChrome.CHROME_PRIVATE_WINDOW, + "New window opened from existing private window should be marked as private" + ); + await BrowserTestUtils.closeWindow(newWin); + await BrowserTestUtils.closeWindow(privateWin); +}); + +add_task(async function privateFeatureSetOnNewWindowExplicitly() { + let privateWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + let newWinOpened = BrowserTestUtils.waitForNewWindow(); + + privateWin.gURLBar._loadURL("about:blank", null, "window", { private: true }); + + let newWin = await newWinOpened; + Assert.equal( + newWin.docShell.treeOwner + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIAppWindow).chromeFlags & + Ci.nsIWebBrowserChrome.CHROME_PRIVATE_WINDOW, + Ci.nsIWebBrowserChrome.CHROME_PRIVATE_WINDOW, + "New window opened from existing private window should be marked as private" + ); + await BrowserTestUtils.closeWindow(newWin); + await BrowserTestUtils.closeWindow(privateWin); +}); + +add_task(async function privateFeatureNotSetOnNewWindowExplicitly() { + let privateWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + let newWinOpened = BrowserTestUtils.waitForNewWindow(); + + privateWin.gURLBar._loadURL("about:blank", null, "window", { + private: false, + }); + + let newWin = await newWinOpened; + Assert.notEqual( + newWin.docShell.treeOwner + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIAppWindow).chromeFlags & + Ci.nsIWebBrowserChrome.CHROME_PRIVATE_WINDOW, + Ci.nsIWebBrowserChrome.CHROME_PRIVATE_WINDOW, + "New window opened from existing private window should be marked as private" + ); + await BrowserTestUtils.closeWindow(newWin); + await BrowserTestUtils.closeWindow(privateWin); +}); diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms.js new file mode 100644 index 0000000000..3d38036d84 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms.js @@ -0,0 +1,306 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// These tests check the behavior of the Urlbar when loading a page +// whose url matches that of the default search engine. + +let defaultTestEngine; + +// The main search string used in tests +const SEARCH_STRING = "chocolate cake"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", true]], + }); + + await SearchTestUtils.installSearchExtension( + { + name: "MozSearch", + search_url: "https://www.example.com/", + search_url_get_params: "q={searchTerms}&pc=fake_code", + }, + { setAsDefault: true } + ); + defaultTestEngine = Services.search.getEngineByName("MozSearch"); + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + }); +}); + +// Starts a search with a tab and asserts that +// the state of the Urlbar contains the search term +async function searchWithTab( + searchString, + tab = null, + engine = defaultTestEngine +) { + if (!tab) { + tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + } + + let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl(engine, searchString); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + expectedSearchUrl + ); + + gURLBar.focus(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: searchString, + fireInputEvent: true, + }); + EventUtils.synthesizeKey("KEY_Enter"); + await browserLoadedPromise; + + assertSearchStringIsInUrlbar(searchString); + + return { tab, expectedSearchUrl }; +} + +// Search terms should show up in the url bar if the pref is on +// and the SERP url matches the one constructed in Firefox +add_task(async function list_of_search_strings() { + const searches = [ + { + // Single word + searchString: "chocolate", + }, + { + // Word with space + searchString: "chocolate cake", + }, + { + // Special characters + searchString: "chocolate;,?:@&=+$-_.!~*'()#cake", + }, + { + searchString: '"chocolate cake" -recipes', + }, + { + // Search with special characters + searchString: "site:example.com chocolate -cake", + }, + ]; + + for (let { searchString } of searches) { + let { tab } = await searchWithTab(searchString); + BrowserTestUtils.removeTab(tab); + } +}); + +// If a user does a search, goes to another page, and then +// goes back to the SERP, the search term should show. +add_task(async function go_back() { + let { tab } = await searchWithTab(SEARCH_STRING); + + let browserLoadedPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + BrowserTestUtils.loadURIString( + tab.linkedBrowser, + "http://www.example.com/some_url" + ); + await browserLoadedPromise; + + let pageShowPromise = BrowserTestUtils.waitForContentEvent( + tab.linkedBrowser, + "pageshow" + ); + tab.linkedBrowser.goBack(); + await pageShowPromise; + + assertSearchStringIsInUrlbar(SEARCH_STRING); + + BrowserTestUtils.removeTab(tab); +}); + +// Manually loading a url that matches a search query url +// should show the search term in the Urlbar. +add_task(async function load_url() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl( + defaultTestEngine, + SEARCH_STRING + ); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + expectedSearchUrl + ); + BrowserTestUtils.loadURIString(tab.linkedBrowser, expectedSearchUrl); + await browserLoadedPromise; + assertSearchStringIsInUrlbar(SEARCH_STRING); + + BrowserTestUtils.removeTab(tab); +}); + +// Focusing and blurring the urlbar while the search terms +// persist should change the pageproxystate. +add_task(async function focus_and_unfocus() { + let { tab } = await searchWithTab(SEARCH_STRING); + + gURLBar.focus(); + Assert.equal( + gURLBar.getAttribute("pageproxystate"), + "invalid", + "Should have matching pageproxystate." + ); + + gURLBar.blur(); + Assert.equal( + gURLBar.getAttribute("pageproxystate"), + "valid", + "Should have matching pageproxystate." + ); + + BrowserTestUtils.removeTab(tab); +}); + +// If the user modifies the search term, blurring the +// urlbar should keep the urlbar in an invalid pageproxystate. +add_task(async function focus_and_unfocus_modified() { + let { tab } = await searchWithTab(SEARCH_STRING); + + gURLBar.focus(); + Assert.equal( + gURLBar.getAttribute("pageproxystate"), + "invalid", + "Should have matching pageproxystate." + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: "another search term", + fireInputEvent: true, + }); + + gURLBar.blur(); + Assert.equal( + gURLBar.getAttribute("pageproxystate"), + "invalid", + "Should have matching pageproxystate." + ); + + BrowserTestUtils.removeTab(tab); +}); + +// If Top Sites is cached in the UrlbarView, don't show it if the search terms +// persist in the Urlbar. +add_task(async function focus_after_top_sites() { + await SpecialPowers.pushPrefEnv({ + set: [ + // Prevent the persist tip from interrupting clicking the Urlbar + // after the the SERP has been loaded. + [ + `browser.urlbar.tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.PERSIST}`, + 10000, + ], + ["browser.newtabpage.activity-stream.feeds.topsites", true], + ], + }); + + // Populate Top Sites on a clean version of Places. + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesTestUtils.promiseAsyncUpdates(); + await TestUtils.waitForTick(); + + const urls = []; + const N_TOP_SITES = 5; + const N_VISITS = 5; + + for (let i = 0; i < N_TOP_SITES; i++) { + let url = `https://${i}.example.com/hello_world${i}`; + urls.unshift(url); + // Each URL needs to be added several times to boost its frecency enough to + // qualify as a top site. + for (let j = 0; j < N_VISITS; j++) { + await PlacesTestUtils.addVisits(url); + } + } + + let changedPromise = TestUtils.topicObserved("newtab-top-sites-changed").then( + () => info("Observed newtab-top-sites-changed") + ); + await updateTopSites(sites => sites?.length == N_TOP_SITES); + await changedPromise; + + // Ensure Top Sites is cached. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + 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), + N_TOP_SITES, + `The number of results should be the same as the number of Top Sites ${N_TOP_SITES}.` + ); + for (let i = 0; i < urls.length; i++) { + let { url } = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + Assert.equal(url, urls[i], "The result url should be a Top Site."); + } + + let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl( + defaultTestEngine, + SEARCH_STRING + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: SEARCH_STRING, + fireInputEvent: true, + }); + EventUtils.synthesizeKey("KEY_Enter"); + + await BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + expectedSearchUrl + ); + Assert.equal( + gBrowser.selectedBrowser.searchTerms, + SEARCH_STRING, + "The search term should be in the Urlbar." + ); + + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.notEqual( + details.url, + urls[0], + "The first result should not be a Top Site." + ); + Assert.equal( + details.heuristic, + true, + "The first result should be the heuristic result." + ); + Assert.equal( + details.url, + expectedSearchUrl, + "The first result url should be the same as the SERP." + ); + Assert.equal( + details.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "The first result be a search result." + ); + Assert.equal( + details.searchParams?.query, + SEARCH_STRING, + "The first result should have a matching query." + ); + + // Clean up. + SpecialPowers.popPrefEnv(); + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_backgroundTabs.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_backgroundTabs.js new file mode 100644 index 0000000000..8ed29a9c5b --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_backgroundTabs.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/. */ + +// These tests check the behavior of the Urlbar when search terms are +// expected to be shown and tabs are opened in the background. + +let defaultTestEngine; + +// The main search string used in tests +const SEARCH_STRING = "chocolate cake"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", true]], + }); + + await SearchTestUtils.installSearchExtension( + { + name: "MozSearch", + search_url: "https://www.example.com/", + search_url_get_params: "q={searchTerms}&pc=fake_code", + }, + { setAsDefault: true } + ); + defaultTestEngine = Services.search.getEngineByName("MozSearch"); + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + }); +}); + +// If a user opens background tab search from the Urlbar, +// the search term should show when the tab is focused. +add_task(async function ctrl_open() { + let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl( + defaultTestEngine, + SEARCH_STRING + ); + // Search for the term in a new background tab. + let newTabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + expectedSearchUrl + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: SEARCH_STRING, + fireInputEvent: true, + }); + gURLBar.focus(); + + EventUtils.synthesizeKey("KEY_Enter", { + altKey: true, + shiftKey: true, + }); + + // Find the background tab that was created, and switch to it. + let backgroundTab = await newTabPromise; + await BrowserTestUtils.switchTab(gBrowser, backgroundTab); + assertSearchStringIsInUrlbar(SEARCH_STRING); + + BrowserTestUtils.removeTab(backgroundTab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_modifiedUrl.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_modifiedUrl.js new file mode 100644 index 0000000000..4c20864171 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_modifiedUrl.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/. */ + +// These tests check the behavior of the Urlbar when search terms are +// expected to be shown but the url is modified from what the browser expects. + +let defaultTestEngine; + +// The main search string used in tests +const SEARCH_STRING = "chocolate cake"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", true]], + }); + + await SearchTestUtils.installSearchExtension( + { + name: "MozSearch", + search_url: "https://www.example.com/", + search_url_get_params: "q={searchTerms}&pc=fake_code", + }, + { setAsDefault: true } + ); + defaultTestEngine = Services.search.getEngineByName("MozSearch"); + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + }); +}); + +// If a SERP uses the History API to modify the URI, +// the search term should still show in the URL bar. +add_task(async function history_push_state() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl( + defaultTestEngine, + SEARCH_STRING + ); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + expectedSearchUrl + ); + BrowserTestUtils.loadURIString(tab.linkedBrowser, expectedSearchUrl); + await browserLoadedPromise; + + let locationChangePromise = BrowserTestUtils.waitForLocationChange(gBrowser); + await SpecialPowers.spawn(tab.linkedBrowser, [], function () { + let url = new URL(content.window.location); + url.searchParams.set("pc", "fake_code_2"); + content.history.pushState({}, "", url); + }); + + await locationChangePromise; + // Check URI to make sure that it's actually been changed + Assert.equal( + gBrowser.currentURI.spec, + `https://www.example.com/?q=chocolate+cake&pc=fake_code_2`, + "URI of Urlbar should have changed" + ); + + Assert.equal( + gURLBar.value, + SEARCH_STRING, + `Search string ${SEARCH_STRING} should be in the url bar` + ); + + BrowserTestUtils.removeTab(tab); +}); + +// Loading a url that looks like a search query url but has additional +// query params should not show the search term in the Urlbar. +add_task(async function url_with_additional_query_params() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl( + defaultTestEngine, + SEARCH_STRING + ); + // Add a query param + expectedSearchUrl += "&another_code=something_else"; + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + expectedSearchUrl + ); + BrowserTestUtils.loadURIString(tab.linkedBrowser, expectedSearchUrl); + await browserLoadedPromise; + + Assert.equal(gURLBar.value, expectedSearchUrl, `URL should be in URL bar`); + Assert.equal( + gURLBar.getAttribute("pageproxystate"), + "valid", + "Pageproxystate should be valid" + ); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_moveTab.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_moveTab.js new file mode 100644 index 0000000000..59f0eca916 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_moveTab.js @@ -0,0 +1,133 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + These tests check the behavior of the Urlbar when search terms are shown + and the tab with the default SERP moves from one window to another. + + Unlike other searchTerm tests, these modify the currentURI to ensure + that the currentURI has a different spec than the default SERP so that + the search terms won't show if the originalURI wasn't properly copied + during the tab swap. +*/ + +let originalEngine, defaultTestEngine; + +// The main search keyword used in tests +const SEARCH_STRING = "chocolate cake"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", true]], + }); + + await SearchTestUtils.installSearchExtension({ + name: "MozSearch", + search_url: "https://www.example.com/", + search_url_get_params: "q={searchTerms}&pc=fake_code", + }); + defaultTestEngine = Services.search.getEngineByName("MozSearch"); + + originalEngine = await Services.search.getDefault(); + await Services.search.setDefault( + defaultTestEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + registerCleanupFunction(async function () { + await Services.search.setDefault( + originalEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + await PlacesUtils.history.clear(); + }); +}); + +async function searchWithTab( + searchString, + tab = null, + engine = defaultTestEngine +) { + if (!tab) { + tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + } + + let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl(engine, searchString); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + expectedSearchUrl + ); + + gURLBar.focus(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: searchString, + fireInputEvent: true, + }); + EventUtils.synthesizeKey("KEY_Enter"); + await browserLoadedPromise; + + return { tab, expectedSearchUrl }; +} + +// Move a tab showing the search term into its own window. +add_task(async function move_tab_into_new_window() { + let { tab, expectedSearchUrl } = await searchWithTab(SEARCH_STRING); + + // Mock the default SERP modifying the existing url + // so that the originalURI and currentURI differ. + await SpecialPowers.spawn( + tab.linkedBrowser, + [expectedSearchUrl], + async url => { + content.history.pushState({}, "", url + "&pc2=firefox"); + } + ); + + // Move the tab into its own window. + let newWindow = gBrowser.replaceTabWithWindow(tab); + await BrowserTestUtils.waitForEvent(tab.linkedBrowser, "SwapDocShells"); + + assertSearchStringIsInUrlbar(SEARCH_STRING, { win: newWindow }); + + // Clean up. + await BrowserTestUtils.closeWindow(newWindow); +}); + +// Move a tab from its own window into an existing window. +add_task(async function move_tab_into_existing_window() { + // Load a second window with the default SERP. + let win = await BrowserTestUtils.openNewBrowserWindow({ remote: true }); + let browser = win.gBrowser.selectedBrowser; + let tab = win.gBrowser.tabs[0]; + + // Load the default SERP into the second window. + let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl( + defaultTestEngine, + SEARCH_STRING + ); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + browser, + false, + expectedSearchUrl + ); + BrowserTestUtils.loadURIString(browser, expectedSearchUrl); + await browserLoadedPromise; + + // Mock the default SERP modifying the existing url + // so that the originalURI and currentURI differ. + await SpecialPowers.spawn(browser, [expectedSearchUrl], async url => { + content.history.pushState({}, "", url + "&pc2=firefox"); + }); + + // Make the first window adopt and switch to that tab. + tab = gBrowser.adoptTab(tab); + await BrowserTestUtils.switchTab(gBrowser, tab); + assertSearchStringIsInUrlbar(SEARCH_STRING); + + // Clean up. + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_popup.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_popup.js new file mode 100644 index 0000000000..d25e17d960 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_popup.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/. */ + +// These tests check the behavior of the Urlbar when persist search terms +// are either enabled or disabled, and a popup notification is shown. + +function waitForPopupNotification() { + let promisePopupShown = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + PopupNotifications.show( + gBrowser.selectedBrowser, + "test-notification", + "This is a sample popup." + ); + return promisePopupShown; +} + +// The main search string used in tests. +const SEARCH_TERM = "chocolate"; +const PREF_FEATUREGATE = "browser.urlbar.showSearchTerms.featureGate"; +let defaultTestEngine; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_FEATUREGATE, true]], + }); + await SearchTestUtils.installSearchExtension( + { + name: "MozSearch", + search_url: "https://www.example.com/", + search_url_get_params: "q={searchTerms}&pc=fake_code", + }, + { setAsDefault: true } + ); + + defaultTestEngine = Services.search.getEngineByName("MozSearch"); + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + }); +}); + +async function searchWithTab( + searchString, + tab = null, + engine = defaultTestEngine, + expectedPersistedSearchTerms = true +) { + if (!tab) { + tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + } + + let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl(engine, searchString); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + expectedSearchUrl + ); + + gURLBar.focus(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: searchString, + fireInputEvent: true, + }); + EventUtils.synthesizeKey("KEY_Enter"); + await browserLoadedPromise; + + if (expectedPersistedSearchTerms) { + assertSearchStringIsInUrlbar(searchString); + } + + return { tab, expectedSearchUrl }; +} + +// A notification should cause the urlbar to revert while +// the search term persists. +add_task(async function generic_popup_when_persist_is_enabled() { + let { tab, expectedSearchUrl } = await searchWithTab(SEARCH_TERM); + + await waitForPopupNotification(); + + Assert.equal( + gURLBar.getAttribute("pageproxystate"), + "valid", + "Urlbar should have a valid pageproxystate." + ); + + Assert.equal( + gURLBar.value, + expectedSearchUrl, + "Search url should be in the urlbar." + ); + + BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPrefEnv(); +}); + +// Ensure the urlbar is not being reverted when a prompt is shown +// and the persist feature is disabled. +add_task(async function generic_popup_no_revert_when_persist_is_disabled() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_FEATUREGATE, false]], + }); + + let { tab } = await searchWithTab( + SEARCH_TERM, + null, + defaultTestEngine, + false + ); + + // Have a user typed value in the urlbar to make + // pageproxystate invalid. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: SEARCH_TERM, + }); + gURLBar.blur(); + + await waitForPopupNotification(); + + // Wait a brief amount of time between when the popup is shown + // and when the event handler should fire if it's enabled. + await TestUtils.waitForTick(); + + Assert.equal( + gURLBar.getAttribute("pageproxystate"), + "invalid", + "Urlbar should not be reverted." + ); + + Assert.equal( + gURLBar.value, + SEARCH_TERM, + "User typed value should remain in urlbar." + ); + + BrowserTestUtils.removeTab(tab); + SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_revert.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_revert.js new file mode 100644 index 0000000000..91d6ea403a --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_revert.js @@ -0,0 +1,170 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// These tests check the behavior of the Urlbar when search terms are shown +// and the user reverts the Urlbar. + +let defaultTestEngine; + +// The main search keyword used in tests +const SEARCH_STRING = "chocolate cake"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", true]], + }); + + await SearchTestUtils.installSearchExtension( + { + name: "MozSearch", + search_url: "https://www.example.com/", + search_url_get_params: "q={searchTerms}&pc=fake_code", + }, + { setAsDefault: true } + ); + defaultTestEngine = Services.search.getEngineByName("MozSearch"); + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + }); +}); + +async function searchWithTab( + searchString, + tab = null, + engine = defaultTestEngine +) { + if (!tab) { + tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + } + + let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl(engine, searchString); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + expectedSearchUrl + ); + + gURLBar.focus(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: searchString, + fireInputEvent: true, + }); + EventUtils.synthesizeKey("KEY_Enter"); + await browserLoadedPromise; + + assertSearchStringIsInUrlbar(searchString); + + return { tab, expectedSearchUrl }; +} + +function synthesizeRevert() { + gURLBar.focus(); + EventUtils.synthesizeKey("KEY_Escape", { repeat: 2 }); +} + +// Users should be able to revert the URL bar +add_task(async function revert() { + let { tab, expectedSearchUrl } = await searchWithTab(SEARCH_STRING); + synthesizeRevert(); + + Assert.equal( + gURLBar.value, + expectedSearchUrl, + `Urlbar should have the reverted URI ${expectedSearchUrl} as its value.` + ); + + BrowserTestUtils.removeTab(tab); +}); + +// Users should be able to revert the URL bar, +// and go to the same page. +add_task(async function revert_and_press_enter() { + let { tab, expectedSearchUrl } = await searchWithTab(SEARCH_STRING); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + expectedSearchUrl + ); + + synthesizeRevert(); + gURLBar.focus(); + EventUtils.synthesizeKey("KEY_Enter"); + await browserLoadedPromise; + + BrowserTestUtils.removeTab(tab); +}); + +// Users should be able to revert the URL, and then if they navigate +// to another tab, the tab that was reverted will show the search term again. +add_task(async function revert_and_change_tab() { + let { tab, expectedSearchUrl } = await searchWithTab(SEARCH_STRING); + + synthesizeRevert(); + + Assert.notEqual( + gURLBar.value, + SEARCH_STRING, + `Search string ${SEARCH_STRING} should not be in the url bar` + ); + Assert.equal( + gURLBar.value, + expectedSearchUrl, + `Urlbar should have ${expectedSearchUrl} as value.` + ); + + // Open another tab + let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + // Switch back to the original tab. + await BrowserTestUtils.switchTab(gBrowser, tab); + + // Because the urlbar is focused, the pageproxystate should be invalid. + assertSearchStringIsInUrlbar(SEARCH_STRING, { pageProxyState: "invalid" }); + + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(tab2); +}); + +// If a user reverts a tab, and then does another search, +// they should be able to see the search term again. +add_task(async function revert_and_search_again() { + let { tab } = await searchWithTab(SEARCH_STRING); + synthesizeRevert(); + await searchWithTab("another search string", tab); + BrowserTestUtils.removeTab(tab); +}); + +// If a user reverts the Urlbar while on a default SERP, +// and they navigate away from the page by visiting another +// link or using the back/forward buttons, the Urlbar should +// show the search term again when returning back to the default SERP. +add_task(async function revert_when_using_content() { + let { tab } = await searchWithTab(SEARCH_STRING); + synthesizeRevert(); + await searchWithTab("another search string", tab); + + // Revert the page, and then go back and forth in history. + // The search terms should show up. + synthesizeRevert(); + let pageShowPromise = BrowserTestUtils.waitForContentEvent( + tab.linkedBrowser, + "pageshow" + ); + tab.linkedBrowser.goBack(); + await pageShowPromise; + assertSearchStringIsInUrlbar(SEARCH_STRING); + + pageShowPromise = BrowserTestUtils.waitForContentEvent( + tab.linkedBrowser, + "pageshow" + ); + tab.linkedBrowser.goForward(); + await pageShowPromise; + assertSearchStringIsInUrlbar("another search string"); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_searchBar.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_searchBar.js new file mode 100644 index 0000000000..784d8932ac --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_searchBar.js @@ -0,0 +1,104 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// These tests check the behavior of the Urlbar when a user enables +// the search bar and showSearchTerms is true. + +const { CustomizableUITestUtils } = ChromeUtils.importESModule( + "resource://testing-common/CustomizableUITestUtils.sys.mjs" +); + +const gCUITestUtils = new CustomizableUITestUtils(window); +const SEARCH_STRING = "example_string"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.widget.inNavBar", true], + ["browser.urlbar.showSearchTerms.featureGate", true], + ], + }); + + await SearchTestUtils.installSearchExtension( + { + name: "MozSearch", + search_url: "https://www.example.com/", + search_url_get_params: "q={searchTerms}&pc=fake_code", + }, + { setAsDefault: true } + ); + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + gCUITestUtils.removeSearchBar(); + }); +}); + +function assertSearchStringIsNotInUrlbar(searchString) { + Assert.notEqual( + gURLBar.value, + searchString, + `Search string ${searchString} should not be in the url bar.` + ); + Assert.equal( + gURLBar.getAttribute("pageproxystate"), + "valid", + "Pageproxystate should be valid." + ); + Assert.equal( + gBrowser.selectedBrowser.searchTerms, + "", + "searchTerms should be blank." + ); +} + +// When a user enables the search bar, and does a search in the search bar, +// the search term should not show in the URL bar. +add_task(async function search_bar_on() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + await gCUITestUtils.addSearchBar(); + + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + `https://www.example.com/?q=${SEARCH_STRING}&pc=fake_code` + ); + + let searchBar = BrowserSearch.searchBar; + searchBar.value = SEARCH_STRING; + searchBar.focus(); + EventUtils.synthesizeKey("KEY_Enter"); + + await browserLoadedPromise; + assertSearchStringIsNotInUrlbar(SEARCH_STRING); + + BrowserTestUtils.removeTab(tab); +}); + +// When a user enables the search bar, and does a search in the URL bar, +// the search term should still not show in the URL bar. +add_task(async function search_bar_on_with_url_bar_search() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + await gCUITestUtils.addSearchBar(); + + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + `https://www.example.com/?q=${SEARCH_STRING}&pc=fake_code` + ); + + gURLBar.focus(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: SEARCH_STRING, + fireInputEvent: true, + }); + EventUtils.synthesizeKey("KEY_Enter"); + + await browserLoadedPromise; + assertSearchStringIsNotInUrlbar(SEARCH_STRING); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_searchMode.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_searchMode.js new file mode 100644 index 0000000000..880b597784 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_searchMode.js @@ -0,0 +1,81 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// These tests check the behavior of the Urlbar when using search mode + +let defaultTestEngine; + +// The main search string used in tests +const SEARCH_STRING = "chocolate cake"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", true]], + }); + + await SearchTestUtils.installSearchExtension({ + name: "MozSearch", + search_url: "https://www.example.com/", + search_url_get_params: "q={searchTerms}&pc=fake_code", + }); + defaultTestEngine = Services.search.getEngineByName("MozSearch"); + + await SearchTestUtils.installSearchExtension( + { + name: "MochiSearch", + search_url: "https://mochi.test:8888/", + search_url_get_params: "q={searchTerms}&pc=fake_code", + }, + { setAsDefault: true } + ); + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + }); +}); + +// When a user does a search with search mode, they should +// not see the search term in the URL bar for that engine. +add_task(async function non_default_search() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl( + defaultTestEngine, + SEARCH_STRING + ); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + expectedSearchUrl + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: SEARCH_STRING, + }); + await UrlbarTestUtils.enterSearchMode(window, { + engineName: defaultTestEngine.name, + }); + gURLBar.focus(); + EventUtils.synthesizeKey("KEY_Enter"); + await browserLoadedPromise; + + Assert.equal(gURLBar.value, expectedSearchUrl, `URL should be in URL bar`); + Assert.equal( + gURLBar.getAttribute("pageproxystate"), + "valid", + "Pageproxystate should be valid" + ); + Assert.equal( + gBrowser.userTypedValue, + null, + "There should not be a userTypedValue for a search on a non-default search engine" + ); + Assert.equal( + gBrowser.selectedBrowser.searchTerms, + "", + "searchTerms should be empty." + ); + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_switch_tab.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_switch_tab.js new file mode 100644 index 0000000000..77ee3e19d7 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_switch_tab.js @@ -0,0 +1,139 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// These tests check the behavior of the Urlbar when search terms are shown +// and the user switches between tabs. + +let defaultTestEngine; + +// The main search keyword used in tests +const SEARCH_STRING = "chocolate cake"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", true]], + }); + + await SearchTestUtils.installSearchExtension( + { + name: "MozSearch", + search_url: "https://www.example.com/", + search_url_get_params: "q={searchTerms}&pc=fake_code", + }, + { setAsDefault: true } + ); + defaultTestEngine = Services.search.getEngineByName("MozSearch"); + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + }); +}); + +async function searchWithTab( + searchString, + tab = null, + engine = defaultTestEngine +) { + if (!tab) { + tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + } + + let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl(engine, searchString); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + expectedSearchUrl + ); + + gURLBar.focus(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: searchString, + fireInputEvent: true, + }); + EventUtils.synthesizeKey("KEY_Enter"); + await browserLoadedPromise; + + assertSearchStringIsInUrlbar(searchString); + + return { tab, expectedSearchUrl }; +} + +// Users should be able to search, change the tab, and come +// back to the original tab to see the search term again +add_task(async function change_tab() { + let { tab: tab1 } = await searchWithTab(SEARCH_STRING); + let { tab: tab2 } = await searchWithTab("another keyword"); + let { tab: tab3 } = await searchWithTab("yet another keyword"); + + await BrowserTestUtils.switchTab(gBrowser, tab1); + assertSearchStringIsInUrlbar(SEARCH_STRING); + + await BrowserTestUtils.switchTab(gBrowser, tab2); + assertSearchStringIsInUrlbar("another keyword"); + + await BrowserTestUtils.switchTab(gBrowser, tab3); + assertSearchStringIsInUrlbar("yet another keyword"); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); + BrowserTestUtils.removeTab(tab3); +}); + +// If a user types in the URL bar, and the user goes to a +// different tab, the original tab should still contain the +// text written by the user. +add_task(async function user_overwrites_search_term() { + let { tab: tab1 } = await searchWithTab(SEARCH_STRING); + + gURLBar.focus(); + gURLBar.select(); + EventUtils.sendString("another_word"); + + Assert.notEqual( + gURLBar.value, + SEARCH_STRING, + `Search string ${SEARCH_STRING} should not be in the url bar` + ); + + // Open a new tab, switch back to the first and + // check that the user typed value is still there. + let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser); + await BrowserTestUtils.switchTab(gBrowser, tab1); + + Assert.equal( + gURLBar.value, + "another_word", + "another_word should be in the url bar" + ); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); + +// If a user clears the URL bar, and goes to a different tab, +// and returns to the initial tab, it should show the search term again. +add_task(async function user_overwrites_search_term() { + let { tab: tab1 } = await searchWithTab(SEARCH_STRING); + + gURLBar.focus(); + gURLBar.select(); + EventUtils.sendKey("delete"); + + Assert.equal(gURLBar.value, "", "Empty string should be in url bar."); + + // Open a new tab, switch back to the first and check + // the blank string is replaced with the search string. + let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser); + await BrowserTestUtils.switchTab(gBrowser, tab1); + + assertSearchStringIsInUrlbar(SEARCH_STRING, { + pageProxyState: "invalid", + userTypedValue: "", + }); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_telemetry.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_telemetry.js new file mode 100644 index 0000000000..bdad68e0ef --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_telemetry.js @@ -0,0 +1,378 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * These tests check that we record the number of times search terms + * persist in the Urlbar, and when search terms are cleared due to a + * PopupNotification. + * + * This is different from existing telemetry that tracks whether users + * interacted with the Urlbar or made another search while the search + * terms were peristed. + */ + +let defaultTestEngine; + +// The main search string used in tests +const SEARCH_STRING = "chocolate cake"; + +// Telemetry keys. +const PERSISTED_VIEWED = "urlbar.persistedsearchterms.view_count"; +const PERSISTED_REVERTED = "urlbar.persistedsearchterms.revert_by_popup_count"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", true]], + }); + await SearchTestUtils.installSearchExtension( + { + name: "MozSearch", + search_url: "https://www.example.com/", + search_url_get_params: "q={searchTerms}&pc=fake_code", + }, + { setAsDefault: true } + ); + defaultTestEngine = Services.search.getEngineByName("MozSearch"); + Services.telemetry.clearScalars(); + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + Services.telemetry.clearScalars(); + }); +}); + +// Starts a search with a tab and asserts that +// the state of the Urlbar contains the search term. +async function searchWithTab( + searchString, + tab = null, + engine = defaultTestEngine, + assertSearchString = true +) { + if (!tab) { + tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + } + + let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl(engine, searchString); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + expectedSearchUrl + ); + + gURLBar.focus(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: searchString, + fireInputEvent: true, + }); + EventUtils.synthesizeKey("KEY_Enter"); + await browserLoadedPromise; + + if (assertSearchString) { + assertSearchStringIsInUrlbar(searchString); + } + + return { tab, expectedSearchUrl }; +} + +add_task(async function load_page_with_persisted_search() { + let { tab } = await searchWithTab(SEARCH_STRING); + const scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 1); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined); + + // Clean up. + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function load_page_without_persisted_search() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", false]], + }); + + let { tab } = await searchWithTab( + SEARCH_STRING, + null, + defaultTestEngine, + false + ); + + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, undefined); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined); + + // Clean up. + BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPrefEnv(); +}); + +// Multiple searches should result in the correct number of recorded views. +add_task(async function load_page_n_times() { + let N = 5; + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + for (let index = 0; index < N; ++index) { + await searchWithTab(SEARCH_STRING, tab); + } + + const scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, N); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined); + + // Clean up. + BrowserTestUtils.removeTab(tab); +}); + +// A persisted search view event should not be recorded when unfocusing the urlbar. +add_task(async function focus_and_unfocus() { + let { tab } = await searchWithTab(SEARCH_STRING); + + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 1); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined); + + gURLBar.focus(); + gURLBar.select(); + gURLBar.blur(); + + // Focusing and unfocusing the urlbar shouldn't change the persisted view count. + scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 1); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined); + + // Clean up. + BrowserTestUtils.removeTab(tab); +}); + +// A persisted search view event should not be recorded by a +// pushState event after a page has been loaded. +add_task(async function history_api() { + let { tab } = await searchWithTab(SEARCH_STRING); + + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 1); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined); + + await SpecialPowers.spawn(tab.linkedBrowser, [], function () { + let url = new URL(content.window.location); + let someState = { value: true }; + url.searchParams.set("pc", "fake_code_2"); + content.history.pushState(someState, "", url); + someState.value = false; + content.history.replaceState(someState, "", url); + }); + + scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 1); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined); + + // Clean up. + BrowserTestUtils.removeTab(tab); +}); + +// A persisted search view event should be recorded when switching back to a tab +// that contains search terms. +add_task(async function switch_tabs() { + let { tab } = await searchWithTab(SEARCH_STRING); + + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 1); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined); + + let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser); + await BrowserTestUtils.switchTab(gBrowser, tab); + + scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 2); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined); + + // Clean up. + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(tab2); +}); + +// A telemetry event should be recorded when returning to a persisted SERP via tabhistory. +add_task(async function tabhistory() { + let { tab } = await searchWithTab(SEARCH_STRING); + + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 1); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined); + + let browserLoadedPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + BrowserTestUtils.loadURIString( + tab.linkedBrowser, + "https://www.example.com/some_url" + ); + await browserLoadedPromise; + + let pageShowPromise = BrowserTestUtils.waitForContentEvent( + tab.linkedBrowser, + "pageshow" + ); + tab.linkedBrowser.goBack(); + await pageShowPromise; + + scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 2); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined); + + // Clean up. + BrowserTestUtils.removeTab(tab); +}); + +// PopupNotification's that rely on an anchor element in the urlbar should trigger +// an increment of the revert counter. +// This assumes the anchor element is present in the Urlbar during a valid pageproxystate. +add_task(async function popup_in_urlbar() { + let { tab } = await searchWithTab(SEARCH_STRING); + let promisePopupShown = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + PopupNotifications.show( + gBrowser.selectedBrowser, + "test-notification", + "This is a sample popup." + ); + await promisePopupShown; + + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 1); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, 1); + + // Clean up. + BrowserTestUtils.removeTab(tab); +}); + +// Non-persistent PopupNotifications won't re-appear if a user switches +// tabs and returns to the tab that had the Popup. +add_task(async function non_persistent_popup_in_urlbar_switch_tab() { + let { tab } = await searchWithTab(SEARCH_STRING); + let promisePopupShown = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + PopupNotifications.show( + gBrowser.selectedBrowser, + "test-notification", + "This is a sample popup.", + "geo-notification-icon" + ); + await promisePopupShown; + + let scalars = TelemetryTestUtils.getProcessScalars("parent", false); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 1); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, 1); + + let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser); + await BrowserTestUtils.switchTab(gBrowser, tab); + + scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 2); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, 1); + + // Clean up. + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(tab2); +}); + +// Persistent PopupNotifications will constantly appear to users +// even if they switch to another tab and switch back. +add_task(async function persistent_popup_in_urlbar_switch_tab() { + let { tab } = await searchWithTab(SEARCH_STRING); + let promisePopupShown = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + PopupNotifications.show( + gBrowser.selectedBrowser, + "test-notification", + "This is a sample popup.", + "geo-notification-icon", + null, + null, + { persistent: true } + ); + await promisePopupShown; + + let scalars = TelemetryTestUtils.getProcessScalars("parent", false); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 1); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, 1); + + let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser); + promisePopupShown = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + await BrowserTestUtils.switchTab(gBrowser, tab); + await promisePopupShown; + + scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 2); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, 2); + + // Clean up. + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(tab2); +}); + +// If the persist feature is not enabled, a telemetry event should not be recorded +// if a PopupNotification uses an anchor in the Urlbar. +add_task(async function popup_in_urlbar_without_feature() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", false]], + }); + + let { tab } = await searchWithTab( + SEARCH_STRING, + null, + defaultTestEngine, + false + ); + let promisePopupShown = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + PopupNotifications.show( + gBrowser.selectedBrowser, + "test-notification", + "This is a sample popup." + ); + await promisePopupShown; + + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, undefined); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined); + + // Clean up. + BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPrefEnv(); +}); + +// If the anchor element for the PopupNotification is not located in the Urlbar, +// a telemetry event should not be recorded. +add_task(async function popup_not_in_urlbar() { + let { tab } = await searchWithTab(SEARCH_STRING); + + let promisePopupShown = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + PopupNotifications.show( + gBrowser.selectedBrowser, + "test-notification", + "This is a sample popup that uses the unified extensions button.", + gUnifiedExtensions.getPopupAnchorID(gBrowser.selectedBrowser, window) + ); + await promisePopupShown; + + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 1); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined); + + // Clean up. + BrowserTestUtils.removeTab(tab); +}); 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..0fa365f8bc --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_setURI.js @@ -0,0 +1,128 @@ +/* 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.loadURIString( + 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..5ecb5e7a90 --- /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..d1bd46f022 --- /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.loadURIString(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") + ? BrowserUIUtils.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..1bb65c0c42 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_aboutHomeLoading.js @@ -0,0 +1,196 @@ +/* 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.importESModule( + "resource:///modules/sessionstore/SessionSaver.sys.mjs" +); +const { TabStateFlusher } = ChromeUtils.importESModule( + "resource:///modules/sessionstore/TabStateFlusher.sys.mjs" +); + +add_task(function addHomeButton() { + CustomizableUI.addWidgetToArea("home-button", "nav-bar"); + registerCleanupFunction(() => + CustomizableUI.removeWidgetFromArea("home-button") + ); +}); + +/** + * 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.loadURIString(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_acknowledgeFeedbackAndDismissal.js b/browser/components/urlbar/tests/browser/browser_acknowledgeFeedbackAndDismissal.js new file mode 100644 index 0000000000..cc1ed29ceb --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_acknowledgeFeedbackAndDismissal.js @@ -0,0 +1,304 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests feedback and dismissal acknowledgments in the view. + */ + +"use strict"; + +// The name of this command must be one that's recognized as not ending the +// urlbar session. See `isSessionOngoing` comments for details. +const FEEDBACK_COMMAND = "show_less_frequently"; + +let gTestProvider; + +add_setup(async function () { + gTestProvider = new TestProvider({ + results: [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + url: "https://example.com/", + isBlockable: true, + blockL10n: { + id: "urlbar-result-menu-dismiss-firefox-suggest", + }, + } + ), + ], + }); + + gTestProvider.commandCount = {}; + UrlbarProvidersManager.registerProvider(gTestProvider); + + // Add a visit so that there's one result above the test result (the + // heuristic) and one below (the visit) just to make sure removing the test + // result doesn't mess up adjacent results. + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + await UrlbarTestUtils.formHistory.clear(); + await PlacesTestUtils.addVisits("https://example.com/aaa"); + + registerCleanupFunction(() => { + UrlbarProvidersManager.unregisterProvider(gTestProvider); + }); +}); + +// Tests dismissal acknowledgment when the dismissed row is not selected. +add_task(async function acknowledgeDismissal_rowNotSelected() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await doDismissTest({ shouldBeSelected: false }); +}); + +// Tests dismissal acknowledgment when the dismissed row is selected. +add_task(async function acknowledgeDismissal_rowSelected() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + + // Select the row. + let resultIndex = await getTestResultIndex(); + while (gURLBar.view.selectedRowIndex != resultIndex) { + this.EventUtils.synthesizeKey("KEY_ArrowDown"); + } + + await doDismissTest({ resultIndex, shouldBeSelected: true }); +}); + +// Tests a feedback acknowledgment command immediately followed by a dismissal +// acknowledgment command. This makes sure that both feedback acknowledgment +// works and a subsequent dismissal command works while the urlbar session +// remains ongoing. +add_task(async function acknowledgeFeedbackAndDismissal() { + // Trigger the suggestion. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + + let resultIndex = await getTestResultIndex(); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex); + + // Click the feedback command. + await UrlbarTestUtils.openResultMenuAndClickItem(window, FEEDBACK_COMMAND, { + resultIndex, + }); + + Assert.equal( + gTestProvider.commandCount[FEEDBACK_COMMAND], + 1, + "One feedback command should have happened" + ); + gTestProvider.commandCount[FEEDBACK_COMMAND] = 0; + + Assert.ok( + gURLBar.view.isOpen, + "The view should remain open clicking the command" + ); + Assert.ok( + details.element.row.hasAttribute("feedback-acknowledgment"), + "Row should have feedback acknowledgment after clicking command" + ); + + info("Doing dismissal"); + await doDismissTest({ resultIndex, shouldBeSelected: true }); +}); + +/** + * Does a dismissal test: + * + * 1. Clicks the dismiss command in the test result + * 2. Verifies a dismissal acknowledgment tip replaces the result + * 3. Clicks the "Got it" button in the tip + * 4. Verifies the tip is dismissed + * + * @param {object} options + * Options object + * @param {boolean} options.shouldBeSelected + * True if the test result is expected to be selected initially. If true, this + * function verifies the "Got it" button in the dismissal acknowledgment tip + * also becomes selected. + * @param {number} options.resultIndex + * The index of the test result, if known beforehand. Leave -1 to find it + * automatically. + */ +async function doDismissTest({ shouldBeSelected, resultIndex = -1 }) { + if (resultIndex < 0) { + resultIndex = await getTestResultIndex(); + } + + let selectedElement = gURLBar.view.selectedElement; + Assert.ok(selectedElement, "There should be an initially selected element"); + + if (shouldBeSelected) { + Assert.equal( + gURLBar.view.selectedRowIndex, + resultIndex, + "The test result should be selected" + ); + } else { + Assert.notEqual( + gURLBar.view.selectedRowIndex, + resultIndex, + "The test result should not be selected" + ); + } + + let resultCount = UrlbarTestUtils.getResultCount(window); + + // Click the dismiss command. + await UrlbarTestUtils.openResultMenuAndClickItem(window, "dismiss", { + resultIndex, + openByMouse: true, + }); + + Assert.equal( + gTestProvider.commandCount.dismiss, + 1, + "One dismissal should have happened" + ); + gTestProvider.commandCount.dismiss = 0; + + // The row should be a tip now. + Assert.ok(gURLBar.view.isOpen, "The view should remain open after dismissal"); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + resultCount, + "The result count should not haved changed after dismissal" + ); + + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex); + Assert.equal( + details.type, + UrlbarUtils.RESULT_TYPE.TIP, + "Row should be a tip after dismissal" + ); + Assert.equal( + details.result.payload.type, + "dismissalAcknowledgment", + "Tip type should be dismissalAcknowledgment" + ); + Assert.ok( + !details.element.row.hasAttribute("selected"), + "Row should not have 'selected' attribute" + ); + Assert.ok( + !details.element.row._content.hasAttribute("selected"), + "Row-inner should not have 'selected' attribute" + ); + Assert.ok( + !details.element.row.hasAttribute("feedback-acknowledgment"), + "Row should not have feedback acknowledgment after dismissal" + ); + + // Get the dismissal acknowledgment's "Got it" button. + let gotItButton = UrlbarTestUtils.getButtonForResultIndex( + window, + "0", + resultIndex + ); + Assert.ok(gotItButton, "Row should have a 'Got it' button"); + + if (shouldBeSelected) { + Assert.equal( + gURLBar.view.selectedElement, + gotItButton, + "The 'Got it' button should be selected" + ); + } else { + Assert.notEqual( + gURLBar.view.selectedElement, + gotItButton, + "The 'Got it' button should not be selected" + ); + Assert.equal( + gURLBar.view.selectedElement, + selectedElement, + "The initially selected element should remain selected" + ); + } + + // Click it. + EventUtils.synthesizeMouseAtCenter(gotItButton, {}, window); + + // The view should remain open and the tip row should be gone. + Assert.ok( + gURLBar.view.isOpen, + "The view should remain open clicking the 'Got it' button" + ); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + resultCount - 1, + "The result count should be one less after clicking 'Got it' button" + ); + for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) { + details = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + Assert.ok( + details.type != UrlbarUtils.RESULT_TYPE.TIP && + details.result.providerName != gTestProvider.name, + "Tip result and test result should not be present" + ); + } + + await UrlbarTestUtils.promisePopupClose(window); +} + +/** + * A provider that acknowledges feedback and dismissals. + */ +class TestProvider extends UrlbarTestUtils.TestProvider { + getResultCommands(result) { + return [ + { + name: FEEDBACK_COMMAND, + l10n: { + id: "firefox-suggest-weather-command-inaccurate-location", + }, + }, + { + name: "dismiss", + l10n: { + id: "firefox-suggest-weather-command-not-interested", + }, + }, + ]; + } + + onEngagement(isPrivate, state, queryContext, details) { + if (details.result?.providerName == this.name) { + let { selType } = details; + + info(`onEngagement called, selType=` + selType); + + if (!this.commandCount.hasOwnProperty(selType)) { + this.commandCount[selType] = 0; + } + this.commandCount[selType]++; + + if (selType == FEEDBACK_COMMAND) { + queryContext.view.acknowledgeFeedback(details.result); + } else if (selType == "dismiss") { + queryContext.view.acknowledgeDismissal(details.result); + } + } + } +} + +async function getTestResultIndex() { + let index = 0; + let resultCount = UrlbarTestUtils.getResultCount(window); + for (; index < resultCount; index++) { + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, index); + if (details.result.providerName == gTestProvider.name) { + break; + } + } + Assert.less(index, resultCount, "The test result should be present"); + return index; +} 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..2520315fa2 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_action_searchengine.js @@ -0,0 +1,125 @@ +/* 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_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.separatePrivateDefault.ui.enabled", true], + ["browser.search.separatePrivateDefault", false], + ], + }); + + await SearchTestUtils.installSearchExtension( + { name: "MozSearch" }, + { setAsDefault: true } + ); + await SearchTestUtils.installSearchExtension({ + name: "MozSearchPrivate", + search_url: "https://example.com/private", + }); + + registerCleanupFunction(async function () { + 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", "https://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", "https://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, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + registerCleanupFunction(async () => { + await BrowserTestUtils.closeWindow(win); + await Services.search.setDefaultPrivate( + originalEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + }); + + const win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + + await testSearch(win, "MozSearchPrivate", "https://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..b79c324a04 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_action_searchengine_alias.js @@ -0,0 +1,63 @@ +/* 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 () { + await SearchTestUtils.installSearchExtension( + { keyword: "moz" }, + { setAsDefault: true } + ); + let engine = Services.search.getEngineByName("Example"); + + 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 () { + 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, + "https://example.com/?q=open+a+search", + "Should have loaded the correct page" + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_add_search_engine.js b/browser/components/urlbar/tests/browser/browser_add_search_engine.js new file mode 100644 index 0000000000..f016aab3e7 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_add_search_engine.js @@ -0,0 +1,325 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test adding engines through the Address Bar context menu. + +const { PromptTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromptTestUtils.sys.mjs" +); +const BASE_URL = + "http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/"; + +add_task(async function context_none() { + info("Checks the context menu with a page that doesn't offer any engines."); + let url = "http://mochi.test:8888/"; + await BrowserTestUtils.withNewTab(url, async () => { + await UrlbarTestUtils.withContextMenu(window, popup => { + info("The separator and the add engine item should not be present."); + let elt = popup.parentNode.getMenuItem("add-engine-separator"); + Assert.ok(!!elt); + Assert.ok(!BrowserTestUtils.is_visible(elt)); + Assert.ok(!popup.parentNode.getMenuItem("add-engine-menu")); + Assert.ok(!popup.parentNode.getMenuItem("add-engine-0")); + }); + }); +}); + +add_task(async function context_one() { + info("Checks the context menu with a page that offers one engine."); + let url = getRootDirectory(gTestPath) + "add_search_engine_one.html"; + await BrowserTestUtils.withNewTab(url, async () => { + await UrlbarTestUtils.withContextMenu(window, async popup => { + info("The separator and the add engine item should be present."); + let elt = popup.parentNode.getMenuItem("add-engine-separator"); + Assert.ok(BrowserTestUtils.is_visible(elt)); + + Assert.ok(!popup.parentNode.getMenuItem("add-engine-menu")); + Assert.ok(!popup.parentNode.getMenuItem("add-engine-1")); + + elt = popup.parentNode.getMenuItem("add-engine-0"); + Assert.ok(BrowserTestUtils.is_visible(elt)); + await document.l10n.translateElements([elt]); + Assert.ok(elt.label.includes("add_search_engine_0")); + Assert.ok(elt.hasAttribute("image")); + Assert.equal( + elt.getAttribute("uri"), + BASE_URL + "add_search_engine_0.xml" + ); + + info("Click on the menuitem"); + let enginePromise = promiseEngine("engine-added", "add_search_engine_0"); + popup.activateItem(elt); + await enginePromise; + Assert.equal(popup.state, "closed"); + }); + + await UrlbarTestUtils.withContextMenu(window, popup => { + info("The separator and the add engine item should not be present."); + let elt = popup.parentNode.getMenuItem("add-engine-separator"); + Assert.ok(!BrowserTestUtils.is_visible(elt)); + Assert.ok(!popup.parentNode.getMenuItem("add-engine-menu")); + Assert.ok(!popup.parentNode.getMenuItem("add-engine-0")); + }); + + info("Remove the engine."); + let engine = await Services.search.getEngineByName("add_search_engine_0"); + await Services.search.removeEngine(engine); + + await UrlbarTestUtils.withContextMenu(window, async popup => { + info("The separator and the add engine item should be present again."); + let elt = popup.parentNode.getMenuItem("add-engine-separator"); + Assert.ok(BrowserTestUtils.is_visible(elt)); + + Assert.ok(!popup.parentNode.getMenuItem("add-engine-menu")); + Assert.ok(!popup.parentNode.getMenuItem("add-engine-1")); + + elt = popup.parentNode.getMenuItem("add-engine-0"); + Assert.ok(BrowserTestUtils.is_visible(elt)); + await document.l10n.translateElements([elt]); + Assert.ok(elt.label.includes("add_search_engine_0")); + }); + }); +}); + +add_task(async function context_invalid() { + info("Checks the context menu with a page that offers an invalid engine."); + await SpecialPowers.pushPrefEnv({ + set: [["prompts.contentPromptSubDialog", false]], + }); + + let url = getRootDirectory(gTestPath) + "add_search_engine_invalid.html"; + await BrowserTestUtils.withNewTab(url, async tab => { + await UrlbarTestUtils.withContextMenu(window, async popup => { + info("The separator and the add engine item should be present."); + Assert.ok(popup.parentNode.getMenuItem("add-engine-separator")); + Assert.ok(!popup.parentNode.getMenuItem("add-engine-menu")); + Assert.ok(!popup.parentNode.getMenuItem("add-engine-1")); + + let elt = popup.parentNode.getMenuItem("add-engine-0"); + Assert.ok(BrowserTestUtils.is_visible(elt)); + await document.l10n.translateElements([elt]); + Assert.ok(elt.label.includes("add_search_engine_404")); + Assert.equal( + elt.getAttribute("uri"), + BASE_URL + "add_search_engine_404.xml" + ); + + info("Click on the menuitem"); + let promptPromise = PromptTestUtils.waitForPrompt(tab.linkedBrowser, { + modalType: Ci.nsIPromptService.MODAL_TYPE_CONTENT, + promptType: "alert", + }); + + popup.activateItem(elt); + + let prompt = await promptPromise; + Assert.ok( + prompt.ui.infoBody.textContent.includes( + BASE_URL + "add_search_engine_404.xml" + ), + "Should have included the url in the prompt body" + ); + await PromptTestUtils.handlePrompt(prompt); + Assert.equal(popup.state, "closed"); + }); + }); +}); + +add_task(async function context_same_name() { + info("Checks the context menu with a page that offers same named engines."); + let url = getRootDirectory(gTestPath) + "add_search_engine_same_names.html"; + await BrowserTestUtils.withNewTab(url, async () => { + await UrlbarTestUtils.withContextMenu(window, async popup => { + info("The separator and the add engine item should be present."); + let elt = popup.parentNode.getMenuItem("add-engine-separator"); + Assert.ok(BrowserTestUtils.is_visible(elt)); + + Assert.ok(!popup.parentNode.getMenuItem("add-engine-menu")); + Assert.ok(!popup.parentNode.getMenuItem("add-engine-1")); + + elt = popup.parentNode.getMenuItem("add-engine-0"); + Assert.ok(BrowserTestUtils.is_visible(elt)); + await document.l10n.translateElements([elt]); + Assert.ok(elt.label.includes("add_search_engine_0")); + }); + }); +}); + +add_task(async function context_two() { + info("Checks the context menu with a page that offers two engines."); + let url = getRootDirectory(gTestPath) + "add_search_engine_two.html"; + await BrowserTestUtils.withNewTab(url, async () => { + await UrlbarTestUtils.withContextMenu(window, async popup => { + info("The separator and the add engine item should be present."); + let elt = popup.parentNode.getMenuItem("add-engine-separator"); + Assert.ok(BrowserTestUtils.is_visible(elt)); + + Assert.ok(!popup.parentNode.getMenuItem("add-engine-menu")); + + elt = popup.parentNode.getMenuItem("add-engine-0"); + Assert.ok(BrowserTestUtils.is_visible(elt)); + await document.l10n.translateElements([elt]); + Assert.ok(elt.label.includes("add_search_engine_0")); + elt = popup.parentNode.getMenuItem("add-engine-1"); + Assert.ok(BrowserTestUtils.is_visible(elt)); + await document.l10n.translateElements([elt]); + Assert.ok(elt.label.includes("add_search_engine_1")); + }); + }); +}); + +add_task(async function context_many() { + info("Checks the context menu with a page that offers many engines."); + let url = getRootDirectory(gTestPath) + "add_search_engine_many.html"; + await BrowserTestUtils.withNewTab(url, async () => { + await UrlbarTestUtils.withContextMenu(window, async popup => { + info("The separator and the add engine menu should be present."); + let separator = popup.parentNode.getMenuItem("add-engine-separator"); + Assert.ok(BrowserTestUtils.is_visible(separator)); + + info("Engines should appear in sub menu"); + let menu = popup.parentNode.getMenuItem("add-engine-menu"); + Assert.ok(BrowserTestUtils.is_visible(menu)); + Assert.ok( + !menu.nextElementSibling + ?.getAttribute("anonid") + .startsWith("add-engine") + ); + Assert.ok(menu.hasAttribute("image"), "Menu should have an icon"); + Assert.ok( + !menu.label.includes("add-engine"), + "Menu should not contain an engine name" + ); + + info("Open the submenu"); + let popupShown = BrowserTestUtils.waitForEvent(menu, "popupshown"); + menu.openMenu(true); + await popupShown; + for (let i = 0; i < 4; ++i) { + let elt = popup.parentNode.getMenuItem(`add-engine-${i}`); + Assert.equal(elt.parentNode, menu.menupopup); + Assert.ok(BrowserTestUtils.is_visible(elt)); + } + + info("Click on the first engine to install it"); + let enginePromise = promiseEngine("engine-added", "add_search_engine_0"); + let elt = popup.parentNode.getMenuItem("add-engine-0"); + + elt.closest("menupopup").activateItem(elt); + await enginePromise; + Assert.equal(popup.state, "closed"); + }); + + await UrlbarTestUtils.withContextMenu(window, async popup => { + info("Check the installed engine has been removed"); + // We're below the limit of engines for the menu now. + Assert.ok(!!popup.parentNode.getMenuItem("add-engine-separator")); + Assert.ok(!popup.parentNode.getMenuItem("add-engine-menu")); + + for (let i = 0; i < 3; ++i) { + let elt = popup.parentNode.getMenuItem(`add-engine-${i}`); + Assert.equal(elt.parentNode, popup); + Assert.ok(BrowserTestUtils.is_visible(elt)); + await document.l10n.translateElements([elt]); + Assert.ok(elt.label.includes(`add_search_engine_${i + 1}`)); + } + }); + + info("Remove the engine."); + let engine = await Services.search.getEngineByName("add_search_engine_0"); + await Services.search.removeEngine(engine); + + await UrlbarTestUtils.withContextMenu(window, async popup => { + info("The separator and the add engine menu should be present."); + let separator = popup.parentNode.getMenuItem("add-engine-separator"); + Assert.ok(BrowserTestUtils.is_visible(separator)); + + info("Engines should appear in sub menu"); + let menu = popup.parentNode.getMenuItem("add-engine-menu"); + Assert.ok(BrowserTestUtils.is_visible(menu)); + Assert.ok( + !menu.nextElementSibling + ?.getAttribute("anonid") + .startsWith("add-engine") + ); + + info("Open the submenu"); + let popupShown = BrowserTestUtils.waitForEvent(menu, "popupshown"); + menu.openMenu(true); + await popupShown; + for (let i = 0; i < 4; ++i) { + let elt = popup.parentNode.getMenuItem(`add-engine-${i}`); + Assert.equal(elt.parentNode, menu.menupopup); + if ( + AppConstants.platform != "macosx" || + !Services.prefs.getBoolPref( + "widget.macos.native-context-menus", + false + ) + ) { + Assert.ok(BrowserTestUtils.is_visible(elt)); + } + } + }); + }); +}); + +add_task(async function context_after_customize() { + info("Checks the context menu after customization."); + let url = getRootDirectory(gTestPath) + "add_search_engine_one.html"; + await BrowserTestUtils.withNewTab(url, async () => { + await UrlbarTestUtils.withContextMenu(window, async popup => { + info("The separator and the add engine item should be present."); + let elt = popup.parentNode.getMenuItem("add-engine-separator"); + Assert.ok(BrowserTestUtils.is_visible(elt)); + + Assert.ok(!popup.parentNode.getMenuItem("add-engine-menu")); + Assert.ok(!popup.parentNode.getMenuItem("add-engine-1")); + + elt = popup.parentNode.getMenuItem("add-engine-0"); + Assert.ok(BrowserTestUtils.is_visible(elt)); + await document.l10n.translateElements([elt]); + Assert.ok(elt.label.includes("add_search_engine_0")); + }); + + let promise = BrowserTestUtils.waitForEvent( + gNavToolbox, + "customizationready" + ); + gCustomizeMode.enter(); + await promise; + promise = BrowserTestUtils.waitForEvent(gNavToolbox, "aftercustomization"); + gCustomizeMode.exit(); + await promise; + + await UrlbarTestUtils.withContextMenu(window, async popup => { + info("The separator and the add engine item should be present."); + let elt = popup.parentNode.getMenuItem("add-engine-separator"); + Assert.ok(BrowserTestUtils.is_visible(elt)); + + Assert.ok(!popup.parentNode.getMenuItem("add-engine-menu")); + Assert.ok(!popup.parentNode.getMenuItem("add-engine-1")); + + elt = popup.parentNode.getMenuItem("add-engine-0"); + Assert.ok(BrowserTestUtils.is_visible(elt)); + await document.l10n.translateElements([elt]); + Assert.ok(elt.label.includes("add_search_engine_0")); + }); + }); +}); + +function promiseEngine(expectedData, expectedEngineName) { + info(`Waiting for engine ${expectedData}`); + return TestUtils.topicObserved( + "browser-search-engine-modified", + (engine, data) => { + info(`Got engine ${engine.wrappedJSObject.name} ${data}`); + return ( + expectedData == data && + expectedEngineName == engine.wrappedJSObject.name + ); + } + ).then(([engine, data]) => engine); +} 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..65f533c0fe --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autoFill_backspaced.js @@ -0,0 +1,268 @@ +/* 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"); + Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions"); + gURLBar.handleRevert(); + await PlacesUtils.history.clear(); + }); + Services.prefs.setBoolPref("browser.urlbar.autoFill", true); + Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false); + + await PlacesTestUtils.addVisits([ + "http://example.com/", + "http://example.com/foo", + ]); + // Bookmark the page so it ignores autofill threshold and doesn't risk to + // not be autofilled. + let bm = await PlacesUtils.bookmarks.insert({ + url: "http://example.com/", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + }); + registerCleanupFunction(async function () { + await PlacesUtils.bookmarks.remove(bm); + }); + + 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..af6a2eb08b --- /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..b3fb932c4c --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autoFill_firstResult.js @@ -0,0 +1,200 @@ +/* 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_setup(async function () { + 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, + fireInputEvent: false, + }); + + // 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..6fcd664de0 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autoFill_placeholder.js @@ -0,0 +1,1017 @@ +/* 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"; + +// Allow more time for verify mode. +requestLongerTimeout(5); + +add_setup(async function () { + await cleanUp(); +}); + +// Basic origin autofill test. +add_task(async function origin() { + await addVisits("http://example.com/"); + + await search({ + searchString: "ex", + valueBefore: "ex", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }); + await search({ + searchString: "exa", + valueBefore: "example.com/", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }); + await search({ + searchString: "EXAM", + valueBefore: "EXAMple.com/", + valueAfter: "EXAMple.com/", + placeholderAfter: "EXAMple.com/", + }); + await search({ + searchString: "eXaMp", + valueBefore: "eXaMple.com/", + valueAfter: "eXaMple.com/", + placeholderAfter: "eXaMple.com/", + }); + await search({ + searchString: "exampL", + valueBefore: "exampLe.com/", + valueAfter: "exampLe.com/", + placeholderAfter: "exampLe.com/", + }); + await search({ + searchString: "example.com", + valueBefore: "example.com/", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }); + await search({ + searchString: "example.com/", + valueBefore: "example.com/", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }); + + await cleanUp(); +}); + +// Basic URL autofill test. +add_task(async function url() { + await addVisits("http://example.com/aaa/bbb/ccc"); + + await search({ + searchString: "example.com/a", + valueBefore: "example.com/a", + valueAfter: "example.com/aaa/", + placeholderAfter: "example.com/aaa/", + }); + await search({ + searchString: "EXAmple.com/aA", + valueBefore: "EXAmple.com/aAa/", + valueAfter: "EXAmple.com/aAa/", + placeholderAfter: "EXAmple.com/aAa/", + }); + await search({ + searchString: "example.com/aAa", + valueBefore: "example.com/aAa/", + valueAfter: "example.com/aAa/", + placeholderAfter: "example.com/aAa/", + }); + await search({ + searchString: "example.com/aaa/", + valueBefore: "example.com/aaa/", + valueAfter: "example.com/aaa/", + placeholderAfter: "example.com/aaa/", + }); + await search({ + searchString: "example.com/aaa/b", + valueBefore: "example.com/aaa/b", + valueAfter: "example.com/aaa/bbb/", + placeholderAfter: "example.com/aaa/bbb/", + }); + await search({ + searchString: "example.com/aAa/bB", + valueBefore: "example.com/aAa/bBb/", + valueAfter: "example.com/aAa/bBb/", + placeholderAfter: "example.com/aAa/bBb/", + }); + await search({ + searchString: "example.com/aAa/bBb", + valueBefore: "example.com/aAa/bBb/", + valueAfter: "example.com/aAa/bBb/", + placeholderAfter: "example.com/aAa/bBb/", + }); + await search({ + searchString: "example.com/aaa/bbb/", + valueBefore: "example.com/aaa/bbb/", + valueAfter: "example.com/aaa/bbb/", + placeholderAfter: "example.com/aaa/bbb/", + }); + await search({ + searchString: "example.com/aaa/bbb/c", + valueBefore: "example.com/aaa/bbb/c", + valueAfter: "example.com/aaa/bbb/ccc", + placeholderAfter: "example.com/aaa/bbb/ccc", + }); + await search({ + searchString: "example.com/aAa/bBb/cC", + valueBefore: "example.com/aAa/bBb/cCc", + valueAfter: "example.com/aAa/bBb/cCc", + placeholderAfter: "example.com/aAa/bBb/cCc", + }); + await search({ + searchString: "example.com/aaa/bbb/ccc", + valueBefore: "example.com/aaa/bbb/ccc", + valueAfter: "example.com/aaa/bbb/ccc", + placeholderAfter: "example.com/aaa/bbb/ccc", + }); + + await cleanUp(); +}); + +// Basic adaptive history autofill test. +add_task(async function adaptiveHistory() { + UrlbarPrefs.set("autoFill.adaptiveHistory.enabled", true); + + await addVisits("http://example.com/test"); + await UrlbarUtils.addToInputHistory("http://example.com/test", "exa"); + + await search({ + searchString: "exa", + valueBefore: "exa", + valueAfter: "example.com/test", + placeholderAfter: "example.com/test", + }); + await search({ + searchString: "EXAM", + valueBefore: "EXAMple.com/test", + valueAfter: "EXAMple.com/test", + placeholderAfter: "EXAMple.com/test", + }); + await search({ + searchString: "eXaMpLe", + valueBefore: "eXaMpLe.com/test", + valueAfter: "eXaMpLe.com/test", + placeholderAfter: "eXaMpLe.com/test", + }); + await search({ + searchString: "example.", + valueBefore: "example.com/test", + valueAfter: "example.com/test", + placeholderAfter: "example.com/test", + }); + await search({ + searchString: "example.c", + valueBefore: "example.com/test", + valueAfter: "example.com/test", + placeholderAfter: "example.com/test", + }); + await search({ + searchString: "example.com", + valueBefore: "example.com/test", + valueAfter: "example.com/test", + placeholderAfter: "example.com/test", + }); + await search({ + searchString: "example.com/", + valueBefore: "example.com/test", + valueAfter: "example.com/test", + placeholderAfter: "example.com/test", + }); + await search({ + searchString: "example.com/T", + valueBefore: "example.com/Test", + valueAfter: "example.com/Test", + placeholderAfter: "example.com/Test", + }); + await search({ + searchString: "eXaMple.com/tE", + valueBefore: "eXaMple.com/tEst", + valueAfter: "eXaMple.com/tEst", + placeholderAfter: "eXaMple.com/tEst", + }); + await search({ + searchString: "example.com/tes", + valueBefore: "example.com/test", + valueAfter: "example.com/test", + placeholderAfter: "example.com/test", + }); + await search({ + searchString: "example.com/test", + valueBefore: "example.com/test", + valueAfter: "example.com/test", + placeholderAfter: "example.com/test", + }); + + UrlbarPrefs.clear("autoFill.adaptiveHistory.enabled"); + await cleanUp(); +}); + +// Search engine token alias test (aliases that start with "@"). +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 SearchTestUtils.installSearchExtension({ keyword: "@__example" }); + + await search({ + searchString: "@__ex", + valueBefore: "@__ex", + valueAfter: "@__example ", + placeholderAfter: "@__example ", + }); + await search({ + searchString: "@__exa", + valueBefore: "@__example ", + valueAfter: "@__example ", + placeholderAfter: "@__example ", + }); + await search({ + searchString: "@__EXAM", + valueBefore: "@__EXAMple ", + valueAfter: "@__EXAMple ", + placeholderAfter: "@__EXAMple ", + }); + await search({ + searchString: "@__eXaMp", + valueBefore: "@__eXaMple ", + valueAfter: "@__eXaMple ", + placeholderAfter: "@__eXaMple ", + }); + await search({ + searchString: "@__exampl", + valueBefore: "@__example ", + valueAfter: "@__example ", + placeholderAfter: "@__example ", + }); + + await cleanUp(); +}); + +// The placeholder should not be used for a search that does not autofill, and +// it should be cleared after the search completes. +add_task(async function noAutofill() { + await addVisits("http://example.com/"); + + // Do an initial search that triggers autofill so that the placeholder has an + // initial value. + await search({ + searchString: "ex", + valueBefore: "ex", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }); + + // Search with a string that does not match the placeholder. Placeholder + // autofill shouldn't happen. + await search({ + searchString: "moz", + valueBefore: "moz", + valueAfter: "moz", + placeholderAfter: "", + }); + + // Search for "ex" again. It should be autofilled when the search completes + // but the placeholder will not be autofilled. + await search({ + searchString: "ex", + valueBefore: "ex", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }); + + // Continue with a series of searches that should all use the placeholder. + await search({ + searchString: "exa", + valueBefore: "example.com/", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }); + await search({ + searchString: "EXAM", + valueBefore: "EXAMple.com/", + valueAfter: "EXAMple.com/", + placeholderAfter: "EXAMple.com/", + }); + await search({ + searchString: "eXaMp", + valueBefore: "eXaMple.com/", + valueAfter: "eXaMple.com/", + placeholderAfter: "eXaMple.com/", + }); + await search({ + searchString: "exampl", + valueBefore: "example.com/", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }); + + await cleanUp(); +}); + +// The placeholder should not be used for a search that autofills a different +// value. +add_task(async function differentAutofill() { + await addVisits("http://mozilla.org/", "http://example.com/"); + + // Do an initial search that triggers autofill so that the placeholder has an + // initial value. + await search({ + searchString: "moz", + valueBefore: "moz", + valueAfter: "mozilla.org/", + placeholderAfter: "mozilla.org/", + }); + + // Search with a string that does not match the placeholder but does trigger + // autofill. Placeholder autofill shouldn't happen. + await search({ + searchString: "ex", + valueBefore: "ex", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }); + + // Continue with a series of searches that should all use the placeholder. + await search({ + searchString: "exa", + valueBefore: "example.com/", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }); + await search({ + searchString: "EXAm", + valueBefore: "EXAmple.com/", + valueAfter: "EXAmple.com/", + placeholderAfter: "EXAmple.com/", + }); + + // Search for "moz" again. It should be autofilled. Placeholder autofill + // shouldn't happen. + await search({ + searchString: "moz", + valueBefore: "moz", + valueAfter: "mozilla.org/", + placeholderAfter: "mozilla.org/", + }); + + // Continue with a series of searches that should all use the placeholder. + await search({ + searchString: "mozi", + valueBefore: "mozilla.org/", + valueAfter: "mozilla.org/", + placeholderAfter: "mozilla.org/", + }); + await search({ + searchString: "MOZil", + valueBefore: "MOZilla.org/", + valueAfter: "MOZilla.org/", + placeholderAfter: "MOZilla.org/", + }); + + await cleanUp(); +}); + +// The placeholder should not be used for a search that uses a bookmark keyword +// even when the keyword matches the placeholder, and the placeholder should be +// cleared after the search completes. +add_task(async function bookmarkKeyword() { + // Add a visit to example.com. + await addVisits("https://example.com/"); + + // Add a bookmark keyword that is a prefix of example.com. + await PlacesUtils.keywords.insert({ + keyword: "ex", + url: "https://somekeyword.com/", + }); + + // Do an initial search that triggers autofill for the visit so that the + // placeholder has an initial value of "example.com/". + await search({ + searchString: "e", + valueBefore: "e", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }); + + // Do a search that matches the bookmark keyword. The placeholder from the + // search above should be autofilled since the autofill placeholder + // ("example.com/") starts with the keyword ("ex"), but then when the bookmark + // result arrives, the autofilled value and placeholder should be cleared. + await search({ + searchString: "ex", + valueBefore: "example.com/", + valueAfter: "ex", + placeholderAfter: "", + }); + + // Do another search that simulates the user continuing to type "example". No + // placeholder should be autofilled, but once the autofill result arrives for + // the visit, "example.com/" should be autofilled. + await search({ + searchString: "exa", + valueBefore: "exa", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }); + + await PlacesUtils.keywords.remove("ex"); + await cleanUp(); +}); + +// The placeholder should not be used for a search that doesn't match its URI +// fragment. This task uses a URL whose path is "/". +add_task(async function noURIFragmentMatch1() { + await addVisits("https://example.com/#TEST"); + + const testData = [ + { + desc: "Autofill example.com/#TEST then search for example.com/#Te", + searches: [ + { + searchString: "example.com/#T", + valueBefore: "example.com/#T", + valueAfter: "example.com/#TEST", + placeholderAfter: "example.com/#TEST", + }, + { + searchString: "example.com/#Te", + valueBefore: "example.com/#Te", + valueAfter: "example.com/#Te", + placeholderAfter: "", + }, + ], + }, + { + desc: "Autofill https://example.com/#TEST then search for https://example.com/#Te", + searches: [ + { + searchString: "https://example.com/#T", + valueBefore: "https://example.com/#T", + valueAfter: "https://example.com/#TEST", + placeholderAfter: "https://example.com/#TEST", + }, + { + searchString: "https://example.com/#Te", + valueBefore: "https://example.com/#Te", + valueAfter: "https://example.com/#Te", + placeholderAfter: "", + }, + ], + }, + { + desc: "Autofill example.com/#TEST then search for example.com/", + searches: [ + { + searchString: "example.com/#T", + valueBefore: "example.com/#T", + valueAfter: "example.com/#TEST", + placeholderAfter: "example.com/#TEST", + }, + { + searchString: "example.com/", + valueBefore: "example.com/", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }, + ], + }, + ]; + + for (const { desc, searches } of testData) { + info("Running subtest: " + desc); + + for (let i = 0; i < searches.length; i++) { + info("Doing search at index " + i); + await search(searches[i]); + } + + // Clear the placeholder for the next subtest. + info("Doing extra search to clear placeholder"); + await search({ + searchString: "no match", + valueBefore: "no match", + valueAfter: "no match", + placeholderAfter: "", + }); + } + + await cleanUp(); +}); + +// The placeholder should not be used for a search that doesn't match its URI +// fragment. This task uses a URL whose path is "/foo". +add_task(async function noURIFragmentMatch2() { + await addVisits("https://example.com/foo#TEST"); + + const testData = [ + { + desc: "Autofill example.com/foo#TEST then search for example.com/foo#Te", + searches: [ + { + searchString: "example.com/foo#T", + valueBefore: "example.com/foo#T", + valueAfter: "example.com/foo#TEST", + placeholderAfter: "example.com/foo#TEST", + }, + { + searchString: "example.com/foo#Te", + valueBefore: "example.com/foo#Te", + valueAfter: "example.com/foo#Te", + placeholderAfter: "", + }, + ], + }, + { + desc: "Autofill https://example.com/foo#TEST then search for https://example.com/foo#Te", + searches: [ + { + searchString: "https://example.com/foo#T", + valueBefore: "https://example.com/foo#T", + valueAfter: "https://example.com/foo#TEST", + placeholderAfter: "https://example.com/foo#TEST", + }, + { + searchString: "https://example.com/foo#Te", + valueBefore: "https://example.com/foo#Te", + valueAfter: "https://example.com/foo#Te", + placeholderAfter: "", + }, + ], + }, + { + desc: "Autofill example.com/foo#TEST then search for example.com/", + searches: [ + { + searchString: "example.com/foo#T", + valueBefore: "example.com/foo#T", + valueAfter: "example.com/foo#TEST", + placeholderAfter: "example.com/foo#TEST", + }, + { + searchString: "example.com/", + valueBefore: "example.com/", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }, + ], + }, + ]; + + for (const { desc, searches } of testData) { + info("Running subtest: " + desc); + + for (let i = 0; i < searches.length; i++) { + info("Doing search at index " + i); + await search(searches[i]); + } + + // Clear the placeholder for the next subtest. + info("Doing extra search to clear placeholder"); + await search({ + searchString: "no match", + valueBefore: "no match", + valueAfter: "no match", + placeholderAfter: "", + }); + } + + await cleanUp(); +}); + +// The placeholder should not be used for a search that does not autofill its +// URL path. +add_task(async function noPathMatch() { + await addVisits("http://example.com/shallow/deep/file"); + + const testData = [ + { + desc: "Autofill example.com/shallow/ then search for exam", + searches: [ + { + searchString: "example.com/s", + valueBefore: "example.com/s", + valueAfter: "example.com/shallow/", + placeholderAfter: "example.com/shallow/", + }, + { + searchString: "exam", + valueBefore: "exam", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }, + ], + }, + { + desc: "Autofill example.com/shallow/ then search for example.com/", + searches: [ + { + searchString: "example.com/s", + valueBefore: "example.com/s", + valueAfter: "example.com/shallow/", + placeholderAfter: "example.com/shallow/", + }, + { + searchString: "example.com/", + valueBefore: "example.com/", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }, + ], + }, + { + desc: "Autofill example.com/shallow/deep/ then search for exam", + searches: [ + { + searchString: "example.com/shallow/d", + valueBefore: "example.com/shallow/d", + valueAfter: "example.com/shallow/deep/", + placeholderAfter: "example.com/shallow/deep/", + }, + { + searchString: "exam", + valueBefore: "exam", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }, + ], + }, + { + desc: "Autofill example.com/shallow/deep/ then search for example.com/", + searches: [ + { + searchString: "example.com/shallow/d", + valueBefore: "example.com/shallow/d", + valueAfter: "example.com/shallow/deep/", + placeholderAfter: "example.com/shallow/deep/", + }, + { + searchString: "example.com/", + valueBefore: "example.com/", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }, + ], + }, + { + desc: "Autofill example.com/shallow/deep/ then search for example.com/s", + searches: [ + { + searchString: "example.com/shallow/d", + valueBefore: "example.com/shallow/d", + valueAfter: "example.com/shallow/deep/", + placeholderAfter: "example.com/shallow/deep/", + }, + { + searchString: "example.com/s", + valueBefore: "example.com/s", + valueAfter: "example.com/shallow/", + placeholderAfter: "example.com/shallow/", + }, + ], + }, + { + desc: "Autofill example.com/shallow/deep/ then search for example.com/shallow/", + searches: [ + { + searchString: "example.com/shallow/d", + valueBefore: "example.com/shallow/d", + valueAfter: "example.com/shallow/deep/", + placeholderAfter: "example.com/shallow/deep/", + }, + { + searchString: "example.com/shallow/", + valueBefore: "example.com/shallow/", + valueAfter: "example.com/shallow/", + placeholderAfter: "example.com/shallow/", + }, + ], + }, + { + desc: "Autofill example.com/shallow/deep/file then search for exam", + searches: [ + { + searchString: "example.com/shallow/deep/f", + valueBefore: "example.com/shallow/deep/f", + valueAfter: "example.com/shallow/deep/file", + placeholderAfter: "example.com/shallow/deep/file", + }, + { + searchString: "exam", + valueBefore: "exam", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }, + ], + }, + { + desc: "Autofill example.com/shallow/deep/file then search for example.com/", + searches: [ + { + searchString: "example.com/shallow/deep/f", + valueBefore: "example.com/shallow/deep/f", + valueAfter: "example.com/shallow/deep/file", + placeholderAfter: "example.com/shallow/deep/file", + }, + { + searchString: "example.com/", + valueBefore: "example.com/", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }, + ], + }, + { + desc: "Autofill example.com/shallow/deep/file then search for example.com/s", + searches: [ + { + searchString: "example.com/shallow/deep/f", + valueBefore: "example.com/shallow/deep/f", + valueAfter: "example.com/shallow/deep/file", + placeholderAfter: "example.com/shallow/deep/file", + }, + { + searchString: "example.com/s", + valueBefore: "example.com/s", + valueAfter: "example.com/shallow/", + placeholderAfter: "example.com/shallow/", + }, + ], + }, + { + desc: "Autofill example.com/shallow/deep/file then search for example.com/shallow/", + searches: [ + { + searchString: "example.com/shallow/deep/f", + valueBefore: "example.com/shallow/deep/f", + valueAfter: "example.com/shallow/deep/file", + placeholderAfter: "example.com/shallow/deep/file", + }, + { + searchString: "example.com/shallow/", + valueBefore: "example.com/shallow/", + valueAfter: "example.com/shallow/", + placeholderAfter: "example.com/shallow/", + }, + ], + }, + { + desc: "Autofill example.com/shallow/deep/file then search for example.com/shallow/d", + searches: [ + { + searchString: "example.com/shallow/deep/f", + valueBefore: "example.com/shallow/deep/f", + valueAfter: "example.com/shallow/deep/file", + placeholderAfter: "example.com/shallow/deep/file", + }, + { + searchString: "example.com/shallow/d", + valueBefore: "example.com/shallow/d", + valueAfter: "example.com/shallow/deep/", + placeholderAfter: "example.com/shallow/deep/", + }, + ], + }, + { + desc: "Autofill example.com/shallow/deep/file then search for example.com/shallow/deep/", + searches: [ + { + searchString: "example.com/shallow/deep/f", + valueBefore: "example.com/shallow/deep/f", + valueAfter: "example.com/shallow/deep/file", + placeholderAfter: "example.com/shallow/deep/file", + }, + { + searchString: "example.com/shallow/deep/fi", + valueBefore: "example.com/shallow/deep/file", + valueAfter: "example.com/shallow/deep/file", + placeholderAfter: "example.com/shallow/deep/file", + }, + { + searchString: "example.com/shallow/deep/", + valueBefore: "example.com/shallow/deep/", + valueAfter: "example.com/shallow/deep/", + placeholderAfter: "example.com/shallow/deep/", + }, + ], + }, + ]; + + for (const { desc, searches } of testData) { + info("Running subtest: " + desc); + + for (let i = 0; i < searches.length; i++) { + info("Doing search at index " + i); + await search(searches[i]); + } + + // Clear the placeholder for the next subtest. + info("Doing extra search to clear placeholder"); + await search({ + searchString: "no match", + valueBefore: "no match", + valueAfter: "no match", + placeholderAfter: "", + }); + } + + await cleanUp(); +}); + +// An adaptive history placeholder should not be used for a search that does not +// autofill it. +add_task(async function noAdaptiveHistoryMatch() { + UrlbarPrefs.set("autoFill.adaptiveHistory.enabled", true); + + await addVisits("http://example.com/test"); + await UrlbarUtils.addToInputHistory("http://example.com/test", "exam"); + + // Search for a longer string than the adaptive history input. Adaptive + // history autofill should be triggered. + await search({ + searchString: "example", + valueBefore: "example", + valueAfter: "example.com/test", + placeholderAfter: "example.com/test", + }); + + // Search for the same string as the adaptive history input. The placeholder + // from the previous search should be used and adaptive history autofill + // should be triggered. + await search({ + searchString: "exam", + valueBefore: "example.com/test", + valueAfter: "example.com/test", + placeholderAfter: "example.com/test", + }); + + // Search for a shorter string than the adaptive history input. The + // placeholder from the previous search should not be used since the search + // string is shorter than the adaptive history input. + await search({ + searchString: "ex", + valueBefore: "ex", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }); + + UrlbarPrefs.clear("autoFill.adaptiveHistory.enabled"); + await cleanUp(); +}); + +/** + * This function does the following: + * + * 1. Starts a search with `searchString` but doesn't wait for it to complete. + * 2. Compares the input value to `valueBefore`. If anything is autofilled at + * this point, it will be due to the placeholder. + * 3. Waits for the search to complete. + * 4. Compares the input value to `valueAfter`. If anything is autofilled at + * this point, it will be due to the autofill result fetched by the search. + * 5. Compares the placeholder to `placeholderAfter`. + * + * @param {object} options + * The options object. + * @param {string} options.searchString + * The search string. + * @param {string} options.valueBefore + * The expected input value before the search completes. + * @param {string} options.valueAfter + * The expected input value after the search completes. + * @param {string} options.placeholderAfter + * The expected placeholder value after the search completes. + * @returns {Promise} + */ +async function search({ + searchString, + valueBefore, + valueAfter, + placeholderAfter, +}) { + info( + "Searching: " + + JSON.stringify({ + searchString, + valueBefore, + valueAfter, + placeholderAfter, + }) + ); + + await SimpleTest.promiseFocus(window); + gURLBar.inputField.focus(); + + // Set the input value and move the caret to the end to simulate the user + // typing. It's important the caret is at the end because otherwise autofill + // won't happen. + gURLBar.value = searchString; + gURLBar.inputField.setSelectionRange( + searchString.length, + searchString.length + ); + + // Placeholder autofill is done on input, so fire an input event. We can't use + // `promiseAutocompleteResultPopup()` or other helpers that wait for the + // search to complete because we are specifically checking placeholder + // 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, + valueBefore, + "gURLBar.value before the search completes" + ); + Assert.equal( + gURLBar.selectionStart, + searchString.length, + "gURLBar.selectionStart before the search completes" + ); + Assert.equal( + gURLBar.selectionEnd, + valueBefore.length, + "gURLBar.selectionEnd before the search completes" + ); + + // Wait for the search to complete. + info("Waiting for the search to complete"); + await UrlbarTestUtils.promiseSearchComplete(window); + + // Check the final value after the results arrived. + Assert.equal( + gURLBar.value, + valueAfter, + "gURLBar.value after the search completes" + ); + Assert.equal( + gURLBar.selectionStart, + searchString.length, + "gURLBar.selectionStart after the search completes" + ); + Assert.equal( + gURLBar.selectionEnd, + valueAfter.length, + "gURLBar.selectionEnd after the search completes" + ); + + // Check the placeholder. + if (placeholderAfter) { + Assert.ok( + gURLBar._autofillPlaceholder, + "gURLBar._autofillPlaceholder exists after the search completes" + ); + Assert.strictEqual( + gURLBar._autofillPlaceholder.value, + placeholderAfter, + "gURLBar._autofillPlaceholder.value after the search completes" + ); + } else { + Assert.strictEqual( + gURLBar._autofillPlaceholder, + null, + "gURLBar._autofillPlaceholder does not exist after the search completes" + ); + } + + // Check the first result. + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + !!details.autofill, + !!placeholderAfter, + "First result is an autofill result iff a placeholder is expected" + ); +} + +/** + * Adds enough visits to URLs so their origins start autofilling. + * + * @param {...string} urls The URLs to add visits to. + */ +async function addVisits(...urls) { + for (let url of urls) { + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits(url); + } + } +} + +async function cleanUp() { + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + 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..a197be8bf1 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autoFill_preserve.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 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_setup(async function () { + await SpecialPowers.pushPrefEnv({ + // The example.com engine can interfere with this test. + set: [["browser.urlbar.suggest.engines", false]], + }); + 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 SearchTestUtils.installSearchExtension({ keyword: "@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 "); + 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, () => gURLBar.blur()); + 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..3e068d52a4 --- /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_setup(async function () { + 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..371e73c400 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autoFill_typed.js @@ -0,0 +1,172 @@ +/* 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_setup(async function () { + 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 SearchTestUtils.installSearchExtension({ keyword: "@__example" }); + // 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..8ed7e8e402 --- /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_setup(async function () { + // 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.loadURIString( + 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..ead026244e --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autocomplete_a11y_label.js @@ -0,0 +1,185 @@ +/* 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 { CommonUtils } = ChromeUtils.importESModule( + "chrome://mochitests/content/browser/accessible/tests/browser/Common.sys.mjs" +); + +const SUGGEST_ALL_PREF = "browser.search.suggest.enabled"; +const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches"; +const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml"; + +let accService; + +async function getResultText(element, expectedValue, description = "") { + await BrowserTestUtils.waitForCondition( + () => { + let accessible = accService.getAccessibleFor(element); + return accessible !== null && accessible.name === expectedValue; + }, + description, + 200 + ); +} + +/** + * Initializes the accessibility service and registers a cleanup function to + * shut it down. If it's not shut down properly, it can crash the current tab + * and cause the test to fail, especially in verify mode. + * + * This function is adapted from from tests in accessible/tests/browser and its + * helper functions are adapted or copied from functions of the same names in + * the same directory. + */ +async function initAccessibilityService() { + const [a11yInitObserver, a11yInit] = initAccService(); + await a11yInitObserver; + accService = Cc["@mozilla.org/accessibilityService;1"].getService( + Ci.nsIAccessibilityService + ); + await a11yInit; + + registerCleanupFunction(async () => { + const [a11yShutdownObserver, a11yShutdownPromise] = shutdownAccService(); + await a11yShutdownObserver; + accService = null; + forceGC(); + await a11yShutdownPromise; + }); +} + +// Adapted from `initAccService()` in accessible/tests/browser/head.js +function initAccService() { + return [ + CommonUtils.addAccServiceInitializedObserver(), + CommonUtils.observeAccServiceInitialized(), + ]; +} + +// Adapted from `shutdownAccService()` in accessible/tests/browser/head.js +function shutdownAccService() { + return [ + CommonUtils.addAccServiceShutdownObserver(), + CommonUtils.observeAccServiceShutdown(), + ]; +} + +// Copied from accessible/tests/browser/shared-head.js +function forceGC() { + SpecialPowers.gc(); + SpecialPowers.forceShrinkingGC(); + SpecialPowers.forceCC(); + SpecialPowers.gc(); + SpecialPowers.forceShrinkingGC(); + SpecialPowers.forceCC(); +} + +add_setup(async function () { + await initAccessibilityService(); +}); + +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 + ); + // The a11y text will include the "Firefox Suggest" pseudo-element label shown + // before the result. + await getResultText( + element._content, + "Firefox Suggest about:robots — Switch to Tab", + "Result a11y text is correct" + ); + + await UrlbarTestUtils.promisePopupClose(window); + gURLBar.handleRevert(); + gBrowser.removeTab(tab); +}); + +add_task(async function searchSuggestions() { + await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME, + setAsDefault: true, + }); + 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 () { + 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. + let searchTerm = "foo"; + let expectedSearches = [searchTerm, "foofoo", "foobar"]; + 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 + ); + + // Select the row so we see the expanded text. + gURLBar.view.selectedRowIndex = i; + + if (result.searchParams.inPrivateWindow) { + await getResultText( + element._content, + searchTerm + " — Search in a Private Window", + "Check result label for search in private window" + ); + } else { + let suggestion = expectedSearches.shift(); + await getResultText( + element._content, + suggestion + + " — Search with browser_searchSuggestionEngine searchSuggestionEngine.xml", + "Check result label for non-private search" + ); + } + } + } + Assert.ok(!expectedSearches.length); + + await UrlbarTestUtils.promisePopupClose(window); +}); 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..ef3da56ef0 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autocomplete_autoselect.js @@ -0,0 +1,122 @@ +/* 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" + ); + + // 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..c17949eb9e --- /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..a684c60e5b --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autocomplete_enter_race.js @@ -0,0 +1,193 @@ +/* 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_setup(async function () { + 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.history.clear(); + }); + // Needs at least one success. + ok(true, "Setup complete"); +}); + +add_task( + taskWithNewTab(async function test_loadSite() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.autofill", false]], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "example.co", + }); + gURLBar.focus(); + EventUtils.sendString("m"); + EventUtils.synthesizeKey("KEY_Enter"); + info("wait for the page to load"); + await BrowserTestUtils.browserLoaded( + gBrowser.selectedTab.linkedBrowser, + false, + "http://example.com/" + ); + await SpecialPowers.popPrefEnv(); + }) +); + +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 SearchTestUtils.installSearchExtension(); + + let engine = Services.search.getEngineByName("Example"); + let originalEngine = await Services.search.getDefault(); + await Services.search.setDefault( + engine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + 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, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + } + 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, + "https://example.com/?q=ex" + ); + await cleanup(); + }) +); + +// Tests that setting a high value for browser.urlbar.delay does not delay the +// fetching of heuristic results. +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); + }); + + gURLBar.focus(); + gURLBar.value = "e"; + let recievedResult = new Promise(resolve => { + gURLBar.controller.addQueryListener({ + onQueryResults(queryContext) { + gURLBar.controller.removeQueryListener(this); + Assert.ok( + queryContext.heuristicResult, + "Recieved a heuristic result." + ); + Assert.equal( + queryContext.searchString, + "ex", + "The heuristic result is based on the correct search string." + ); + resolve(); + }, + }); + }); + let start = Cu.now(); + EventUtils.sendString("x"); + EventUtils.synthesizeKey("KEY_Enter"); + await recievedResult; + 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..fa30a7608a --- /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..36f990503e --- /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..10e26f6f71 --- /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_bestMatch.js b/browser/components/urlbar/tests/browser/browser_bestMatch.js new file mode 100644 index 0000000000..1e384e389f --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_bestMatch.js @@ -0,0 +1,229 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests best match rows in the view. See also: +// +// browser_quicksuggest_bestMatch.js +// UI test for quick suggest best matches specifically +// test_quicksuggest_bestMatch.js +// Tests triggering quick suggest best matches and things that don't depend on +// the view + +"use strict"; + +// Tests a non-sponsored best match row. +add_task(async function nonsponsored() { + let result = makeBestMatchResult(); + await withProvider(result, async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await checkBestMatchRow({ result }); + await UrlbarTestUtils.promisePopupClose(window); + }); +}); + +// Tests a non-sponsored best match row with a help button. +add_task(async function nonsponsoredHelpButton() { + let result = makeBestMatchResult({ helpUrl: "https://example.com/help" }); + await withProvider(result, async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await checkBestMatchRow({ result, hasHelpUrl: true }); + await UrlbarTestUtils.promisePopupClose(window); + }); +}); + +// Tests a sponsored best match row. +add_task(async function sponsored() { + let result = makeBestMatchResult({ isSponsored: true }); + await withProvider(result, async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await checkBestMatchRow({ result, isSponsored: true }); + await UrlbarTestUtils.promisePopupClose(window); + }); +}); + +// Tests a sponsored best match row with a help button. +add_task(async function sponsoredHelpButton() { + let result = makeBestMatchResult({ + isSponsored: true, + helpUrl: "https://example.com/help", + }); + await withProvider(result, async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await checkBestMatchRow({ result, isSponsored: true, hasHelpUrl: true }); + await UrlbarTestUtils.promisePopupClose(window); + }); +}); + +// Tests keyboard selection. +add_task(async function keySelection() { + let result = makeBestMatchResult({ + isSponsored: true, + helpUrl: "https://example.com/help", + }); + + await withProvider(result, async () => { + // Ordered list of class names of the elements that should be selected. + let expectedClassNames = [ + "urlbarView-row-inner", + UrlbarPrefs.get("resultMenu") + ? "urlbarView-button-menu" + : "urlbarView-button-help", + ]; + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await checkBestMatchRow({ + result, + isSponsored: true, + hasHelpUrl: true, + }); + + // Test with the tab key in order vs. reverse order. + for (let reverse of [false, true]) { + info("Doing TAB key selection: " + JSON.stringify({ reverse })); + + let classNames = [...expectedClassNames]; + if (reverse) { + classNames.reverse(); + } + + let sendKey = () => { + EventUtils.synthesizeKey("KEY_Tab", { shiftKey: reverse }); + }; + + // Move selection through each expected element. + for (let className of classNames) { + info("Expecting selection: " + className); + sendKey(); + Assert.ok(gURLBar.view.isOpen, "View remains open"); + let { selectedElement } = gURLBar.view; + Assert.ok(selectedElement, "Selected element exists"); + Assert.ok( + selectedElement.classList.contains(className), + "Expected element is selected" + ); + } + sendKey(); + Assert.ok( + gURLBar.view.isOpen, + "View remains open after keying through best match row" + ); + } + + await UrlbarTestUtils.promisePopupClose(window); + }); +}); + +async function checkBestMatchRow({ + result, + isSponsored = false, + hasHelpUrl = false, +}) { + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 1, + "One result is present" + ); + + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + let { row } = details.element; + + Assert.equal(row.getAttribute("type"), "bestmatch", "row[type] is bestmatch"); + + let favicon = row._elements.get("favicon"); + Assert.ok(favicon, "Row has a favicon"); + + let title = row._elements.get("title"); + Assert.ok(title, "Row has a title"); + Assert.ok(title.textContent, "Row title has non-empty textContext"); + Assert.equal(title.textContent, result.payload.title, "Row title is correct"); + + let url = row._elements.get("url"); + Assert.ok(url, "Row has a URL"); + Assert.ok(url.textContent, "Row URL has non-empty textContext"); + Assert.equal( + url.textContent, + result.payload.displayUrl, + "Row URL is correct" + ); + + let bottom = row._elements.get("bottom"); + Assert.ok(bottom, "Row has a bottom"); + Assert.equal( + !!result.payload.isSponsored, + isSponsored, + "Sanity check: Row's expected isSponsored matches result's" + ); + if (isSponsored) { + Assert.equal( + bottom.textContent, + "Sponsored", + "Sponsored row bottom has Sponsored textContext" + ); + } else { + Assert.equal( + bottom.textContent, + "", + "Non-sponsored row bottom has empty textContext" + ); + } + + let button = row._buttons.get( + UrlbarPrefs.get("resultMenu") ? "menu" : "help" + ); + Assert.equal( + !!result.payload.helpUrl, + hasHelpUrl, + "Sanity check: Row's expected hasHelpUrl matches result" + ); + if (hasHelpUrl) { + Assert.ok(button, "Row with helpUrl has a help or menu button"); + } else { + Assert.ok( + !button, + "Row without helpUrl does not have a help or menu button" + ); + } +} + +async function withProvider(result, callback) { + let provider = new UrlbarTestUtils.TestProvider({ + results: [result], + priority: Infinity, + }); + UrlbarProvidersManager.registerProvider(provider); + try { + await callback(); + } finally { + UrlbarProvidersManager.unregisterProvider(provider); + } +} + +function makeBestMatchResult(payloadExtra = {}) { + return Object.assign( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.SEARCH, + ...UrlbarResult.payloadAndSimpleHighlights([], { + title: "Test best match", + url: "https://example.com/best-match", + ...payloadExtra, + }) + ), + { isBestMatch: true } + ); +} 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..eabaa2575d --- /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_bufferer_onQueryResults.js b/browser/components/urlbar/tests/browser/browser_bufferer_onQueryResults.js new file mode 100644 index 0000000000..7325d44b2c --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_bufferer_onQueryResults.js @@ -0,0 +1,82 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This test covers a race condition of input events followed by Enter. +// The test is putting the event bufferer in a situation where a new query has +// already results in the context object, but onQueryResults has not been +// invoked yet. The EventBufferer should wait for onQueryResults to proceed, +// otherwise the view cannot yet contain the updated query string and we may +// end up searching for a partial string. + +add_setup(async function () { + sandbox = sinon.createSandbox(); + await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + "searchSuggestionEngine.xml", + setAsDefault: true, + }); + // To reproduce the race condition it's important to disable any provider + // having `deferUserSelection` == true; + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.engines", false]], + }); + await PlacesUtils.history.clear(); + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + sandbox.restore(); + }); +}); + +add_task(async function test() { + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: "about:robots", + }); + + let defer = PromiseUtils.defer(); + let waitFirstSearchResults = PromiseUtils.defer(); + let count = 0; + let original = gURLBar.controller.notify; + sandbox.stub(gURLBar.controller, "notify").callsFake(async (msg, context) => { + if (context?.deferUserSelectionProviders.size) { + Assert.ok(false, "Any provider deferring selection should be disabled"); + } + if (msg == "onQueryResults") { + waitFirstSearchResults.resolve(); + count++; + } + // Delay any events after the second onQueryResults call. + if (count >= 2) { + await defer.promise; + } + return original.call(gURLBar.controller, msg, context); + }); + + gURLBar.focus(); + gURLBar.select(); + EventUtils.synthesizeKey("t", {}); + await waitFirstSearchResults.promise; + EventUtils.synthesizeKey("e", {}); + + let promiseLoaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + EventUtils.synthesizeKey("KEY_Enter", {}); + + let context = await UrlbarTestUtils.promiseSearchComplete(window); + await TestUtils.waitForCondition( + () => context.results.length, + "Waiting for any result in the QueryContext" + ); + info("Simulate a request to replay deferred events at this point"); + gURLBar.eventBufferer.replayDeferredEvents(true); + + defer.resolve(); + await promiseLoaded; + + let expectedURL = UrlbarPrefs.isPersistedSearchTermsEnabled() + ? "http://mochi.test:8888/?terms=" + gURLBar.value + : gURLBar.untrimmedValue; + Assert.equal(gBrowser.selectedBrowser.currentURI.spec, expectedURL); + + BrowserTestUtils.removeTab(tab); + sandbox.restore(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_calculator.js b/browser/components/urlbar/tests/browser/browser_calculator.js new file mode 100644 index 0000000000..899cbc6d5b --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_calculator.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const FORMULA = "8 * 8"; +const RESULT = "64"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.calculator", true]], + }); +}); + +add_task(async function test_calculator() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: FORMULA, + }); + + let result = (await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)) + .result; + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.DYNAMIC); + Assert.equal(result.payload.input, FORMULA); + Assert.equal(result.payload.value, RESULT); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + + // Ensure the RESULT get written to the clipboard when selected. + await SimpleTest.promiseClipboardChange(RESULT, () => { + EventUtils.synthesizeKey("KEY_Enter"); + }); +}); 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..04f153e7d2 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_canonizeURL.js @@ -0,0 +1,277 @@ +/* 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 }, + ], + ]; + + // Disable autoFill for this test, since it could mess up the results. + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.autoFill", false], + ["browser.urlbar.ctrlCanonizesURLs", true], + ], + }); + + const win = await BrowserTestUtils.openNewBrowserWindow(); + + for (let [inputValue, expectedURL, options] of testcases) { + info(`Testing input string: "${inputValue}" - expected: "${expectedURL}"`); + let promiseLoad = BrowserTestUtils.waitForDocLoadAndStopIt( + expectedURL, + win.gBrowser.selectedBrowser + ); + let promiseStopped = BrowserTestUtils.browserStopped( + win.gBrowser.selectedBrowser, + undefined, + true + ); + win.gURLBar.focus(); + win.gURLBar.inputField.value = inputValue.slice(0, -1); + EventUtils.sendString(inputValue.slice(-1), win); + EventUtils.synthesizeKey("KEY_Enter", options, win); + await Promise.all([promiseLoad, promiseStopped]); + } + + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function checkPrefTurnsOffCanonize() { + // Add a dummy search engine to avoid hitting the network. + await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME, + setAsDefault: true, + }); + + const win = await BrowserTestUtils.openNewBrowserWindow(); + + // Ensure we don't end up loading something in the current tab becuase it's empty: + let initialTab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser: win.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( + win.gBrowser.selectedBrowser, + false, + newURL + ) + : BrowserTestUtils.waitForNewTab(win.gBrowser); + + win.gURLBar.focus(); + win.gURLBar.selectionStart = win.gURLBar.selectionEnd = + win.gURLBar.inputField.value.length; + win.gURLBar.inputField.value = "exampl"; + EventUtils.sendString("e", win); + EventUtils.synthesizeKey("KEY_Enter", { ctrlKey: true }, win); + + 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( + win.gBrowser.selectedBrowser.currentURI.spec, + newURL, + "New tab should have navigated" + ); + } + while (win.gBrowser.tabs.length > 1) { + win.gBrowser.removeTab(win.gBrowser.selectedTab, { animate: false }); + } + + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function autofill() { + // Re-enable autofill and canonization. + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.autoFill", true], + ["browser.urlbar.ctrlCanonizesURLs", true], + ], + }); + + const win = await BrowserTestUtils.openNewBrowserWindow(); + + // 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. + win.gURLBar.select(); + EventUtils.sendString("blah", win); + + // 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(win.gURLBar.inputField, "select"); + } + + for (let [inputValue, expectedURL, options] of testcases) { + let promiseLoad = BrowserTestUtils.waitForDocLoadAndStopIt( + expectedURL, + win.gBrowser.selectedBrowser + ); + win.gURLBar.select(); + let autofillPromise = promiseAutofill(); + EventUtils.sendString(inputValue, win); + await autofillPromise; + EventUtils.synthesizeKey("KEY_Enter", options, win); + await promiseLoad; + + // Here again, make sure autofill isn't disabled for the next search. See + // the comment above. + win.gURLBar.select(); + EventUtils.sendString("blah", win); + } + + await PlacesUtils.history.clear(); + + await BrowserTestUtils.closeWindow(win); +}); + +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]], + }); + + const win = await BrowserTestUtils.openNewBrowserWindow(); + + info("Paste the word to the urlbar"); + const testWord = "example"; + simulatePastingToUrlbar(testWord, win); + is(win.gURLBar.value, testWord, "Paste the test word correctly"); + + info("Send enter key while pressing the ctrl key"); + EventUtils.synthesizeKey("VK_RETURN", { ctrlKey: true }, win); + await BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + is( + win.gBrowser.selectedBrowser.documentURI.spec, + `http://mochi.test:8888/?terms=${testWord}`, + "The loaded url is not canonized" + ); + EventUtils.synthesizeKey("VK_CONTROL", { type: "keyup" }, win); + + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function () { + info("Test whether canonization is enabled again after releasing the ctrl"); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.ctrlCanonizesURLs", true]], + }); + + const win = await BrowserTestUtils.openNewBrowserWindow(); + + info("Paste the word to the urlbar"); + const testWord = "example"; + simulatePastingToUrlbar(testWord, win); + is(win.gURLBar.value, testWord, "Paste the test word correctly"); + + info("Release the ctrl key befoer typing Enter key"); + EventUtils.synthesizeKey("VK_CONTROL", { type: "keyup" }, win); + + info("Send enter key with the ctrl"); + const onLoad = BrowserTestUtils.waitForDocLoadAndStopIt( + `https://www.${testWord}.com/`, + win.gBrowser.selectedBrowser + ); + const onStop = BrowserTestUtils.browserStopped( + win.gBrowser.selectedBrowser, + undefined, + true + ); + EventUtils.synthesizeKey("VK_RETURN", { ctrlKey: true }, win); + await Promise.all([onLoad, onStop]); + info("The loaded url is canonized"); + + await BrowserTestUtils.closeWindow(win); +}); + +function simulatePastingToUrlbar(text, win) { + win.gURLBar.focus(); + + const keyForPaste = win.document + .getElementById("key_paste") + .getAttribute("key") + .toLowerCase(); + EventUtils.synthesizeKey( + keyForPaste, + { type: "keydown", ctrlKey: true }, + win + ); + + win.gURLBar.select(); + EventUtils.sendString(text, win); +} diff --git a/browser/components/urlbar/tests/browser/browser_caret_position.js b/browser/components/urlbar/tests/browser/browser_caret_position.js new file mode 100644 index 0000000000..35eee55efe --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_caret_position.js @@ -0,0 +1,359 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const LARGE_DATA_URL = + "data:text/plain," + [...Array(1000)].map(() => "0123456789").join(""); + +// Tests for the caret position after gURLBar.setURI(). +add_task(async function setURI() { + const testData = [ + { + firstURL: "https://example.com/test", + secondURL: "https://example.com/test", + initialSelectionStart: 0, + initialSelectionEnd: 0, + expectedSelectionStart: 0, + expectedSelectionEnd: 0, + }, + { + firstURL: "https://example.com/test", + secondURL: "https://example.com/test", + initialSelectionStart: 20, + initialSelectionEnd: 20, + expectedSelectionStart: 20, + expectedSelectionEnd: 20, + }, + { + firstURL: "https://example.com/test", + secondURL: "https://example.com/test", + initialSelectionStart: 1, + initialSelectionEnd: 20, + expectedSelectionStart: 1, + expectedSelectionEnd: 20, + }, + { + firstURL: "https://example.com/test", + secondURL: "https://example.com/test", + initialSelectionStart: "https://example.com/test".length, + initialSelectionEnd: "https://example.com/test".length, + expectedSelectionStart: "https://example.com/test".length, + expectedSelectionEnd: "https://example.com/test".length, + }, + { + firstURL: "https://example.com/test", + secondURL: "https://example.com/test", + initialSelectionStart: 0, + initialSelectionEnd: "https://example.com/test".length, + expectedSelectionStart: 0, + expectedSelectionEnd: "https://example.com/test".length, + }, + { + firstURL: "https://example.com/test", + secondURL: "https://example.org/test", + initialSelectionStart: 0, + initialSelectionEnd: 0, + expectedSelectionStart: 0, + expectedSelectionEnd: 0, + }, + { + firstURL: "https://example.com/test", + secondURL: "https://example.org/test", + initialSelectionStart: 20, + initialSelectionEnd: 20, + expectedSelectionStart: 20, + expectedSelectionEnd: 20, + }, + { + firstURL: "https://example.com/test", + secondURL: "https://example.org/test", + initialSelectionStart: 1, + initialSelectionEnd: 10, + expectedSelectionStart: 1, + expectedSelectionEnd: 10, + }, + { + firstURL: "https://example.com/test", + secondURL: "https://example.org/test", + initialSelectionStart: "https://example.".length, + initialSelectionEnd: "https://example.c".length, + expectedSelectionStart: "https://example.c".length, + expectedSelectionEnd: "https://example.c".length, + }, + { + firstURL: "https://example.com/test", + secondURL: "https://example.org/test", + initialSelectionStart: "https://example.com/test".length, + initialSelectionEnd: "https://example.com/test".length, + expectedSelectionStart: "https://example.org/test".length, + expectedSelectionEnd: "https://example.org/test".length, + }, + { + firstURL: "https://example.com/test", + secondURL: "https://example.org/test", + initialSelectionStart: 0, + initialSelectionEnd: "https://example.com/test".length, + expectedSelectionStart: "https://example.org/test".length, + expectedSelectionEnd: "https://example.org/test".length, + }, + { + firstURL: "https://example.com/test", + secondURL: "https://example.com/longer", + initialSelectionStart: "https://example.com/test".length, + initialSelectionEnd: "https://example.com/test".length, + expectedSelectionStart: "https://example.com/longer".length, + expectedSelectionEnd: "https://example.com/longer".length, + }, + { + firstURL: "https://example.com/test", + secondURL: "https://example.com/longer", + initialSelectionStart: 20, + initialSelectionEnd: 20, + expectedSelectionStart: 20, + expectedSelectionEnd: 20, + }, + { + firstURL: "https://example.com/longer", + secondURL: "https://example.com/test", + initialSelectionStart: 0, + initialSelectionEnd: "https://example.com/longer".length, + expectedSelectionStart: "https://example.com/test".length, + expectedSelectionEnd: "https://example.com/test".length, + }, + { + firstURL: "https://example.com/longer", + secondURL: "https://example.com/test", + initialSelectionStart: "https://example.com/longer".length, + initialSelectionEnd: "https://example.com/longer".length, + expectedSelectionStart: "https://example.com/test".length, + expectedSelectionEnd: "https://example.com/test".length, + }, + { + firstURL: "https://example.com/longer", + secondURL: "https://example.com/test", + initialSelectionStart: "https://example.com/longer".length - 1, + initialSelectionEnd: "https://example.com/longer".length - 1, + expectedSelectionStart: "https://example.com/test".length, + expectedSelectionEnd: "https://example.com/test".length, + }, + { + firstURL: "https://example.com/longer", + secondURL: "https://example.com/test", + initialSelectionStart: 0, + initialSelectionEnd: "https://example.com/longer".length - 1, + expectedSelectionStart: "https://example.com/test".length, + expectedSelectionEnd: "https://example.com/test".length, + }, + { + firstURL: "https://example.com/test", + secondURL: "about:blank", + initialSelectionStart: 0, + initialSelectionEnd: 0, + expectedSelectionStart: 0, + expectedSelectionEnd: 0, + }, + { + firstURL: "https://example.com/test", + secondURL: "about:blank", + initialSelectionStart: 0, + initialSelectionEnd: "https://example.com/test".length, + expectedSelectionStart: 0, + expectedSelectionEnd: 0, + }, + { + firstURL: "https://example.com/test", + secondURL: "about:blank", + initialSelectionStart: 3, + initialSelectionEnd: 4, + expectedSelectionStart: 0, + expectedSelectionEnd: 0, + }, + { + firstURL: "https://example.com/test", + secondURL: "about:blank", + initialSelectionStart: "https://example.com/test".length, + initialSelectionEnd: "https://example.com/test".length, + expectedSelectionStart: 0, + expectedSelectionEnd: 0, + }, + { + firstURL: "about:blank", + secondURL: "https://example.com/test", + initialSelectionStart: 0, + initialSelectionEnd: 0, + expectedSelectionStart: 0, + expectedSelectionEnd: 0, + }, + { + firstURL: "about:blank", + secondURL: LARGE_DATA_URL, + initialSelectionStart: 0, + initialSelectionEnd: 0, + expectedSelectionStart: 0, + expectedSelectionEnd: 0, + }, + { + firstURL: "about:telemetry", + secondURL: LARGE_DATA_URL, + initialSelectionStart: "about:telemetry".length, + initialSelectionEnd: "about:telemetry".length, + expectedSelectionStart: LARGE_DATA_URL.length, + expectedSelectionEnd: LARGE_DATA_URL.length, + }, + ]; + + for (const data of testData) { + info( + `Test for ${data.firstURL} -> ${data.secondURL} with initial selection: ${data.initialSelectionStart}, ${data.initialSelectionEnd}` + ); + info("Check the caret position after setting second URL"); + gURLBar.setURI(makeURI(data.firstURL)); + gURLBar.selectionStart = data.initialSelectionStart; + gURLBar.selectionEnd = data.initialSelectionEnd; + + // The change of the scroll amount dependent on the selection change will be + // ignored if the previous processing is unfinished yet. Therefore, make the + // processing finalize explicitly here. + await flushScrollStyle(); + + gURLBar.focus(); + gURLBar.setURI(makeURI(data.secondURL)); + await flushScrollStyle(); + + Assert.equal(gURLBar.selectionStart, data.expectedSelectionStart); + Assert.equal(gURLBar.selectionEnd, data.expectedSelectionEnd); + if (data.secondURL.length === data.expectedSelectionStart) { + // If the caret is at the end of url, the input field shows the end of + // text. + Assert.equal( + gURLBar.inputField.scrollLeft, + gURLBar.inputField.scrollLeftMax + ); + } + + info("Check the caret position while the input is not focused"); + gURLBar.setURI(makeURI(data.firstURL)); + gURLBar.selectionStart = data.initialSelectionStart; + gURLBar.selectionEnd = data.initialSelectionEnd; + + await flushScrollStyle(); + + gURLBar.blur(); + gURLBar.setURI(makeURI(data.secondURL)); + await flushScrollStyle(); + + if (data.firstURL === data.secondURL) { + Assert.equal(gURLBar.selectionStart, data.initialSelectionStart); + Assert.equal(gURLBar.selectionEnd, data.initialSelectionEnd); + } else { + Assert.equal(gURLBar.selectionStart, gURLBar.value.length); + Assert.equal(gURLBar.selectionEnd, gURLBar.value.length); + } + Assert.equal(gURLBar.inputField.scrollLeft, 0); + } +}); + +// 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 navigation() { + 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") { + await checkCaretMoves( + "KEY_ArrowDown", + gURLBar.value.length, + "Caret should have moved to the end", + window + ); + await checkPopupOpens("KEY_ArrowDown", window); + + await checkCaretMoves( + "KEY_ArrowUp", + 0, + "Caret should have moved to the start", + window + ); + await checkPopupOpens("KEY_ArrowUp", window); + } else { + await checkPopupOpens("KEY_ArrowDown", window); + await checkPopupOpens("KEY_ArrowUp", window); + } +}); + +async function checkCaretMoves(key, pos, msg, win) { + checkIfKeyStartsQuery(key, false, win); + Assert.equal( + UrlbarTestUtils.isPopupOpen(win), + false, + `${key}: Popup shouldn't be open` + ); + Assert.equal( + win.gURLBar.selectionStart, + win.gURLBar.selectionEnd, + `${key}: Input selection should be empty` + ); + Assert.equal(win.gURLBar.selectionStart, pos, `${key}: ${msg}`); +} + +async function checkPopupOpens(key, win) { + // Store current selection and check it doesn't change. + let selectionStart = win.gURLBar.selectionStart; + let selectionEnd = win.gURLBar.selectionEnd; + await UrlbarTestUtils.promisePopupOpen(win, () => { + checkIfKeyStartsQuery(key, true, win); + }); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(win), + 0, + `${key}: Heuristic result should be selected` + ); + Assert.equal( + win.gURLBar.selectionStart, + selectionStart, + `${key}: Input selection start should not change` + ); + Assert.equal( + win.gURLBar.selectionEnd, + selectionEnd, + `${key}: Input selection end should not change` + ); + await UrlbarTestUtils.promisePopupClose(win); +} + +function checkIfKeyStartsQuery(key, shouldStartQuery, win) { + let queryStarted = false; + let queryListener = { + onQueryStarted() { + queryStarted = true; + }, + }; + win.gURLBar.controller.addQueryListener(queryListener); + EventUtils.synthesizeKey(key, {}, win); + win.gURLBar.eventBufferer.replayDeferredEvents(false); + win.gURLBar.controller.removeQueryListener(queryListener); + Assert.equal( + queryStarted, + shouldStartQuery, + `${key}: Should${shouldStartQuery ? "" : "n't"} have started a query` + ); +} + +async function flushScrollStyle() { + // Flush pending notifications for the style. + /* eslint-disable no-unused-expressions */ + gURLBar.inputField.scrollLeft; + // Ensure to apply the style. + await new Promise(resolve => + gURLBar.inputField.ownerGlobal.requestAnimationFrame(resolve) + ); +} diff --git a/browser/components/urlbar/tests/browser/browser_click_row_border.js b/browser/components/urlbar/tests/browser/browser_click_row_border.js new file mode 100644 index 0000000000..59915ed3b1 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_click_row_border.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URL = "https://example.com/autocomplete"; + +add_setup(async function () { + await PlacesTestUtils.addVisits(TEST_URL); + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + }); +}); + +add_task(async function test_click_row_border() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "example.com/autocomplete", + }); + let resultRow = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0); + let loaded = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + TEST_URL + ); + info("Clicking on the result's top pixel row"); + EventUtils.synthesizeMouse( + resultRow, + parseInt(getComputedStyle(resultRow).borderTopLeftRadius) * 2, + 1, + {} + ); + info("Waiting for page to load"); + await loaded; + ok(true, "Page loaded"); +}); 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..2bbe412acb --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_closePanelOnClick.js @@ -0,0 +1,34 @@ +/* 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.synthesizeNativeMouseEvent({ + type: "click", + target: elt, + atCenter: true, + }) + ); + } + }); +}); 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..0cf4865ad7 --- /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_contextualsearch.js b/browser/components/urlbar/tests/browser/browser_contextualsearch.js new file mode 100644 index 0000000000..5de1673e6a --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_contextualsearch.js @@ -0,0 +1,119 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { UrlbarProviderContextualSearch } = ChromeUtils.importESModule( + "resource:///modules/UrlbarProviderContextualSearch.sys.mjs" +); + +add_setup(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.contextualSearch.enabled", true]], + }); +}); + +add_task(async function test_selectContextualSearchResult_already_installed() { + await SearchTestUtils.installSearchExtension({ + name: "Contextual", + search_url: "https://example.com/browser", + }); + + const ENGINE_TEST_URL = "https://example.com/"; + let onLoaded = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + ENGINE_TEST_URL + ); + BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, ENGINE_TEST_URL); + await onLoaded; + + const query = "search"; + let engine = Services.search.getEngineByName("Contextual"); + const [expectedUrl] = UrlbarUtils.getSearchQueryUrl(engine, query); + + Assert.ok( + expectedUrl.includes(`?q=${query}`), + "Expected URL should be a search URL" + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: query, + }); + const resultIndex = UrlbarTestUtils.getResultCount(window) - 1; + const result = await UrlbarTestUtils.getDetailsOfResultAt( + window, + resultIndex + ); + + is( + result.dynamicType, + "contextualSearch", + "Second last result is a contextual search result" + ); + + info("Focus and select the contextual search result"); + UrlbarTestUtils.setSelectedRowIndex(window, resultIndex); + let onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + expectedUrl + ); + EventUtils.synthesizeKey("KEY_Enter"); + await onLoad; + + Assert.equal( + gBrowser.selectedBrowser.currentURI.spec, + expectedUrl, + "Selecting the contextual search result opens the search URL" + ); +}); + +add_task(async function test_selectContextualSearchResult_not_installed() { + const ENGINE_TEST_URL = + "http://mochi.test:8888/browser/browser/components/search/test/browser/opensearch.html"; + const EXPECTED_URL = + "http://mochi.test:8888/browser/browser/components/search/test/browser/?search&test=search"; + let onLoaded = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + ENGINE_TEST_URL + ); + BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, ENGINE_TEST_URL); + await onLoaded; + + const query = "search"; + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: query, + }); + const resultIndex = UrlbarTestUtils.getResultCount(window) - 1; + const result = await UrlbarTestUtils.getDetailsOfResultAt( + window, + resultIndex + ); + + Assert.equal( + result.dynamicType, + "contextualSearch", + "Second last result is a contextual search result" + ); + + info("Focus and select the contextual search result"); + UrlbarTestUtils.setSelectedRowIndex(window, resultIndex); + let onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + EXPECTED_URL + ); + EventUtils.synthesizeKey("KEY_Enter"); + await onLoad; + + Assert.equal( + gBrowser.selectedBrowser.currentURI.spec, + EXPECTED_URL, + "Selecting the contextual search result opens the search URL" + ); +}); 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..4a81ff08be --- /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..9c32115fb4 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_copying.js @@ -0,0 +1,416 @@ +/* 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.loadURIString( + 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: "xample.com", + copyExpected: "e", + }, + { + copyVal: "xmple.com", + copyExpected: "ea", + }, + { + copyVal: "mple.com", + copyExpected: "exa", + }, + { + copyVal: "mple.co", + copyExpected: "exam", + }, + { + copyVal: "", + 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: "m", + copyExpected: "example.co", + }, + { + copyVal: "eample.com", + copyExpected: "x", + }, + { + copyVal: "xample.com", + copyExpected: "e", + }, + { + copyVal: "xample.co", + copyExpected: "em", + }, + { + copyVal: "", + copyExpected: "example.com", + }, + + { + loadURL: "http://example.com/foo", + expectedURL: "example.com/foo", + copyExpected: "http://example.com/foo", + }, + { + copyVal: "/foo", + copyExpected: "http://example.com", + }, + { + copyVal: ".com/foo", + copyExpected: "example", + }, + // Test that partially selected URL is copied with encoded spaces + { + loadURL: "http://example.com/%20space/test", + expectedURL: "example.com/ space/test", + copyExpected: "http://example.com/%20space/test", + }, + { + copyVal: "/test", + copyExpected: "http://example.com/%20space", + }, + { + copyVal: "", + copyExpected: "http://example.com/%20space/test", + }, + { + loadURL: "http://example.com/%20foo%20bar%20baz/", + expectedURL: "example.com/ foo bar baz/", + copyExpected: "http://example.com/%20foo%20bar%20baz/", + }, + { + copyVal: " baz/", + copyExpected: "http://example.com/%20foo%20bar", + }, + { + copyVal: "example. baz/", + copyExpected: "com/ foo bar", + }, + + // 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: ")()\xe9", + copyExpected: "http://example.com/(", + }, + { + copyVal: "e)()\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\xe9", + copyExpected: "xample.com/\xe9", + }, + { + copyVal: "\xe9", + copyExpected: "http://example.com/%C3%A9", + }, + { + // 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: "soo", + copyExpected: "ub2.ält.mochi.test:8888/f", + }, + { + copyVal: "oo", + copyExpected: "http://sub2.%C3%A4lt.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\xf7", + copyExpected: "xample.com/?\xf7", + }, + { + copyVal: "\xf7", + copyExpected: "http://example.com/?%C3%B7", + }, + { + 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: "%C3%A9 %25P)", + copyExpected: "data:text/html,(", + }, + { + copyVal: ")", + 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: "ография", + copyExpected: "http://example.com/%D0%B1%D0%B8", + }, + + { + 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..ae0b4dfda1 --- /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..f4a883ea30 --- /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..a0aacc83d2 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_dns_first_for_single_words.js @@ -0,0 +1,52 @@ +/* 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.importESModule( + "resource://testing-common/Sinon.sys.mjs" + ); + 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..2f9cc19983 --- /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_setup(async function () { + 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..52c19e8965 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_dragdropURL.js @@ -0,0 +1,106 @@ +/* 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 SearchTestUtils.installSearchExtension({}, { setAsDefault: true }); + + registerCleanupFunction(async function cleanup() { + while (gBrowser.tabs.length > 1) { + BrowserTestUtils.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]); + } + }); + + CustomizableUI.addWidgetToArea("home-button", "nav-bar"); + registerCleanupFunction(() => + CustomizableUI.removeWidgetFromArea("home-button") + ); +}); + +/** + * 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 = "https://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 = "https://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..a4e9013be5 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_dynamicResults.js @@ -0,0 +1,799 @@ +/* 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", + attribute_to_remove: "value", + }, + }, + { + 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. +async function checkViewUpdated(provider) { + 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")' + ); + } + + let button1 = row.querySelector( + `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-button1` + ); + + Assert.equal( + button1.hasAttribute("attribute_to_remove"), + false, + "Attribute should be removed" + ); + + // text.textContent should be updated. + Assert.equal( + text.textContent, + `result.payload.searchString is: ${searchString}`, + "text.textContent" + ); + + await UrlbarTestUtils.promisePopupClose(window); + } + }, provider); +} + +add_task(async function checkViewUpdatedPlain() { + await checkViewUpdated(new TestProvider()); +}); + +add_task(async function checkViewUpdatedWDynamicViewTemplate() { + /** + * A dummy provider that provides the viewTemplate dynamically. + */ + class TestShouldCallGetViewTemplateProvider extends TestProvider { + getViewTemplateWasCalled = false; + + getViewTemplate() { + this.getViewTemplateWasCalled = true; + return DYNAMIC_TYPE_VIEW_TEMPLATE; + } + } + + let provider = new TestShouldCallGetViewTemplateProvider(); + Assert.ok( + !provider.getViewTemplateWasCalled, + "getViewTemplate has not yet been called for the provider" + ); + Assert.ok( + !UrlbarView.dynamicViewTemplatesByName.get(DYNAMIC_TYPE_NAME), + "No template has been registered" + ); + await checkViewUpdated(provider); + Assert.ok( + provider.getViewTemplateWasCalled, + "getViewTemplate was called for the provider" + ); +}); + +// 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. TAB 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_Tab"); + 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"); + } + + // TAB again to select the result after the dynamic result. + EventUtils.synthesizeKey("KEY_Tab"); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 2, + "Row at index 2 selected" + ); + Assert.notEqual( + UrlbarTestUtils.getSelectedRow(window), + row, + "Row is not selected" + ); + + // SHIFT+TAB back 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_Tab", { shiftKey: true }); + 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"); + } + + // SHIFT+TAB again to select the heuristic result. + EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }); + 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. TAB 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_Tab", { 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. TAB 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_Tab", { 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 onEngagement 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.loadURIString(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 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, + attribute_to_remove: null, + }, + }, + button2: { + textContent: "Button 2", + attributes: { + searchString: result.payload.searchString, + }, + }, + }; + } + + onEngagement(isPrivate, state, queryContext, details) { + if (this._pickPromiseResolve) { + let { result, element } = details; + 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); + if (!provider.getViewTemplate) { + 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); + if (!provider.getViewTemplate) { + 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 || {})) { + if (name == "attribute_to_remove") { + Assert.equal( + actualChild.hasAttribute(name), + false, + `attribute: ${name}` + ); + continue; + } + 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..5a710c1285 --- /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 = BrowserUIUtils.trimURLProtocol + "invalid.somehost/mytest"; + +add_setup(async function () { + 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_engagement.js b/browser/components/urlbar/tests/browser/browser_engagement.js new file mode 100644 index 0000000000..b964a61a75 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_engagement.js @@ -0,0 +1,206 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests the UrlbarProvider.onEngagement() method. + +"use strict"; + +add_task(async function abandonment() { + await doTest({ + expectedEndState: "abandonment", + endEngagement: async () => { + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + }, + }); +}); + +add_task(async function engagement() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + await doTest({ + expectedEndState: "engagement", + endEngagement: async () => { + let result, element; + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_ArrowDown"); + result = gURLBar.view.selectedResult; + element = gURLBar.view.selectedElement; + EventUtils.synthesizeKey("KEY_Enter"); + }); + return { result, element }; + }, + expectedEndDetails: { + selIndex: 0, + selType: "history", + provider: "", + searchSource: "urlbar", + isSessionOngoing: false, + }, + }); + }); +}); + +add_task(async function privateWindow_abandonment() { + let win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + await doTest({ + win, + expectedEndState: "abandonment", + expectedIsPrivate: true, + endEngagement: async () => { + await UrlbarTestUtils.promisePopupClose(win, () => win.gURLBar.blur()); + }, + }); + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function privateWindow_engagement() { + let win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + await doTest({ + win, + expectedEndState: "engagement", + expectedIsPrivate: true, + endEngagement: async () => { + let result, element; + await UrlbarTestUtils.promisePopupClose(win, () => { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + result = win.gURLBar.view.selectedResult; + element = win.gURLBar.view.selectedElement; + EventUtils.synthesizeKey("KEY_Enter", {}, win); + }); + return { result, element }; + }, + expectedEndDetails: { + selIndex: 0, + selType: "history", + provider: "", + searchSource: "urlbar", + isSessionOngoing: false, + }, + }); + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Performs an engagement test. + * + * @param {object} options + * Options object. + * @param {string} options.expectedEndState + * The expected state at the end of the engagement. + * @param {Function} options.endEngagement + * A function that should end the engagement. If the expected end state is + * "engagement", the function should return `{ result, element }` with the + * expected engaged result and element. + * @param {window} [options.win] + * The window to perform the test in. + * @param {boolean} [options.expectedIsPrivate] + * Whether the engagement and query context are expected to be private. + * @param {object} [options.expectedEndDetails] + * The expected `details` at the end of the engagement. `searchString` is + * automatically included since it's always present. If `provider` is + * expected, then include it and set it to any value; this function will + * replace it with the name of the test provider. + */ +async function doTest({ + expectedEndState, + endEngagement, + win = window, + expectedIsPrivate = false, + expectedEndDetails = {}, +}) { + let provider = new TestProvider(); + UrlbarProvidersManager.registerProvider(provider); + + let startPromise = provider.promiseEngagement(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "test", + fireInputEvent: true, + }); + + let [isPrivate, state, queryContext, details] = await startPromise; + Assert.equal(isPrivate, expectedIsPrivate, "Start isPrivate"); + Assert.equal(state, "start", "Start state"); + + // `queryContext` isn't always defined for `start`, and `onEngagement` + // shouldn't rely on it being defined on start, but there's no good reason to + // assert that it's not defined here. + + // Similarly, `details` is never defined for `start`, but there's no good + // reason to assert that it's not defined. + + let endPromise = provider.promiseEngagement(); + let { result, element } = (await endEngagement()) ?? {}; + + [isPrivate, state, queryContext, details] = await endPromise; + Assert.equal(isPrivate, expectedIsPrivate, "End isPrivate"); + Assert.equal(state, expectedEndState, "End state"); + Assert.ok(queryContext, "End queryContext"); + Assert.equal( + queryContext.isPrivate, + expectedIsPrivate, + "End queryContext.isPrivate" + ); + + let detailsDefaults = { + searchString: "test", + searchSource: "urlbar", + provider: undefined, + selIndex: -1, + }; + if ("provider" in expectedEndDetails) { + detailsDefaults.provider = provider.name; + delete expectedEndDetails.provider; + } + + if (expectedEndState == "engagement") { + Assert.ok( + result, + "endEngagement() should have returned the expected engaged result" + ); + Assert.ok( + element, + "endEngagement() should have returned the expected engaged element" + ); + expectedEndDetails.result = result; + expectedEndDetails.element = element; + } + + Assert.deepEqual( + details, + Object.assign(detailsDefaults, expectedEndDetails), + "End details" + ); + + UrlbarProvidersManager.unregisterProvider(provider); +} + +/** + * Test provider that resolves promises when onEngagement is called. + */ +class TestProvider extends UrlbarTestUtils.TestProvider { + _resolves = []; + + constructor() { + super({ + priority: Infinity, + results: [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://example.com/" } + ), + ], + }); + } + + onEngagement(...args) { + let resolve = this._resolves.shift(); + if (resolve) { + resolve(args); + } + } + + promiseEngagement() { + return new Promise(resolve => this._resolves.push(resolve)); + } +} 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..de1cda7cc1 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_enter.js @@ -0,0 +1,331 @@ +/* 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_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.quickactions", false]], + }); + const engine = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + "searchSuggestionEngine.xml", + setAsDefault: true, + }); + engine.alias = "@default"; +}); + +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"); + await SpecialPowers.pushPrefEnv({ + // The test checks that the untrimmed value is equal to the spec. + // When using showSearchTerms, the untrimmed value becomes + // the search terms. + set: [["browser.urlbar.showSearchTerms.featureGate", false]], + }); + + // 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); + await SpecialPowers.popPrefEnv(); +}); + +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 onPageHide = SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + return new Promise(resolve => { + content.window.addEventListener("pagehide", () => { + 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 pagehide event in the content. + await onPageHide; + 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 pagehide 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); +}); + +add_task(async function typeCharWhileProcessingEnter() { + info("Typing a char while processing enter key"); + const tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + START_VALUE + ); + + const onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + `http://${START_VALUE}` + ); + gURLBar.focus(); + + info("Keydown Enter"); + EventUtils.synthesizeKey("KEY_Enter", { type: "keydown" }); + await TestUtils.waitForCondition( + () => gURLBar._keyDownEnterDeferred, + "Wait for starting process for the enter key" + ); + + info("Keydown a char"); + EventUtils.synthesizeKey("x", { type: "keydown" }); + + info("Keyup both"); + EventUtils.synthesizeKey("x", { type: "keyup" }); + EventUtils.synthesizeKey("KEY_Enter", { type: "keyup" }); + + Assert.equal( + gURLBar.inputField.value, + TEST_VALUE, + "The value of urlbar is correct" + ); + + await onLoad; + Assert.ok("Browser loaded the correct url"); + + // Cleanup. + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function keyupEnterWhilePressingMeta() { + const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + info("Keydown Meta+Enter"); + gURLBar.focus(); + gURLBar.value = ""; + EventUtils.synthesizeKey("KEY_Enter", { type: "keydown", metaKey: true }); + + // Pressing Enter key while pressing Meta key, and next, even when releasing + // Enter key before releasing Meta key, the keyup event is not fired. + // Therefor, we fire Meta keyup event only. + info("Keyup Meta"); + EventUtils.synthesizeKey("KEY_Meta", { type: "keyup" }); + + // Check whether we can input on URL bar. + EventUtils.synthesizeKey("a"); + is(gURLBar.value, "a", "Can input a char"); + + // 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..e102fda09c --- /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..fc32c2c13c --- /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_groupLabels.js b/browser/components/urlbar/tests/browser/browser_groupLabels.js new file mode 100644 index 0000000000..2b43990b77 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_groupLabels.js @@ -0,0 +1,629 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests group labels in the view. + +"use strict"; + +const SUGGESTIONS_FIRST_PREF = "browser.urlbar.showSearchSuggestionsFirst"; +const SUGGESTIONS_PREF = "browser.urlbar.suggest.searches"; + +const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml"; +const TEST_ENGINE_2_BASENAME = "searchSuggestionEngine2.xml"; +const MAX_RESULTS = UrlbarPrefs.get("maxRichResults"); + +const TOP_SITES = [ + "http://example-1.com/", + "http://example-2.com/", + "http://example-3.com/", +]; + +const FIREFOX_SUGGEST_LABEL = "Firefox Suggest"; + +// %s is replaced with the engine name. +const ENGINE_SUGGESTIONS_LABEL = "%s suggestions"; + +// Allow more time for Mac machines so they don't time out in verify mode. +if (AppConstants.platform == "macosx") { + requestLongerTimeout(3); +} + +add_setup(async function () { + Assert.ok( + UrlbarPrefs.get("showSearchSuggestionsFirst"), + "Precondition: Search suggestions shown first by default" + ); + + // Add some history. + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + await UrlbarTestUtils.formHistory.clear(); + await addHistory(); + + // Make sure we have some top sites. + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.topsites", true], + ["browser.newtabpage.activity-stream.default.sites", TOP_SITES.join(",")], + ], + }); + // Waiting for all top sites to be added intermittently times out, so just + // wait for any to be added. We're not testing top sites here; we only need + // the view to open in top-sites mode. + await updateTopSites(sites => sites && sites.length); + + // Add a mock engine so we don't hit the network. + await SearchTestUtils.installSearchExtension({}, { setAsDefault: true }); + + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + }); +}); + +// The Firefox Suggest label should not appear when the labels pref is disabled. +add_task(async function prefDisabled() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.groupLabels.enabled", false]], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await checkLabels(MAX_RESULTS, {}); + await UrlbarTestUtils.promisePopupClose(window); + await SpecialPowers.popPrefEnv(); +}); + +// The Firefox Suggest label should not appear when the view shows top sites. +add_task(async function topSites() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + await checkLabels(-1, {}); + await UrlbarTestUtils.promisePopupClose(window); +}); + +// The Firefox Suggest label should appear when the search string is non-empty +// and there are only general results. +add_task(async function general() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await checkLabels(MAX_RESULTS, { + 1: FIREFOX_SUGGEST_LABEL, + }); + await UrlbarTestUtils.promisePopupClose(window); +}); + +// The Firefox Suggest label should appear when the search string is non-empty +// and there are suggestions followed by general results. +add_task(async function suggestionsBeforeGeneral() { + await withSuggestions(async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await checkLabels(MAX_RESULTS, { + 3: FIREFOX_SUGGEST_LABEL, + }); + await UrlbarTestUtils.promisePopupClose(window); + }); +}); + +// Both the Firefox Suggest and Suggestions labels should appear when the search +// string is non-empty, general results are shown before suggestions, and there +// are general and suggestion results. +add_task(async function generalBeforeSuggestions() { + await withSuggestions(async engine => { + Assert.ok(engine.name, "Engine name is non-empty"); + await SpecialPowers.pushPrefEnv({ + set: [[SUGGESTIONS_FIRST_PREF, false]], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await checkLabels(MAX_RESULTS, { + 1: FIREFOX_SUGGEST_LABEL, + [MAX_RESULTS - 2]: engineSuggestionsLabel(engine.name), + }); + await UrlbarTestUtils.promisePopupClose(window); + }); +}); + +// Neither the Firefox Suggest nor Suggestions label should appear when the +// search string is non-empty, general results are shown before suggestions, and +// there are only suggestion results. +add_task(async function generalBeforeSuggestions_suggestionsOnly() { + await PlacesUtils.history.clear(); + + await withSuggestions(async engine => { + await SpecialPowers.pushPrefEnv({ + set: [[SUGGESTIONS_FIRST_PREF, false]], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await checkLabels(3, {}); + await UrlbarTestUtils.promisePopupClose(window); + }); + + // Add back history so subsequent tasks run with this test's initial state. + await addHistory(); +}); + +// The Suggestions label should be updated when the default engine changes. +add_task(async function generalBeforeSuggestions_defaultChanged() { + // Install both test engines, one after the other. Engine 2 will be the final + // default engine. + await withSuggestions(async engine1 => { + await withSuggestions(async engine2 => { + Assert.ok(engine2.name, "Engine 2 name is non-empty"); + Assert.notEqual(engine1.name, engine2.name, "Engine names are different"); + Assert.equal( + Services.search.defaultEngine.name, + engine2.name, + "Engine 2 is default" + ); + await SpecialPowers.pushPrefEnv({ + set: [[SUGGESTIONS_FIRST_PREF, false]], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await checkLabels(MAX_RESULTS, { + 1: FIREFOX_SUGGEST_LABEL, + [MAX_RESULTS - 2]: engineSuggestionsLabel(engine2.name), + }); + await UrlbarTestUtils.promisePopupClose(window); + }, TEST_ENGINE_2_BASENAME); + }); +}); + +// The Firefox Suggest label should appear above a suggested-index result when +// the result is the only result with that label. +add_task(async function suggestedIndex_only() { + // Clear history, add a provider that returns a result with suggestedIndex = + // -1, set up an engine with suggestions, and start a query. The suggested- + // index result will be the only result with a label. + await PlacesUtils.history.clear(); + + let index = -1; + let provider = new SuggestedIndexProvider(index); + UrlbarProvidersManager.registerProvider(provider); + + await withSuggestions(async engine => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 3); + Assert.equal( + result.element.row.result.suggestedIndex, + index, + "Sanity check: Our suggested-index result is present" + ); + await checkLabels(4, { + 3: FIREFOX_SUGGEST_LABEL, + }); + await UrlbarTestUtils.promisePopupClose(window); + }); + + UrlbarProvidersManager.unregisterProvider(provider); + + // Add back history so subsequent tasks run with this test's initial state. + await addHistory(); +}); + +// The Firefox Suggest label should appear above a suggested-index result when +// the result is the first but not the only result with that label. +add_task(async function suggestedIndex_first() { + let index = 1; + let provider = new SuggestedIndexProvider(index); + UrlbarProvidersManager.registerProvider(provider); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index); + Assert.equal( + result.element.row.result.suggestedIndex, + index, + "Sanity check: Our suggested-index result is present" + ); + await checkLabels(MAX_RESULTS, { + [index]: FIREFOX_SUGGEST_LABEL, + }); + await UrlbarTestUtils.promisePopupClose(window); + + UrlbarProvidersManager.unregisterProvider(provider); +}); + +// The Firefox Suggest label should not appear above a suggested-index result +// when the result is not the first with that label. +add_task(async function suggestedIndex_notFirst() { + let index = -1; + let provider = new SuggestedIndexProvider(index); + UrlbarProvidersManager.registerProvider(provider); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt( + window, + MAX_RESULTS + index + ); + Assert.equal( + result.element.row.result.suggestedIndex, + index, + "Sanity check: Our suggested-index result is present" + ); + await checkLabels(MAX_RESULTS, { + 1: FIREFOX_SUGGEST_LABEL, + }); + await UrlbarTestUtils.promisePopupClose(window); + + UrlbarProvidersManager.unregisterProvider(provider); +}); + +// Labels that appear multiple times but not consecutively should be shown. +add_task(async function repeatLabels() { + let engineName = Services.search.defaultEngine.name; + let results = [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { url: "http://example.com/1" } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.SEARCH, + UrlbarUtils.RESULT_SOURCE.SEARCH, + { suggestion: "test1", engine: engineName } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { url: "http://example.com/2" } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.SEARCH, + UrlbarUtils.RESULT_SOURCE.SEARCH, + { suggestion: "test2", engine: engineName } + ), + ]; + + for (let i = 0; i < results.length; i++) { + results[i].suggestedIndex = i; + } + + let provider = new UrlbarTestUtils.TestProvider({ + results, + priority: Infinity, + }); + UrlbarProvidersManager.registerProvider(provider); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await checkLabels(results.length, { + 0: FIREFOX_SUGGEST_LABEL, + 1: engineSuggestionsLabel(engineName), + 2: FIREFOX_SUGGEST_LABEL, + 3: engineSuggestionsLabel(engineName), + }); + await UrlbarTestUtils.promisePopupClose(window); + + UrlbarProvidersManager.unregisterProvider(provider); +}); + +// Clicking a row label shouldn't do anything. +add_task(async function clickLabel() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + // Do a search. The mock history added in init() should appear with the + // Firefox Suggest label at index 1. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await checkLabels(MAX_RESULTS, { + 1: FIREFOX_SUGGEST_LABEL, + }); + + // Check the result at index 2. + let result2 = await UrlbarTestUtils.getDetailsOfResultAt(window, 2); + Assert.ok(result2.url, "Result at index 2 has a URL"); + let url2 = result2.url; + Assert.ok( + url2.startsWith("http://example.com/"), + "Result at index 2 is one of our mock history results" + ); + + // Get the row at index 3 and click above it. The click should hit the row + // at index 2 and load its URL. We do this to make sure our click code + // here in the test works properly and that performing a similar click + // relative to index 1 (see below) would hit the row at index 0 if not for + // the label at index 1. + let result3 = await UrlbarTestUtils.getDetailsOfResultAt(window, 3); + let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + info("Performing click relative to index 3"); + await UrlbarTestUtils.promisePopupClose(window, () => + click(result3.element.row, { y: -2 }) + ); + info("Waiting for load after performing click relative to index 3"); + await loadPromise; + Assert.equal(gBrowser.currentURI.spec, url2, "Loaded URL at index 2"); + // Now do the search again. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + + await checkLabels(MAX_RESULTS, { + 1: FIREFOX_SUGGEST_LABEL, + }); + + // Check the result at index 1, the one with the label. + let result1 = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.ok(result1.url, "Result at index 1 has a URL"); + let url1 = result1.url; + Assert.ok( + url1.startsWith("http://example.com/"), + "Result at index 1 is one of our mock history results" + ); + Assert.notEqual(url1, url2, "URLs at indexes 1 and 2 are different"); + + // Do a click on the row at index 1 in the same way as before. This time + // nothing should happen because the click should hit the label, not the + // row at index 0. + info("Clicking row label at index 1"); + click(result1.element.row, { y: -2 }); + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(r => setTimeout(r, 500)); + Assert.ok(UrlbarTestUtils.isPopupOpen(window), "View remains open"); + Assert.equal( + gBrowser.currentURI.spec, + url2, + "Current URL is still URL from index 2" + ); + + // Now click the main part of the row at index 1. Its URL should load. + loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + let { height } = result1.element.row.getBoundingClientRect(); + info(`Clicking main part of the row at index 1, height=${height}`); + await UrlbarTestUtils.promisePopupClose(window, () => + click(result1.element.row) + ); + info("Waiting for load after clicking row at index 1"); + await loadPromise; + Assert.equal(gBrowser.currentURI.spec, url1, "Loaded URL at index 1"); + }); +}); + +add_task(async function ariaLabel() { + const helpUrl = "http://example.com/help"; + const results = [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { url: "http://example.com/1", helpUrl } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { url: "http://example.com/2", helpUrl } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { url: "http://example.com/3" } + ), + ]; + + for (let i = 0; i < results.length; i++) { + results[i].suggestedIndex = i; + } + + const provider = new UrlbarTestUtils.TestProvider({ + results, + priority: Infinity, + }); + UrlbarProvidersManager.registerProvider(provider); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await checkLabels(results.length, { + 0: FIREFOX_SUGGEST_LABEL, + }); + + const expectedRows = [ + { hasGroupAriaLabel: true, ariaLabel: FIREFOX_SUGGEST_LABEL }, + { hasGroupAriaLabel: false }, + { hasGroupAriaLabel: false }, + ]; + await checkGroupAriaLabels(expectedRows); + + await UrlbarTestUtils.promisePopupClose(window); + + UrlbarProvidersManager.unregisterProvider(provider); +}); + +/** + * Provider that returns a suggested-index result. + */ +class SuggestedIndexProvider extends UrlbarTestUtils.TestProvider { + constructor(suggestedIndex) { + super({ + results: [ + Object.assign( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { url: "http://example.com/" } + ), + { suggestedIndex } + ), + ], + }); + } +} + +async function addHistory() { + for (let i = 0; i < MAX_RESULTS; i++) { + await PlacesTestUtils.addVisits("http://example.com/" + i); + } +} + +/** + * Asserts that each result in the view does or doesn't have a label, depending + * on `labelsByIndex`. + * + * @param {number} resultCount + * The expected number of results. Pass -1 to use the max index in + * `labelsByIndex` or the actual result count if `labelsByIndex` is empty. + * @param {object} labelsByIndex + * A mapping from indexes to expected labels. + */ +async function checkLabels(resultCount, labelsByIndex) { + if (resultCount >= 0) { + Assert.equal( + UrlbarTestUtils.getResultCount(window), + resultCount, + "Expected result count" + ); + } else { + // This `else` branch is only necessary because waiting for all top sites to + // be added intermittently times out. Don't let the test fail for such a + // dumb reason. + let indexes = Object.keys(labelsByIndex); + if (indexes.length) { + resultCount = indexes.sort((a, b) => b - a)[0] + 1; + } else { + resultCount = UrlbarTestUtils.getResultCount(window); + Assert.greater(resultCount, 0, "Actual result count is > 0"); + } + } + for (let i = 0; i < resultCount; i++) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + let { row } = result.element; + let before = getComputedStyle(row, "::before"); + if (labelsByIndex.hasOwnProperty(i)) { + Assert.equal( + before.content, + "attr(label)", + `::before.content is correct at index ${i}` + ); + Assert.equal( + row.getAttribute("label"), + labelsByIndex[i], + `Row has correct label at index ${i}` + ); + } else { + Assert.equal( + before.content, + "none", + `::before.content is 'none' at index ${i}` + ); + Assert.ok( + !row.hasAttribute("label"), + `Row does not have label attribute at index ${i}` + ); + } + } +} + +/** + * Asserts that an element for group aria label. + * + * @param {Array} expectedRows The expected rows. + */ +async function checkGroupAriaLabels(expectedRows) { + Assert.equal( + UrlbarTestUtils.getResultCount(window), + expectedRows.length, + "Expected result count" + ); + + for (let i = 0; i < expectedRows.length; i++) { + const result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + const { row } = result.element; + const groupAriaLabel = row.querySelector(".urlbarView-group-aria-label"); + + const expected = expectedRows[i]; + + Assert.equal( + !!groupAriaLabel, + expected.hasGroupAriaLabel, + `Group aria label exists as expected in the results[${i}]` + ); + + if (expected.hasGroupAriaLabel) { + Assert.equal( + groupAriaLabel.getAttribute("aria-label"), + expected.ariaLabel, + `Content of aria-label attribute in the element for group aria label in the results[${i}] is correct` + ); + } + } +} + +function engineSuggestionsLabel(engineName) { + return ENGINE_SUGGESTIONS_LABEL.replace("%s", engineName); +} + +/** + * Adds a search engine that provides suggestions, calls your callback, and then + * remove the engine. + * + * @param {Function} callback + * Your callback function. + * @param {string} [engineBasename] + * The basename of the engine file. + */ +async function withSuggestions( + callback, + engineBasename = TEST_ENGINE_BASENAME +) { + await SpecialPowers.pushPrefEnv({ + set: [[SUGGESTIONS_PREF, true]], + }); + let engine = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + engineBasename, + }); + let oldDefaultEngine = await Services.search.getDefault(); + await Services.search.setDefault( + engine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + try { + await callback(engine); + } finally { + await Services.search.setDefault( + oldDefaultEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + await Services.search.removeEngine(engine); + await SpecialPowers.popPrefEnv(); + } +} + +function click(element, { x = undefined, y = undefined } = {}) { + let { width, height } = element.getBoundingClientRect(); + if (typeof x != "number") { + x = width / 2; + } + if (typeof y != "number") { + y = height / 2; + } + EventUtils.synthesizeMouse(element, x, y, { type: "mousedown" }); + EventUtils.synthesizeMouse(element, x, y, { type: "mouseup" }); +} 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..9d8ac8754c --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_handleCommand_fallback.js @@ -0,0 +1,142 @@ +/* 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(); + await SearchTestUtils.installSearchExtension(); + await SearchTestUtils.installSearchExtension({ name: "Example2" }); + + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "https://example.com/?q=%s", + title: "test", + }); + await PlacesUtils.keywords.insert({ + keyword: "keyword", + url: "https://example.com/?q=%s", + }); + registerCleanupFunction(async () => { + sandbox.restore(); + 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. Value: ${value}`); + 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..d0e236fe7e --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_hashChangeProxyState.js @@ -0,0 +1,151 @@ +/* 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_helpUrl.js b/browser/components/urlbar/tests/browser/browser_helpUrl.js new file mode 100644 index 0000000000..5182a8ddb0 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_helpUrl.js @@ -0,0 +1,428 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests the help/info button that appears for results whose payloads have a +// `helpUrl` property. + +"use strict"; + +const MAX_RESULTS = UrlbarPrefs.get("maxRichResults"); +const RESULT_URL = "http://example.com/test"; +const RESULT_HELP_URL = "http://example.com/help"; + +add_setup(async function () { + // Add enough results to fill up the view. + await PlacesUtils.history.clear(); + for (let i = 0; i < MAX_RESULTS; i++) { + await PlacesTestUtils.addVisits("http://example.com/" + i); + } + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + }); +}); + +// Sets `helpL10n` on the result payload and makes sure the help button ends +// up with a corresponding l10n attribute. +add_task(async function title_helpL10n() { + if (UrlbarPrefs.get("resultMenu")) { + return; + } + let provider = registerTestProvider(1); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "example", + window, + }); + + await assertIsTestResult(1); + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + let helpButton = result.element.row._buttons.get("help"); + Assert.ok(helpButton, "Sanity check: help button should exist"); + + let l10nAttrs = document.l10n.getAttributes(helpButton); + Assert.deepEqual( + l10nAttrs, + { id: "urlbar-tip-help-icon", args: null }, + "The l10n ID attribute was correctly set" + ); + + await UrlbarTestUtils.promisePopupClose(window); + UrlbarProvidersManager.unregisterProvider(provider); +}); + +// (SHIFT+)TABs through a result with a help button. The result is the +// second result and has other results after it. +add_task(async function keyboardSelection_secondResult() { + let provider = registerTestProvider(1); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "example", + window, + }); + + // Sanity-check initial state. + Assert.equal( + UrlbarTestUtils.getResultCount(window), + MAX_RESULTS, + "There should be MAX_RESULTS results in the view" + ); + Assert.equal( + UrlbarTestUtils.getSelectedElementIndex(window), + 0, + "The heuristic result should be selected" + ); + await assertIsTestResult(1); + + info("Arrow down to the main part of the result."); + EventUtils.synthesizeKey("KEY_ArrowDown"); + assertMainPartSelected(1); + + info("TAB to the button."); + EventUtils.synthesizeKey("KEY_Tab"); + assertButtonSelected(2); + + info("TAB to the next (third) result."); + EventUtils.synthesizeKey("KEY_Tab"); + assertOtherResultSelected(3, "next result"); + + info("SHIFT+TAB to the help button."); + EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }); + assertButtonSelected(2); + + info("SHIFT+TAB to the main part of the result."); + EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }); + assertMainPartSelected(1); + + info("Arrow up to the previous (first) result."); + EventUtils.synthesizeKey("KEY_ArrowUp"); + assertOtherResultSelected(0, "previous result"); + + await UrlbarTestUtils.promisePopupClose(window); + UrlbarProvidersManager.unregisterProvider(provider); +}); + +// (SHIFT+)TABs through a result with a help button. The result is the +// last result. +add_task(async function keyboardSelection_lastResult() { + let provider = registerTestProvider(MAX_RESULTS - 1); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "example", + window, + }); + + // Sanity-check initial state. + Assert.equal( + UrlbarTestUtils.getResultCount(window), + MAX_RESULTS, + "There should be MAX_RESULTS results in the view" + ); + Assert.equal( + UrlbarTestUtils.getSelectedElementIndex(window), + 0, + "The heuristic result should be selected" + ); + await assertIsTestResult(MAX_RESULTS - 1); + + let numSelectable = UrlbarPrefs.get("resultMenu") + ? MAX_RESULTS * 2 - 2 + : MAX_RESULTS; + + // Arrow down to the main part of the result. + EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: MAX_RESULTS - 1 }); + assertMainPartSelected(numSelectable - 1); + + // TAB to the help button. + EventUtils.synthesizeKey("KEY_Tab"); + assertButtonSelected(numSelectable); + + // Arrow down to the first one-off. 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." + ); + + // SHIFT+TAB to the help button. + EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }); + assertButtonSelected(numSelectable); + + // SHIFT+TAB to the main part of the result. + EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }); + assertMainPartSelected(numSelectable - 1); + + // Arrow up to the previous result. + EventUtils.synthesizeKey("KEY_ArrowUp"); + assertOtherResultSelected( + numSelectable - (UrlbarPrefs.get("resultMenu") ? 3 : 2), + "previous result" + ); + + await UrlbarTestUtils.promisePopupClose(window); + UrlbarProvidersManager.unregisterProvider(provider); +}); + +// Picks the main part of the test result -- the non-help-button part -- with +// the keyboard. +add_task(async function pick_mainPart_keyboard() { + await doPickTest({ pickButton: false, useKeyboard: true }); +}); + +// Picks the help button with the keyboard. +add_task(async function pick_helpButton_keyboard() { + await doPickTest({ pickButton: true, useKeyboard: true }); +}); + +// Picks the main part of the test result -- the non-help-button part -- with +// the mouse. +add_task(async function pick_mainPart_mouse() { + await doPickTest({ pickButton: false, useKeyboard: false }); +}); + +// Picks the help button with the mouse. +add_task(async function pick_helpButton_mouse() { + await doPickTest({ pickButton: true, useKeyboard: false }); +}); + +async function doPickTest({ pickButton, useKeyboard }) { + await BrowserTestUtils.withNewTab("about:blank", async () => { + let index = 1; + let provider = registerTestProvider(index); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "example", + window, + }); + + // Sanity-check initial state. + Assert.equal( + UrlbarTestUtils.getSelectedElementIndex(window), + 0, + "The heuristic result should be selected" + ); + await assertIsTestResult(index); + + if (useKeyboard) { + // Arrow down to the result. + EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: index }); + assertMainPartSelected( + UrlbarPrefs.get("resultMenu") ? index * 2 - 1 : index + ); + } + + // Pick the result. The appropriate URL should load. + let loadPromise = pickButton + ? BrowserTestUtils.waitForNewTab(gBrowser) + : BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + await Promise.all([ + loadPromise, + UrlbarTestUtils.promisePopupClose(window, async () => { + if (pickButton && UrlbarPrefs.get("resultMenu")) { + await UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "h", { + openByMouse: !useKeyboard, + resultIndex: index, + }); + } else if (useKeyboard) { + if (pickButton) { + // TAB to the button. + EventUtils.synthesizeKey("KEY_Tab"); + assertButtonSelected(index + 1); + } + EventUtils.synthesizeKey("KEY_Enter"); + } else { + // Get the click target. + let result = await UrlbarTestUtils.getDetailsOfResultAt( + window, + index + ); + let clickTarget = pickButton + ? result.element.row._buttons.get("help") + : result.element.row._content; + Assert.ok( + clickTarget, + "Click target found, pickButton=" + pickButton + ); + EventUtils.synthesizeMouseAtCenter(clickTarget, {}); + } + }), + ]); + Assert.equal( + gBrowser.selectedBrowser.currentURI.spec, + pickButton ? RESULT_HELP_URL : RESULT_URL, + "Expected URL should have loaded" + ); + + if (pickButton) { + BrowserTestUtils.removeTab(gBrowser.selectedTab); + } + UrlbarProvidersManager.unregisterProvider(provider); + + // Avoid showing adaptive history autofill. + await PlacesTestUtils.clearInputHistory(); + }); +} + +/** + * Registers a provider that creates a result with a help button. + * + * @param {number} suggestedIndex + * The result's suggestedIndex. + * @returns {UrlbarProvider} + * The new provider. + */ +function registerTestProvider(suggestedIndex) { + let results = [ + Object.assign( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + url: RESULT_URL, + helpUrl: RESULT_HELP_URL, + helpL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-tip-get-help" + : "urlbar-tip-help-icon", + }, + } + ), + { suggestedIndex } + ), + ]; + let provider = new UrlbarTestUtils.TestProvider({ results }); + UrlbarProvidersManager.registerProvider(provider); + return provider; +} + +/** + * Asserts that the result at the given index is our test result with a help + * button. + * + * @param {number} index + * The expected index of the test result. + */ +async function assertIsTestResult(index) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.URL, + "The second result should be a URL" + ); + Assert.equal( + result.url, + RESULT_URL, + "The result's URL should be the expected URL" + ); + + let { row } = result.element; + if (UrlbarPrefs.get("resultMenu")) { + Assert.ok(row._buttons.get("menu"), "The result should have a menu button"); + } else { + let helpButton = row._buttons.get("help"); + Assert.ok(helpButton, "The result should have a help button"); + Assert.ok(helpButton.id, "Help button has an ID"); + } + Assert.ok(row._content.id, "Row-inner has an ID"); + Assert.equal( + row.getAttribute("role"), + "presentation", + "Row should have role=presentation" + ); + Assert.equal( + row._content.getAttribute("role"), + "option", + "Row-inner should have role=option" + ); +} + +/** + * Asserts that a particular element is selected. + * + * @param {number} expectedSelectedElementIndex + * The expected selected element index. + * @param {string} expectedClassName + * A class name of the expected selected element. + * @param {string} msg + * A string to include in the assertion message. + */ +function assertSelection(expectedSelectedElementIndex, expectedClassName, msg) { + Assert.equal( + UrlbarTestUtils.getSelectedElementIndex(window), + expectedSelectedElementIndex, + "Expected selected element index: " + msg + ); + Assert.ok( + UrlbarTestUtils.getSelectedElement(window).classList.contains( + expectedClassName + ), + `Expected selected element: ${msg} (${ + UrlbarTestUtils.getSelectedElement(window).classList + } == ${expectedClassName})` + ); +} + +/** + * Asserts that the main part of our test resut -- the non-help-button part -- + * is selected. + * + * @param {number} expectedSelectedElementIndex + * The expected selected element index. + */ +function assertMainPartSelected(expectedSelectedElementIndex) { + assertSelection( + expectedSelectedElementIndex, + "urlbarView-row-inner", + "main part of test result" + ); +} + +/** + * Asserts that the help button part of our test result is selected. + * + * @param {number} expectedSelectedElementIndex + * The expected selected element index. + */ +function assertButtonSelected(expectedSelectedElementIndex) { + if (UrlbarPrefs.get("resultMenu")) { + assertSelection( + expectedSelectedElementIndex, + "urlbarView-button-menu", + "menu button" + ); + } else { + assertSelection( + expectedSelectedElementIndex, + "urlbarView-button-help", + "help button" + ); + } +} + +/** + * Asserts that a result other than our test result is selected. + * + * @param {number} expectedSelectedElementIndex + * The expected selected element index. + * @param {string} msg + * A string to include in the assertion message. + */ +function assertOtherResultSelected(expectedSelectedElementIndex, msg) { + Assert.equal( + UrlbarTestUtils.getSelectedElementIndex(window), + expectedSelectedElementIndex, + "Expected other selected element index: " + msg + ); +} 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..fa7c65b378 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_heuristicNotAddedFirst.js @@ -0,0 +1,159 @@ +/* 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 = makeTipResult(); + nonHeuristicResult.suggestedIndex = 1; + let nonHeuristicProvider = new UrlbarTestUtils.TestProvider({ + results: [nonHeuristicResult], + name: "nonHeuristicProvider", + priority: Infinity, + }); + UrlbarProvidersManager.registerProvider(nonHeuristicProvider); + + // Do a search. + const win = await BrowserTestUtils.openNewBrowserWindow(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "test", + window: win, + }); + + // The first result should be the heuristic and it should be selected. + let actualHeuristic = await UrlbarTestUtils.getDetailsOfResultAt(win, 0); + Assert.equal(actualHeuristic.type, UrlbarUtils.RESULT_TYPE.SEARCH); + Assert.equal(UrlbarTestUtils.getSelectedElementIndex(win), 0); + + // Check the second result for good measure. + let actualNonHeuristic = await UrlbarTestUtils.getDetailsOfResultAt(win, 1); + Assert.equal(actualNonHeuristic.type, UrlbarUtils.RESULT_TYPE.TIP); + + await UrlbarTestUtils.promisePopupClose(win); + UrlbarProvidersManager.unregisterProvider(heuristicProvider); + UrlbarProvidersManager.unregisterProvider(nonHeuristicProvider); + await BrowserTestUtils.closeWindow(win); +}); + +// 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 = makeTipResult(); + 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. + const win = await BrowserTestUtils.openNewBrowserWindow(); + let searchPromise = UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "test", + window: win, + }); + + // 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(win, () => {}); + EventUtils.synthesizeKey("KEY_ArrowUp", {}, win); + + // Wait for the search to finish. + await searchPromise; + + // The first result should be the heuristic. + let actualHeuristic = await UrlbarTestUtils.getDetailsOfResultAt(win, 0); + Assert.equal(actualHeuristic.type, UrlbarUtils.RESULT_TYPE.SEARCH); + + // Check the second result for good measure. + let actualNonHeuristic = await UrlbarTestUtils.getDetailsOfResultAt(win, 1); + Assert.equal(actualNonHeuristic.type, UrlbarUtils.RESULT_TYPE.TIP); + + // No result should be selected. + Assert.equal(UrlbarTestUtils.getSelectedElement(win), null); + Assert.equal(UrlbarTestUtils.getSelectedElementIndex(win), -1); + + // The one-off settings button should be selected. + Assert.equal( + win.gURLBar.view.oneOffSearchButtons.selectedButton, + win.gURLBar.view.oneOffSearchButtons.settingsButton + ); + + await UrlbarTestUtils.promisePopupClose(win); + UrlbarProvidersManager.unregisterProvider(heuristicProvider); + UrlbarProvidersManager.unregisterProvider(nonHeuristicProvider); + await BrowserTestUtils.closeWindow(win); +}); + +function makeTipResult() { + return new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TIP, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + helpUrl: "http://example.com/", + type: "test", + titleL10n: { id: "urlbar-search-tips-confirm" }, + buttons: [ + { + url: "http://example.com/", + l10n: { id: "urlbar-search-tips-confirm" }, + }, + ], + } + ); +} diff --git a/browser/components/urlbar/tests/browser/browser_hideHeuristic.js b/browser/components/urlbar/tests/browser/browser_hideHeuristic.js new file mode 100644 index 0000000000..e8f8774e01 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_hideHeuristic.js @@ -0,0 +1,513 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Basic smoke tests for the `browser.urlbar.experimental.hideHeuristic` pref, +// which hides the heuristic result. Each task performs a search that triggers a +// specific heuristic, verifies that it's hidden or shown as appropriate, and +// verifies that it's picked when enter is pressed. +// +// If/when it becomes the default, we should update existing tests as necessary +// and remove this one. + +"use strict"; + +// Allow more time for Mac machines so they don't time out in verify mode. +if (AppConstants.platform == "macosx") { + requestLongerTimeout(3); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.experimental.hideHeuristic", true], + ["browser.urlbar.suggest.quickactions", false], + ], + }); + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +}); + +// UrlbarUtils.RESULT_GROUP.HEURISTIC_EXTENSION should be hidden. +add_task(async function extension() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + await withVisits(async visitURLs => { + // Add an extension provider that returns a heuristic. + let url = "http://example.com/extension-test"; + let provider = new UrlbarTestUtils.TestProvider({ + name: "ExtensionTest", + type: UrlbarUtils.PROVIDER_TYPE.EXTENSION, + results: [ + Object.assign( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + url, + title: "Test", + } + ), + { heuristic: true } + ), + ], + }); + UrlbarProvidersManager.registerProvider(provider); + + // Do a search that fetches the provider's result and check it. + let heuristic = await search({ + value: "test", + expectedGroup: UrlbarUtils.RESULT_GROUP.HEURISTIC_EXTENSION, + }); + Assert.equal(heuristic.payload.url, url, "Heuristic URL is correct"); + + // Check the other visit results. + await checkVisitResults(visitURLs); + + // Press enter to verify the heuristic result is loaded. + await synthesizeEnterAndAwaitLoad(url); + + UrlbarProvidersManager.unregisterProvider(provider); + }); + }); +}); + +// UrlbarUtils.RESULT_GROUP.HEURISTIC_OMNIBOX should be hidden. +add_task(async function omnibox() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + // Load an extension. + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + omnibox: { + keyword: "omniboxtest", + }, + }, + background() { + /* global browser */ + browser.omnibox.onInputEntered.addListener(() => { + browser.test.sendMessage("onInputEntered"); + }); + }, + }); + await extension.startup(); + + // Do a search using the omnibox keyword and check the hidden heuristic + // result. + let heuristic = await search({ + value: "omniboxtest foo", + expectedGroup: UrlbarUtils.RESULT_GROUP.HEURISTIC_OMNIBOX, + }); + Assert.equal( + heuristic.payload.keyword, + "omniboxtest", + "Heuristic keyword is correct" + ); + + // Press enter to verify the heuristic result is picked. + let messagePromise = extension.awaitMessage("onInputEntered"); + EventUtils.synthesizeKey("KEY_Enter"); + await messagePromise; + + await extension.unload(); + }); +}); + +// UrlbarUtils.RESULT_GROUP.HEURISTIC_SEARCH_TIP should be shown. +add_task(async function searchTip() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.searchTips.test.ignoreShowLimits", true]], + }); + await BrowserTestUtils.withNewTab( + { + gBrowser: window.gBrowser, + url: "about:newtab", + // `withNewTab` hangs waiting for about:newtab to load without this. + waitForLoad: false, + }, + async () => { + await UrlbarTestUtils.promisePopupOpen(window, () => {}); + Assert.ok(true, "View opened"); + Assert.equal(UrlbarTestUtils.getResultCount(window), 1); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.TIP); + Assert.ok(result.heuristic); + Assert.ok(UrlbarTestUtils.getSelectedElement(window), "Selection exists"); + } + ); + await SpecialPowers.popPrefEnv(); +}); + +// UrlbarUtils.RESULT_GROUP.HEURISTIC_ENGINE_ALIAS should be hidden. +add_task(async function engineAlias() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + await withVisits(async visitURLs => { + // Add an engine with an alias. + await withEngine({ keyword: "test" }, async () => { + // Do a search using the alias and check the hidden heuristic result. + // The heuristic will be HEURISTIC_FALLBACK, not HEURISTIC_ENGINE_ALIAS, + // because two searches are performed and + // `UrlbarTestUtils.promiseAutocompleteResultPopup` waits for both. The + // first returns a HEURISTIC_ENGINE_ALIAS that triggers search mode and + // then an immediate second search, which returns HEURISTIC_FALLBACK. + let heuristic = await search({ + value: "test foo", + expectedGroup: UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK, + }); + Assert.equal( + heuristic.payload.engine, + "Example", + "Heuristic engine is correct" + ); + Assert.equal( + heuristic.payload.query, + "foo", + "Heuristic query is correct" + ); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: "Example", + entry: "typed", + }); + + // Check the other visit results. + await checkVisitResults(visitURLs); + + // Press enter to verify the heuristic result is loaded. + await synthesizeEnterAndAwaitLoad("https://example.com/?q=foo"); + }); + }); + }); +}); + +// UrlbarUtils.RESULT_GROUP.HEURISTIC_BOOKMARK_KEYWORD should be hidden. +add_task(async function bookmarkKeyword() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + await withVisits(async visitURLs => { + // Add a bookmark with a keyword. + let keyword = "bm"; + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/?q=%s", + title: "test", + }); + await PlacesUtils.keywords.insert({ keyword, url: bm.url }); + + // Do a search using the keyword and check the hidden heuristic result. + let heuristic = await search({ + value: "bm foo", + expectedGroup: UrlbarUtils.RESULT_GROUP.HEURISTIC_BOOKMARK_KEYWORD, + }); + Assert.equal( + heuristic.payload.keyword, + keyword, + "Heuristic keyword is correct" + ); + let heuristicURL = "http://example.com/?q=foo"; + Assert.equal( + heuristic.payload.url, + heuristicURL, + "Heuristic URL is correct" + ); + + // Check the other visit results. + await checkVisitResults(visitURLs); + + // Press enter to verify the heuristic result is loaded. + await synthesizeEnterAndAwaitLoad(heuristicURL); + + await PlacesUtils.bookmarks.eraseEverything(); + await UrlbarTestUtils.formHistory.clear(window); + }); + }); +}); + +// UrlbarUtils.RESULT_GROUP.HEURISTIC_AUTOFILL should be hidden. +add_task(async function autofill() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + await withVisits(async visitURLs => { + // Do a search that triggers autofill and check the hidden heuristic + // result. + let heuristic = await search({ + value: "ex", + expectedGroup: UrlbarUtils.RESULT_GROUP.HEURISTIC_AUTOFILL, + }); + Assert.ok(heuristic.autofill, "Heuristic is autofill"); + let heuristicURL = "http://example.com/"; + Assert.equal( + heuristic.payload.url, + heuristicURL, + "Heuristic URL is correct" + ); + Assert.equal(gURLBar.value, "example.com/", "Input has been autofilled"); + + // Check the other visit results. + await checkVisitResults(visitURLs); + + // Press enter to verify the heuristic result is loaded. + await synthesizeEnterAndAwaitLoad(heuristicURL); + }); + }); +}); + +// UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK with an unknown URL should be +// hidden. +add_task(async function fallback_unknownURL() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + // Do a search for an unknown URL and check the hidden heuristic result. + let url = "http://example.com/unknown-url"; + let heuristic = await search({ + value: url, + expectedGroup: UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK, + }); + Assert.equal(heuristic.payload.url, url, "Heuristic URL is correct"); + + // Press enter to verify the heuristic result is loaded. + await synthesizeEnterAndAwaitLoad(url); + }); +}); + +// UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK with the search restriction token +// should be hidden. +add_task(async function fallback_searchRestrictionToken() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + await withVisits(async visitURLs => { + // Add a mock default engine so we don't hit the network. + await withEngine({ makeDefault: true }, async () => { + // Do a search with `?` and check the hidden heuristic result. + let heuristic = await search({ + value: UrlbarTokenizer.RESTRICT.SEARCH + " foo", + expectedGroup: UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK, + }); + Assert.equal( + heuristic.payload.engine, + "Example", + "Heuristic engine is correct" + ); + Assert.equal( + heuristic.payload.query, + "foo", + "Heuristic query is correct" + ); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: "Example", + entry: "typed", + }); + + // Check the other visit results. + await checkVisitResults(visitURLs); + + // Press enter to verify the heuristic result is loaded. + await synthesizeEnterAndAwaitLoad("https://example.com/?q=foo"); + + await UrlbarTestUtils.formHistory.clear(window); + }); + }); + }); +}); + +// UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK with a search string that falls +// back to a search result should be hidden. +add_task(async function fallback_search() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + await withVisits(async visitURLs => { + // Add a mock default engine so we don't hit the network. + await withEngine({ makeDefault: true }, async () => { + // Do a search and check the hidden heuristic result. + let heuristic = await search({ + value: "foo", + expectedGroup: UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK, + }); + Assert.equal( + heuristic.payload.engine, + "Example", + "Heuristic engine is correct" + ); + Assert.equal( + heuristic.payload.query, + "foo", + "Heuristic query is correct" + ); + + // Check the other visit results. + await checkVisitResults(visitURLs); + + // Press enter to verify the heuristic result is loaded. + await synthesizeEnterAndAwaitLoad("https://example.com/?q=foo"); + + await UrlbarTestUtils.formHistory.clear(window); + }); + }); + }); +}); + +// Picking a non-heuristic result should work correctly (and not pick the +// heuristic). +add_task(async function pickNonHeuristic() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + await withVisits(async visitURLs => { + // Do a search that triggers autofill and check the hidden heuristic + // result. + let heuristic = await search({ + value: "ex", + expectedGroup: UrlbarUtils.RESULT_GROUP.HEURISTIC_AUTOFILL, + }); + Assert.ok(heuristic.autofill, "Heuristic is autofill"); + Assert.equal( + heuristic.payload.url, + "http://example.com/", + "Heuristic URL is correct" + ); + + // Pick the first visit result. + Assert.notEqual( + heuristic.payload.url, + visitURLs[0], + "Sanity check: Heuristic and first results have different URLs" + ); + EventUtils.synthesizeKey("KEY_ArrowDown"); + await synthesizeEnterAndAwaitLoad(visitURLs[0]); + }); + }); +}); + +/** + * Adds `maxRichResults` visits, calls your callback, and clears history. We add + * `maxRichResults` visits to verify that the view correctly contains the + * maximum number of results when the heuristic is hidden. + * + * @param {Function} callback + * The callback to call after adding visits. Can be async + */ +async function withVisits(callback) { + let urls = []; + for (let i = 0; i < UrlbarPrefs.get("maxRichResults"); i++) { + urls.push("http://example.com/foo/" + i); + } + await PlacesTestUtils.addVisits(urls); + + // The URLs will appear in the view in reverse order so that newer visits are + // first. Reverse the array now so callers to `checkVisitResults` or + // `checkVisitResults` itself doesn't need to do it. + urls.reverse(); + + await callback(urls); + await PlacesUtils.history.clear(); +} + +/** + * Adds a search engine, calls your callback, and removes the engine. + * + * @param {object} options + * Options object + * @param {string} [options.keyword] + * The keyword/alias for the engine. + * @param {boolean} [options.makeDefault] + * Whether to make the engine default. + * @param {Function} callback + * The callback to call after changing the default search engine. Can be async + */ +async function withEngine( + { keyword = undefined, makeDefault = false }, + callback +) { + await SearchTestUtils.installSearchExtension({ keyword }); + let engine = Services.search.getEngineByName("Example"); + let originalEngine; + if (makeDefault) { + originalEngine = await Services.search.getDefault(); + await Services.search.setDefault( + engine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + } + await callback(); + if (originalEngine) { + await Services.search.setDefault( + originalEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + } + await Services.search.removeEngine(engine); +} + +/** + * Asserts the view contains visit results with the given URLs. + * + * @param {Array} expectedURLs + * The expected urls. + */ +async function checkVisitResults(expectedURLs) { + Assert.equal( + UrlbarTestUtils.getResultCount(window), + expectedURLs.length, + "The view has other results" + ); + for (let i = 0; i < expectedURLs.length; i++) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.URL, + "Other result type is correct at index " + i + ); + Assert.equal( + result.source, + UrlbarUtils.RESULT_SOURCE.HISTORY, + "Other result source is correct at index " + i + ); + Assert.equal( + result.url, + expectedURLs[i], + "Other result URL is correct at index " + i + ); + } +} + +/** + * Performs a search and makes some basic assertions under the assumption that + * the heuristic should be hidden. + * + * @param {object} options + * Options object + * @param {string} options.value + * The search string. + * @param {UrlbarUtils.RESULT_GROUP} options.expectedGroup + * The expected result group of the hidden heuristic. + * @returns {UrlbarResult} + * The hidden heuristic result. + */ +async function search({ value, expectedGroup }) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value, + fireInputEvent: true, + }); + + // _resultForCurrentValue should be the hidden heuristic result. + let { _resultForCurrentValue: result } = gURLBar; + Assert.ok(result, "_resultForCurrentValue is defined"); + Assert.ok(result.heuristic, "_resultForCurrentValue.heuristic is true"); + Assert.equal( + UrlbarUtils.getResultGroup(result), + expectedGroup, + "_resultForCurrentValue has expected group" + ); + + Assert.ok(!UrlbarTestUtils.getSelectedElement(window), "No selection exists"); + + return result; +} + +/** + * Synthesizes the enter key and waits for a load in the current tab. + * + * @param {string} expectedURL + * The URL that should load. + */ +async function synthesizeEnterAndAwaitLoad(expectedURL) { + let loadPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + expectedURL + ); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + await PlacesUtils.history.clear(); +} 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..13a0cf0584 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_ime_composition.js @@ -0,0 +1,327 @@ +/* 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], + ["browser.urlbar.suggest.quickactions", false], + ], + }); + + 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, + }); + + await SearchTestUtils.installSearchExtension( + { + name: "Test", + keyword: "@test", + }, + { setAsDefault: true } + ); + + registerCleanupFunction(async () => { + 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.keepPanelOpenDuringImeComposition", val]], + }); + await test_composition(val); + await test_composition_searchMode_preview(val); + await test_composition_tabToSearch(val); + await test_composition_autofill(val); + } + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); +}); + +async function test_composition(keepPanelOpenDuringImeComposition) { + 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", keepPanelOpenDuringImeComposition); + Assert.equal(gURLBar.value, "Int", "Check urlbar value"); + composeAndCheckPanel("te", keepPanelOpenDuringImeComposition); + 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", keepPanelOpenDuringImeComposition); + Assert.equal(gURLBar.value, "Inter", "Check urlbar value"); + composeAndCheckPanel("", keepPanelOpenDuringImeComposition); + 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", keepPanelOpenDuringImeComposition); + Assert.equal(gURLBar.value, "Int", "Check urlbar value"); + composeAndCheckPanel("te", keepPanelOpenDuringImeComposition); + Assert.equal(gURLBar.value, "Inte", "Check urlbar value"); + composeAndCheckPanel("", keepPanelOpenDuringImeComposition); + 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", keepPanelOpenDuringImeComposition); + Assert.equal(gURLBar.value, "I", "Check urlbar value"); + composeAndCheckPanel("In", keepPanelOpenDuringImeComposition); + Assert.equal(gURLBar.value, "In", "Check urlbar value"); + composeAndCheckPanel("", keepPanelOpenDuringImeComposition); + 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( + keepPanelOpenDuringImeComposition +) { + 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", keepPanelOpenDuringImeComposition); + Assert.equal(gURLBar.value, "I", "Check urlbar value"); + if (keepPanelOpenDuringImeComposition) { + 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(keepPanelOpenDuringImeComposition) { + info("Check Tab-to-Search is retained by composition"); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exa", + fireInputEvent: true, + }); + + 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", keepPanelOpenDuringImeComposition); + Assert.equal(gURLBar.value, "I", "Check urlbar value"); + if (keepPanelOpenDuringImeComposition) { + await UrlbarTestUtils.promiseSearchComplete(window); + } + // Test that we are in confirmed search mode. + await UrlbarTestUtils.assertSearchMode(window, { + engineName: "Test", + entry: "tabtosearch", + }); + await UrlbarTestUtils.exitSearchMode(window); +} + +async function test_composition_autofill(keepPanelOpenDuringImeComposition) { + info("Check whether autofills or not"); + await UrlbarTestUtils.promisePopupClose(window); + + info("Check the urlbar value during composition"); + composeAndCheckPanel("m", false); + + if (keepPanelOpenDuringImeComposition) { + info("Wait for search suggestions"); + await UrlbarTestUtils.promiseSearchComplete(window); + } + + Assert.equal( + gURLBar.value, + "m", + "The urlbar value is not autofilled while turning IME on" + ); + + info("Check the urlbar value after committing composition"); + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeComposition({ + type: "compositioncommitasis", + key: { key: "KEY_Enter" }, + }); + }); + await UrlbarTestUtils.promiseSearchComplete(window); + Assert.equal(gURLBar.value, "mozilla.org/", "The urlbar value is autofilled"); + + // Clean-up. + gURLBar.value = ""; +} 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..7fb93ca35d --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_inputHistory.js @@ -0,0 +1,548 @@ +/* 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, + needToLoad = 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 = needToLoad + ? BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser) + : 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() { + await Cc["@mozilla.org/places/frecency-recalculator;1"] + .getService(Ci.nsIObserver) + .wrappedJSObject.decay(); +} + +add_setup(async function () { + 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("Up to 3 adaptive results should be added at the top, 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, + }); + + // After 1 heuristic and 3 input history results. + let expectedBookmarkIndex = 4; + 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. + let historyUrl = "http://site.tld/1"; + await bumpScore(historyUrl, "site", { visits: 1, picks: 1 }); + + let bookmarkURL = "http://bookmarked.site.tld/1"; + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "test_book", + url: bookmarkURL, + }); + + await SpecialPowers.pushPrefEnv({ + set: [ + // Search only bookmarks. + ["browser.urlbar.suggest.bookmark", true], + ["browser.urlbar.suggest.history", false], + ], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "site", + }); + let result = (await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)) + .result; + Assert.equal(result.payload.url, bookmarkURL, "Check bookmarked result"); + Assert.notEqual( + result.providerName, + "InputHistory", + "The bookmarked result is not from InputHistory." + ); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "Check there are no unexpected results" + ); + await PlacesUtils.bookmarks.remove(bm); + + // Repeat the previous case but now the bookmark has the same URL as the + // history result. We expect the returned result comes from InputHistory. + bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "test_book", + url: historyUrl, + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "sit", + }); + result = (await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)) + .result; + Assert.equal(result.payload.url, historyUrl, "Check bookmarked result"); + Assert.equal( + result.providerName, + "InputHistory", + "The bookmarked result is from InputHistory." + ); + Assert.equal( + result.source, + UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + "The input history result is a bookmark." + ); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "Check there are no unexpected results" + ); + + await SpecialPowers.popPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + // Search only open pages. We don't provide an open page, but we want to + // enable at least one of these prefs so that UrlbarProviderInputHistory + // is active. + ["browser.urlbar.suggest.bookmark", false], + ["browser.urlbar.suggest.history", false], + ["browser.urlbar.suggest.openpage", true], + ], + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "site", + }); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 1, + "There is no adaptive history result because it is not an open page." + ); + await SpecialPowers.popPrefEnv(); + + // Clearing history but not deleting the bookmark. This simulates the case + // where the user has cleared their history or is using permanent private + // browsing mode. + await PlacesUtils.history.clear(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.bookmark", true], + ["browser.urlbar.suggest.history", false], + ["browser.urlbar.suggest.openpage", false], + ], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "sit", + }); + result = (await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)) + .result; + Assert.equal(result.payload.url, historyUrl, "Check bookmarked result"); + Assert.equal( + result.providerName, + "InputHistory", + "The bookmarked result is from InputHistory." + ); + Assert.equal( + result.source, + UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + "The input history result is a bookmark." + ); + + 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"); + 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({ + url: 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, + }); + + await Services.search.removeEngine(suggestionsEngine); +}); + +add_task(async function test_ignore_case() { + const url1 = "http://example.com/yes"; + const url2 = "http://example.com/no"; + await PlacesUtils.history.clear(); + await PlacesTestUtils.addVisits([url1, url2]); + await UrlbarUtils.addToInputHistory(url1, "SampLE"); + await UrlbarUtils.addToInputHistory(url1, "SaMpLE"); + await UrlbarUtils.addToInputHistory(url1, "SAMPLE"); + await UrlbarUtils.addToInputHistory(url1, "sample"); + await UrlbarUtils.addToInputHistory(url2, "sample"); + await UrlbarUtils.addToInputHistory(url2, "sample"); + await UrlbarUtils.addToInputHistory(url2, "sample"); + await UrlbarUtils.addToInputHistory(url2, "sample"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "sAM", + }); + const result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal( + result.url, + url1, + "Seaching for input history is case-insensitive" + ); +}); + +add_task(async function test_adaptive_history_in_privatewindow() { + info( + "Check adaptive history is not shown in private window as tab switching candidate." + ); + + await PlacesUtils.history.clear(); + + info("Add a test url as an input history."); + const url = "http://example.com/"; + // We need to wait for loading the page in order to register the url into + // moz_openpages_temp table. + await bumpScore(url, "exa", { visits: 1, picks: 1 }, false, true); + + info("Check the url could be registered properly."); + const connection = await PlacesUtils.promiseLargeCacheDBConnection(); + const rows = await connection.executeCached( + "SELECT userContextId FROM moz_openpages_temp WHERE url = :url", + { url } + ); + Assert.equal(rows.length, 1, "Length of rows for the url is 1."); + Assert.greaterOrEqual( + rows[0].getResultByName("userContextId"), + 0, + "The url is registered as non-private-browsing context." + ); + + info("Open popup in private window."); + const privateWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: privateWindow, + value: "ex", + }); + + info("Check the popup results."); + let hasResult = false; + for (let i = 0; i < UrlbarTestUtils.getResultCount(privateWindow); i++) { + const result = await UrlbarTestUtils.getDetailsOfResultAt(privateWindow, i); + + if (result.url !== url) { + continue; + } + + Assert.notEqual( + result.type, + UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + "Result type of the url is not for tab switch." + ); + + hasResult = true; + } + Assert.ok(hasResult, "Popup has a result for the url."); + + await BrowserTestUtils.closeWindow(privateWindow); +}); diff --git a/browser/components/urlbar/tests/browser/browser_inputHistory_autofill.js b/browser/components/urlbar/tests/browser/browser_inputHistory_autofill.js new file mode 100644 index 0000000000..19457884b6 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_inputHistory_autofill.js @@ -0,0 +1,207 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests for input history related to autofill. + +"use strict"; + +let addToInputHistorySpy; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.autoFill.adaptiveHistory.enabled", true]], + }); + + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); + + let sandbox = sinon.createSandbox(); + addToInputHistorySpy = sandbox.spy(UrlbarUtils, "addToInputHistory"); + + registerCleanupFunction(async () => { + sandbox.restore(); + }); +}); + +// Input history use count should be bumped when an adaptive history autofill +// result is triggered and picked. +add_task(async function bumped() { + let input = "exam"; + let tests = [ + // Basic test where the search string = the adaptive history input. + { + url: "http://example.com/test", + searchString: "exam", + }, + // The history with input "exam" should be bumped, not "example", even + // though the search string is "example". + { + url: "http://example.com/test", + searchString: "example", + }, + // The history with URL "http://www.example.com/test" should be bumped, not + // "http://example.com/test", even though the search string starts with + // "example". + { + url: "http://www.example.com/test", + searchString: "exam", + }, + ]; + + for (let { url, searchString } of tests) { + info("Running subtest: " + JSON.stringify({ url, searchString })); + + await PlacesTestUtils.addVisits(url); + await UrlbarUtils.addToInputHistory(url, input); + addToInputHistorySpy.resetHistory(); + + let initialUseCount = await getUseCount({ url, input }); + info("Got initial use count: " + initialUseCount); + + await triggerAutofillAndPickResult(searchString, "example.com/test"); + + let calls = addToInputHistorySpy.getCalls(); + Assert.equal( + calls.length, + 1, + "UrlbarUtils.addToInputHistory() called once" + ); + Assert.deepEqual( + calls[0].args, + [url, input], + "UrlbarUtils.addToInputHistory() called with expected args" + ); + + Assert.greater( + await getUseCount({ url, input }), + initialUseCount, + "New use count > initial use count" + ); + + if (searchString != input) { + Assert.strictEqual( + await getUseCount({ input: searchString }), + undefined, + "Search string not present in input history: " + searchString + ); + } + + await PlacesUtils.history.clear(); + await PlacesTestUtils.clearInputHistory(); + addToInputHistorySpy.resetHistory(); + } +}); + +// Input history use count should not be bumped when an origin autofill result +// is triggered and picked. +add_task(async function notBumped_origin() { + // Add enough visits to trigger origin autofill. + let url = "http://example.com/test"; + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits(url); + } + + await triggerAutofillAndPickResult("exam", "example.com/"); + + let calls = addToInputHistorySpy.getCalls(); + Assert.equal(calls.length, 0, "UrlbarUtils.addToInputHistory() not called"); + + Assert.strictEqual( + await getUseCount({ url }), + undefined, + "URL not present in input history: " + url + ); + + await PlacesUtils.history.clear(); +}); + +// Input history use count should not be bumped when a URL autofill result is +// triggered and picked. +add_task(async function notBumped_url() { + let url = "http://example.com/test"; + await PlacesTestUtils.addVisits(url); + + await triggerAutofillAndPickResult("example.com/t", "example.com/test"); + + let calls = addToInputHistorySpy.getCalls(); + Assert.equal(calls.length, 0, "UrlbarUtils.addToInputHistory() not called"); + + Assert.strictEqual( + await getUseCount({ url }), + undefined, + "URL not present in input history: " + url + ); + + await PlacesUtils.history.clear(); +}); + +/** + * Performs a search and picks the first result. + * + * @param {string} searchString + * The search string. Assumed to trigger an autofill result. + * @param {string} autofilledValue + * The input's expected value after autofill occurs. + */ +async function triggerAutofillAndPickResult(searchString, autofilledValue) { + await BrowserTestUtils.withNewTab("about:blank", async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: searchString, + fireInputEvent: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill, "Result is autofill"); + Assert.equal(gURLBar.value, autofilledValue, "gURLBar.value"); + Assert.equal(gURLBar.selectionStart, searchString.length, "selectionStart"); + Assert.equal(gURLBar.selectionEnd, autofilledValue.length, "selectionEnd"); + + let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + }); +} + +/** + * Gets the use count of an input history record. + * + * @param {object} options + * Options object. + * @param {string} [options.url] + * The URL of the `moz_places` row corresponding to the record. + * @param {string} [options.input] + * The `input` value in the record. + * @returns {number} + * The use count. If no record exists with the URL and/or input, undefined is + * returned. + */ +async function getUseCount({ url = undefined, input = undefined }) { + return PlacesUtils.withConnectionWrapper("test::getUseCount", async db => { + let rows; + if (input && url) { + rows = await db.executeCached( + `SELECT i.use_count + FROM moz_inputhistory i + JOIN moz_places h ON h.id = i.place_id + WHERE h.url = :url AND i.input = :input`, + { url, input } + ); + } else if (url) { + rows = await db.executeCached( + `SELECT i.use_count + FROM moz_inputhistory i + JOIN moz_places h ON h.id = i.place_id + WHERE h.url = :url`, + { url } + ); + } else if (input) { + rows = await db.executeCached( + `SELECT use_count + FROM moz_inputhistory i + WHERE input = :input`, + { input } + ); + } + return rows[0]?.getResultByIndex(0); + }); +} 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..28c967a851 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_inputHistory_emptystring.js @@ -0,0 +1,94 @@ +/* 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"); + } + } + ); +} + +const TEST_URL = "http://example.com/"; + +async function do_test(openFn, pickMethod) { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "about:blank", + }, + async function (browser) { + await PlacesTestUtils.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_setup(async function () { + 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.loadURIString(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..03ba6a6473 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_keepStateAcrossTabSwitches.js @@ -0,0 +1,224 @@ +/* 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); +}); + +/** + * Test the urlbar status of text selection and focusing by tab switching. + */ +add_task(async function selectAndFocus() { + // Create a tab with normal web page. + const webpageTabURL = "https://example.com"; + const webpageTab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: webpageTabURL, + }); + + // Create a tab with userTypedValue. + const userTypedTabText = "test"; + const userTypedTab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + }); + await UrlbarTestUtils.inputIntoURLBar(window, userTypedTabText); + + // Create an empty tab. + const emptyTab = await BrowserTestUtils.openNewForegroundTab({ gBrowser }); + + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + BrowserTestUtils.removeTab(webpageTab); + BrowserTestUtils.removeTab(userTypedTab); + BrowserTestUtils.removeTab(emptyTab); + }); + + await doSelectAndFocusTest({ + targetTab: webpageTab, + targetSelectionStart: 0, + targetSelectionEnd: 0, + anotherTab: userTypedTab, + }); + await doSelectAndFocusTest({ + targetTab: webpageTab, + targetSelectionStart: 2, + targetSelectionEnd: 5, + anotherTab: userTypedTab, + }); + await doSelectAndFocusTest({ + targetTab: webpageTab, + targetSelectionStart: webpageTabURL.length, + targetSelectionEnd: webpageTabURL.length, + anotherTab: userTypedTab, + }); + await doSelectAndFocusTest({ + targetTab: webpageTab, + targetSelectionStart: 0, + targetSelectionEnd: 0, + anotherTab: emptyTab, + }); + await doSelectAndFocusTest({ + targetTab: userTypedTab, + targetSelectionStart: 0, + targetSelectionEnd: 0, + anotherTab: webpageTab, + }); + await doSelectAndFocusTest({ + targetTab: userTypedTab, + targetSelectionStart: 0, + targetSelectionEnd: 0, + anotherTab: emptyTab, + }); + await doSelectAndFocusTest({ + targetTab: userTypedTab, + targetSelectionStart: 1, + targetSelectionEnd: 2, + anotherTab: emptyTab, + }); + await doSelectAndFocusTest({ + targetTab: userTypedTab, + targetSelectionStart: userTypedTabText.length, + targetSelectionEnd: userTypedTabText.length, + anotherTab: emptyTab, + }); + await doSelectAndFocusTest({ + targetTab: emptyTab, + targetSelectionStart: 0, + targetSelectionEnd: 0, + anotherTab: webpageTab, + }); + await doSelectAndFocusTest({ + targetTab: emptyTab, + targetSelectionStart: 0, + targetSelectionEnd: 0, + anotherTab: userTypedTab, + }); +}); + +async function doSelectAndFocusTest({ + targetTab, + targetSelectionStart, + targetSelectionEnd, + anotherTab, +}) { + const testCases = [ + { + targetFocus: false, + anotherFocus: false, + }, + { + targetFocus: true, + anotherFocus: false, + }, + { + targetFocus: true, + anotherFocus: true, + }, + ]; + + for (const { targetFocus, anotherFocus } of testCases) { + // Setup the target tab. + await switchTab(targetTab); + setURLBarFocus(targetFocus); + gURLBar.inputField.setSelectionRange( + targetSelectionStart, + targetSelectionEnd + ); + const targetValue = gURLBar.value; + + // Switch to another tab. + await switchTab(anotherTab); + setURLBarFocus(anotherFocus); + + // Switch back to the target tab. + await switchTab(targetTab); + + // Check whether the value, selection and focusing status are reverted. + Assert.equal(gURLBar.value, targetValue); + Assert.equal(gURLBar.focused, targetFocus); + if (gURLBar.focused) { + Assert.equal(gURLBar.selectionStart, targetSelectionStart); + Assert.equal(gURLBar.selectionEnd, targetSelectionEnd); + } else { + Assert.equal(gURLBar.selectionStart, gURLBar.value.length); + Assert.equal(gURLBar.selectionEnd, gURLBar.value.length); + } + } +} + +function setURLBarFocus(focus) { + if (focus) { + gURLBar.focus(); + } else { + gURLBar.blur(); + } +} + +async function switchTab(tab) { + if (gBrowser.selectedTab !== tab) { + EventUtils.synthesizeMouseAtCenter(tab, {}); + await BrowserTestUtils.waitForCondition(() => gBrowser.selectedTab === 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..618bcad3c7 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_keyword.js @@ -0,0 +1,238 @@ +/* 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_setup(async function () { + 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]], + }); + await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + "searchSuggestionEngine.xml", + setAsDefault: true, + }); + + registerCleanupFunction(async function () { + 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..c10fcdd9c3 --- /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 () { + 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..b8402a4e90 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_keywordSearch.js @@ -0,0 +1,57 @@ +/** + * 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_setup(async function () { + await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + "searchSuggestionEngine.xml", + setAsDefault: true, + }); +}); + +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..d2b3aa253a --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_keywordSearch_postData.js @@ -0,0 +1,74 @@ +/** + * 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_setup(async function () { + await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + "POSTSearchEngine.xml", + setAsDefault: true, + }); +}); + +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..b358f3a4ac --- /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..a3222c293f --- /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..b257625f30 --- /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_setup(async function () { + 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.loadURIString(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..f0077c3334 --- /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_setup(async function () { + 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..5a44db54ce --- /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,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..b50446a4c9 --- /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_middleClick.js b/browser/components/urlbar/tests/browser/browser_middleClick.js new file mode 100644 index 0000000000..1a5088f827 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_middleClick.js @@ -0,0 +1,255 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test for middle click behavior. + */ + +add_task(async function test_setup() { + CustomizableUI.addWidgetToArea("home-button", "nav-bar"); + await SpecialPowers.pushPrefEnv({ + set: [["browser.tabs.searchclipboardfor.middleclick", false]], + }); + + registerCleanupFunction(() => { + Services.prefs.clearUserPref("middlemouse.paste"); + Services.prefs.clearUserPref("middlemouse.openNewWindow"); + Services.prefs.clearUserPref("browser.tabs.opentabfor.middleclick"); + Services.prefs.clearUserPref("browser.startup.homepage"); + Services.prefs.clearUserPref("browser.tabs.loadBookmarksInBackground"); + SpecialPowers.clipboardCopyString(""); + + CustomizableUI.removeWidgetFromArea("home-button"); + }); +}); + +add_task(async function test_middleClickOnTab() { + await testMiddleClickOnTab(false); + await testMiddleClickOnTab(true); +}); + +add_task(async function test_middleClickToOpenNewTab() { + await testMiddleClickToOpenNewTab(false, "#tabs-newtab-button"); + await testMiddleClickToOpenNewTab(true, "#tabs-newtab-button"); + await testMiddleClickToOpenNewTab(false, "#TabsToolbar"); + await testMiddleClickToOpenNewTab(true, "#TabsToolbar"); +}); + +add_task(async function test_middleClickOnURLBar() { + await testMiddleClickOnURLBar(false); + await testMiddleClickOnURLBar(true); +}); + +add_task(async function test_middleClickOnHomeButton() { + const TEST_DATA = [ + { + isMiddleMousePastePrefOn: false, + isLoadInBackground: false, + startPagePref: "about:home", + expectedURLBarFocus: true, + expectedURLBarValue: "", + }, + { + isMiddleMousePastePrefOn: false, + isLoadInBackground: false, + startPagePref: "about:blank", + expectedURLBarFocus: true, + expectedURLBarValue: "", + }, + { + isMiddleMousePastePrefOn: false, + isLoadInBackground: false, + startPagePref: "https://example.com", + expectedURLBarFocus: false, + expectedURLBarValue: "https://example.com", + }, + { + isMiddleMousePastePrefOn: true, + isLoadInBackground: false, + startPagePref: "about:home", + expectedURLBarFocus: true, + expectedURLBarValue: "", + }, + { + isMiddleMousePastePrefOn: true, + isLoadInBackground: false, + startPagePref: "https://example.com", + expectedURLBarFocus: false, + expectedURLBarValue: "https://example.com", + }, + { + isMiddleMousePastePrefOn: false, + isLoadInBackground: true, + startPagePref: "about:home", + expectedURLBarFocus: true, + expectedURLBarValue: "", + }, + { + isMiddleMousePastePrefOn: false, + isLoadInBackground: true, + startPagePref: "https://example.com", + expectedURLBarFocus: true, + expectedURLBarValue: "", + }, + { + isMiddleMousePastePrefOn: true, + isLoadInBackground: true, + startPagePref: "about:home", + expectedURLBarFocus: true, + expectedURLBarValue: "", + }, + { + isMiddleMousePastePrefOn: true, + isLoadInBackground: true, + startPagePref: "https://example.com", + expectedURLBarFocus: true, + expectedURLBarValue: "", + }, + ]; + + for (const testData of TEST_DATA) { + await testMiddleClickOnHomeButton(testData); + } +}); + +add_task(async function test_middleClickOnHomeButtonWithNewWindow() { + await testMiddleClickOnHomeButtonWithNewWindow(false); + await testMiddleClickOnHomeButtonWithNewWindow(true); +}); + +async function testMiddleClickOnTab(isMiddleMousePastePrefOn) { + info(`Set middlemouse.paste [${isMiddleMousePastePrefOn}]`); + Services.prefs.setBoolPref("middlemouse.paste", isMiddleMousePastePrefOn); + + info("Set initial value"); + SpecialPowers.clipboardCopyString("test\nsample"); + gURLBar.value = ""; + gURLBar.focus(); + + info("Open two tabs"); + const tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser); + const tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + info("Middle click on tab2 to remove it"); + EventUtils.synthesizeMouseAtCenter(tab2, { button: 1 }); + + info("Wait until the tab1 is selected"); + await TestUtils.waitForCondition(() => gBrowser.selectedTab === tab1); + + Assert.equal(gURLBar.value, "", "URLBar has no pasted value"); + + BrowserTestUtils.removeTab(tab1); +} + +async function testMiddleClickToOpenNewTab(isMiddleMousePastePrefOn, selector) { + info(`Set middlemouse.paste [${isMiddleMousePastePrefOn}]`); + Services.prefs.setBoolPref("middlemouse.paste", isMiddleMousePastePrefOn); + + info("Set initial value"); + SpecialPowers.clipboardCopyString("test\nsample"); + gURLBar.value = ""; + gURLBar.focus(); + + info(`Click on ${selector}`); + const originalTab = gBrowser.selectedTab; + const element = document.querySelector(selector); + EventUtils.synthesizeMouseAtCenter(element, { button: 1 }); + + info("Wait until the new tab is opened"); + await TestUtils.waitForCondition(() => gBrowser.selectedTab !== originalTab); + + Assert.equal(gURLBar.value, "", "URLBar has no pasted value"); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +} + +async function testMiddleClickOnURLBar(isMiddleMousePastePrefOn) { + info(`Set middlemouse.paste [${isMiddleMousePastePrefOn}]`); + Services.prefs.setBoolPref("middlemouse.paste", isMiddleMousePastePrefOn); + + info("Set initial value"); + SpecialPowers.clipboardCopyString("test\nsample"); + gURLBar.value = ""; + gURLBar.focus(); + + info("Middle click on the urlbar"); + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, { button: 1 }); + + if (isMiddleMousePastePrefOn) { + Assert.equal(gURLBar.value, "test sample", "URLBar has pasted value"); + } else { + Assert.equal(gURLBar.value, "", "URLBar has no pasted value"); + } +} + +async function testMiddleClickOnHomeButton({ + isMiddleMousePastePrefOn, + isLoadInBackground, + startPagePref, + expectedURLBarFocus, + expectedURLBarValue, +}) { + info(`middlemouse.paste [${isMiddleMousePastePrefOn}]`); + info(`browser.startup.homepage [${startPagePref}]`); + info(`browser.tabs.loadBookmarksInBackground [${isLoadInBackground}]`); + + info("Set initial value"); + Services.prefs.setCharPref("browser.startup.homepage", startPagePref); + Services.prefs.setBoolPref( + "browser.tabs.loadBookmarksInBackground", + isLoadInBackground + ); + Services.prefs.setBoolPref("middlemouse.paste", isMiddleMousePastePrefOn); + SpecialPowers.clipboardCopyString("test\nsample"); + gURLBar.value = ""; + gURLBar.focus(); + + info("Middle click on the home button"); + const currentTab = gBrowser.selectedTab; + const homeButton = document.getElementById("home-button"); + EventUtils.synthesizeMouseAtCenter(homeButton, { button: 1 }); + + if (!isLoadInBackground) { + info("Wait until the a new tab is selected"); + await TestUtils.waitForCondition(() => gBrowser.selectedTab !== currentTab); + } + + info("Wait until the focus moves"); + await TestUtils.waitForCondition( + () => + (document.activeElement === gURLBar.inputField) === expectedURLBarFocus + ); + + Assert.ok(true, "The focus is correct"); + Assert.equal(gURLBar.value, expectedURLBarValue, "URLBar value is correct"); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +} + +async function testMiddleClickOnHomeButtonWithNewWindow( + isMiddleMousePastePrefOn +) { + info(`Set middlemouse.paste [${isMiddleMousePastePrefOn}]`); + Services.prefs.setBoolPref("middlemouse.paste", isMiddleMousePastePrefOn); + + info("Set prefs to open in a new window"); + Services.prefs.setBoolPref("middlemouse.openNewWindow", true); + Services.prefs.setBoolPref("browser.tabs.opentabfor.middleclick", false); + + info("Set initial value"); + SpecialPowers.clipboardCopyString("test\nsample"); + gURLBar.value = ""; + gURLBar.focus(); + + info("Middle click on the home button"); + const homeButton = document.getElementById("home-button"); + const onNewWindowOpened = BrowserTestUtils.waitForNewWindow(); + EventUtils.synthesizeMouseAtCenter(homeButton, { button: 1 }); + + const newWindow = await onNewWindowOpened; + Assert.equal(newWindow.gURLBar.value, "", "URLBar value is correct"); + + await BrowserTestUtils.closeWindow(newWindow); +} 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..b2bce4b22e --- /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..d9a6a8d416 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_oneOffs.js @@ -0,0 +1,980 @@ +/* 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_setup(async function () { + 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({ + url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME, + }); + await Services.search.moveEngine(engine, 0); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.separatePrivateDefault.ui.enabled", false], + ["browser.urlbar.suggest.quickactions", false], + ["browser.urlbar.shortcuts.quickactions", true], + ], + }); + + 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" + ), + "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" + ), + "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, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + 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, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + 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, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + 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, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + 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, 4, "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; + case "urlbar-engine-one-off-item-actions": + expectedSource = UrlbarUtils.RESULT_SOURCE.ACTIONS; + 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..3513fd2dac --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_oneOffs_heuristicRestyle.js @@ -0,0 +1,516 @@ +/* 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://global/skin/icons/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]) + : [""]; + + if ( + expectedType === UrlbarUtils.RESULT_TYPE.URL && + resultDetails.result.heuristic && + resultDetails.result.payload.title + ) { + Assert.equal( + resultDetails.displayed.url, + resultDetails.result.payload.displayUrl + ); + } else { + 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.ok(false, "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_setup(async function () { + await SearchTestUtils.installSearchExtension( + { + name: TEST_DEFAULT_ENGINE_NAME, + keyword: "@test", + }, + { setAsDefault: true } + ); + let engine = Services.search.getEngineByName(TEST_DEFAULT_ENGINE_NAME); + 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 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..30b241b3b3 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_oneOffs_keyModifiers.js @@ -0,0 +1,392 @@ +/* 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_setup(async function () { + // Add a search suggestion engine and move it to the front so that it appears + // as the first one-off. + engine = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME, + }); + await Services.search.moveEngine(engine, 0); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.separatePrivateDefault.ui.enabled", false], + ["browser.urlbar.suggest.quickactions", 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..ef324b08cd --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_oneOffs_searchSuggestions.js @@ -0,0 +1,358 @@ +/* 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_setup(async function () { + 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({ + url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME, + }); + gEngine2 = await SearchTestUtils.promiseNewSearchEngine({ + url: 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, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + registerCleanupFunction(async function () { + await Services.search.setDefault( + oldDefaultEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + 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..b4b1e7006e --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_oneOffs_settings.js @@ -0,0 +1,89 @@ +/* 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_setup(async function () { + 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(win, activateFn) { + await BrowserTestUtils.withNewTab( + { gBrowser: win.gBrowser, url: "about:blank" }, + async browser => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "example.com", + }); + await UrlbarTestUtils.waitForAutocompleteResultAt(win, gMaxResults - 1); + + await UrlbarTestUtils.promisePopupClose(win, async () => { + let prefPaneLoaded = TestUtils.topicObserved( + "sync-pane-loaded", + () => true + ); + + activateFn(); + + await prefPaneLoaded; + }); + + Assert.equal( + win.gBrowser.contentWindow.history.state, + "paneSearch", + "Should have opened the search preferences pane" + ); + } + ); +} + +add_task(async function test_open_settings_with_enter() { + const win = await BrowserTestUtils.openNewBrowserWindow(); + await selectSettings(win, () => { + EventUtils.synthesizeKey("KEY_ArrowUp", {}, win); + + Assert.ok( + UrlbarTestUtils.getOneOffSearchButtons( + win + ).selectedButton.classList.contains("search-setting-button"), + "Should have selected the settings button" + ); + + EventUtils.synthesizeKey("KEY_Enter", {}, win); + }); + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function test_open_settings_with_click() { + const win = await BrowserTestUtils.openNewBrowserWindow(); + await selectSettings(win, () => { + UrlbarTestUtils.getOneOffSearchButtons(win).settingsButton.click(); + }); + await BrowserTestUtils.closeWindow(win); +}); 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..8d2a27afc3 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_pasteAndGo.js @@ -0,0 +1,80 @@ +/* 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, "") + ); + menuitem.closest("menupopup").activateItem(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, "") + ); + menuitem.closest("menupopup").activateItem(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 + ); + menuitem.closest("menupopup").activateItem(menuitem); + // Using toSource in order to get the newlines escaped: + info("Paste and go, loading " + url.toSource()); + await browserLoadedPromise; + ok(true, "Successfully loaded " + url); +}); diff --git a/browser/components/urlbar/tests/browser/browser_paste_multi_lines.js b/browser/components/urlbar/tests/browser/browser_paste_multi_lines.js new file mode 100644 index 0000000000..8c4be18a7b --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_paste_multi_lines.js @@ -0,0 +1,239 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test handling whitespace chars such as "\n”. + +const TEST_DATA = [ + { + input: "this is a\ntest", + expected: { + urlbar: "this is a test", + autocomplete: "this is a test", + type: UrlbarUtils.RESULT_TYPE.SEARCH, + }, + }, + { + input: "this is a\n\ttest", + expected: { + urlbar: "this is a test", + autocomplete: "this is a test", + type: UrlbarUtils.RESULT_TYPE.SEARCH, + }, + }, + { + input: "http:\n//\nexample.\ncom", + expected: { + urlbar: "http://example.com", + autocomplete: "http://example.com/", + type: UrlbarUtils.RESULT_TYPE.URL, + }, + }, + { + input: "htp:example.\ncom", + expected: { + urlbar: "htp:example.com", + autocomplete: "http://example.com/", + type: UrlbarUtils.RESULT_TYPE.URL, + }, + }, + { + input: "example.\ncom", + expected: { + urlbar: "example.com", + autocomplete: "http://example.com/", + type: UrlbarUtils.RESULT_TYPE.URL, + }, + }, + { + input: "http://example.com/foo bar/", + expected: { + urlbar: "http://example.com/foo bar/", + autocomplete: "http://example.com/foo bar/", + type: UrlbarUtils.RESULT_TYPE.URL, + }, + }, + { + input: "http://exam\nple.com/foo bar/", + expected: { + urlbar: "http://example.com/foo bar/", + autocomplete: "http://example.com/foo bar/", + type: UrlbarUtils.RESULT_TYPE.URL, + }, + }, + { + input: "javasc\nript:\nalert(1)", + expected: { + urlbar: "alert(1)", + autocomplete: "alert(1)", + type: UrlbarUtils.RESULT_TYPE.SEARCH, + }, + }, + { + input: "a\nb\nc", + expected: { + urlbar: "a b c", + autocomplete: "a b c", + type: UrlbarUtils.RESULT_TYPE.SEARCH, + }, + }, + { + input: "lo\ncal\nhost", + expected: { + urlbar: "localhost", + autocomplete: "http://localhost/", + type: UrlbarUtils.RESULT_TYPE.URL, + }, + }, + { + input: "data:text/html,\n", + expected: { + urlbar: "data:text/html,", + autocomplete: "data:text/html,", + type: UrlbarUtils.RESULT_TYPE.URL, + }, + }, + { + input: "data:,123\n4 5\n6", + expected: { + urlbar: "data:,123 4 5 6", + autocomplete: "data:,123 4 5 6", + type: UrlbarUtils.RESULT_TYPE.URL, + }, + }, + { + input: "data:text/html;base64,123\n4 5\n6", + expected: { + urlbar: "data:text/html;base64,1234 56", + autocomplete: "data:text/html;base64,123456", + type: UrlbarUtils.RESULT_TYPE.URL, + }, + }, + { + input: "http://example.com\n", + expected: { + urlbar: "http://example.com", + autocomplete: "http://example.com/", + type: UrlbarUtils.RESULT_TYPE.URL, + }, + }, + { + input: "http://example.com\r", + expected: { + urlbar: "http://example.com", + autocomplete: "http://example.com/", + type: UrlbarUtils.RESULT_TYPE.URL, + }, + }, + { + input: "http://ex\ra\nmp\r\nle.com\r\n", + expected: { + urlbar: "http://example.com", + autocomplete: "http://example.com/", + type: UrlbarUtils.RESULT_TYPE.URL, + }, + }, + { + input: "http://example.com/titled", + expected: { + urlbar: "http://example.com/titled", + autocomplete: "example title", + type: UrlbarUtils.RESULT_TYPE.URL, + }, + }, + { + input: "127.0.0.1\r", + expected: { + urlbar: "127.0.0.1", + autocomplete: "http://127.0.0.1/", + type: UrlbarUtils.RESULT_TYPE.URL, + }, + }, + { + input: "\r\n\r\n\r\n\r\n\r\n", + expected: { + urlbar: "", + autocomplete: "", + type: UrlbarUtils.RESULT_TYPE.SEARCH, + }, + }, +]; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + // There are cases that URLBar loses focus before assertion of this test. + // In that case, this test will be failed since the result is closed + // before it. We use this pref so that keep the result even if lose focus. + ["ui.popup.disable_autohide", true], + ], + }); + + await PlacesUtils.history.clear(); + await PlacesTestUtils.addVisits({ + uri: "http://example.com/titled", + title: "example title", + }); + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + SpecialPowers.clipboardCopyString(""); + }); +}); + +add_task(async function test_paste_onto_urlbar() { + for (const { input, expected } of TEST_DATA) { + gURLBar.value = ""; + gURLBar.focus(); + + await paste(input); + await assertResult(expected); + + await UrlbarTestUtils.promisePopupClose(window); + } +}); + +add_task(async function test_paste_after_opening_autocomplete_panel() { + for (const { input, expected } of TEST_DATA) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + + await paste(input); + await assertResult(expected); + + await UrlbarTestUtils.promisePopupClose(window); + } +}); + +async function assertResult(expected) { + Assert.equal(gURLBar.value, expected.urlbar, "Pasted value is correct"); + + const result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + result.title, + expected.autocomplete, + "Title of autocomplete is correct" + ); + Assert.equal(result.type, expected.type, "Type of autocomplete is correct"); + + if (gURLBar.value) { + Assert.equal(gURLBar.getAttribute("usertyping"), "true"); + Assert.ok(BrowserTestUtils.is_visible(gURLBar.goButton)); + } else { + Assert.ok(!gURLBar.hasAttribute("usertyping")); + Assert.ok(BrowserTestUtils.is_hidden(gURLBar.goButton)); + } +} + +async function paste(input) { + await SimpleTest.promiseClipboardChange(input.replace(/\r\n?/g, "\n"), () => { + clipboardHelper.copyString(input); + }); + + document.commandDispatcher + .getControllerForCommand("cmd_paste") + .doCommand("cmd_paste"); +} diff --git a/browser/components/urlbar/tests/browser/browser_paste_then_focus.js b/browser/components/urlbar/tests/browser/browser_paste_then_focus.js new file mode 100644 index 0000000000..23d603fd80 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_paste_then_focus.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the urlbar value when focusing after pasting value. + +const TEST_DATA = [ + { + input: "this is a\ntest", + expected: "this is a test", + }, + { + input: "http:\n//\nexample.\ncom", + expected: "http://example.com", + }, + { + input: "javasc\nript:\nalert(1)", + expected: "alert(1)", + }, + { + input: "javascript:alert(1)", + expected: "alert(1)", + }, + { + input: "test", + expected: "test", + }, +]; + +add_task(async function test_paste_then_focus() { + for (const testData of TEST_DATA) { + gURLBar.value = ""; + gURLBar.focus(); + + EventUtils.synthesizeKey("x"); + gURLBar.select(); + + await paste(testData.input); + + gURLBar.blur(); + gURLBar.focus(); + + Assert.equal( + gURLBar.value, + testData.expected, + "Value on urlbar is correct" + ); + } +}); + +async function paste(input) { + await SimpleTest.promiseClipboardChange(input, () => { + clipboardHelper.copyString(input); + }); + + document.commandDispatcher + .getControllerForCommand("cmd_paste") + .doCommand("cmd_paste"); +} diff --git a/browser/components/urlbar/tests/browser/browser_paste_then_switch_tab.js b/browser/components/urlbar/tests/browser/browser_paste_then_switch_tab.js new file mode 100644 index 0000000000..883b128c60 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_paste_then_switch_tab.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the urlbar value when switching tab after pasting value. + +const TEST_DATA = [ + { + input: "this is a\ntest", + expected: "this is a test", + }, + { + input: "https:\n//\nexample.\ncom", + expected: "https://example.com", + }, + { + input: "http:\n//\nexample.\ncom", + expected: "example.com", + }, + { + input: "javasc\nript:\nalert(1)", + expected: "alert(1)", + }, + { + input: "javascript:alert(1)", + expected: "alert(1)", + }, + { + // Has U+3000 IDEOGRAPHIC SPACE. + input: "Mozilla Firefox", + expected: "Mozilla Firefox", + }, + { + input: "test", + expected: "test", + }, +]; + +add_task(async function test_paste_then_switch_tab() { + for (const testData of TEST_DATA) { + gURLBar.focus(); + gURLBar.select(); + + await paste(testData.input); + + // Switch to a new tab. + const originalTab = gBrowser.selectedTab; + const newTab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + await BrowserTestUtils.waitForCondition(() => !gURLBar.value); + + // Switch back to original tab. + gBrowser.selectedTab = originalTab; + + Assert.equal( + gURLBar.value, + testData.expected, + "Value on urlbar is correct" + ); + + BrowserTestUtils.removeTab(newTab); + } +}); + +async function paste(input) { + await SimpleTest.promiseClipboardChange(input, () => { + clipboardHelper.copyString(input); + }); + + document.commandDispatcher + .getControllerForCommand("cmd_paste") + .doCommand("cmd_paste"); +} 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..c334c03a09 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_percent_encoded.js @@ -0,0 +1,59 @@ +/* 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 + ); + }); + 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..fbf0a5007c --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_placeholder.js @@ -0,0 +1,412 @@ +/* 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"; + +var originalEngine, extraEngine, extraPrivateEngine, expectedString; +var tabs = []; + +var noEngineString; + +add_setup(async function () { + 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 rootUrl = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://mochi.test:8888/" + ); + await SearchTestUtils.installSearchExtension({ + name: "extraEngine", + search_url: "https://mochi.test:8888/", + suggest_url: `${rootUrl}/searchSuggestionEngine.sjs`, + }); + extraEngine = Services.search.getEngineByName("extraEngine"); + await SearchTestUtils.installSearchExtension({ + name: "extraPrivateEngine", + search_url: "https://mochi.test:8888/", + suggest_url: `${rootUrl}/searchSuggestionEngine.sjs`, + }); + extraPrivateEngine = Services.search.getEngineByName("extraPrivateEngine"); + + // 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], + ["browser.urlbar.suggest.quickactions", false], + ], + }); + + registerCleanupFunction(async () => { + await Services.search.setDefault( + originalEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + 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, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + 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, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + 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, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + // 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, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + 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, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + 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, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + 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, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + }); + + 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, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + await Services.search.setDefaultPrivate( + extraPrivateEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + 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, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + await Services.search.setDefaultPrivate( + originalEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + 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); + + // Verify that the placeholder for private windows is updated even when no + // private window is visible (https://bugzilla.mozilla.org/1792816). + await Services.search.setDefault( + originalEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + await Services.search.setDefaultPrivate( + extraPrivateEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + const win2 = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + Assert.equal(win2.gURLBar.placeholder, noEngineString); + await BrowserTestUtils.closeWindow(win2); + + // And ensure this doesn't affect the placeholder for non private windows. + tabs.push(await BrowserTestUtils.openNewForegroundTab(gBrowser)); + Assert.equal(win.gURLBar.placeholder, expectedString); +}); + +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. + SearchUtils.GENERAL_SEARCH_ENGINE_IDS.add( + extraEngine.wrappedJSObject._extensionID + ); + + await doSearchModeTest( + { + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + engineName: extraEngine.name, + }, + { + id: "urlbar-placeholder-search-mode-web-2", + args: { name: extraEngine.name }, + } + ); + + SearchUtils.GENERAL_SEARCH_ENGINE_IDS.delete( + extraEngine.wrappedJSObject._extensionID + ); +}); + +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 } + ); +}); + +add_task(async function test_change_default_engine_updates_placeholder() { + tabs.push(await BrowserTestUtils.openNewForegroundTab(gBrowser)); + + info(`Set engine to ${extraEngine.name}`); + await Services.search.setDefault( + extraEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + await TestUtils.waitForCondition( + () => gURLBar.placeholder == noEngineString, + "The placeholder should match the default placeholder for non-built-in engines." + ); + Assert.equal(gURLBar.placeholder, noEngineString); + + info(`Set engine to ${originalEngine.name}`); + await Services.search.setDefault( + originalEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + await TestUtils.waitForCondition( + () => gURLBar.placeholder == expectedString, + "The placeholder should include the engine name for built-in engines." + ); + + // Simulate the placeholder not having changed due to the delayed update + // on startup. + BrowserSearch._setURLBarPlaceholder(""); + await TestUtils.waitForCondition( + () => gURLBar.placeholder == noEngineString, + "The placeholder should have been reset." + ); + + info("Show search engine removal info bar"); + BrowserSearch.removalOfSearchEngineNotificationBox( + extraEngine.name, + originalEngine.name + ); + const notificationBox = gNotificationBox.getNotificationWithValue( + "search-engine-removal" + ); + Assert.ok(notificationBox, "Search engine removal should be shown."); + + await TestUtils.waitForCondition( + () => gURLBar.placeholder == expectedString, + "The placeholder should include the engine name for built-in engines." + ); + + Assert.equal(gURLBar.placeholder, expectedString); + + notificationBox.close(); +}); + +/** + * 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..8d383092fe --- /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..941c44441d --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_primary_selection_safe_on_new_tab.js @@ -0,0 +1,73 @@ +/* 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.isClipboardTypeSupported( + Services.clipboard.kSelectionClipboard +); +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/plain", + 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..eeeda93687 --- /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_queryContextCache.js b/browser/components/urlbar/tests/browser/browser_queryContextCache.js new file mode 100644 index 0000000000..758043233d --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_queryContextCache.js @@ -0,0 +1,482 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests the view's QueryContextCache. When the view opens and a context is +// cached for the search, the view should *synchronously* open and update. + +"use strict"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + UrlbarProviderTopSites: "resource:///modules/UrlbarProviderTopSites.sys.mjs", +}); + +const TEST_URLS = []; +const TEST_URLS_COUNT = 5; +const TOP_SITES_VISIT_COUNT = 5; +const SEARCH_STRING = "example"; + +// Allow more time for Mac machines so they don't time out in verify mode. +if (AppConstants.platform == "macosx") { + requestLongerTimeout(3); +} + +add_setup(async function () { + // Clear history and bookmarks to make sure the URLs we add below are truly + // the top sites. If any existing history or bookmarks were the top sites, + // which is likely but not guaranteed, one or more "newtab-top-sites-changed" + // notifications will be sent, potentially interfering with the rest of the + // test. Waiting for Places updates to finish and then an extra tick should be + // enough to make sure no more notfications occur. + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesTestUtils.promiseAsyncUpdates(); + await TestUtils.waitForTick(); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.quickactions", false]], + }); + + // Add some URLs to populate both history and top sites. Each URL needs to + // match `SEARCH_STRING`. + for (let i = 0; i < TEST_URLS_COUNT; i++) { + let url = `https://${i}.example.com/${SEARCH_STRING}`; + TEST_URLS.unshift(url); + // Each URL needs to be added several times to boost its frecency enough to + // qualify as a top site. + for (let j = 0; j < TOP_SITES_VISIT_COUNT; j++) { + await PlacesTestUtils.addVisits(url); + } + } + await updateTopSitesAndAwaitChanged(TEST_URLS_COUNT); + + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + }); +}); + +add_task(async function search() { + await withNewBrowserWindow(async win => { + // Do a search and then close the view. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: SEARCH_STRING, + }); + await UrlbarTestUtils.promisePopupClose(win); + + // Open the view. It should open synchronously and the cached search context + // should be used. + await openViewAndAssertCached({ + win, + searchString: SEARCH_STRING, + cached: true, + }); + }); +}); + +add_task(async function topSites_simple() { + await withNewBrowserWindow(async win => { + // Open the view to show top sites and then close it. + await openViewAndAssertCached({ win, cached: false }); + + // Open the view again. It should open synchronously and the cached + // top-sites context should be used. + await openViewAndAssertCached({ win, cached: true }); + }); +}); + +add_task(async function topSites_nonEmptySearch() { + await withNewBrowserWindow(async win => { + // Open the view to show top sites and then close it. + await openViewAndAssertCached({ win, cached: false }); + + // Do a search, close the view, and revert the input. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "test", + }); + await UrlbarTestUtils.promisePopupClose(win); + win.gURLBar.handleRevert(); + + // Open the view. It should open synchronously and the cached top-sites + // context should be used. + await openViewAndAssertCached({ win, cached: true }); + }); +}); + +add_task(async function topSites_otherEmptySearch() { + await withNewBrowserWindow(async win => { + // Open the view to show top sites and then close it. + await openViewAndAssertCached({ win, cached: false }); + + // Enter search mode with an empty search string (by pressing accel+K), + // starting a new search. The view should *not* open synchronously and the + // cached top-sites context should not be used. + let searchPromise = UrlbarTestUtils.promiseSearchComplete(win); + EventUtils.synthesizeKey("k", { accelKey: true }, win); + Assert.ok(!win.gURLBar.view.isOpen, "View is not open"); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(win, { + engineName: Services.search.defaultEngine.name, + isGeneralPurposeEngine: true, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + isPreview: false, + entry: "shortcut", + }); + + // Close the view and revert the input. + await UrlbarTestUtils.promisePopupClose(win); + win.gURLBar.handleRevert(); + await UrlbarTestUtils.assertSearchMode(win, null); + + // Open the view. It should open synchronously and the cached top-sites + // context should be used. + await openViewAndAssertCached({ win, cached: true }); + }); +}); + +add_task(async function topSites_changed() { + await withNewBrowserWindow(async win => { + // Open the view to show top sites and then close it. + await openViewAndAssertCached({ win, cached: false }); + + // Change the top sites by adding visits to a new URL. + let newURL = "https://changed.example.com/"; + for (let j = 0; j < TOP_SITES_VISIT_COUNT; j++) { + await PlacesTestUtils.addVisits(newURL); + } + await updateTopSitesAndAwaitChanged(TEST_URLS_COUNT + 1); + + // Open the view. It should *not* open synchronously and the cached + // top-sites context should not be used. + await openViewAndAssertCached({ win, cached: false }); + + // Open the view again. It should open synchronously and the new cached + // top-sites context with the new URL should be used. + await openViewAndAssertCached({ + win, + cached: true, + urls: [newURL, ...TEST_URLS], + // The new URL is sometimes at the end of the list of top sites instead of + // the start, so ignore the order of the results. + ignoreOrder: true, + }); + + // Remove the new URL. The top sites will update themselves automatically, + // so we only need to wait for newtab-top-sites-changed. + info("Removing new URL and awaiting newtab-top-sites-changed"); + let changedPromise = TestUtils.topicObserved("newtab-top-sites-changed"); + await PlacesUtils.history.remove([newURL]); + await changedPromise; + + // Open the view. It should *not* open synchronously and the cached + // top-sites context should not be used. + await openViewAndAssertCached({ win, cached: false }); + + // Open the view again. It should open synchronously and the new cached + // top-sites context with the new URL should be used. + await openViewAndAssertCached({ win, cached: true }); + }); +}); + +add_task(async function topSites_nonTopSitesResults() { + await withNewBrowserWindow(async win => { + // Open the view to show top sites and then close it. + await openViewAndAssertCached({ win, cached: false }); + + // Add a provider that returns a result with a suggested index of zero so + // that the first result in the view is not from the top-sites provider. + let suggestedIndexURL = "https://example.com/suggested-index-0"; + let provider = new UrlbarTestUtils.TestProvider({ + priority: lazy.UrlbarProviderTopSites.PRIORITY, + results: [ + Object.assign( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + url: suggestedIndexURL, + } + ), + { suggestedIndex: 0 } + ), + ], + }); + UrlbarProvidersManager.registerProvider(provider); + + // Open the view. It should open synchronously and the cached top-sites + // context should be used. The suggested-index result should not be + // immediately present in the view since it's not in the cached context. + await openViewAndAssertCached({ win, cached: true, keepOpen: true }); + + // After the search has finished, the suggested-index result should be in + // the first row. The search's context should become the newly cached + // top-sites context and it should include the suggested-index result. + Assert.equal( + UrlbarTestUtils.getResultCount(win), + TEST_URLS.length + 1, + "Should be one more result after search finishes" + ); + let details = await UrlbarTestUtils.getDetailsOfResultAt(win, 0); + Assert.equal( + details.url, + suggestedIndexURL, + "First result after search finishes should be the suggested index result" + ); + + // At this point, the search's context should have become the newly cached + // top-sites context and it should include the suggested-index result. + + await UrlbarTestUtils.promisePopupClose(win); + + // Open the view again. It should open synchronously and the new cached + // top-sites context with the suggested-index URL should be used. + await openViewAndAssertCached({ + win, + cached: true, + urls: [suggestedIndexURL, ...TEST_URLS], + }); + + UrlbarProvidersManager.unregisterProvider(provider); + }); +}); + +add_task(async function topSites_disabled_1() { + await withNewBrowserWindow(async win => { + // Open the view to show top sites and then close it. + await openViewAndAssertCached({ win, cached: false }); + + // Disable `browser.urlbar.suggest.topsites`. + UrlbarPrefs.set("suggest.topsites", false); + + // Open the view. It should *not* open synchronously and the cached + // top-sites context should not be used. + await openViewAndAssertCached({ + win, + cached: false, + cachedAfterOpen: false, + }); + + // Clear the pref, open the view to show top sites, and close it. + UrlbarPrefs.clear("suggest.topsites"); + await openViewAndAssertCached({ win, cached: false }); + + // Open the view. It should open synchronously and the cached top-sites + // context should be used. + await openViewAndAssertCached({ win, cached: true }); + }); +}); + +add_task(async function topSites_disabled_2() { + await withNewBrowserWindow(async win => { + // Open the view to show top sites and then close it. + await openViewAndAssertCached({ win, cached: false }); + + // Disable `browser.newtabpage.activity-stream.feeds.system.topsites`. + Services.prefs.setBoolPref( + "browser.newtabpage.activity-stream.feeds.system.topsites", + false + ); + + // Open the view. It should *not* open synchronously and the cached + // top-sites context should not be used. + await openViewAndAssertCached({ + win, + cached: false, + cachedAfterOpen: false, + }); + + // Clear the pref, open the view to show top sites, and close it. + Services.prefs.clearUserPref( + "browser.newtabpage.activity-stream.feeds.system.topsites" + ); + await openViewAndAssertCached({ win, cached: false }); + + // Open the view. It should open synchronously and the cached top-sites + // context should be used. + await openViewAndAssertCached({ win, cached: true }); + }); +}); + +add_task(async function evict() { + await withNewBrowserWindow(async win => { + let cache = win.gURLBar.view.queryContextCache; + Assert.equal( + typeof cache.size, + "number", + "Sanity check: queryContextCache.size is a number" + ); + + // Open the view to show top sites and then close it. + await openViewAndAssertCached({ win, cached: false }); + + // Do `cache.size` + 1 searches. + for (let i = 0; i < cache.size + 1; i++) { + let searchString = "test" + i; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: searchString, + }); + await UrlbarTestUtils.promisePopupClose(win); + Assert.ok( + cache.get(searchString), + "Cache includes search string: " + searchString + ); + } + + // The first search string should have been evicted from the cache, but the + // one after that should still be cached. + Assert.ok(!cache.get("test0"), "test0 has been evicted from the cache"); + Assert.ok(cache.get("test1"), "Cache includes test1"); + + // Revert the input and open the view to show the top sites. It should open + // synchronously and the cached top-sites context should be used. + win.gURLBar.handleRevert(); + Assert.equal(win.gURLBar.value, "", "Input is empty after reverting"); + await openViewAndAssertCached({ win, cached: true }); + }); +}); + +/** + * Opens the view and checks that it is or is not synchronously opened and + * populated as specified. + * + * @param {object} options + * Options object. + * @param {window} options.win + * The window to open the view in. + * @param {boolean} options.cached + * Whether a query context is expected to already be cached for the search + * that's performed when the view opens. If true, then the view should + * synchronously open and populate using the cached context. If false, then + * the view should asynchronously open once the first results are fetched. + * @param {boolean} [options.cachedAfterOpen] + * Whether the context is expected to be cached after the view opens and the + * query finishes. + * @param {string} [options.searchString] + * The search string for which the context should or should not be cached. If + * falsey, then the relevant context is assumed to be the top-sites context. + * @param {Array} [options.urls] + * Array of URLs that are expected to be shown in the view. + * @param {boolean} [options.ignoreOrder] + * Whether to treat `urls` as an unordered set instead of an array. When true, + * the order of results is ignored. + * @param {boolean} [options.keepOpen] + * Whether to keep the view open when the function returns. + */ +async function openViewAndAssertCached({ + win, + cached, + cachedAfterOpen = true, + searchString = "", + urls = TEST_URLS, + ignoreOrder = false, + keepOpen = false, +}) { + let cache = win.gURLBar.view.queryContextCache; + let getContext = () => + searchString ? cache.get(searchString) : cache.topSitesContext; + + Assert.equal( + !!getContext(), + cached, + "Context is present or not in cache as expected for search string: " + + JSON.stringify(searchString) + ); + + // Open the view by performing the accel+L command. + await SimpleTest.promiseFocus(win); + win.document.getElementById("Browser:OpenLocation").doCommand(); + + Assert.equal( + win.gURLBar.view.isOpen, + cached, + "View is open or not as expected" + ); + + if (!cached && cachedAfterOpen) { + // Wait for the search to finish and the context to be cached since callers + // generally expect it. + await TestUtils.waitForCondition( + getContext, + "Waiting for context to be cached for search string: " + + JSON.stringify(searchString) + ); + } else if (cached) { + // The view is expected to open synchronously. Check the results. We don't + // do this in the `!cached` case, when the view is expected to open + // asynchronously, because there are plenty of other tests for that. Here we + // want to make sure results are correct before the new search finishes in + // order to avoid any flicker. + let startIndex = 0; + let resultCount = urls.length; + if (searchString) { + // Plus heuristic + startIndex++; + resultCount++; + } + + // In all the checks below, check the rows container directly instead of + // relying on `UrlbarTestUtils` functions that wait for the search to + // finish. Here we're specifically checking cached results that should be + // used before the search finishes. + let rows = UrlbarTestUtils.getResultsContainer(win).children; + Assert.equal(rows.length, resultCount, "View has expected row count"); + + // Check the search heuristic row. + if (searchString) { + let result = rows[0].result; + Assert.ok(result.heuristic, "First row should be a heuristic"); + Assert.equal( + result.payload.query, + searchString, + "First row's query should be the search string" + ); + } + + // Check the URL rows. + let actualURLs = []; + let urlRows = Array.from(rows).slice(startIndex); + for (let row of urlRows) { + actualURLs.push(row.result.payload.url); + } + if (ignoreOrder) { + urls.sort(); + actualURLs.sort(); + } + Assert.deepEqual(actualURLs, urls, "View should contain the expected URLs"); + } + + // Now wait for the search to finish before returning. We await + // `lastQueryContextPromise` instead of the promise returned from + // `UrlbarTestUtils.promiseSearchComplete()` because the latter assumes the + // view will open, which isn't the case for every task here. + await win.gURLBar.lastQueryContextPromise; + if (!keepOpen) { + await UrlbarTestUtils.promisePopupClose(win); + } +} + +/** + * Updates the top sites and waits for the "newtab-top-sites-changed" + * notification. Note that this notification is not sent if the sites don't + * actually change. In that case, use only `updateTopSites()` instead. + * + * @param {number} expectedCount + * The new expected number of top sites. + */ +async function updateTopSitesAndAwaitChanged(expectedCount) { + info("Updating top sites and awaiting newtab-top-sites-changed"); + let changedPromise = TestUtils.topicObserved("newtab-top-sites-changed").then( + () => info("Observed newtab-top-sites-changed") + ); + await updateTopSites(sites => sites?.length == expectedCount); + await changedPromise; +} + +async function withNewBrowserWindow(callback) { + let win = await BrowserTestUtils.openNewBrowserWindow(); + await callback(win); + await BrowserTestUtils.closeWindow(win); +} diff --git a/browser/components/urlbar/tests/browser/browser_quickactions.js b/browser/components/urlbar/tests/browser/browser_quickactions.js new file mode 100644 index 0000000000..5945910067 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_quickactions.js @@ -0,0 +1,783 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests QuickActions. + */ + +"use strict"; + +requestLongerTimeout(3); + +ChromeUtils.defineESModuleGetters(this, { + AppConstants: "resource://gre/modules/AppConstants.sys.mjs", + UpdateService: "resource://gre/modules/UpdateService.sys.mjs", + + UrlbarProviderQuickActions: + "resource:///modules/UrlbarProviderQuickActions.sys.mjs", +}); +XPCOMUtils.defineLazyModuleGetters(this, { + BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm", +}); + +const DUMMY_PAGE = + "http://example.com/browser/browser/base/content/test/general/dummy_page.html"; + +let testActionCalled = 0; + +add_setup(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.quickactions.enabled", true], + ["browser.urlbar.suggest.quickactions", true], + ["browser.urlbar.shortcuts.quickactions", true], + ], + }); + + UrlbarProviderQuickActions.addAction("testaction", { + commands: ["testaction"], + label: "quickactions-downloads2", + onPick: () => testActionCalled++, + }); + + registerCleanupFunction(() => { + UrlbarProviderQuickActions.removeAction("testaction"); + }); +}); + +add_task(async function basic() { + info("The action isnt shown when not matched"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "nomatch", + }); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 1, + "We did no match anything" + ); + + info("A prefix of the command matches"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "testact", + }); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "We matched the action" + ); + + info("The callback of the action is fired when selected"); + EventUtils.synthesizeKey("KEY_ArrowDown", {}, window); + EventUtils.synthesizeKey("KEY_Enter", {}, window); + Assert.equal(testActionCalled, 1, "Test actionwas called"); +}); + +add_task(async function test_label_command() { + info("A prefix of the label matches"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "View Dow", + }); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "We matched the action" + ); + + let { result } = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.DYNAMIC); + Assert.equal(result.providerName, "quickactions"); + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Escape"); + }); +}); + +add_task(async function enter_search_mode_button() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + let oneOffButton = await TestUtils.waitForCondition(() => + window.document.getElementById("urlbar-engine-one-off-item-actions") + ); + Assert.ok(oneOffButton, "One off button is available when preffed on"); + + EventUtils.synthesizeMouseAtCenter(oneOffButton, {}, window); + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.ACTIONS, + entry: "oneoff", + }); + + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0); + Assert.ok(true, "Actions are shown when we enter actions search mode."); + + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); + EventUtils.synthesizeKey("KEY_Escape"); +}); + +add_task(async function enter_search_mode_oneoff_by_key() { + // Select actions oneoff button by keyboard. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + await UrlbarTestUtils.enterSearchMode(window); + const oneOffButtons = UrlbarTestUtils.getOneOffSearchButtons(window); + for (;;) { + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + if ( + oneOffButtons.selectedButton.source === UrlbarUtils.RESULT_SOURCE.ACTIONS + ) { + break; + } + } + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: " ", + }); + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.ACTIONS, + entry: "oneoff", + }); + + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); + EventUtils.synthesizeKey("KEY_Escape"); +}); + +add_task(async function enter_search_mode_key() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "> ", + }); + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.ACTIONS, + entry: "typed", + }); + Assert.equal( + await hasQuickActions(window), + true, + "Actions are shown in search mode" + ); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); + EventUtils.synthesizeKey("KEY_Escape"); +}); + +add_task(async function test_disabled() { + UrlbarProviderQuickActions.addAction("disabledaction", { + commands: ["disabledaction"], + isActive: () => false, + label: "quickactions-restart", + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "disabled", + }); + + Assert.equal( + await hasQuickActions(window), + false, + "Result for quick actions is hidden" + ); + + await UrlbarTestUtils.promisePopupClose(window); + UrlbarProviderQuickActions.removeAction("disabledaction"); +}); + +/** + * The first part of this test confirms that when the screenshots component is enabled + * the screenshot quick action button will be enabled on about: pages. + * The second part confirms that when the screenshots extension is enabled the + * screenshot quick action button will be disbaled on about: pages. + */ +add_task(async function test_screenshot_enabled_or_disabled() { + let onLoaded = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + "about:blank" + ); + BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, "about:blank"); + await onLoaded; + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "screenshot", + }); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "The action is displayed" + ); + let screenshotButton = window.document.querySelector( + ".urlbarView-row[dynamicType=quickactions] .urlbarView-quickaction-row" + ); + Assert.ok( + !screenshotButton.hasAttribute("disabled"), + "Screenshot button is enabled on about pages" + ); + + await UrlbarTestUtils.promisePopupClose(window); + EventUtils.synthesizeKey("KEY_Escape"); + + await SpecialPowers.pushPrefEnv({ + set: [["screenshots.browser.component.enabled", false]], + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "screenshot", + }); + Assert.equal( + await hasQuickActions(window), + false, + "Result for quick actions is hidden" + ); + + await UrlbarTestUtils.promisePopupClose(window); +}); + +add_task(async function match_in_phrase() { + UrlbarProviderQuickActions.addAction("newtestaction", { + commands: ["matchingstring"], + label: "quickactions-downloads2", + }); + + info("The action is matched when at end of input"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "Test we match at end of matchingstring", + }); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "We matched the action" + ); + await UrlbarTestUtils.promisePopupClose(window); + EventUtils.synthesizeKey("KEY_Escape"); + UrlbarProviderQuickActions.removeAction("newtestaction"); +}); + +async function isScreenshotInitialized() { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + let screenshotsChild = content.windowGlobalChild.getActor( + "ScreenshotsComponent" + ); + return screenshotsChild?._overlay?._initialized; + }); +} + +add_task(async function test_screenshot() { + await SpecialPowers.pushPrefEnv({ + set: [["screenshots.browser.component.enabled", true]], + }); + + BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, DUMMY_PAGE); + await BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + DUMMY_PAGE + ); + + info("The action is matched when at end of input"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "screenshot", + }); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "We matched the action" + ); + let { result } = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.DYNAMIC); + Assert.equal(result.providerName, "quickactions"); + + info("Trigger the screenshot mode"); + EventUtils.synthesizeKey("KEY_ArrowDown", {}, window); + EventUtils.synthesizeKey("KEY_Enter", {}, window); + await TestUtils.waitForCondition( + isScreenshotInitialized, + "Screenshot component is active", + 200, + 100 + ); + + info("Press Escape to exit screenshot mode"); + EventUtils.synthesizeKey("KEY_Escape", {}, window); + await TestUtils.waitForCondition( + async () => !(await isScreenshotInitialized()), + "Screenshot component has been dismissed" + ); + + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Escape"); + }); +}); + +add_task(async function test_other_search_mode() { + let defaultEngine = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + "searchSuggestionEngine.xml", + }); + defaultEngine.alias = "testalias"; + let oldDefaultEngine = await Services.search.getDefault(); + Services.search.setDefault( + defaultEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: defaultEngine.alias + " ", + }); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 0, + "The results should be empty as no actions are displayed in other search modes" + ); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: defaultEngine.name, + entry: "typed", + }); + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Escape"); + }); + Services.search.setDefault( + oldDefaultEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); +}); + +add_task(async function test_no_quickactions_suggestions() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.quickactions", false]], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "screenshot", + }); + Assert.ok( + !window.document.querySelector( + ".urlbarView-row[dynamicType=quickactions] .urlbarView-quickaction-row" + ), + "Screenshot button is not suggested" + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "> screenshot", + }); + Assert.ok( + window.document.querySelector( + ".urlbarView-row[dynamicType=quickactions] .urlbarView-quickaction-row" + ), + "Screenshot button is suggested" + ); + + await UrlbarTestUtils.promisePopupClose(window); + EventUtils.synthesizeKey("KEY_Escape"); + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_quickactions_disabled() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.quickactions.enabled", false], + ["browser.urlbar.suggest.quickactions", true], + ], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "screenshot", + }); + + Assert.ok( + !window.document.querySelector( + ".urlbarView-row[dynamicType=quickactions] .urlbarView-quickaction-row" + ), + "Screenshot button is not suggested" + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "> screenshot", + }); + Assert.ok( + !window.document.querySelector( + ".urlbarView-row[dynamicType=quickactions] .urlbarView-quickaction-row" + ), + "Screenshot button is not suggested" + ); + + await UrlbarTestUtils.promisePopupClose(window); + EventUtils.synthesizeKey("KEY_Escape"); + + await SpecialPowers.popPrefEnv(); +}); + +let COMMANDS_TESTS = [ + { + cmd: "add-ons", + uri: "about:addons", + testFun: async () => isSelected("button[name=discover]"), + }, + { + cmd: "plugins", + uri: "about:addons", + testFun: async () => isSelected("button[name=plugin]"), + }, + { + cmd: "extensions", + uri: "about:addons", + testFun: async () => isSelected("button[name=extension]"), + }, + { + cmd: "themes", + uri: "about:addons", + testFun: async () => isSelected("button[name=theme]"), + }, + { + cmd: "add-ons", + setup: async () => { + const onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + "http://example.com/" + ); + BrowserTestUtils.loadURIString( + gBrowser.selectedBrowser, + "http://example.com/" + ); + await onLoad; + }, + uri: "about:addons", + isNewTab: true, + testFun: async () => isSelected("button[name=discover]"), + }, + { + cmd: "plugins", + setup: async () => { + const onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + "http://example.com/" + ); + BrowserTestUtils.loadURIString( + gBrowser.selectedBrowser, + "http://example.com/" + ); + await onLoad; + }, + uri: "about:addons", + isNewTab: true, + testFun: async () => isSelected("button[name=plugin]"), + }, + { + cmd: "extensions", + setup: async () => { + const onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + "http://example.com/" + ); + BrowserTestUtils.loadURIString( + gBrowser.selectedBrowser, + "http://example.com/" + ); + await onLoad; + }, + uri: "about:addons", + isNewTab: true, + testFun: async () => isSelected("button[name=extension]"), + }, + { + cmd: "themes", + setup: async () => { + const onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + "http://example.com/" + ); + BrowserTestUtils.loadURIString( + gBrowser.selectedBrowser, + "http://example.com/" + ); + await onLoad; + }, + uri: "about:addons", + isNewTab: true, + testFun: async () => isSelected("button[name=theme]"), + }, +]; + +let isSelected = async selector => + SpecialPowers.spawn(gBrowser.selectedBrowser, [selector], arg => { + return ContentTaskUtils.waitForCondition(() => + content.document.querySelector(arg)?.hasAttribute("selected") + ); + }); + +add_task(async function test_pages() { + for (const { cmd, uri, setup, isNewTab, testFun } of COMMANDS_TESTS) { + info(`Testing ${cmd} command is triggered`); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + if (setup) { + info("Setup"); + await setup(); + } + + let onLoad = isNewTab + ? BrowserTestUtils.waitForNewTab(gBrowser, uri, true) + : BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false, uri); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: cmd, + }); + EventUtils.synthesizeKey("KEY_ArrowDown", {}, window); + EventUtils.synthesizeKey("KEY_Enter", {}, window); + + const newTab = await onLoad; + + Assert.ok( + await testFun(), + `The command "${cmd}" passed completed its test` + ); + + if (isNewTab) { + await BrowserTestUtils.removeTab(newTab); + } + await BrowserTestUtils.removeTab(tab); + } +}); + +const assertActionButtonStatus = async (name, expectedEnabled, description) => { + await BrowserTestUtils.waitForCondition(() => + window.document.querySelector(`[data-key=${name}]`) + ); + const target = window.document.querySelector(`[data-key=${name}]`); + Assert.equal(!target.hasAttribute("disabled"), expectedEnabled, description); +}; + +add_task(async function test_viewsource() { + info("Check the button status of when the page is not web content"); + const tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: "about:home", + waitForLoad: true, + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "viewsource", + }); + await assertActionButtonStatus( + "viewsource", + true, + "Should be enabled even if the page is not web content" + ); + + info("Check the button status of when the page is web content"); + BrowserTestUtils.loadURIString( + gBrowser.selectedBrowser, + "http://example.com" + ); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "viewsource", + }); + await assertActionButtonStatus( + "viewsource", + true, + "Should be enabled on web content as well" + ); + + info("Do view source action"); + const onLoad = BrowserTestUtils.waitForNewTab( + gBrowser, + "view-source:http://example.com/" + ); + EventUtils.synthesizeKey("KEY_ArrowDown", {}, window); + EventUtils.synthesizeKey("KEY_Enter", {}, window); + const viewSourceTab = await onLoad; + + info("Do view source action on the view-source page"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "viewsource", + }); + + Assert.equal( + await hasQuickActions(window), + false, + "Result for quick actions is hidden" + ); + + // Clean up. + BrowserTestUtils.removeTab(viewSourceTab); + BrowserTestUtils.removeTab(tab); +}); + +async function doAlertDialogTest({ input, dialogContentURI }) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: input, + }); + + const onDialog = BrowserTestUtils.promiseAlertDialog(null, null, { + isSubDialog: true, + callback: win => { + Assert.equal(win.location.href, dialogContentURI, "The dialog is opened"); + EventUtils.synthesizeKey("KEY_Escape", {}, win); + }, + }); + + EventUtils.synthesizeKey("KEY_ArrowDown", {}, window); + EventUtils.synthesizeKey("KEY_Enter", {}, window); + + await onDialog; +} + +add_task(async function test_refresh() { + await doAlertDialogTest({ + input: "refresh", + dialogContentURI: "chrome://global/content/resetProfile.xhtml", + }); +}); + +add_task(async function test_clear() { + await doAlertDialogTest({ + input: "clear", + dialogContentURI: "chrome://browser/content/sanitize.xhtml", + }); +}); + +async function doUpdateActionTest(isActiveExpected, description) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "update", + }); + + if (isActiveExpected) { + await assertActionButtonStatus("update", isActiveExpected, description); + } else { + Assert.equal(await hasQuickActions(window), false, description); + } +} + +add_task(async function test_update() { + if (!AppConstants.MOZ_UPDATER) { + await doUpdateActionTest( + false, + "Should be disabled since not AppConstants.MOZ_UPDATER" + ); + return; + } + + const sandbox = sinon.createSandbox(); + try { + sandbox + .stub(UpdateService.prototype, "currentState") + .get(() => Ci.nsIApplicationUpdateService.STATE_IDLE); + await doUpdateActionTest( + false, + "Should be disabled since current update state is not pending" + ); + sandbox + .stub(UpdateService.prototype, "currentState") + .get(() => Ci.nsIApplicationUpdateService.STATE_PENDING); + await doUpdateActionTest( + true, + "Should be enabled since current update state is pending" + ); + } finally { + sandbox.restore(); + } +}); + +async function hasQuickActions(win) { + for (let i = 0, count = UrlbarTestUtils.getResultCount(win); i < count; i++) { + const { result } = await UrlbarTestUtils.getDetailsOfResultAt(win, i); + if (result.providerName === "quickactions") { + return true; + } + } + return false; +} + +add_task(async function test_show_in_zero_prefix() { + for (const minimumSearchString of [0, 3]) { + info( + `Test when quickactions.minimumSearchString pref is ${minimumSearchString}` + ); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "browser.urlbar.quickactions.minimumSearchString", + minimumSearchString, + ], + ], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + + Assert.equal( + await hasQuickActions(window), + !minimumSearchString, + "Result for quick actions is as expected" + ); + await SpecialPowers.popPrefEnv(); + } +}); + +add_task(async function test_whitespace() { + info("Test with quickactions.showInZeroPrefix pref is false"); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.quickactions.showInZeroPrefix", false]], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: " ", + }); + Assert.equal( + await hasQuickActions(window), + false, + "Result for quick actions is not shown" + ); + await SpecialPowers.popPrefEnv(); + + info("Test with quickactions.showInZeroPrefix pref is true"); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.quickactions.showInZeroPrefix", true]], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + const countForEmpty = window.document.querySelectorAll( + ".urlbarView-quickaction-row" + ).length; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: " ", + }); + const countForWhitespace = window.document.querySelectorAll( + ".urlbarView-quickaction-row" + ).length; + Assert.equal( + countForEmpty, + countForWhitespace, + "Count of quick actions of empty and whitespace are same" + ); + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_quickactions_devtools.js b/browser/components/urlbar/tests/browser/browser_quickactions_devtools.js new file mode 100644 index 0000000000..d113a4c3a8 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_quickactions_devtools.js @@ -0,0 +1,176 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests QuickActions related to DevTools. + */ + +"use strict"; + +requestLongerTimeout(2); + +ChromeUtils.defineESModuleGetters(this, { + DevToolsShim: "chrome://devtools-startup/content/DevToolsShim.sys.mjs", +}); + +add_setup(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.quickactions.enabled", true], + ["browser.urlbar.suggest.quickactions", true], + ["browser.urlbar.shortcuts.quickactions", true], + ], + }); +}); + +const assertActionButtonStatus = async (name, expectedEnabled, description) => { + await BrowserTestUtils.waitForCondition(() => + window.document.querySelector(`[data-key=${name}]`) + ); + const target = window.document.querySelector(`[data-key=${name}]`); + Assert.equal(!target.hasAttribute("disabled"), expectedEnabled, description); +}; + +async function hasQuickActions(win) { + for (let i = 0, count = UrlbarTestUtils.getResultCount(win); i < count; i++) { + const { result } = await UrlbarTestUtils.getDetailsOfResultAt(win, i); + if (result.providerName === "quickactions") { + return true; + } + } + return false; +} + +add_task(async function test_inspector() { + const testData = [ + { + description: "Test for 'about:' page", + page: "about:home", + isDevToolsUser: true, + actionVisible: true, + actionEnabled: true, + }, + { + description: "Test for another 'about:' page", + page: "about:about", + isDevToolsUser: true, + actionVisible: true, + actionEnabled: true, + }, + { + description: "Test for another devtools-toolbox page", + page: "about:devtools-toolbox", + isDevToolsUser: true, + actionVisible: true, + actionEnabled: false, + }, + { + description: "Test for web content", + page: "https://example.com", + isDevToolsUser: true, + actionVisible: true, + actionEnabled: true, + }, + { + description: "Test for disabled DevTools", + page: "https://example.com", + prefs: [["devtools.policy.disabled", true]], + isDevToolsUser: true, + actionVisible: true, + actionEnabled: false, + }, + { + description: "Test for not DevTools user", + page: "https://example.com", + isDevToolsUser: false, + actionVisible: true, + actionEnabled: false, + }, + { + description: "Test for fully disabled", + page: "https://example.com", + prefs: [["devtools.policy.disabled", true]], + isDevToolsUser: false, + actionVisible: false, + }, + ]; + + const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + for (const { + description, + page, + prefs = [], + isDevToolsUser, + actionEnabled, + actionVisible, + } of testData) { + info(description); + + info("Set preferences"); + await SpecialPowers.pushPrefEnv({ + set: [...prefs, ["devtools.selfxss.count", isDevToolsUser ? 5 : 0]], + }); + + info("Check the button status"); + const onLoad = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, page); + await onLoad; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "inspector", + }); + + if (actionVisible && actionEnabled) { + await assertActionButtonStatus( + "inspect", + true, + "The status of action button is correct" + ); + } else { + Assert.equal( + await hasQuickActions(window), + false, + "Result for quick actions is not shown since the inspector tool is disabled" + ); + } + + await SpecialPowers.popPrefEnv(); + + if (!actionVisible || !actionEnabled) { + continue; + } + + info("Do inspect action"); + EventUtils.synthesizeKey("KEY_ArrowDown", {}, window); + EventUtils.synthesizeKey("KEY_Enter", {}, window); + await BrowserTestUtils.waitForCondition( + () => DevToolsShim.hasToolboxForTab(gBrowser.selectedTab), + "Wait for opening inspector for current selected tab" + ); + const toolbox = await DevToolsShim.getToolboxForTab(gBrowser.selectedTab); + await BrowserTestUtils.waitForCondition( + () => toolbox.getPanel("inspector"), + "Wait until the inspector is ready" + ); + + info("Do inspect action again in the same page during opening inspector"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "inspector", + }); + Assert.equal( + await hasQuickActions(window), + false, + "Result for quick actions is not shown since the inspector is already opening" + ); + + info( + "Select another tool to check whether the inspector will be selected in next test even if the previous tool is not inspector" + ); + await toolbox.selectTool("options"); + await toolbox.destroy(); + } + + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_quickactions_tab_refocus.js b/browser/components/urlbar/tests/browser/browser_quickactions_tab_refocus.js new file mode 100644 index 0000000000..f969528806 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_quickactions_tab_refocus.js @@ -0,0 +1,194 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests for QuickActions that re-focus tab.. + */ + +"use strict"; + +requestLongerTimeout(3); + +add_setup(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.quickactions.enabled", true], + ["browser.urlbar.suggest.quickactions", true], + ["browser.urlbar.shortcuts.quickactions", true], + ], + }); +}); + +let isSelected = async selector => + SpecialPowers.spawn(gBrowser.selectedBrowser, [selector], arg => { + return ContentTaskUtils.waitForCondition(() => + content.document.querySelector(arg)?.hasAttribute("selected") + ); + }); + +add_task(async function test_about_pages() { + const testData = [ + { + firstInput: "downloads", + uri: "about:downloads", + }, + { + firstInput: "logins", + uri: "about:logins", + }, + { + firstInput: "settings", + uri: "about:preferences", + }, + { + firstInput: "add-ons", + uri: "about:addons", + component: "button[name=discover]", + }, + { + firstInput: "extensions", + uri: "about:addons", + component: "button[name=extension]", + }, + { + firstInput: "plugins", + uri: "about:addons", + component: "button[name=plugin]", + }, + { + firstInput: "themes", + uri: "about:addons", + component: "button[name=theme]", + }, + { + firstLoad: "about:preferences#home", + secondInput: "settings", + uri: "about:preferences#home", + }, + ]; + + for (const { + firstInput, + firstLoad, + secondInput, + uri, + component, + } of testData) { + info("Setup initial state"); + let firstTab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + let onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + uri + ); + if (firstLoad) { + info("Load initial URI"); + BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, uri); + } else { + info("Open about page by quick action"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: firstInput, + }); + EventUtils.synthesizeKey("KEY_ArrowDown", {}, window); + EventUtils.synthesizeKey("KEY_Enter", {}, window); + } + await onLoad; + + if (component) { + info("Check whether the component is in the page"); + Assert.ok(await isSelected(component), "There is expected component"); + } + + info("Do the second quick action in second tab"); + let secondTab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: secondInput || firstInput, + }); + EventUtils.synthesizeKey("KEY_ArrowDown", {}, window); + EventUtils.synthesizeKey("KEY_Enter", {}, window); + Assert.equal( + gBrowser.selectedTab, + firstTab, + "Switched to the tab that is opening the about page" + ); + Assert.equal( + gBrowser.selectedBrowser.currentURI.spec, + uri, + "URI is not changed" + ); + Assert.equal(gBrowser.tabs.length, 3, "Not opened a new tab"); + + if (component) { + info("Check whether the component is still in the page"); + Assert.ok(await isSelected(component), "There is expected component"); + } + + BrowserTestUtils.removeTab(secondTab); + BrowserTestUtils.removeTab(firstTab); + } +}); + +add_task(async function test_about_addons_pages() { + let testData = [ + { + cmd: "add-ons", + testFun: async () => isSelected("button[name=discover]"), + }, + { + cmd: "plugins", + testFun: async () => isSelected("button[name=plugin]"), + }, + { + cmd: "extensions", + testFun: async () => isSelected("button[name=extension]"), + }, + { + cmd: "themes", + testFun: async () => isSelected("button[name=theme]"), + }, + ]; + + info("Pick all actions related about:addons"); + let originalTab = gBrowser.selectedTab; + for (const { cmd, testFun } of testData) { + await BrowserTestUtils.openNewForegroundTab(gBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: cmd, + }); + EventUtils.synthesizeKey("KEY_ArrowDown", {}, window); + EventUtils.synthesizeKey("KEY_Enter", {}, window); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + Assert.ok(await testFun(), "The page content is correct"); + } + Assert.equal( + gBrowser.tabs.length, + testData.length + 1, + "Tab length is correct" + ); + + info("Pick all again"); + for (const { cmd, testFun } of testData) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: cmd, + }); + EventUtils.synthesizeKey("KEY_ArrowDown", {}, window); + EventUtils.synthesizeKey("KEY_Enter", {}, window); + await BrowserTestUtils.waitForCondition(() => testFun()); + Assert.ok(true, "The tab correspondent action is selected"); + } + Assert.equal( + gBrowser.tabs.length, + testData.length + 1, + "Tab length is not changed" + ); + + for (const tab of gBrowser.tabs) { + if (tab !== originalTab) { + BrowserTestUtils.removeTab(tab); + } + } +}); 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..17560ea101 --- /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 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..2fc6155cd5 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_redirect_error.js @@ -0,0 +1,137 @@ +/* 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 , which displayes a cert + error page. + +3. Switch the tab to foreground. + +4. Check the URLbar's value, expecting + +5. Load redirect_bug623155.sjs#FG in the foreground tab. + +6. The redirected URI is . And this is also + a cert-error page. + +7. Check the URLbar's value, expecting + +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.loadURIString( + 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..364eeff1b2 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_remoteness_switch.js @@ -0,0 +1,56 @@ +"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.loadURIString(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..1fde855dbd --- /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.importESModule( + "resource://services-sync/SyncedTabs.sys.mjs" +); + +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: Math.floor(Date.now() / 1000), + }, + ], +}; + +add_setup(async function () { + 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..4dfbc5c01b --- /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,hi", "data:text/html,hi"], + ["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,hi", + "data:data:text/html,hi", + ], + ["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 nonsense = + "\u000a\u000b\u000c\u000e\u000f\u0010\u0011\u0012\u0013\u0014javascript:foo"; +if (supportsNullBytes) { + nonsense = "\u0000" + nonsense; +} +pairs.push([nonsense, "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..94a1c874bf --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_remove_match.js @@ -0,0 +1,297 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +ChromeUtils.defineESModuleGetters(this, { + FormHistory: "resource://gre/modules/FormHistory.sys.mjs", +}); + +add_setup(async function () { + await SearchTestUtils.installSearchExtension({}, { setAsDefault: true }); + + let engine = Services.search.getEngineByName("Example"); + await Services.search.moveEngine(engine, 0); +}); + +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( + "page-removed", + events => events[0].url === TEST_URL + ); + + 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 }); + + const removeEvents = await promiseVisitRemoved; + Assert.ok( + removeEvents[0].isRemovedFromStore, + "isRemovedFromStore should be true" + ); + + 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], + ], + }); + + 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_Tab", { 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(); +}); + +// 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 query = "ciao"; + let url = `https://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(); +}); + +add_task(async function blockButton() { + if (UrlbarPrefs.get("resultMenu")) { + // This case is covered by browser_result_menu.js. + return; + } + + let url = "https://example.com/has-block-button"; + let provider = new UrlbarTestUtils.TestProvider({ + priority: Infinity, + results: [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + url, + isBlockable: true, + blockL10n: { id: "firefox-suggest-urlbar-block" }, + } + ), + ], + }); + + // Implement the provider's `onEngagement()` so it removes the result. + let onEngagementCallCount = 0; + provider.onEngagement = (isPrivate, state, queryContext, details) => { + onEngagementCallCount++; + queryContext.view.controller.removeResult(details.result); + }; + + UrlbarProvidersManager.registerProvider(provider); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 1, + "There should be one result" + ); + + let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0); + Assert.equal( + row.result.payload.url, + url, + "The result should be in the first row" + ); + + let button = row.querySelector(".urlbarView-button-block"); + Assert.ok(button, "The row should have a block button"); + + info("Tabbing down to block button"); + EventUtils.synthesizeKey("KEY_Tab", { repeat: 2 }); + + Assert.equal( + UrlbarTestUtils.getSelectedElement(window), + button, + "The block button should be selected after tabbing down" + ); + + info("Pressing Enter on block button"); + EventUtils.synthesizeKey("KEY_Enter"); + + Assert.equal( + onEngagementCallCount, + 1, + "onEngagement() should have been called once" + ); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 0, + "There should be no results after blocking" + ); + + await UrlbarTestUtils.promisePopupClose(window); + UrlbarProvidersManager.unregisterProvider(provider); +}); 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..096d8e2134 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_restoreEmptyInput.js @@ -0,0 +1,64 @@ +/* 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() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.quickactions", false]], + }); + + 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..9b17fb71f5 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_resultSpan.js @@ -0,0 +1,254 @@ +/* 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" } + ), + makeTipResult(), +]; + +const MAX_RESULTS = UrlbarPrefs.get("maxRichResults"); +const TIP_SPAN = UrlbarUtils.getSpanForResult({ + type: UrlbarUtils.RESULT_TYPE.TIP, +}); + +add_setup(async function () { + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.quickactions", false]], + }); +}); + +// 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(makeTipResult()); + } + 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); + + // UrlbarProviderHeuristicFallback'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(makeTipResult()); + } + 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); + + // UrlbarProviderHeuristicFallback'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; +} + +function makeTipResult() { + return new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TIP, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + helpUrl: "http://example.com/", + type: "test", + titleL10n: { id: "urlbar-search-tips-confirm" }, + buttons: [ + { + url: "http://example.com/", + l10n: { id: "urlbar-search-tips-confirm" }, + }, + ], + } + ); +} diff --git a/browser/components/urlbar/tests/browser/browser_result_menu.js b/browser/components/urlbar/tests/browser/browser_result_menu.js new file mode 100644 index 0000000000..b5df97f863 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_result_menu.js @@ -0,0 +1,266 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.resultMenu", true]], + }); +}); + +add_task(async function test_history() { + const TEST_URL = "https://remove.me/from_urlbar/"; + await PlacesTestUtils.addVisits(TEST_URL); + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + }); + + const resultIndex = 1; + let result; + let startQuery = async () => { + await UrlbarTestUtils.promisePopupClose(window); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "from_urlbar", + }); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex); + Assert.equal(result.url, TEST_URL, "Found the expected result"); + gURLBar.view.selectedRowIndex = resultIndex; + }; + + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.resultMenu.keyboardAccessible", false]], + }); + await startQuery(); + EventUtils.synthesizeKey("KEY_Tab"); + isnot( + UrlbarTestUtils.getSelectedElement(window), + UrlbarTestUtils.getButtonForResultIndex(window, "menu", resultIndex), + "Tab key skips over menu button with resultMenu.keyboardAccessible pref set to false" + ); + info( + "Checking that the mouse can still activate the menu button with resultMenu.keyboardAccessible = false" + ); + await UrlbarTestUtils.openResultMenu(window, { + byMouse: true, + resultIndex, + }); + gURLBar.view.resultMenu.hidePopup(); + await SpecialPowers.popPrefEnv(); + await startQuery(); + EventUtils.synthesizeKey("KEY_Tab"); + is( + UrlbarTestUtils.getSelectedElement(window), + UrlbarTestUtils.getButtonForResultIndex(window, "menu", resultIndex), + "Tab key doesn't skip over menu button with resultMenu.keyboardAccessible pref reset to true" + ); + + info("Checking that Space activates the menu button"); + await startQuery(); + await UrlbarTestUtils.openResultMenu(window, { + activationKey: " ", + }); + gURLBar.view.resultMenu.hidePopup(); + + info("Selecting Learn more item from the result menu"); + let tabOpenPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + Services.urlFormatter.formatURLPref("app.support.baseURL") + + "awesome-bar-result-menu" + ); + await UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "L"); + info("Waiting for Learn more link to open in a new tab"); + await tabOpenPromise; + gBrowser.removeCurrentTab(); + + info("Restarting query in order to remove history entry via the menu"); + await startQuery(); + let promiseVisitRemoved = PlacesTestUtils.waitForNotification( + "page-removed", + events => events[0].url === TEST_URL + ); + let expectedResultCount = UrlbarTestUtils.getResultCount(window) - 1; + + await UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "R"); + const removeEvents = await promiseVisitRemoved; + Assert.ok( + removeEvents[0].isRemovedFromStore, + "isRemovedFromStore should be true" + ); + 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_search_history() { + await SearchTestUtils.installSearchExtension({}, { setAsDefault: true }); + let engine = Services.search.getEngineByName("Example"); + await Services.search.moveEngine(engine, 0); + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.searches", true], + ["browser.urlbar.maxHistoricalSearchSuggestions", 1], + ], + }); + + 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 resultIndex = 1; + let count = UrlbarTestUtils.getResultCount(window); + for (; resultIndex < count; resultIndex++) { + let result = await UrlbarTestUtils.getDetailsOfResultAt( + window, + resultIndex + ); + if ( + result.type == UrlbarUtils.RESULT_TYPE.SEARCH && + result.source == UrlbarUtils.RESULT_SOURCE.HISTORY + ) { + break; + } + } + Assert.ok(resultIndex < count, "Result found"); + + await UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "R", { + resultIndex, + }); + 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(); +}); + +add_task(async function firefoxSuggest() { + const url = "https://example.com/hey-there"; + const helpUrl = "https://example.com/help"; + let provider = new UrlbarTestUtils.TestProvider({ + priority: Infinity, + results: [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + url, + isBlockable: true, + blockL10n: { id: "urlbar-result-menu-dismiss-firefox-suggest" }, + helpUrl, + helpL10n: { + id: "urlbar-result-menu-learn-more-about-firefox-suggest", + }, + } + ), + ], + }); + + // Implement the provider's `onEngagement()` so it removes the result. + let onEngagementCallCount = 0; + provider.onEngagement = (isPrivate, state, queryContext, details) => { + onEngagementCallCount++; + queryContext.view.controller.removeResult(details.result); + }; + + UrlbarProvidersManager.registerProvider(provider); + + async function openResults() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 1, + "There should be one result" + ); + + let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0); + Assert.equal( + row.result.payload.url, + url, + "The result should be in the first row" + ); + } + + await openResults(); + let tabOpenPromise = BrowserTestUtils.waitForNewTab(gBrowser, helpUrl); + await UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "L", { + resultIndex: 0, + }); + info("Waiting for help URL to load in a new tab"); + await tabOpenPromise; + gBrowser.removeCurrentTab(); + + await openResults(); + await UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "D", { + resultIndex: 0, + }); + + Assert.greater( + onEngagementCallCount, + 0, + "onEngagement() should have been called" + ); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 0, + "There should be no results after blocking" + ); + + await UrlbarTestUtils.promisePopupClose(window); + UrlbarProvidersManager.unregisterProvider(provider); +}); 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..18c16a3072 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_result_onSelection.js @@ -0,0 +1,67 @@ +/* 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, + { + helpUrl: "http://example.com/", + type: "test", + titleL10n: { id: "urlbar-search-tips-confirm" }, + buttons: [ + { + url: "http://example.com/", + l10n: { id: "urlbar-search-tips-confirm" }, + }, + ], + } + ), + ]; + + 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", + }); + + EventUtils.synthesizeKey("KEY_Tab", { + repeat: UrlbarPrefs.get("resultMenu") ? 5 : 3, + }); + EventUtils.synthesizeKey("KEY_ArrowDown"); + ok( + UrlbarTestUtils.getOneOffSearchButtons(window).selectedButton, + "a one off button is selected" + ); + + Assert.equal( + selectionCount, + UrlbarPrefs.get("resultMenu") ? 6 : 4, + "Number of elements selected 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..22ec47403e --- /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_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.autoFill", true]], + }); + // Add some history for the empty panel and autofill. + await PlacesTestUtils.addVisits([ + { + uri: "https://example.com/", + transition: PlacesUtils.history.TRANSITIONS.TYPED, + }, + { + uri: "https://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", "https://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 == "https://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([ + "https://example.com/", + "https://example.org/", + ]); + registerCleanupFunction(PlacesUtils.history.clear); + + let win = await BrowserTestUtils.openNewBrowserWindow(); + BrowserTestUtils.loadURIString(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..b68ad0ff91 --- /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..0a272f9f01 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchFunction.js @@ -0,0 +1,278 @@ +/* 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_setup(async function () { + // Run this in a new tab, to ensure all the locationchange notifications have + // fired. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + await SearchTestUtils.installSearchExtension({ + keyword: ALIAS, + }); + aliasEngine = Services.search.getEngineByName("Example"); + + registerCleanupFunction(async function () { + 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: "topsites_urlbar", + }) + ); + Assert.ok(gURLBar.hasAttribute("focused"), "Urlbar is focused"); + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: aliasEngine.name, + entry: "topsites_urlbar", + }); + await assertUrlbarValue("test"); + assertOneOffButtonsVisible(true); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); +}); + +// Calls search() and passes in a search engine without including a restriction +// token or engine alias in the search string. Simulates pasting into the newtab +// handoff field with search suggestions disabled. +add_task(async function searchEngineWithNoToken() { + await UrlbarTestUtils.promisePopupOpen(window, async () => + gURLBar.search("no-alias", { + searchEngine: aliasEngine, + searchModeEntry: "handoff", + }) + ); + + Assert.ok(gURLBar.hasAttribute("focused"), "Urlbar is focused"); + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: aliasEngine.name, + entry: "handoff", + }); + await assertUrlbarValue("no-alias"); + 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..6fcde0882b --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchHistoryLimit.js @@ -0,0 +1,87 @@ +/* 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.importESModule( + "resource://gre/modules/SearchSuggestionController.sys.mjs" +); + +let gEngine; + +add_setup(async function () { + await SearchTestUtils.installSearchExtension({}, { setAsDefault: true }); + gEngine = Services.search.getEngineByName("Example"); + await UrlbarTestUtils.formHistory.clear(); + + registerCleanupFunction(async function () { + 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..9f4558e6c9 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_alias_replacement.js @@ -0,0 +1,274 @@ +/* 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_setup(async function () { + defaultEngine = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME, + setAsDefault: true, + }); + defaultEngine.alias = "@default"; + await SearchTestUtils.installSearchExtension({ + keyword: ALIAS, + }); + aliasEngine = Services.search.getEngineByName("Example"); +}); + +// 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..3186d96b92 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_autofill.js @@ -0,0 +1,133 @@ +/* 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"; + +add_setup(async function () { + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits([{ uri: "http://example.com/" }]); + } + + await SearchTestUtils.installSearchExtension({}, { setAsDefault: true }); + let defaultEngine = Services.search.getEngineByName("Example"); + await Services.search.moveEngine(defaultEngine, 0); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.quickactions", false]], + }); + + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + }); +}); + +// 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." + ); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); +}); + +// 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." + ); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); +}); 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..f5eab77789 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_engineRemoval.js @@ -0,0 +1,109 @@ +/* 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 extension = await SearchTestUtils.installSearchExtension( + {}, + { skipUnload: true } + ); + let engine = Services.search.getEngineByName("Example"); + 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 extension.unload(); + // 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 extension = await SearchTestUtils.installSearchExtension( + {}, + { skipUnload: true } + ); + let engine = Services.search.getEngineByName("Example"); + 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 extension.unload(); + + // 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 extension = await SearchTestUtils.installSearchExtension( + {}, + { skipUnload: true } + ); + let engine = Services.search.getEngineByName("Example"); + 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 extension.unload(); + + // 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..0e9471280e --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_excludeResults.js @@ -0,0 +1,217 @@ +/* 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"; + +ChromeUtils.defineESModuleGetters(this, { + SyncedTabs: "resource://services-sync/SyncedTabs.sys.mjs", +}); + +add_setup(async function () { + 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(); + + // 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. + await SearchTestUtils.installSearchExtension( + { + search_url: "https://subdomain.example.ca/", + }, + { setAsDefault: true } + ); + let engine = Services.search.getEngineByName("Example"); + 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: "https://example.com", + icon: UrlbarUtils.ICON.DEFAULT, + client: "7cqCr77ptzX3", + lastUsed: Math.floor(Date.now() / 1000), + }, + { + type: "tab", + title: "Test Remote 2", + url: "https://example-2.com", + icon: UrlbarUtils.ICON.DEFAULT, + client: "7cqCr77ptzX3", + lastUsed: Math.floor(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 UrlbarProviderRemoteTabs. + Services.obs.notifyObservers(null, "weave:engine:sync:finish", "tabs"); + + registerCleanupFunction(async function () { + sandbox.restore(); + weaveXPCService.ready = oldWeaveServiceReady; + SyncedTabs._internal = originalSyncedTabsInternal; + 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() { + await SearchTestUtils.installSearchExtension({ + name: "TestMalformed", + search_url: "https://example.foobar/", + }); + let badEngine = Services.search.getEngineByName("TestMalformed"); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "example", + }); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 4, + "We have four 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.DYNAMIC, + "The second result is the tab-to-search onboarding result for our malformed engine." + ); + let thirdResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 2); + Assert.equal( + thirdResult.type, + UrlbarUtils.RESULT_TYPE.REMOTE_TAB, + "The third result is a remote tab." + ); + let fourthResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 3); + Assert.equal( + fourthResult.type, + UrlbarUtils.RESULT_TYPE.REMOTE_TAB, + "The fourth 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); +}); 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..bd8f00a512 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_heuristic.js @@ -0,0 +1,221 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests heuristic results in search mode. + */ + +"use strict"; + +add_setup(async function () { + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + + // Add a new mock default engine so we don't hit the network. + await SearchTestUtils.installSearchExtension( + { name: "Test" }, + { setAsDefault: true } + ); + + // 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, + "https://example.com/?q=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..357a5d17f9 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_indicator.js @@ -0,0 +1,377 @@ +/* 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 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_setup(async function () { + suggestionsEngine = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + SUGGESTIONS_ENGINE_NAME, + }); + + await SearchTestUtils.installSearchExtension({}, { setAsDefault: true }); + defaultEngine = Services.search.getEngineByName("Example"); + await Services.search.moveEngine(suggestionsEngine, 0); + + // 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], + ["browser.urlbar.suggest.quickactions", 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_indicator_clickthrough.js b/browser/components/urlbar/tests/browser/browser_searchMode_indicator_clickthrough.js new file mode 100644 index 0000000000..acfb60922d --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_indicator_clickthrough.js @@ -0,0 +1,100 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check clicking on the search mode indicator when the urlbar is not focused puts + * focus in the urlbar. + */ + +add_task(async function test() { + // Avoid remote connections. + await SpecialPowers.pushPrefEnv({ + set: [["browser.search.suggest.enabled", false]], + }); + + await BrowserTestUtils.withNewTab("about:robots", async browser => { + // View open, with string. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + const indicator = document.getElementById("urlbar-search-mode-indicator"); + Assert.ok(!BrowserTestUtils.is_visible(indicator)); + const indicatorCloseButton = document.getElementById( + "urlbar-search-mode-indicator-close" + ); + Assert.ok(!BrowserTestUtils.is_visible(indicatorCloseButton)); + const labelBox = document.getElementById("urlbar-label-box"); + Assert.ok(!BrowserTestUtils.is_visible(labelBox)); + + await UrlbarTestUtils.enterSearchMode(window); + Assert.ok(BrowserTestUtils.is_visible(indicator)); + Assert.ok(BrowserTestUtils.is_visible(indicatorCloseButton)); + Assert.ok(!BrowserTestUtils.is_visible(labelBox)); + + info("Blur the urlbar"); + gURLBar.blur(); + Assert.ok(BrowserTestUtils.is_visible(indicator)); + Assert.ok(BrowserTestUtils.is_visible(indicatorCloseButton)); + Assert.ok(!BrowserTestUtils.is_visible(labelBox)); + Assert.notEqual( + document.activeElement, + gURLBar.inputField, + "URL Bar should not be focused" + ); + + info("Focus the urlbar clicking on the indicator"); + EventUtils.synthesizeMouseAtCenter(indicator, {}); + Assert.ok(BrowserTestUtils.is_visible(indicator)); + Assert.ok(BrowserTestUtils.is_visible(indicatorCloseButton)); + Assert.ok(!BrowserTestUtils.is_visible(labelBox)); + Assert.equal( + document.activeElement, + gURLBar.inputField, + "URL Bar should be focused" + ); + + info("Leave search mode clicking on the close button"); + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + Assert.ok(!BrowserTestUtils.is_visible(indicator)); + Assert.ok(!BrowserTestUtils.is_visible(indicatorCloseButton)); + Assert.ok(!BrowserTestUtils.is_visible(labelBox)); + }); + + await BrowserTestUtils.withNewTab("about:robots", async browser => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + const indicator = document.getElementById("urlbar-search-mode-indicator"); + Assert.ok(!BrowserTestUtils.is_visible(indicator)); + const indicatorCloseButton = document.getElementById( + "urlbar-search-mode-indicator-close" + ); + Assert.ok(!BrowserTestUtils.is_visible(indicatorCloseButton)); + + await UrlbarTestUtils.enterSearchMode(window); + Assert.ok(BrowserTestUtils.is_visible(indicator)); + Assert.ok(BrowserTestUtils.is_visible(indicatorCloseButton)); + + info("Blur the urlbar"); + gURLBar.blur(); + Assert.ok(BrowserTestUtils.is_visible(indicator)); + Assert.ok(BrowserTestUtils.is_visible(indicatorCloseButton)); + Assert.notEqual( + document.activeElement, + gURLBar.inputField, + "URL Bar should not be focused" + ); + + info("Leave search mode clicking on the close button while unfocussing"); + await UrlbarTestUtils.exitSearchMode(window, { + clickClose: true, + waitForSearch: false, + }); + Assert.ok(!BrowserTestUtils.is_visible(indicator)); + Assert.ok(!BrowserTestUtils.is_visible(indicatorCloseButton)); + }); +}); 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..74a2a3caba --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_localOneOffs_actionText.js @@ -0,0 +1,459 @@ +/* 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_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.searches", true], + ["browser.urlbar.suggest.quickactions", false], + ["browser.urlbar.shortcuts.quickactions", false], + ], + }); + engine = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + SUGGESTIONS_ENGINE_NAME, + setAsDefault: true, + }); + await Services.search.moveEngine(engine, 0); + + await PlacesUtils.history.clear(); + + registerCleanupFunction(async () => { + 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://global/skin/icons/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] = + await document.l10n.formatValues([ + { id: "urlbar-result-action-search-history" }, + { id: "urlbar-result-action-search-tabs" }, + { id: "urlbar-result-action-search-bookmarks" }, + ]); + + 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.url, + result.result.payload.displayUrl, + "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, + result.result.payload.title || `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://global/skin/icons/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://global/skin/icons/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://global/skin/icons/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://global/skin/icons/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_newWindow.js b/browser/components/urlbar/tests/browser/browser_searchMode_newWindow.js new file mode 100644 index 0000000000..e5a3eb848a --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_newWindow.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests immediately entering search mode in a new window and then exiting it. +// No errors should be thrown and search mode should be exited successfully. + +"use strict"; + +add_task(async function escape() { + await doTest(win => + EventUtils.synthesizeKey("KEY_Escape", { repeat: 2 }, win) + ); +}); + +add_task(async function backspace() { + await doTest(win => EventUtils.synthesizeKey("KEY_Backspace", {}, win)); +}); + +async function doTest(exitSearchMode) { + let win = await BrowserTestUtils.openNewBrowserWindow(); + + // Press accel+K to enter search mode. + await UrlbarTestUtils.promisePopupOpen(win, () => + EventUtils.synthesizeKey("k", { accelKey: true }, win) + ); + await UrlbarTestUtils.assertSearchMode(win, { + engineName: Services.search.defaultEngine.name, + isGeneralPurposeEngine: true, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + isPreview: false, + entry: "shortcut", + }); + + // Exit search mode. + await exitSearchMode(win); + await UrlbarTestUtils.assertSearchMode(win, null); + + await UrlbarTestUtils.promisePopupClose(win); + await BrowserTestUtils.closeWindow(win); +} 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..9ecc5573fc --- /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_setup(async function () { + // 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({ + url: 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..1ba0d3283b --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_oneOffButton.js @@ -0,0 +1,108 @@ +/* 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"; + +add_setup(async function () { + await SearchTestUtils.installSearchExtension({ + name: TEST_ENGINE_NAME, + keyword: "@test", + }); +}); + +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..ac45b3e5c7 --- /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_setup(async function () { + // 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..19df744663 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_preview.js @@ -0,0 +1,489 @@ +/* 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"; + +add_setup(async function () { + await SearchTestUtils.installSearchExtension({ + name: TEST_ENGINE_NAME, + keyword: "@test", + }); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.quickactions", false]], + }); + + registerCleanupFunction(async () => { + 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; + let engine = Services.search.getEngineByName(button.engine.name); + if (engine.isGeneralPurposeEngine) { + 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"); + let index = UrlbarTestUtils.getSelectedRowIndex(window); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, index); + let expectedSearchMode = { + engineName: result.searchParams.engine, + isPreview: true, + entry: "keywordoffer", + }; + let engine = Services.search.getEngineByName(result.searchParams.engine); + if (engine.isGeneralPurposeEngine) { + 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"); + } + + 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"); + 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"); + } + 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"); + } + 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.settingsButton) { + 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.settingsButton, + "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("https://example.com/"); + } + await updateTopSites( + sites => + sites && + sites[0]?.searchTopSite && + sites[1]?.url == "https://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.settingsButton) { + 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([ + "https://1.example.com/", + "https://2.example.com/", + "https://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..ef3fabe636 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_sessionStore.js @@ -0,0 +1,332 @@ +/* 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"; + +ChromeUtils.defineESModuleGetters(this, { + SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", + TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.sys.mjs", +}); + +// 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 {object} options + * Options object + * @param {Array} options.urls + * Array of string URLs to open. + * @param {number} options.searchModeTabIndex + * The index of the tab in which to enter search mode. + * @param {boolean} options.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} options.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); +} + +async function openTabMenuFor(tab) { + let tabMenu = tab.ownerDocument.getElementById("tabContextMenu"); + + let tabMenuShown = BrowserTestUtils.waitForEvent(tabMenu, "popupshown"); + EventUtils.synthesizeMouseAtCenter( + tab, + { type: "contextmenu" }, + tab.ownerGlobal + ); + await tabMenuShown; + + return tabMenu; +} + +// Tests that search mode is duplicated when duplicating tabs. Note that tab +// duplication is handled by session store. +add_task(async function duplicateTabs() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "http://example.net/" + ); + gBrowser.selectedTab = tab; + // 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. + const menu = await openTabMenuFor(gBrowser.selectedTab); + let tabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + gBrowser.currentURI.spec + ); + menu.activateItem(document.getElementById("context_duplicateTab")); + 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(tab); + 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..5aa3412580 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_suggestions.js @@ -0,0 +1,579 @@ +/* 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_RESULT_COUNT = UrlbarPrefs.get("maxRichResults"); + +let suggestionsEngine; +let expectedFormHistoryResults = []; + +add_setup(async function () { + suggestionsEngine = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + SUGGESTIONS_ENGINE_NAME, + }); + + await SearchTestUtils.installSearchExtension( + { + name: DEFAULT_ENGINE_NAME, + keyword: "@test", + }, + { setAsDefault: true } + ); + 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. + for (let i = 0; i < MAX_RESULT_COUNT; 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 UrlbarTestUtils.formHistory.clear(); + }); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.separatePrivateDefault.ui.enabled", false], + ["browser.urlbar.suggest.quickactions", 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.slice(0, MAX_RESULT_COUNT - 3), + { + 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."); + // maxHistoricalSearchSuggestions == 0, so form history should not be + // present. + 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: DEFAULT_ENGINE_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, MAX_RESULT_COUNT - 3), + { + 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_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({ + url: 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"), + 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); + + info("Test again with history before suggestions"); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchSuggestionsFirst", false]], + }); + + 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..c4f541c9cd --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_switchTabs.js @@ -0,0 +1,317 @@ +/* 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() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.quickactions", false]], + }); + + // 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 extension = await SearchTestUtils.installSearchExtension( + { + name: engineName, + }, + { skipUnload: true } + ); + + 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 SpecialPowers.popPrefEnv(); + await extension.unload(); +}); + +// 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 extension = await SearchTestUtils.installSearchExtension( + { + name: engineName, + }, + { skipUnload: true } + ); + + 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.loadURIString( + 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 extension.unload(); +}); + +// 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..2cded38c99 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchSettings.js @@ -0,0 +1,30 @@ +/* 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"); + 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..36a065d58e --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchSingleWordNotification.js @@ -0,0 +1,372 @@ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ +"use strict"; + +let gDNSResolved = false; +add_setup(async function () { + registerCleanupFunction(function () { + 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); + let topic = "uri-fixup-check-dns"; + let observer = (aSubject, aTopicInner, aData) => { + if (aTopicInner == topic) { + gDNSResolved = true; + } + }; + Services.obs.addObserver(observer, topic); + + 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.buttonContainer.querySelector("button").click(); + await docLoadPromise; + } else { + notificationBox.currentNotification.close(); + } + } + + Services.obs.removeObserver(observer, topic); + 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); + } else { + win = window; + } + + // Remove the domain from the whitelist + Services.prefs.setBoolPref(pref, false); + + // The notification should not appear because the default value of + // browser.urlbar.dnsResolveSingleWordsAfterSearch is 0 + await BrowserTestUtils.withNewTab( + { + gBrowser: win.gBrowser, + url: "about:blank", + }, + browser => + runURLBarSearchTest({ + valueToOpen: hostName, + expectSearch: true, + expectNotification: false, + expectDNSResolve: false, + aWindow: win, + }) + ); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.dnsResolveSingleWordsAfterSearch", 1]], + }); + + // The notification should appear, unless we are in private browsing mode. + 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); + } + + await SpecialPowers.popPrefEnv(); + }; +} + +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..8a226a3c4c --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchSuggestions.js @@ -0,0 +1,341 @@ +/* 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); + await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME, + setAsDefault: true, + }); + await UrlbarTestUtils.formHistory.clear(); + registerCleanupFunction(async function () { + Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, suggestionsEnabled); + + // 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..61ddff4c2d --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchTelemetry.js @@ -0,0 +1,220 @@ +"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], + ], + }); + + await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME, + setAsDefault: true, + }); + + registerCleanupFunction(async function () { + // 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 EventUtils.promiseNativeMouseEvent({ + type: "mousemove", + target: window.document.documentElement, + offsetX: 0, + offsetY: 0, + }); +}); + +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_search_bookmarks_from_bookmarks_menu.js b/browser/components/urlbar/tests/browser/browser_search_bookmarks_from_bookmarks_menu.js new file mode 100644 index 0000000000..e42fcc9f7f --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_search_bookmarks_from_bookmarks_menu.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 searchBookmarksFromBooksmarksMenu() { + // Add Button to toolbar + CustomizableUI.addWidgetToArea( + "bookmarks-menu-button", + CustomizableUI.AREA_NAVBAR, + 0 + ); + let bookmarksMenuButton = document.getElementById("bookmarks-menu-button"); + ok(bookmarksMenuButton, "Bookmarks Menu Button added"); + + // Open Bookmarks-Menu-Popup + let bookmarksMenuPopup = document.getElementById("BMB_bookmarksPopup"); + let PopupShownPromise = BrowserTestUtils.waitForEvent( + bookmarksMenuPopup, + "popupshown" + ); + EventUtils.synthesizeMouseAtCenter(bookmarksMenuButton, { + type: "mousedown", + }); + await PopupShownPromise; + ok(true, "Bookmarks Menu Popup shown"); + + // Click on 'Search Bookmarks' + let searchBookmarksButton = document.getElementById("BMB_searchBookmarks"); + ok( + BrowserTestUtils.is_visible( + searchBookmarksButton, + "'Search Bookmarks Button' is visible." + ) + ); + EventUtils.synthesizeMouseAtCenter(searchBookmarksButton, {}); + + await new Promise(resolve => { + window.gURLBar.controller.addQueryListener({ + onViewOpen() { + window.gURLBar.controller.removeQueryListener(this); + resolve(); + }, + }); + }); + + // Verify URLBar is in search mode with correct restriction + is( + gURLBar.searchMode?.source, + UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + "Addressbar in correct mode." + ); + + resetCUIAndReinitUrlbarInput(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_search_history_from_history_panel.js b/browser/components/urlbar/tests/browser/browser_search_history_from_history_panel.js new file mode 100644 index 0000000000..b901a87736 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_search_history_from_history_panel.js @@ -0,0 +1,97 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { CustomizableUITestUtils } = ChromeUtils.importESModule( + "resource://testing-common/CustomizableUITestUtils.sys.mjs" +); +let gCUITestUtils = new CustomizableUITestUtils(window); + +add_task(async function searchHistoryFromHistoryPanel() { + // Add Button to toolbar + CustomizableUI.addWidgetToArea( + "history-panelmenu", + CustomizableUI.AREA_NAVBAR, + 0 + ); + registerCleanupFunction(() => { + resetCUIAndReinitUrlbarInput(); + }); + + let historyButton = document.getElementById("history-panelmenu"); + ok(historyButton, "History button appears in Panel Menu"); + + historyButton.click(); + + let historyPanel = document.getElementById("PanelUI-history"); + let promise = BrowserTestUtils.waitForEvent(historyPanel, "ViewShown"); + await promise; + ok(historyPanel.getAttribute("visible"), "History Panel is in view"); + + // Click on 'Search Bookmarks' + let searchHistoryButton = document.getElementById("appMenuSearchHistory"); + ok( + BrowserTestUtils.is_visible( + searchHistoryButton, + "'Search History Button' is visible." + ) + ); + EventUtils.synthesizeMouseAtCenter(searchHistoryButton, {}); + + await new Promise(resolve => { + window.gURLBar.controller.addQueryListener({ + onViewOpen() { + window.gURLBar.controller.removeQueryListener(this); + resolve(); + }, + }); + }); + + // Verify URLBar is in search mode with correct restriction + is( + gURLBar.searchMode?.source, + UrlbarUtils.RESULT_SOURCE.HISTORY, + "Addressbar in correct mode." + ); + gURLBar.searchMode = null; + gURLBar.blur(); +}); + +add_task(async function searchHistoryFromAppMenuHistoryButton() { + // Open main menu and click on 'History' button + await gCUITestUtils.openMainMenu(); + let historyButton = document.getElementById("appMenu-history-button"); + historyButton.click(); + + let historyPanel = document.getElementById("PanelUI-history"); + let promise = BrowserTestUtils.waitForEvent(historyPanel, "ViewShown"); + await promise; + ok(historyPanel.getAttribute("visible"), "History Panel is in view"); + + // Click on 'Search Bookmarks' + let searchHistoryButton = document.getElementById("appMenuSearchHistory"); + ok( + BrowserTestUtils.is_visible( + searchHistoryButton, + "'Search History Button' is visible." + ) + ); + EventUtils.synthesizeMouseAtCenter(searchHistoryButton, {}); + + await new Promise(resolve => { + window.gURLBar.controller.addQueryListener({ + onViewOpen() { + window.gURLBar.controller.removeQueryListener(this); + resolve(); + }, + }); + }); + + // Verify URLBar is in search mode with correct restriction + is( + gURLBar.searchMode?.source, + UrlbarUtils.RESULT_SOURCE.HISTORY, + "Addressbar in correct mode." + ); +}); 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..16366f5b33 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_selectStaleResults.js @@ -0,0 +1,311 @@ +/* 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"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarView: "resource:///modules/UrlbarView.sys.mjs", +}); + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.quickactions", false]], + }); + + // 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 = UrlbarTestUtils.getRowAt(window, 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( + UrlbarTestUtils.getResultsContainer(window).children + ).filter(r => BrowserTestUtils.is_visible(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({ + url: getRootDirectory(gTestPath) + "searchSuggestionEngineSlow.xml", + }); + let oldDefaultEngine = await Services.search.getDefault(); + await Services.search.moveEngine(engine, 0); + await Services.search.setDefault( + engine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + 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 = UrlbarTestUtils.getRowAt(window, maxResults - 2); + if (row && row._elements.get("title").textContent == "test2") { + observer.disconnect(); + resolve(); + } + }); + observer.observe(UrlbarTestUtils.getResultsContainer(window), { + 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, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); +}); 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..89ba179833 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_selectionKeyNavigation.js @@ -0,0 +1,200 @@ +/* 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_setup(async function () { + 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() { + for (const ctrlKey of [false, true]) { + 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", { ctrlKey }); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), i); + } + EventUtils.synthesizeKey("KEY_ArrowDown", { ctrlKey }); + let oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window); + Assert.ok(oneOffs.selectedButton, "A one-off should now be selected"); + while (oneOffs.selectedButton) { + EventUtils.synthesizeKey("KEY_ArrowDown", { ctrlKey }); + } + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 0, + "The heuristic autofill result should be selected again" + ); + } +}); + +add_task(async function upKey() { + for (const ctrlKey of [false, true]) { + 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", { ctrlKey }); + let oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window); + Assert.ok(oneOffs.selectedButton, "A one-off should now be selected"); + while (oneOffs.selectedButton) { + EventUtils.synthesizeKey("KEY_ArrowUp", { ctrlKey }); + } + 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", { ctrlKey }); + 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); +}); + +add_task(async function pageDownKeyWithCtrlKey() { + const previousTab = gBrowser.selectedTab; + const currentTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + fireInputEvent: true, + }); + EventUtils.synthesizeKey("KEY_PageDown", { ctrlKey: true }); + await UrlbarTestUtils.promisePopupClose(window); + await UrlbarTestUtils.promiseSearchComplete(window); + Assert.equal(gBrowser.selectedTab, previousTab); + BrowserTestUtils.removeTab(currentTab); +}); + +add_task(async function pageUpKeyWithCtrlKey() { + const previousTab = gBrowser.selectedTab; + const currentTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + fireInputEvent: true, + }); + EventUtils.synthesizeKey("KEY_PageUp", { ctrlKey: true }); + await UrlbarTestUtils.promisePopupClose(window); + await UrlbarTestUtils.promiseSearchComplete(window); + Assert.equal(gBrowser.selectedTab, previousTab); + BrowserTestUtils.removeTab(currentTab); +}); 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..8cdc0e746b --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_separatePrivateDefault.js @@ -0,0 +1,223 @@ +/* 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 +}; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.separatePrivateDefault.ui.enabled", true], + ["browser.search.separatePrivateDefault.urlbarResult.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. + await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + "searchSuggestionEngine.xml", + setAsDefault: true, + setAsDefaultPrivate: true, + }); + + // Add another engine in the first one-off position. + let engine2 = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + "POSTSearchEngine.xml", + }); + await Services.search.moveEngine(engine2, 0); + + // Add an engine with an alias. + await SearchTestUtils.installSearchExtension({ + name: "MozSearch", + keyword: "alias", + }); + + registerCleanupFunction(async () => { + 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_urlbar_result_disabled() { + info("Test that 'Search in a Private Window' does not appear when disabled"); + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.separatePrivateDefault.urlbarResult.enabled", false], + ], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "unique198273982173", + }); + await AssertNoPrivateResult(window); + await SpecialPowers.popPrefEnv(); +}); + +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..58a60d68a9 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_separatePrivateDefault_differentEngine.js @@ -0,0 +1,354 @@ +/* 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_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.separatePrivateDefault.ui.enabled", true], + ["browser.search.separatePrivateDefault.urlbarResult.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. + await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + "searchSuggestionEngine.xml", + setAsDefault: true, + }); + gPrivateEngine = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + "searchSuggestionEngine2.xml", + setAsDefaultPrivate: true, + }); + + // Add another engine in the first one-off position. + let engine2 = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + "POSTSearchEngine.xml", + }); + await Services.search.moveEngine(engine2, 0); + + // Add an engine with an alias. + await SearchTestUtils.installSearchExtension({ + name: "MozSearch", + keyword: "alias", + }); + gAliasEngine = Services.search.getEngineByName("MozSearch"); + + registerCleanupFunction(async () => { + 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_shortcuts_add_search_engine.js b/browser/components/urlbar/tests/browser/browser_shortcuts_add_search_engine.js new file mode 100644 index 0000000000..1fef68de30 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_shortcuts_add_search_engine.js @@ -0,0 +1,243 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test adding engines through search shortcut buttons. +// A more complete coverage of the detection of engines is available in +// browser_add_search_engine.js + +const { PromptTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromptTestUtils.sys.mjs" +); +const BASE_URL = + "http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.searches", false]], + }); + // Ensure initial state. + UrlbarTestUtils.getOneOffSearchButtons(window).invalidateCache(); +}); + +add_task(async function shortcuts_none() { + info("Checks the shortcuts with a page that doesn't offer any engines."); + let url = "http://mochi.test:8888/"; + await BrowserTestUtils.withNewTab(url, async () => { + let shortcutButtons = UrlbarTestUtils.getOneOffSearchButtons(window); + let rebuildPromise = BrowserTestUtils.waitForEvent( + shortcutButtons, + "rebuild" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await rebuildPromise; + + Assert.ok( + !Array.from(shortcutButtons.buttons.children).some(b => + b.classList.contains("searchbar-engine-one-off-add-engine") + ), + "Check there's no buttons to add engines" + ); + }); +}); + +add_task(async function test_shortcuts() { + await do_test_shortcuts(button => { + info("Click on button"); + EventUtils.synthesizeMouseAtCenter(button, {}); + }); + await do_test_shortcuts(button => { + info("Enter on button"); + let shortcuts = UrlbarTestUtils.getOneOffSearchButtons(window); + while (shortcuts.selectedButton != button) { + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + } + EventUtils.synthesizeKey("KEY_Enter"); + }); +}); + +/** + * Test add engine shortcuts. + * + * @param {Function} activateTask a function receiveing the shortcut button to + * activate as argument. The scope of this function is to activate the + * shortcut button. + */ +async function do_test_shortcuts(activateTask) { + info("Checks the shortcuts with a page that offers two engines."); + let url = getRootDirectory(gTestPath) + "add_search_engine_two.html"; + await BrowserTestUtils.withNewTab(url, async () => { + let shortcutButtons = UrlbarTestUtils.getOneOffSearchButtons(window); + let rebuildPromise = BrowserTestUtils.waitForEvent( + shortcutButtons, + "rebuild" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await rebuildPromise; + + let addEngineButtons = Array.from(shortcutButtons.buttons.children).filter( + b => b.classList.contains("searchbar-engine-one-off-add-engine") + ); + Assert.equal( + addEngineButtons.length, + 2, + "Check there's two buttons to add engines" + ); + + for (let button of addEngineButtons) { + Assert.ok(BrowserTestUtils.is_visible(button)); + Assert.ok(button.hasAttribute("image")); + await document.l10n.translateElements([button]); + Assert.ok( + button.getAttribute("tooltiptext").includes("add_search_engine_") + ); + Assert.ok( + button.getAttribute("engine-name").startsWith("add_search_engine_") + ); + Assert.ok( + button.classList.contains("searchbar-engine-one-off-add-engine") + ); + } + + info("Activate the first button"); + rebuildPromise = BrowserTestUtils.waitForEvent(shortcutButtons, "rebuild"); + let enginePromise = promiseEngine("engine-added", "add_search_engine_0"); + await activateTask(addEngineButtons[0]); + info("await engine install"); + let engine = await enginePromise; + info("await rebuild"); + await rebuildPromise; + + Assert.ok( + UrlbarTestUtils.isPopupOpen(window), + "Urlbar view is still open." + ); + + addEngineButtons = Array.from(shortcutButtons.buttons.children).filter(b => + b.classList.contains("searchbar-engine-one-off-add-engine") + ); + Assert.equal( + addEngineButtons.length, + 1, + "Check there's one button to add engines" + ); + Assert.equal( + addEngineButtons[0].getAttribute("engine-name"), + "add_search_engine_1" + ); + let installedEngineButton = addEngineButtons[0].previousElementSibling; + Assert.equal(installedEngineButton.engine.name, "add_search_engine_0"); + + info("Remove the added engine"); + rebuildPromise = BrowserTestUtils.waitForEvent(shortcutButtons, "rebuild"); + await Services.search.removeEngine(engine); + await rebuildPromise; + Assert.equal( + Array.from(shortcutButtons.buttons.children).filter(b => + b.classList.contains("searchbar-engine-one-off-add-engine") + ).length, + 2, + "Check there's two buttons to add engines" + ); + await UrlbarTestUtils.promisePopupClose(window); + + info("Switch to a new tab and check the buttons are not persisted"); + await BrowserTestUtils.withNewTab("about:robots", async () => { + rebuildPromise = BrowserTestUtils.waitForEvent( + shortcutButtons, + "rebuild" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await rebuildPromise; + Assert.ok( + !Array.from(shortcutButtons.buttons.children).some(b => + b.classList.contains("searchbar-engine-one-off-add-engine") + ), + "Check there's no option to add engines" + ); + }); + }); +} + +add_task(async function shortcuts_many() { + info("Checks the shortcuts with a page that offers many engines."); + let url = getRootDirectory(gTestPath) + "add_search_engine_many.html"; + await BrowserTestUtils.withNewTab(url, async () => { + let shortcutButtons = UrlbarTestUtils.getOneOffSearchButtons(window); + let rebuildPromise = BrowserTestUtils.waitForEvent( + shortcutButtons, + "rebuild" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await rebuildPromise; + + let addEngineButtons = Array.from(shortcutButtons.buttons.children).filter( + b => b.classList.contains("searchbar-engine-one-off-add-engine") + ); + Assert.equal( + addEngineButtons.length, + gURLBar.addSearchEngineHelper.maxInlineEngines, + "Check there's a maximum of `maxInlineEngines` buttons to add engines" + ); + }); +}); + +function promiseEngine(expectedData, expectedEngineName) { + info(`Waiting for engine ${expectedData}`); + return TestUtils.topicObserved( + "browser-search-engine-modified", + (engine, data) => { + info(`Got engine ${engine.wrappedJSObject.name} ${data}`); + return ( + expectedData == data && + expectedEngineName == engine.wrappedJSObject.name + ); + } + ).then(([engine, data]) => engine); +} + +add_task(async function shortcuts_without_other_engines() { + info("Checks the shortcuts without other engines."); + + info("Remove search engines except default"); + const defaultEngine = Services.search.defaultEngine; + const engines = await Services.search.getVisibleEngines(); + for (const engine of engines) { + if (defaultEngine.name !== engine.name) { + await Services.search.removeEngine(engine); + } + } + + info("Remove local engines"); + for (const { pref } of UrlbarUtils.LOCAL_SEARCH_MODES) { + await SpecialPowers.pushPrefEnv({ + set: [[`browser.urlbar.${pref}`, false]], + }); + } + + const url = getRootDirectory(gTestPath) + "add_search_engine_many.html"; + await BrowserTestUtils.withNewTab(url, async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + + const shortcutButtons = UrlbarTestUtils.getOneOffSearchButtons(window); + Assert.ok(shortcutButtons.container.hidden, "It should be hidden"); + }); + + Services.search.restoreDefaultEngines(); +}); 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..3b98169699 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_speculative_connect.js @@ -0,0 +1,199 @@ +/* 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_setup(async function () { + 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, + }, + ]); + + await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME, + setAsDefault: true, + }); + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + }); +}); + +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..de352efb59 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_speculative_connect_not_with_client_cert.js @@ -0,0 +1,236 @@ +/* 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.importESModule( + "resource://testing-common/MockRegistrar.sys.mjs" +); + +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; + ok( + !chooseCertificateCalled, + "chooseCertificate should only be called once" + ); + chooseCertificateCalled = true; + return true; + }, + + QueryInterface: ChromeUtils.generateQI(["nsIClientAuthDialogs"]), +}; + +/** + * A helper class to use with nsITLSServerConnectionInfo.setSecurityObserver. + * Implements nsITLSServerSecurityObserver and simulates an extremely + * rudimentary HTTP server that expects an HTTP/1.1 GET request and responds + * with a 200 OK. + */ +class SecurityObserver { + constructor(input, output) { + this.input = input; + this.output = output; + } + + onHandshakeDone(socket, status) { + info("TLS handshake done"); + handshakeDone = true; + + let output = this.output; + this.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) { + console.log(e.message); + // This will fail when we close the speculative connection. + } + }, + }, + 0, + 0, + Services.tm.currentThread + ); + } +} + +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 securityObservers = []; + + let listener = { + onSocketAccepted(socket, transport) { + info("Accepted TLS client connection"); + let connectionInfo = transport.securityCallbacks.getInterface( + Ci.nsITLSServerConnectionInfo + ); + let input = transport.openInputStream(0, 0, 0); + let output = transport.openOutputStream(0, 0, 0); + connectionInfo.setSecurityObserver(new SecurityObserver(input, output)); + }, + + onStopListening() { + info("onStopListening"); + for (let securityObserver of securityObservers) { + securityObserver.input.close(); + securityObserver.output.close(); + } + }, + }; + + tlsServer.setSessionTickets(false); + tlsServer.setRequestClientCertificate(Ci.nsITLSServerSocket.REQUEST_ALWAYS); + + tlsServer.asyncListen(listener); + + return tlsServer; +} + +let server; + +function getTestServerCertificate() { + const certDB = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + for (const cert of certDB.getCerts()) { + if (cert.commonName == "Mochitest client") { + return cert; + } + } + return null; +} + +add_setup(async function () { + 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 = getTestServerCertificate(); + 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, + }, + ]); + + certOverrideService.rememberValidityOverride( + "localhost", + server.port, + {}, + cert, + 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..285071a3ff --- /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, + BrowserUIUtils.trimURL(goodURL), + "location bar reflects loaded page" + ); + + await typeAndSubmitAndStop(badURL); + is( + gURLBar.value, + BrowserUIUtils.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, + BrowserUIUtils.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..0a1ef1b057 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_stopSearchOnSelection.js @@ -0,0 +1,113 @@ +/* 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_setup(async function () { + 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({ + url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME, + setAsDefault: true, + }); + await Services.search.moveEngine(engine, 0); + registerCleanupFunction(async () => { + 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..2e7056e972 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_stop_pending.js @@ -0,0 +1,459 @@ +/* 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); +}); + +/** + * 1) Try to load page 0 and wait for it to finish loading. + * 2) Try to load page 1 and wait for it to finish loading. + * 3) Try to load SLOW_PAGE, and then before it finishes loading, navigate back. + * - We should be taken to page 0. + */ +add_task(async function testCorrectUrlBarAfterGoingBackDuringAnotherLoad() { + // Load example.org + let page0 = "http://example.org/"; + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + page0, + true, + true + ); + + // Load example.com in the same browser + let page1 = "http://example.com/"; + let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, page1); + BrowserTestUtils.loadURIString(tab.linkedBrowser, page1); + await loaded; + + let initialValue = gURLBar.untrimmedValue; + let expectedURLBarChange = SLOW_PAGE; + let sawChange = false; + let goneBack = false; + let handler = () => { + if (!goneBack) { + isnot( + gURLBar.untrimmedValue, + initialValue, + `Should not revert URL bar value to ${initialValue}` + ); + } + + if (gURLBar.getAttribute("pageproxystate") == "valid") { + sawChange = true; + is( + gURLBar.untrimmedValue, + expectedURLBarChange, + `Should set expected URL bar value - ${expectedURLBarChange}` + ); + } + }; + + let obs = new MutationObserver(handler); + + obs.observe(gURLBar.textbox, { attributes: true }); + // Set the value of url bar to SLOW_PAGE + gURLBar.value = SLOW_PAGE; + gURLBar.handleCommand(); + + // Copied from the first test case: + // If this ever starts going intermittent, we've broken this. + await new Promise(resolve => setTimeout(resolve, 200)); + + expectedURLBarChange = page0; + let pageLoadPromise = BrowserTestUtils.browserStopped( + tab.linkedBrowser, + page0 + ); + + // Wait until we can go back + await TestUtils.waitForCondition(() => tab.linkedBrowser.canGoBack); + ok(tab.linkedBrowser.canGoBack, "can go back"); + + // Navigate back from SLOW_PAGE. We should be taken to page 0 now. + tab.linkedBrowser.goBack(); + goneBack = true; + is( + gURLBar.untrimmedValue, + SLOW_PAGE, + "Should not have changed URL bar value synchronously." + ); + // Wait until page 0 have finished loading. + await pageLoadPromise; + is( + gURLBar.untrimmedValue, + page0, + "Should not have changed URL bar value synchronously." + ); + 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); +}); + +/** + * 1) Try to load page 1 and wait for it to finish loading. + * 2) Start loading SLOW_PAGE (it won't finish loading) + * 3) Reload the page. We should have loaded page 1 now. + */ +add_task(async function testCorrectUrlBarAfterReloadingDuringSlowPageLoad() { + // Load page 1 - example.com + let page1 = "http://example.com/"; + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + page1, + true, + true + ); + + let initialValue = gURLBar.untrimmedValue; + let expectedURLBarChange = SLOW_PAGE; + let sawChange = false; + let hasReloaded = false; + let handler = () => { + if (!hasReloaded) { + 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 }); + // Start loading SLOW_PAGE + gURLBar.value = SLOW_PAGE; + gURLBar.handleCommand(); + + // Copied from the first test: If this ever starts going intermittent, + // we've broken this. + await new Promise(resolve => setTimeout(resolve, 200)); + + expectedURLBarChange = page1; + let pageLoadPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + page1 + ); + // Reload the page + tab.linkedBrowser.reload(); + hasReloaded = true; + is( + gURLBar.untrimmedValue, + SLOW_PAGE, + "Should not have changed URL bar value synchronously." + ); + // Wait for page1 to be loaded due to a reload while the slow page was still loading + 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); +}); + +/** + * 1) Try to load example.com and wait for it to finish loading. + * 2) Start loading SLOW_PAGE and then stop the load before the load completes + * 3) Check that example.com has been loaded as a result of stopping SLOW_PAGE + * load. + */ +add_task(async function testCorrectUrlBarAfterStoppingTheLoad() { + // Load page 1 + let page1 = "http://example.com/"; + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + page1, + true, + true + ); + + let initialValue = gURLBar.untrimmedValue; + let expectedURLBarChange = SLOW_PAGE; + let sawChange = false; + let hasStopped = false; + let handler = () => { + if (!hasStopped) { + 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 }); + // Start loading SLOW_PAGE + gURLBar.value = SLOW_PAGE; + gURLBar.handleCommand(); + + // Copied from the first test case: + // If this ever starts going intermittent, we've broken this. + await new Promise(resolve => setTimeout(resolve, 200)); + + // We expect page 1 to be loaded after the SLOW_PAGE load is stopped. + expectedURLBarChange = page1; + let pageLoadPromise = BrowserTestUtils.browserStopped( + tab.linkedBrowser, + SLOW_PAGE, + true + ); + // Stop the SLOW_PAGE load + tab.linkedBrowser.stop(); + hasStopped = true; + is( + gURLBar.untrimmedValue, + SLOW_PAGE, + "Should not have changed URL bar value synchronously." + ); + // Wait for SLOW_PAGE load to stop + 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); +}); diff --git a/browser/components/urlbar/tests/browser/browser_strip_on_share.js b/browser/components/urlbar/tests/browser/browser_strip_on_share.js new file mode 100644 index 0000000000..39a3e23aa7 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_strip_on_share.js @@ -0,0 +1,125 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let listService; + +// Tests for the strip on share functionality of the urlbar. + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.query_stripping.strip_list", "stripParam"], + ["privacy.query_stripping.enabled", false], + ], + }); + + // Get the list service so we can wait for it to be fully initialized before running tests. + listService = Cc["@mozilla.org/query-stripping-list-service;1"].getService( + Ci.nsIURLQueryStrippingListService + ); + + await listService.testWaitForInit(); +}); + +// Menu item should be visible, the whole url is copied without a selection, url should be stripped. +add_task(async function testQueryParamIsStripped() { + await testMenuItemEnabled(false); +}); + +// Menu item should be visible, selecting the whole url, url should be stripped. +add_task(async function testQueryParamIsStrippedSelectURL() { + await testMenuItemEnabled(true); +}); + +// We cannot strip anything, menu item should be hidden +add_task(async function testUnknownQueryParam() { + await testMenuItemDisabled( + "https://www.example.com/?noStripParam=1234", + true, + false + ); +}); + +// Selection is not a valid URI, menu item should be hidden +add_task(async function testInvalidURI() { + await testMenuItemDisabled( + "https://www.example.com/?stripParam=1234", + true, + true + ); +}); + +// Pref is not enabled, menu item should be hidden +add_task(async function testPrefDisabled() { + await testMenuItemDisabled( + "https://www.example.com/?stripParam=1234", + false, + false + ); +}); + +/** + * Opens a new tab, opens the ulr bar context menu and checks that the strip-on-share menu item is not visible + * + * @param {string} url - The url to be loaded + * @param {boolean} prefEnabled - Whether privacy.query_stripping.strip_on_share.enabled should be enabled for the test + * @param {boolean} selection - True: The whole url will be selected, false: Only part of the url will be selected + */ +async function testMenuItemDisabled(url, prefEnabled, selection) { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.query_stripping.strip_on_share.enabled", prefEnabled]], + }); + await BrowserTestUtils.withNewTab(url, async function (browser) { + gURLBar.focus(); + if (selection) { + //select only part of the url + gURLBar.selectionStart = url.indexOf("example"); + gURLBar.selectionEnd = url.indexOf("4"); + } + let menuitem = await promiseContextualMenuitem("strip-on-share"); + Assert.ok( + !BrowserTestUtils.is_visible(menuitem), + "Menu item is not visible" + ); + let hidePromise = BrowserTestUtils.waitForEvent( + menuitem.parentElement, + "popuphidden" + ); + menuitem.parentElement.hidePopup(); + await hidePromise; + }); +} + +/** + * Opens a new tab, opens the url bar context menu and checks that the strip-on-share menu item is visible. + * Checks that the stripped version of the url is copied to the clipboard. + * + * @param {boolean} selectWholeUrl - Whether the whole url should be explicitely selected + */ +async function testMenuItemEnabled(selectWholeUrl) { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.query_stripping.strip_on_share.enabled", true]], + }); + let validUrl = "https://www.example.com/?stripParam=1234"; + let strippedUrl = "https://www.example.com/"; + await BrowserTestUtils.withNewTab(validUrl, async function (browser) { + gURLBar.focus(); + if (selectWholeUrl) { + //select the whole url + gURLBar.select(); + } + let menuitem = await promiseContextualMenuitem("strip-on-share"); + Assert.ok(BrowserTestUtils.is_visible(menuitem), "Menu item is visible"); + let hidePromise = BrowserTestUtils.waitForEvent( + menuitem.parentElement, + "popuphidden" + ); + // Make sure the clean copy of the link will be copied to the clipboard + await SimpleTest.promiseClipboardChange(strippedUrl, () => { + menuitem.closest("menupopup").activateItem(menuitem); + }); + await hidePromise; + }); +} 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_suppressFocusBorder.js b/browser/components/urlbar/tests/browser/browser_suppressFocusBorder.js new file mode 100644 index 0000000000..c8d84e5c4c --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_suppressFocusBorder.js @@ -0,0 +1,391 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that the suppress-focus-border attribute is applied to the Urlbar + * correctly. Its purpose is to hide the focus border after the panel is closed. + * It also ensures we don't flash the border at the user after they click the + * Urlbar but before we decide we're opening the view. + */ + +let TEST_RESULT = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/" } +); + +/** + * A test provider that awaits a promise before returning results. + */ +class AwaitPromiseProvider extends UrlbarTestUtils.TestProvider { + /** + * @param {object} args + * The constructor arguments for UrlbarTestUtils.TestProvider. + * @param {Promise} promise + * The promise that will be awaited before returning results. + */ + constructor(args, promise) { + super(args); + this._promise = promise; + } + + async startQuery(context, add) { + await this._promise; + for (let result of this._results) { + add(this, result); + } + } +} + +add_setup(async function () { + await SearchTestUtils.installSearchExtension({}, { setAsDefault: true }); + + registerCleanupFunction(function () { + SpecialPowers.clipboardCopyString(""); + }); +}); + +add_task(async function afterMousedown_topSites() { + const win = await BrowserTestUtils.openNewBrowserWindow(); + win.gURLBar.blur(); + + await withAwaitProvider( + { results: [TEST_RESULT], priority: Infinity }, + getSuppressFocusPromise(win), + async () => { + Assert.ok( + !win.gURLBar.hasAttribute("suppress-focus-border"), + "Sanity check: the Urlbar does not have the supress-focus-border attribute." + ); + + await UrlbarTestUtils.promisePopupOpen(win, () => { + if (win.gURLBar.getAttribute("pageproxystate") == "invalid") { + gURLBar.handleRevert(); + } + EventUtils.synthesizeMouseAtCenter(win.gURLBar.inputField, {}, win); + }); + + let result = await UrlbarTestUtils.waitForAutocompleteResultAt(win, 0); + Assert.ok( + result, + "The provider returned a result after waiting for the suppress-focus-border attribute." + ); + + await UrlbarTestUtils.promisePopupClose(win); + Assert.ok( + !gURLBar.hasAttribute("suppress-focus-border"), + "The Urlbar no longer has the supress-focus-border attribute after close." + ); + } + ); + + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function openLocation_topSites() { + const win = await BrowserTestUtils.openNewBrowserWindow(); + + await withAwaitProvider( + { results: [TEST_RESULT], priority: Infinity }, + getSuppressFocusPromise(win), + async () => { + Assert.ok( + !win.gURLBar.hasAttribute("suppress-focus-border"), + "Sanity check: the Urlbar does not have the supress-focus-border attribute." + ); + + await UrlbarTestUtils.promisePopupOpen(win, () => { + EventUtils.synthesizeKey("l", { accelKey: true }, win); + }); + + let result = await UrlbarTestUtils.waitForAutocompleteResultAt(win, 0); + Assert.ok( + result, + "The provider returned a result after waiting for the suppress-focus-border attribute." + ); + + await UrlbarTestUtils.promisePopupClose(win); + Assert.ok( + !win.gURLBar.hasAttribute("suppress-focus-border"), + "The Urlbar no longer has the supress-focus-border attribute after close." + ); + } + ); + + await BrowserTestUtils.closeWindow(win); +}); + +// Tests that the address bar loses the suppress-focus-border attribute if no +// results are returned by a query. This simulates the user disabling Top Sites +// then clicking the address bar. +add_task(async function afterMousedown_noTopSites() { + const win = await BrowserTestUtils.openNewBrowserWindow(); + + await withAwaitProvider( + // Note that the provider returns no results. + { results: [], priority: Infinity }, + getSuppressFocusPromise(win), + async () => { + Assert.ok( + !win.gURLBar.hasAttribute("suppress-focus-border"), + "Sanity check: the Urlbar does not have the supress-focus-border attribute." + ); + + 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(!UrlbarTestUtils.isPopupOpen(win), "The popup is not open."); + + Assert.ok( + !win.gURLBar.hasAttribute("suppress-focus-border"), + "The Urlbar no longer has the supress-focus-border attribute." + ); + } + ); + + await BrowserTestUtils.closeWindow(win); +}); + +// Tests that we show the focus border when new tabs are opened. +add_task(async function newTab() { + const win = await BrowserTestUtils.openNewBrowserWindow(); + + // Tabs opened with withNewTab don't focus the Urlbar, so we have to open one + // manually. + let tab = await openAboutNewTab(win); + await BrowserTestUtils.waitForCondition( + () => win.gURLBar.hasAttribute("focused"), + "Waiting for the Urlbar to become focused." + ); + Assert.ok( + !win.gURLBar.hasAttribute( + "suppress-focus-border", + "The Urlbar does not have the suppress-focus-border attribute." + ) + ); + + BrowserTestUtils.removeTab(tab); + await BrowserTestUtils.closeWindow(win); +}); + +// Tests that we show the focus border when a new tab is opened and the address +// bar panel is already open. +add_task(async function newTab_alreadyOpen() { + const win = await BrowserTestUtils.openNewBrowserWindow(); + + await withAwaitProvider( + { results: [TEST_RESULT], priority: Infinity }, + getSuppressFocusPromise(win), + async () => { + await UrlbarTestUtils.promisePopupOpen(win, () => { + EventUtils.synthesizeKey("l", { accelKey: true }, win); + }); + + let tab = await openAboutNewTab(win); + await BrowserTestUtils.waitForCondition( + () => !UrlbarTestUtils.isPopupOpen(win), + "Waiting for the Urlbar panel to close." + ); + Assert.ok( + !win.gURLBar.hasAttribute( + "suppress-focus-border", + "The Urlbar does not have the suppress-focus-border attribute." + ) + ); + BrowserTestUtils.removeTab(tab); + } + ); + + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function searchTip() { + const win = await BrowserTestUtils.openNewBrowserWindow(); + + info("Set a pref to show a search tip button."); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.searchTips.test.ignoreShowLimits", true]], + }); + + info("Open new tab."); + const tab = await openAboutNewTab(win); + + info("Click the tip button."); + const result = await UrlbarTestUtils.getDetailsOfResultAt(win, 0); + const button = result.element.row._buttons.get("0"); + await UrlbarTestUtils.promisePopupClose(win, () => { + EventUtils.synthesizeMouseAtCenter(button, {}, win); + }); + + Assert.ok( + !win.gURLBar.hasAttribute( + "suppress-focus-border", + "The Urlbar does not have the suppress-focus-border attribute." + ) + ); + + BrowserTestUtils.removeTab(tab); + await BrowserTestUtils.closeWindow(win); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function interactionOnNewTab() { + const win = await BrowserTestUtils.openNewBrowserWindow(); + + info("Open about:newtab in new tab"); + const tab = await openAboutNewTab(win); + await BrowserTestUtils.waitForCondition( + () => win.gBrowser.selectedTab === tab + ); + + await testInteractionsOnAboutNewTab(win); + + BrowserTestUtils.removeTab(tab); + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function interactionOnNewTabInPrivateWindow() { + const win = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + waitForTabURL: "about:privatebrowsing", + }); + await testInteractionsOnAboutNewTab(win); + await BrowserTestUtils.closeWindow(win); + await SimpleTest.promiseFocus(window); +}); + +add_task(async function clickOnEdgeOfURLBar() { + const win = await BrowserTestUtils.openNewBrowserWindow(); + win.gURLBar.blur(); + + Assert.ok( + !win.gURLBar.hasAttribute("suppress-focus-border"), + "URLBar does not have suppress-focus-border attribute" + ); + + const onHiddenFocusRemoved = BrowserTestUtils.waitForCondition( + () => !win.gURLBar._hideFocus + ); + + const container = win.document.getElementById("urlbar-input-container"); + container.click(); + + await onHiddenFocusRemoved; + Assert.ok( + win.gURLBar.hasAttribute("suppress-focus-border"), + "suppress-focus-border is set from the beginning" + ); + + await UrlbarTestUtils.promisePopupClose(win.window); + await BrowserTestUtils.closeWindow(win); +}); + +async function testInteractionsOnAboutNewTab(win) { + info("Test for clicking on URLBar while showing about:newtab"); + await testInteractionFeature(() => { + info("Click on URLBar"); + EventUtils.synthesizeMouseAtCenter(win.gURLBar.inputField, {}, win); + }, win); + + info("Test for typing on .fake-editable while showing about:newtab"); + await testInteractionFeature(() => { + info("Type a character on .fake-editable"); + EventUtils.synthesizeKey("v", {}, win); + }, win); + Assert.equal(win.gURLBar.value, "v", "URLBar value is correct"); + + info("Test for typing on .fake-editable while showing about:newtab"); + await testInteractionFeature(() => { + info("Paste some words on .fake-editable"); + SpecialPowers.clipboardCopyString("paste test"); + win.document.commandDispatcher + .getControllerForCommand("cmd_paste") + .doCommand("cmd_paste"); + SpecialPowers.clipboardCopyString(""); + }, win); + Assert.equal(win.gURLBar.value, "paste test", "URLBar value is correct"); +} + +async function testInteractionFeature(interaction, win) { + info("Focus on URLBar"); + win.gURLBar.value = ""; + win.gURLBar.focus(); + Assert.ok( + !win.gURLBar.hasAttribute("suppress-focus-border"), + "URLBar does not have suppress-focus-border attribute" + ); + + info("Click on search-handoff-button in newtab page"); + await ContentTask.spawn(win.gBrowser.selectedBrowser, null, async () => { + await ContentTaskUtils.waitForCondition(() => + content.document.querySelector(".search-handoff-button") + ); + content.document.querySelector(".search-handoff-button").click(); + }); + + await BrowserTestUtils.waitForCondition( + () => win.gURLBar._hideFocus, + "Wait until _hideFocus will be true" + ); + + const onHiddenFocusRemoved = BrowserTestUtils.waitForCondition( + () => !win.gURLBar._hideFocus + ); + + await interaction(); + + await onHiddenFocusRemoved; + Assert.ok( + win.gURLBar.hasAttribute("suppress-focus-border"), + "suppress-focus-border is set from the beginning" + ); + + const result = await UrlbarTestUtils.waitForAutocompleteResultAt(win, 0); + Assert.ok(result, "The provider returned a result"); + await UrlbarTestUtils.promisePopupClose(win); +} + +function getSuppressFocusPromise(win = window) { + return new Promise(resolve => { + let observer = new MutationObserver(() => { + if ( + win.gURLBar.hasAttribute("suppress-focus-border") && + !UrlbarTestUtils.isPopupOpen(win) + ) { + resolve(); + observer.disconnect(); + } + }); + observer.observe(win.gURLBar.textbox, { + attributes: true, + attributeFilter: ["suppress-focus-border"], + }); + }); +} + +async function withAwaitProvider(args, promise, callback) { + let provider = new AwaitPromiseProvider(args, promise); + UrlbarProvidersManager.registerProvider(provider); + try { + await callback(); + } catch (ex) { + console.error(ex); + } finally { + UrlbarProvidersManager.unregisterProvider(provider); + } +} + +async function openAboutNewTab(win = window) { + // 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. + const tabCount = win.gBrowser.tabs.length; + EventUtils.synthesizeKey("t", { accelKey: true }, win); + await TestUtils.waitForCondition( + () => win.gBrowser.tabs.length === tabCount + 1, + "Waiting for background about:newtab to open." + ); + return win.gBrowser.tabs[win.gBrowser.tabs.length - 1]; +} 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..a9b0eb7b1a --- /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_inputHistory.js b/browser/components/urlbar/tests/browser/browser_switchTab_inputHistory.js new file mode 100644 index 0000000000..0da3161d0e --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_switchTab_inputHistory.js @@ -0,0 +1,91 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This tests ensures that the urlbar adaptive behavior updates + * when using switch to tab in the address bar dropdown. + */ + +"use strict"; + +add_setup(async function () { + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + }); +}); + +add_task(async function test_adaptive_with_search_term_and_switch_tab() { + await PlacesUtils.history.clear(); + let urls = [ + "https://example.com/", + "https://example.com/#cat", + "https://example.com/#cake", + "https://example.com/#car", + ]; + + info(`Load tabs in same order as urls`); + let tabs = []; + for (let url of urls) { + let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, url, false, true); + gBrowser.loadTabs([url], { + inBackground: true, + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }); + + let tab = await tabPromise; + tabs.push(tab); + } + + info(`Switch to tab 0`); + await BrowserTestUtils.switchTab(gBrowser, tabs[0]); + + info("Wait for autocomplete"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "ca", + }); + + let result1 = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.notEqual(result1.url, urls[1], `${urls[1]} url should not be first`); + + info(`Scroll down to select the ${urls[1]} entry using keyboard`); + let result2 = await UrlbarTestUtils.getDetailsOfResultAt( + window, + UrlbarTestUtils.getSelectedRowIndex(window) + ); + + while (result2.url != urls[1]) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + result2 = await UrlbarTestUtils.getDetailsOfResultAt( + window, + UrlbarTestUtils.getSelectedRowIndex(window) + ); + } + + Assert.equal( + result2.type, + UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + "Selected entry should be tab switch" + ); + Assert.equal(result2.url, urls[1]); + + info("Visiting tab 1"); + EventUtils.synthesizeKey("KEY_Enter"); + Assert.equal(gBrowser.selectedTab, tabs[1], "Should have switched to tab 1"); + + info("Switch back to tab 0"); + await BrowserTestUtils.switchTab(gBrowser, tabs[0]); + + info("Wait for autocomplete"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "ca", + }); + + let result3 = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result3.url, urls[1], `${urls[1]} url should be first`); + + for (let tab of tabs) { + BrowserTestUtils.removeTab(tab); + } +}); 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..1a0d2eef70 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_switchToTabHavingURI_aOpenParams.js @@ -0,0 +1,217 @@ +/* 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_chiclet.js b/browser/components/urlbar/tests/browser/browser_switchToTab_chiclet.js new file mode 100644 index 0000000000..6702ce340a --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_switchToTab_chiclet.js @@ -0,0 +1,110 @@ +/* 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 chiclet upon switching tab mode. + */ + +"use strict"; + +const TEST_URL = `${TEST_BASE_URL}dummy_page.html`; + +add_task(async function test_with_oneoff_button() { + info("Loading test page into first tab"); + await BrowserTestUtils.loadURIString(gBrowser, TEST_URL); + + info("Opening a new tab"); + const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + info("Wait for autocomplete"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + + info("Enter Tabs mode"); + await UrlbarTestUtils.enterSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.TABS, + }); + + info("Select first popup entry"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "dummy", + }); + EventUtils.synthesizeKey("KEY_ArrowDown"); + const result = await UrlbarTestUtils.getDetailsOfResultAt( + window, + UrlbarTestUtils.getSelectedRowIndex(window) + ); + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.TAB_SWITCH); + + info("Enter escape key"); + EventUtils.synthesizeKey("KEY_Escape"); + + info("Check label visibility"); + const searchModeTitle = document.getElementById( + "urlbar-search-mode-indicator-title" + ); + const switchTabLabel = document.getElementById("urlbar-label-switchtab"); + await BrowserTestUtils.waitForCondition( + () => + BrowserTestUtils.is_visible(searchModeTitle) && + searchModeTitle.textContent === "Tabs", + "Waiting until the search mode title will be visible" + ); + await BrowserTestUtils.waitForCondition( + () => BrowserTestUtils.is_hidden(switchTabLabel), + "Waiting until the switch tab label will be hidden" + ); + + await PlacesUtils.history.clear(); + gBrowser.removeTab(tab); +}); + +add_task(async function test_with_keytype() { + info("Loading test page into first tab"); + await BrowserTestUtils.loadURIString(gBrowser, TEST_URL); + + info("Opening a new tab"); + const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + info("Enter Tabs mode with keytype"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "%", + }); + + info("Select second popup entry"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "dummy", + }); + EventUtils.synthesizeKey("KEY_ArrowDown"); + const result = await UrlbarTestUtils.getDetailsOfResultAt( + window, + UrlbarTestUtils.getSelectedRowIndex(window) + ); + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.TAB_SWITCH); + + info("Enter escape key"); + EventUtils.synthesizeKey("KEY_Escape"); + + info("Check label visibility"); + const searchModeTitle = document.getElementById( + "urlbar-search-mode-indicator-title" + ); + const switchTabLabel = document.getElementById("urlbar-label-switchtab"); + await BrowserTestUtils.waitForCondition( + () => BrowserTestUtils.is_hidden(searchModeTitle), + "Waiting until the search mode title will be hidden" + ); + await BrowserTestUtils.waitForCondition( + () => BrowserTestUtils.is_visible(switchTabLabel), + "Waiting until the switch tab label will be visible" + ); + + await PlacesUtils.history.clear(); + gBrowser.removeTab(tab); +}); 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..e326939581 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_tabKeyBehavior.js @@ -0,0 +1,378 @@ +/* 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_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.quickactions", false]], + }); + + for (let i = 0; i < UrlbarPrefs.get("maxRichResults"); i++) { + await PlacesTestUtils.addVisits("http://example.com/" + i); + } + + registerCleanupFunction(PlacesUtils.history.clear); + + CustomizableUI.addWidgetToArea("home-button", "nav-bar", 0); + CustomizableUI.addWidgetToArea("sidebar-button", "nav-bar"); + registerCleanupFunction(() => { + CustomizableUI.removeWidgetFromArea("home-button"); + CustomizableUI.removeWidgetFromArea("sidebar-button"); + }); +}); + +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."); + await SearchTestUtils.installSearchExtension(); + + for (let i = 0; i < 3; i++) { + await PlacesTestUtils.addVisits(["https://example.com/"]); + } + + // Search for a tab-to-search result. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + }); + 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(); +}); + +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); +}); + +add_task(async function tabOnTopSites() { + info("Tab through the toolbar when focusing the Address Bar on top sites."); + for (let val of [true, false]) { + info(`Test with keyboard_navigation set to "${val}"`); + await SpecialPowers.pushPrefEnv({ + set: [["browser.toolbars.keyboard_navigation", val]], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + fireInputEvent: true, + }); + Assert.ok( + UrlbarTestUtils.getResultCount(window) > 0, + "There should be some results" + ); + Assert.deepEqual( + UrlbarTestUtils.getSelectedElement(window), + null, + "There should be no selection" + ); + + await expectTabThroughToolbar(); + await UrlbarTestUtils.promisePopupClose(window); + await SpecialPowers.popPrefEnv(); + } +}); + +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 }); + if ( + UrlbarTestUtils.getButtonForResultIndex( + window, + "menu", + UrlbarTestUtils.getSelectedRowIndex(window) + ) + ) { + 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) { + if ( + !Services.prefs.getBoolPref("browser.toolbars.keyboard_navigation", true) + ) { + return BrowserTestUtils.waitForCondition( + () => document.activeElement == gBrowser.selectedBrowser + ); + } + let urlbar = document.getElementById("urlbar-container"); + let nextFocusableElement = reverse + ? urlbar.previousElementSibling + : urlbar.nextElementSibling; + while ( + nextFocusableElement && + (!nextFocusableElement.classList.contains("toolbarbutton-1") || + nextFocusableElement.hasAttribute("hidden") || + nextFocusableElement.hasAttribute("disabled") || + BrowserTestUtils.is_hidden(nextFocusableElement)) + ) { + nextFocusableElement = reverse + ? nextFocusableElement.previousElementSibling + : nextFocusableElement.nextElementSibling; + } + info( + `Next focusable element: ${nextFocusableElement.localName}.#${nextFocusableElement.id}` + ); + + 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..e186681907 --- /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,' + ); + 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.loadURIString(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..b7b13eecf8 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_tabMatchesInAwesomebar_perwindowpb.js @@ -0,0 +1,120 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test ensures that we don't switch between tabs from normal window to + * private browsing window or opposite. + */ + +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, true); + 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..b029682eda --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_tabToSearch.js @@ -0,0 +1,641 @@ +/* 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"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarProviderTabToSearch: + "resource:///modules/UrlbarProviderTabToSearch.sys.mjs", +}); + +add_setup(async function () { + 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], + ], + }); + + await SearchTestUtils.installSearchExtension({ + name: TEST_ENGINE_NAME, + search_url: `https://${TEST_ENGINE_DOMAIN}/`, + }); + + for (let i = 0; i < 3; i++) { + await PlacesTestUtils.addVisits([`https://${TEST_ENGINE_DOMAIN}/`]); + } + + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + }); +}); + +// 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), + fireInputEvent: true, + }); + 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: Services.search.getEngineByName( + tabToSearchDetails.searchParams.engine + ).isGeneralPurposeEngine + ? "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_ArrowDown"); + 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, () => gURLBar.blur()); +}); + +// 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), + fireInputEvent: true, + }); + 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.equal(aadID, null, "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._content.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._content.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, () => gURLBar.blur()); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_ENGINE_DOMAIN.slice(0, 4), + fireInputEvent: true, + }); + 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.equal(aadID, null, "aria-activedescendant was not set."); + + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + 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), + fireInputEvent: true, + }); + 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._content.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, + "aria-activedescendant was moved to the first one-off." + ); + EventUtils.synthesizeKey("KEY_ArrowUp"); + aadID = gURLBar.inputField.getAttribute("aria-activedescendant"); + Assert.equal( + aadID, + tabToSearchRow._content.id, + "aria-activedescendant was set to the tab-to-search result." + ); + + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); +}); + +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 down arrow 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); + }); + }); + gURLBar.focus(); + 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_ArrowDown"); + await UrlbarTestUtils.promiseSearchComplete(window); + await TestUtils.waitForCondition( + () => UrlbarTestUtils.getSelectedRowIndex(window) == 1, + "Wait for down arrow key to be handled" + ); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ENGINE_NAME, + entry: "tabtosearch", + isPreview: true, + }); + + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); +}); + +// 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), + fireInputEvent: true, + }); + 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_ArrowDown"); + 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: Services.search.getEngineByName( + onboardingElement.result.payload.engine + ).isGeneralPurposeEngine + ? "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, () => gURLBar.blur()); + 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), + fireInputEvent: true, + }); + 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), + fireInputEvent: true, + }); + 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), + fireInputEvent: true, + }); + 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), + fireInputEvent: true, + }); + 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), + fireInputEvent: true, + }); + 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, () => gURLBar.blur()); + 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 extension = await SearchTestUtils.installSearchExtension( + { + name: `${TEST_ENGINE_NAME}Maps`, + search_url: `https://${TEST_ENGINE_DOMAIN}/maps/`, + }, + { skipUnload: true } + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_ENGINE_DOMAIN.slice(0, 4), + fireInputEvent: true, + }); + + 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 UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + await extension.unload(); + UrlbarPrefs.set("tabToSearch.onboard.interactionsLeft", 3); + delete UrlbarProviderTabToSearch.onboardingInteractionAtTime; + await SpecialPowers.popPrefEnv(); +}); 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..ed7a61e6b0 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_textruns.js @@ -0,0 +1,55 @@ +/* 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]], + }); + await SearchTestUtils.installSearchExtension( + { name: "Test" }, + { setAsDefault: true } + ); + + 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 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..d215c2536f --- /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 TEST_ALIAS_ENGINE_NAME = "Test"; +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 "]; + +// 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_setup(async function () { + // Add a default engine with suggestions, to avoid hitting the network when + // fetching them. + let defaultEngine = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME, + setAsDefault: true, + }); + defaultEngine.alias = "@default"; + await SearchTestUtils.installSearchExtension({ + name: TEST_ALIAS_ENGINE_NAME, + keyword: ALIAS, + }); + + // 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: TEST_ALIAS_ENGINE_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: TEST_ALIAS_ENGINE_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: TEST_ALIAS_ENGINE_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: TEST_ALIAS_ENGINE_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: TEST_ALIAS_ENGINE_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: TEST_ALIAS_ENGINE_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 (Services.search.getEngineByName(engineName).isGeneralPurposeEngine) { + 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: TEST_ALIAS_ENGINE_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: TEST_ALIAS_ENGINE_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: TEST_ALIAS_ENGINE_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: TEST_ALIAS_ENGINE_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 extension = await SearchTestUtils.installSearchExtension( + { + name, + keyword: alias, + }, + { skipUnload: true } + ); + + 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 extension.unload(); +}); + +// Tests that we show all engines with a token alias that match the search +// string. +add_task(async function multipleMatchingEngines() { + let extension = await SearchTestUtils.installSearchExtension( + { + name: "TestFoo", + keyword: `${ALIAS}foo`, + }, + { skipUnload: true } + ); + + 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 extension.unload(); +}); + +// 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..cb6502d2b2 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_top_sites.js @@ -0,0 +1,481 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(this, { + AboutNewTab: "resource:///modules/AboutNewTab.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_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.topsites", true], + ["browser.urlbar.suggest.quickactions", false], + ["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..bcc6a70d88 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_top_sites_private.js @@ -0,0 +1,174 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(this, { + AboutNewTab: "resource:///modules/AboutNewTab.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_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.topsites", true], + ["browser.urlbar.suggest.quickactions", false], + ["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..ca4566d172 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_typed_value.js @@ -0,0 +1,69 @@ +/* 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_setup(async function () { + registerCleanupFunction(async function () { + Services.prefs.clearUserPref("browser.urlbar.autoFill"); + Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions"); + gURLBar.handleRevert(); + await PlacesUtils.history.clear(); + }); + Services.prefs.setBoolPref("browser.urlbar.autoFill", true); + Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false); + + 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_unitConversion.js b/browser/components/urlbar/tests/browser/browser_unitConversion.js new file mode 100644 index 0000000000..566300b7d4 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_unitConversion.js @@ -0,0 +1,88 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests unit conversion on browser. + */ + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.unitConversion.enabled", true]], + }); + + registerCleanupFunction(function () { + SpecialPowers.clipboardCopyString(""); + }); +}); + +add_task(async function test_selectByMouse() { + const win = await BrowserTestUtils.openNewBrowserWindow(); + + // Clear clipboard content. + SpecialPowers.clipboardCopyString(""); + + const row = await doUnitConversion(win); + + info("Check if the result is copied to clipboard when selecting by mouse"); + EventUtils.synthesizeMouseAtCenter( + row.querySelector(".urlbarView-no-wrap"), + {}, + win + ); + assertClipboard(); + + await UrlbarTestUtils.promisePopupClose(win); + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function test_selectByKey() { + const win = await BrowserTestUtils.openNewBrowserWindow(); + + // Clear clipboard content. + SpecialPowers.clipboardCopyString(""); + + await doUnitConversion(win); + + // As gURLBar might lost focus, + // give focus again in order to enable key event on the result. + win.gURLBar.focus(); + + info("Check if the result is copied to clipboard when selecting by key"); + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + EventUtils.synthesizeKey("KEY_Enter", {}, win); + assertClipboard(); + + await UrlbarTestUtils.promisePopupClose(win); + await BrowserTestUtils.closeWindow(win); +}); + +function assertClipboard() { + Assert.equal( + SpecialPowers.getClipboardData("text/plain"), + "100 cm", + "The result of conversion is copied to clipboard" + ); +} + +async function doUnitConversion(win) { + info("Do unit conversion then wait the result"); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "1m to cm", + waitForFocus: SimpleTest.waitForFocus, + }); + + const row = await UrlbarTestUtils.waitForAutocompleteResultAt(win, 1); + + Assert.ok(row.querySelector(".urlbarView-favicon"), "The icon is displayed"); + Assert.equal( + row.querySelector(".urlbarView-dynamic-unitConversion-output").textContent, + "100 cm", + "The unit is converted" + ); + + return row; +} 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..4f34b5d52a --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_updateForDomainCompletion.js @@ -0,0 +1,51 @@ +"use strict"; + +/** + * Disable keyword.enabled (so no keyword search), and check that when + * you type in "example" and hit enter, the browser shows an error page. + */ +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.waitForErrorPage(browser); + EventUtils.sendKey("return"); + await loadPromise; + ok(true, "error page is loaded correctly"); + } + ); +}); + +/** + * Disable keyword.enabled (so no keyword search) and enable fixup.alternate, 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], + ["browser.fixup.alternate.enabled", true], + ], + }); + 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_urlbar_annotation.js b/browser/components/urlbar/tests/browser/browser_urlbar_annotation.js new file mode 100644 index 0000000000..9f736ea6af --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_annotation.js @@ -0,0 +1,333 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test whether a visit information is annotated correctly when picking a result. + +if (AppConstants.platform === "macosx") { + requestLongerTimeout(2); +} + +const FRECENCY = { + ORGANIC: 2000, + SPONSORED: -1, + BOOKMARKED: 2075, + SEARCHED: 100, +}; + +const { + VISIT_SOURCE_ORGANIC, + VISIT_SOURCE_SPONSORED, + VISIT_SOURCE_BOOKMARKED, + VISIT_SOURCE_SEARCHED, +} = PlacesUtils.history; + +/** + * To be used before checking database contents when they depend on a visit + * being added to History. + * + * @param {string} href the page to await notifications for. + */ +async function waitForVisitNotification(href) { + await PlacesTestUtils.waitForNotification("page-visited", events => + events.some(e => e.url === href) + ); +} + +async function assertDatabase({ targetURL, expected }) { + const frecency = await PlacesTestUtils.getDatabaseValue( + "moz_places", + "frecency", + { url: targetURL } + ); + Assert.equal(frecency, expected.frecency, "Frecency is correct"); + + const placesId = await PlacesTestUtils.getDatabaseValue("moz_places", "id", { + url: targetURL, + }); + const expectedTriggeringPlaceId = expected.triggerURL + ? await PlacesTestUtils.getDatabaseValue("moz_places", "id", { + url: expected.triggerURL, + }) + : null; + const db = await PlacesUtils.promiseDBConnection(); + const rows = await db.execute( + "SELECT source, triggeringPlaceId FROM moz_historyvisits WHERE place_id = :place_id AND source = :source", + { + place_id: placesId, + source: expected.source, + } + ); + Assert.equal(rows.length, 1); + Assert.equal( + rows[0].getResultByName("triggeringPlaceId"), + expectedTriggeringPlaceId, + `The triggeringPlaceId in database is correct for ${targetURL}` + ); +} + +function registerProvider(payload) { + const provider = new UrlbarTestUtils.TestProvider({ + results: [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.SEARCH, + ...UrlbarResult.payloadAndSimpleHighlights([], { + ...payload, + }) + ), + ], + priority: Infinity, + }); + UrlbarProvidersManager.registerProvider(provider); + return provider; +} + +async function pickResult({ input, payloadURL, redirectTo }) { + const destinationURL = redirectTo || payloadURL; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: input, + fireInputEvent: true, + }); + + const result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal(result.url, payloadURL); + UrlbarTestUtils.setSelectedRowIndex(window, 0); + + info("Show result and wait for loading"); + const onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + destinationURL + ); + EventUtils.synthesizeKey("KEY_Enter"); + await onLoad; +} + +add_setup(async function () { + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + }); +}); + +add_task(async function basic() { + const testData = [ + { + description: "Sponsored result", + input: "exa", + payload: { + url: "http://example.com/", + isSponsored: true, + }, + expected: { + source: VISIT_SOURCE_SPONSORED, + frecency: FRECENCY.SPONSORED, + }, + }, + { + description: "Bookmarked result", + input: "exa", + payload: { + url: "http://example.com/", + }, + bookmarks: [ + { + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: Services.io.newURI("http://example.com/"), + title: "test bookmark", + }, + ], + expected: { + source: VISIT_SOURCE_BOOKMARKED, + frecency: FRECENCY.BOOKMARKED, + }, + }, + { + description: "Sponsored and bookmarked result", + input: "exa", + payload: { + url: "http://example.com/", + isSponsored: true, + }, + bookmarks: [ + { + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: Services.io.newURI("http://example.com/"), + title: "test bookmark", + }, + ], + expected: { + source: VISIT_SOURCE_SPONSORED, + frecency: FRECENCY.BOOKMARKED, + }, + }, + { + description: "Organic result", + input: "exa", + payload: { + url: "http://example.com/", + }, + expected: { + source: VISIT_SOURCE_ORGANIC, + frecency: FRECENCY.ORGANIC, + }, + }, + ]; + + for (const { description, input, payload, bookmarks, expected } of testData) { + info(description); + const provider = registerProvider(payload); + + for (const bookmark of bookmarks || []) { + await PlacesUtils.bookmarks.insert(bookmark); + } + + await BrowserTestUtils.withNewTab("about:blank", async () => { + info("Pick result"); + let promiseVisited = waitForVisitNotification(payload.url); + await pickResult({ input, payloadURL: payload.url }); + await promiseVisited; + info("Check database"); + await assertDatabase({ targetURL: payload.url, expected }); + }); + + UrlbarProvidersManager.unregisterProvider(provider); + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + } +}); + +add_task(async function redirection() { + const redirectTo = "http://example.com/"; + const payload = { + url: "http://example.com/browser/browser/components/urlbar/tests/browser/redirect_to.sjs?/", + isSponsored: true, + }; + const input = "exa"; + const provider = registerProvider(payload); + + await BrowserTestUtils.withNewTab("about:home", async () => { + info("Pick result"); + let promises = [ + waitForVisitNotification(payload.url), + waitForVisitNotification(redirectTo), + ]; + await pickResult({ input, payloadURL: payload.url, redirectTo }); + await Promise.all(promises); + + info("Check database"); + await assertDatabase({ + targetURL: payload.url, + expected: { + source: VISIT_SOURCE_SPONSORED, + frecency: FRECENCY.SPONSORED, + }, + }); + await assertDatabase({ + targetURL: redirectTo, + expected: { + source: VISIT_SOURCE_SPONSORED, + triggerURL: payload.url, + frecency: FRECENCY.SPONSORED, + }, + }); + }); + + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + UrlbarProvidersManager.unregisterProvider(provider); +}); + +add_task(async function search() { + const originalDefaultEngine = await Services.search.getDefault(); + await SearchTestUtils.installSearchExtension({ + name: "test engine", + keyword: "@test", + }); + + const testData = [ + { + description: "Searched result", + input: "@test abc", + resultURL: "https://example.com/?q=abc", + expected: { + source: VISIT_SOURCE_SEARCHED, + frecency: FRECENCY.SEARCHED, + }, + }, + { + description: "Searched bookmarked result", + input: "@test abc", + resultURL: "https://example.com/?q=abc", + bookmarks: [ + { + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: Services.io.newURI("https://example.com/?q=abc"), + title: "test bookmark", + }, + ], + expected: { + source: VISIT_SOURCE_BOOKMARKED, + frecency: FRECENCY.BOOKMARKED, + }, + }, + ]; + + for (const { + description, + input, + resultURL, + bookmarks, + expected, + } of testData) { + info(description); + await BrowserTestUtils.withNewTab("about:blank", async () => { + for (const bookmark of bookmarks || []) { + await PlacesUtils.bookmarks.insert(bookmark); + } + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: input, + }); + const onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + resultURL + ); + let promiseVisited = waitForVisitNotification(resultURL); + EventUtils.synthesizeKey("KEY_Enter"); + await onLoad; + await promiseVisited; + await assertDatabase({ targetURL: resultURL, expected }); + + // Open another URL to check whther the source is not inherited. + const payload = { url: "http://example.com/" }; + const provider = registerProvider(payload); + promiseVisited = waitForVisitNotification(payload.url); + await pickResult({ input, payloadURL: payload.url }); + await promiseVisited; + await assertDatabase({ + targetURL: payload.url, + expected: { + source: VISIT_SOURCE_ORGANIC, + frecency: FRECENCY.ORGANIC, + }, + }); + UrlbarProvidersManager.unregisterProvider(provider); + + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + }); + } + + await Services.search.setDefault( + originalDefaultEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry_abandonment.js b/browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry_abandonment.js new file mode 100644 index 0000000000..6f30392e48 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry_abandonment.js @@ -0,0 +1,357 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs", +}); + +const TEST_ENGINE_NAME = "Test"; +const TEST_ENGINE_ALIAS = "@test"; +const TEST_ENGINE_DOMAIN = "example.com"; + +// Each test is a function that executes an urlbar action and returns the +// expected event object. +const 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 BrowserTestUtils.waitForCondition(async () => { + await UrlbarTestUtils.promisePopupOpen(win, () => { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + }); + + if (UrlbarTestUtils.getResultCount(win) > 1) { + return true; + } + + win.gURLBar.view.close(); + return false; + }); + + 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(win); + 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 [ + // engagement on the top sites search engine to enter search mode + { + category: "urlbar", + method: "engagement", + object: "click", + value: "topsites", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "0", + numWords: "0", + selIndex: "0", + selType: "searchengine", + provider: "UrlbarProviderTopSites", + }, + }, + // abandonment + { + 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", {}, win); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(win); + EventUtils.synthesizeKey("KEY_Enter", {}, win); + 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 [ + // engagement on the tab-to-search to enter search mode + { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "4", + numWords: "1", + selIndex: "1", + selType: "tabtosearch", + provider: "TabToSearch", + }, + }, + // abandonment + { + 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", + }, + }; + }, +]; + +add_setup(async function () { + await PlacesUtils.history.clear(); + + // Create a new search engine and mark it as default + let engine = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + "searchSuggestionEngine.xml", + setAsDefault: true, + }); + await Services.search.moveEngine(engine, 0); + + await SearchTestUtils.installSearchExtension({ + name: TEST_ENGINE_NAME, + keyword: TEST_ENGINE_ALIAS, + search_url: `https://${TEST_ENGINE_DOMAIN}/`, + }); + + // This test used to rely on the initial timer of + // TestUtils.waitForCondition. See bug 1667216. + let originalWaitForCondition = TestUtils.waitForCondition; + TestUtils.waitForCondition = async function ( + condition, + msg, + interval = 100, + maxTries = 50 + ) { + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 100)); + + return originalWaitForCondition(condition, msg, interval, maxTries); + }; + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + TestUtils.waitForCondition = originalWaitForCondition; + }); +}); + +async function doTest(eventTelemetryEnabled) { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.eventTelemetry.enabled", eventTelemetryEnabled]], + }); + + const win = await BrowserTestUtils.openNewBrowserWindow(); + + // This is not necessary after each loop, because assertEvents does it. + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); + + for (let i = 0; i < tests.length; i++) { + info(`Running test at index ${i}`); + let events = await tests[i](win); + if (!Array.isArray(events)) { + events = [events]; + } + // Always blur to ensure it's not accounted as an additional abandonment. + win.gURLBar.setSearchMode({}); + win.gURLBar.blur(); + TelemetryTestUtils.assertEvents(eventTelemetryEnabled ? events : [], { + category: "urlbar", + }); + + // Scalars should be recorded regardless of `eventTelemetry.enabled`. + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + TelemetryTestUtils.assertScalar( + scalars, + "urlbar.engagement", + events.filter(e => e.method == "engagement").length || undefined + ); + TelemetryTestUtils.assertScalar( + scalars, + "urlbar.abandonment", + events.filter(e => e.method == "abandonment").length || undefined + ); + + await UrlbarTestUtils.formHistory.clear(win); + } + + await BrowserTestUtils.closeWindow(win); + await SpecialPowers.popPrefEnv(); +} + +add_task(async function enabled() { + await doTest(true); +}); + +add_task(async function disabled() { + await doTest(false); +}); + +/** + * 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); +} diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry_engagement.js b/browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry_engagement.js new file mode 100644 index 0000000000..c1fd36b452 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry_engagement.js @@ -0,0 +1,1340 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarProvider: "resource:///modules/UrlbarUtils.sys.mjs", + UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs", + UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs", +}); + +const TEST_ENGINE_NAME = "Test"; +const TEST_ENGINE_ALIAS = "@test"; +const TEST_ENGINE_DOMAIN = "example.com"; + +// This test has many subtests and can time out in verify mode. +requestLongerTimeout(5); + +// Each test is a function that executes an urlbar action and returns the +// expected event object. +const 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(win, { + 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: "BookmarkKeywords", + }, + }; + }, + + 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 option."); + 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); + if (UrlbarPrefs.get("resultMenu")) { + await UrlbarTestUtils.openResultMenuAndPressAccesskey(win, "h"); + } else { + EventUtils.synthesizeKey("KEY_Tab", {}, 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/", + win.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."); + // Add a clean bookmark. + const bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/bookmark", + title: "bookmark", + }); + + win.gURLBar.select(); + let url = "http://example.com/bookmark"; + let promise = BrowserTestUtils.browserLoaded( + win.gBrowser.selectedBrowser, + false, + url + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "boo", + fireInputEvent: true, + }); + while (win.gURLBar.untrimmedValue != url) { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + } + let element = UrlbarTestUtils.getSelectedRow(win); + EventUtils.synthesizeMouseAtCenter(element, {}, win); + await promise; + await PlacesUtils.bookmarks.remove(bookmark); + 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: "Places", + }, + }; + }, + + 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_origin", + provider: "Autofill", + }, + }; + }, + + async function (win) { + info("Type something, select bookmark entry, Enter."); + + // Add a clean bookmark and the input history in order to detect in InputHistory + // provider and to not show adaptive history autofill. + const bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/bookmark", + title: "bookmark", + }); + await UrlbarUtils.addToInputHistory( + "http://example.com/bookmark", + "bookmark" + ); + + win.gURLBar.select(); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "boo", + fireInputEvent: true, + }); + while (win.gURLBar.untrimmedValue != "http://example.com/bookmark") { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + } + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + await PlacesUtils.bookmarks.remove(bookmark); + 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: "InputHistory", + }, + }; + }, + + 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 [ + // engagement on the keyword offer result to enter search mode + { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "1", + numWords: "1", + selIndex: "6", + selType: "searchengine", + provider: "TokenAliasEngines", + }, + }, + // engagement on the search heuristic + { + 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"; + await SearchTestUtils.installSearchExtension({ + name: "AliasTest", + keyword: alias, + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: `${alias} `, + }); + + await UrlbarTestUtils.assertSearchMode(win, { + 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; + + 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("back-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 and 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"); + cxmenu.activateItem(menuitem); + 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_origin", + 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_origin", + 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."); + // Bug 1797801: If the search mode used is the same as the default engine and + // showSearchTerms is enabled, the chiclet will remain in the urlbar on the search. + // Subsequent tests rely on search mode not already been selected. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", false]], + }); + let defaultEngine = await Services.search.getDefault(); + win.gURLBar.select(); + EventUtils.synthesizeKey("k", { accelKey: true }, win); + 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; + + 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("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", {}, win); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(win); + EventUtils.synthesizeKey("KEY_Enter", {}, win); + 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 [ + // engagement on the tab-to-search to enter search mode + { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "4", + numWords: "1", + selIndex: "1", + selType: "tabtosearch", + provider: "TabToSearch", + }, + }, + // engagement on the search heuristic + { + 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", + }, + }; + }, +]; + +add_setup(async function () { + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + + // Create a new search engine and mark it as default + let engine = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + "searchSuggestionEngine.xml", + setAsDefault: true, + }); + await Services.search.moveEngine(engine, 0); + + await SearchTestUtils.installSearchExtension({ + name: TEST_ENGINE_NAME, + keyword: TEST_ENGINE_ALIAS, + search_url: `https://${TEST_ENGINE_DOMAIN}/`, + }); + + // 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", + }); + + registerCleanupFunction(async function () { + await PlacesUtils.keywords.remove("kw"); + await PlacesUtils.bookmarks.remove(bm); + await PlacesUtils.history.clear(); + }); +}); + +async function doTest(eventTelemetryEnabled) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.eventTelemetry.enabled", eventTelemetryEnabled], + ["browser.urlbar.suggest.searches", true], + ], + }); + + const win = await BrowserTestUtils.openNewBrowserWindow(); + + // This is not necessary after each loop, because assertEvents does it. + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); + + for (let i = 0; i < tests.length; i++) { + info(`Running test at index ${i}`); + let events = await tests[i](win); + if (events === null) { + info("Skipping test"); + continue; + } + if (!Array.isArray(events)) { + events = [events]; + } + // Always blur to ensure it's not accounted as an additional abandonment. + win.gURLBar.setSearchMode({}); + win.gURLBar.blur(); + TelemetryTestUtils.assertEvents(eventTelemetryEnabled ? events : [], { + category: "urlbar", + }); + + // Scalars should be recorded regardless of `eventTelemetry.enabled`. + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + TelemetryTestUtils.assertScalar( + scalars, + "urlbar.engagement", + events.filter(e => e.method == "engagement").length || undefined + ); + TelemetryTestUtils.assertScalar( + scalars, + "urlbar.abandonment", + events.filter(e => e.method == "abandonment").length || undefined + ); + + await UrlbarTestUtils.formHistory.clear(win); + } + + await BrowserTestUtils.closeWindow(win); + await SpecialPowers.popPrefEnv(); +} + +add_task(async function enabled() { + await doTest(true); +}); + +add_task(async function disabled() { + await doTest(false); +}); + +/** + * 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, + { + helpUrl: "http://example.com/", + helpL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-tip-get-help" + : "urlbar-tip-help-icon", + }, + type: "test", + titleL10n: { id: "urlbar-search-tips-confirm" }, + buttons: [ + { + url: "http://example.com/", + l10n: { id: "urlbar-search-tips-confirm" }, + }, + ], + } + ), + 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_event_telemetry_noEvent.js b/browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry_noEvent.js new file mode 100644 index 0000000000..bdba6888b7 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry_noEvent.js @@ -0,0 +1,81 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const tests = [ + async function (win) { + info("Type something, click on search settings."); + await BrowserTestUtils.withNewTab( + { gBrowser: win.gBrowser, url: "about:blank" }, + async browser => { + win.gURLBar.select(); + const promise = onSyncPaneLoaded(); + 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(); + const promise = onSyncPaneLoaded(); + 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"), + "Should have selected the settings button" + ); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + } + ); + return null; + }, +]; + +function onSyncPaneLoaded() { + return new Promise(resolve => { + Services.obs.addObserver(function panesLoadedObs() { + Services.obs.removeObserver(panesLoadedObs, "sync-pane-loaded"); + resolve(); + }, "sync-pane-loaded"); + }); +} + +add_task(async function test() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.eventTelemetry.enabled", true]], + }); + + const win = await BrowserTestUtils.openNewBrowserWindow(); + + // This is not necessary after each loop, because assertEvents does it. + Services.telemetry.clearEvents(); + + for (let i = 0; i < tests.length; i++) { + info(`Running no event test at index ${i}`); + await tests[i](win); + // Always blur to ensure it's not accounted as an additional abandonment. + win.gURLBar.blur(); + TelemetryTestUtils.assertEvents([], { category: "urlbar" }); + } + + await BrowserTestUtils.closeWindow(win); +}); 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..3440c35e6f --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_selection.js @@ -0,0 +1,307 @@ +/* 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.isClipboardTypeSupported( + Services.clipboard.kSelectionClipboard + ) + ) { + // Reset the clipboard. + clipboardHelper.copyStringToClipboard( + val, + Services.clipboard.kSelectionClipboard + ); + } +} + +function checkPrimarySelection(expectedVal = "") { + if ( + Services.clipboard.isClipboardTypeSupported( + Services.clipboard.kSelectionClipboard + ) + ) { + let primaryAsText = SpecialPowers.getClipboardData( + "text/plain", + SpecialPowers.Ci.nsIClipboard.kSelectionClipboard + ); + Assert.equal(primaryAsText, expectedVal); + } +} + +add_setup(async function () { + // 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..4e48a946d5 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry.js @@ -0,0 +1,1218 @@ +/* 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"; + +ChromeUtils.defineESModuleGetters(this, { + SearchSERPTelemetry: "resource:///modules/SearchSERPTelemetry.sys.mjs", +}); + +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) { + let suggestionEngine = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + "urlbarTelemetrySearchSuggestions.xml", + }); + let previousEngine = await Services.search.getDefault(); + await Services.search.setDefault( + suggestionEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + try { + await taskFn(suggestionEngine); + } finally { + await Services.search.setDefault( + previousEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + await Services.search.removeEngine(suggestionEngine); + } +} + +add_setup(async function () { + await SearchTestUtils.installSearchExtension( + { + name: "MozSearch", + keyword: "mozalias", + search_url: "https://example.com/", + }, + { setAsDefault: true } + ); + + // Make it the first one-off engine. + let engine = Services.search.getEngineByName("MozSearch"); + 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]], + }); + + // This test assumes that general results are shown before suggestions. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchSuggestionsFirst", false]], + }); + + // Make sure to restore the engine once we're done. + registerCleanupFunction(async function () { + Services.telemetry.canRecordExtended = oldCanRecord; + Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, suggestionsEnabled); + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(); + Services.telemetry.setEventRecordingEnabled("navigation", false); + }); +}); + +add_task(async function test_simpleQuery() { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + + 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" } + ); + + 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 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" } + ); + + 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 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" } + ); + + 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 () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.maxHistoricalSearchSuggestions", 1]], + }); + + 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 + ); + + await SpecialPowers.popPrefEnv(); + 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 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" } + ); + + 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 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" } + ); + + 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 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."); + 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" } + ); + + 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() { + // This test assumes the showSearchTerms feature is not enabled, + // as multiple searches are made one after another, relying on + // urlbar as the keyed scalar SAP, not urlbar_persisted. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", false]], + }); + + // Override the search telemetry search provider info to + // count in-content SEARCH_COUNTs telemetry for our test engine. + SearchSERPTelemetry.overrideSearchTelemetryForTests([ + { + telemetryId: "example", + searchPageRegexp: "^https://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 + ); + let scalars = TelemetryTestUtils.getProcessScalars("parent", true); + console.log(scalars); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "browser.search.content.urlbar", + "example: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 + ); + scalars = TelemetryTestUtils.getProcessScalars("parent", true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "browser.search.content.urlbar", + "example: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 + ); + scalars = TelemetryTestUtils.getProcessScalars("parent", true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "browser.search.content.urlbar", + "example: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 + ); + scalars = TelemetryTestUtils.getProcessScalars("parent", true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "browser.search.content.urlbar", + "example: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 + ); + scalars = TelemetryTestUtils.getProcessScalars("parent", true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "browser.search.content.urlbar", + "example: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 + ); + scalars = TelemetryTestUtils.getProcessScalars("parent", true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "browser.search.content.urlbar", + "example: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 + ); + scalars = TelemetryTestUtils.getProcessScalars("parent", true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "browser.search.content.urlbar", + "example: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 + ); + scalars = TelemetryTestUtils.getProcessScalars("parent", true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "browser.search.content.urlbar", + "example:organic:none", + 7 + ); + + await BrowserTestUtils.closeWindow(win); + + // Reset the search provider info. + SearchSERPTelemetry.overrideSearchTelemetryForTests(); + await UrlbarTestUtils.formHistory.clear(); + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_autofill.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_autofill.js new file mode 100644 index 0000000000..8336bde462 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_autofill.js @@ -0,0 +1,733 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This file tests urlbar autofill telemetry. + */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarProviderPreloadedSites: + "resource:///modules/UrlbarProviderPreloadedSites.sys.mjs", +}); + +const SCALAR_URLBAR = "browser.engagement.navigation.urlbar"; + +function assertSearchTelemetryEmpty(search_hist) { + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false); + Assert.ok( + !(SCALAR_URLBAR in scalars), + `Should not have recorded ${SCALAR_URLBAR}` + ); + + // SEARCH_COUNTS should not contain any engine counts at all. The keys in this + // histogram are search engine telemetry identifiers. + Assert.deepEqual( + Object.keys(search_hist.snapshot()), + [], + "SEARCH_COUNTS is empty" + ); + + // 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 { + resultMethodHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ), + search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"), + }; +} + +function assertTelemetryResults(histograms, type, index, method) { + TelemetryTestUtils.assertHistogram(histograms.resultMethodHist, method, 1); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + `urlbar.picked.${type}`, + index, + 1 + ); +} + +/** + * Performs a search and picks the first result. + * + * @param {string} searchString + * The search string. Assumed to trigger an autofill result + * @param {string} autofilledValue + * The input's expected value after autofill occurs. + * @param {string} unpickResult + * Optional: If true, do not pick any result. Default value is false. + * @param {string} urlToSelect + * Optional: If want to select result except autofill, pass the URL. + */ +async function triggerAutofillAndPickResult( + searchString, + autofilledValue, + unpickResult = false, + urlToSelect = null +) { + await BrowserTestUtils.withNewTab("about:blank", async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: searchString, + fireInputEvent: true, + }); + + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill, "Result is autofill"); + Assert.equal(gURLBar.value, autofilledValue, "gURLBar.value"); + Assert.equal(gURLBar.selectionStart, searchString.length, "selectionStart"); + Assert.equal(gURLBar.selectionEnd, autofilledValue.length, "selectionEnd"); + + if (urlToSelect) { + for (let row = 0; row < UrlbarTestUtils.getResultCount(window); row++) { + const result = await UrlbarTestUtils.getDetailsOfResultAt(window, row); + if (result.url === urlToSelect) { + UrlbarTestUtils.setSelectedRowIndex(window, row); + break; + } + } + } + + if (unpickResult) { + // Close popup without any action. + await UrlbarTestUtils.promisePopupClose(window); + return; + } + + let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + + let url; + if (urlToSelect) { + url = urlToSelect; + } else { + url = autofilledValue.includes(":") + ? autofilledValue + : "http://" + autofilledValue; + } + Assert.equal(gBrowser.currentURI.spec, url, "Loaded URL is correct"); + }); +} + +function createOtherAutofillProvider(searchString, autofilledValue) { + return new UrlbarTestUtils.TestProvider({ + priority: Infinity, + type: UrlbarUtils.PROVIDER_TYPE.HEURISTIC, + results: [ + Object.assign( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + title: "Test", + url: "http://example.com/", + } + ), + { + heuristic: true, + autofill: { + value: autofilledValue, + selectionStart: searchString.length, + selectionEnd: autofilledValue.length, + // Leave out `type` to trigger "other" + }, + } + ), + ], + }); +} + +// Allow more time for Mac machines so they don't time out in verify mode. +if (AppConstants.platform == "macosx") { + requestLongerTimeout(3); +} + +add_setup(async function () { + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesTestUtils.clearInputHistory(); + + // Enable local telemetry recording for the duration of the tests. + const originalCanRecord = Services.telemetry.canRecordExtended; + Services.telemetry.canRecordExtended = true; + + registerCleanupFunction(async () => { + Services.telemetry.canRecordExtended = originalCanRecord; + await PlacesTestUtils.clearInputHistory(); + await PlacesUtils.history.clear(); + }); +}); + +// Checks adaptive history, origin, and URL autofill. +add_task(async function history() { + const testData = [ + { + useAdaptiveHistory: true, + visitHistory: ["http://example.com/test"], + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + userInput: "ex", + autofilled: "example.com/", + expected: "autofill_origin", + }, + { + useAdaptiveHistory: true, + visitHistory: ["http://example.com/test"], + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + userInput: "exa", + autofilled: "example.com/test", + expected: "autofill_adaptive", + }, + { + useAdaptiveHistory: true, + visitHistory: ["http://example.com/test"], + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + userInput: "exam", + autofilled: "example.com/test", + expected: "autofill_adaptive", + }, + { + useAdaptiveHistory: true, + visitHistory: ["http://example.com/test"], + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + userInput: "example.com", + autofilled: "example.com/test", + expected: "autofill_adaptive", + }, + { + useAdaptiveHistory: true, + visitHistory: ["http://example.com/test"], + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + userInput: "example.com/", + autofilled: "example.com/test", + expected: "autofill_adaptive", + }, + { + useAdaptiveHistory: true, + visitHistory: ["http://example.com/test"], + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + userInput: "example.com/test", + autofilled: "example.com/test", + expected: "autofill_adaptive", + }, + { + useAdaptiveHistory: true, + visitHistory: ["http://example.com/test", "http://example.org/test"], + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + userInput: "example.org", + autofilled: "example.org/", + expected: "autofill_origin", + }, + { + useAdaptiveHistory: true, + visitHistory: ["http://example.com/test", "http://example.com/test/url"], + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + userInput: "example.com/test/", + autofilled: "example.com/test/", + expected: "autofill_url", + }, + { + useAdaptiveHistory: true, + visitHistory: [{ uri: "http://example.com/test" }], + inputHistory: [ + { uri: "http://example.com/test", input: "http://example.com/test" }, + ], + userInput: "http://example.com/test", + autofilled: "http://example.com/test", + expected: "autofill_adaptive", + }, + { + useAdaptiveHistory: false, + visitHistory: [{ uri: "http://example.com/test" }], + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + userInput: "example", + autofilled: "example.com/", + expected: "autofill_origin", + }, + { + useAdaptiveHistory: false, + visitHistory: [{ uri: "http://example.com/test" }], + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + userInput: "example.com/te", + autofilled: "example.com/test", + expected: "autofill_url", + }, + ]; + + for (const { + useAdaptiveHistory, + visitHistory, + inputHistory, + userInput, + autofilled, + expected, + } of testData) { + const histograms = snapshotHistograms(); + + await PlacesTestUtils.addVisits(visitHistory); + for (const { uri, input } of inputHistory) { + await UrlbarUtils.addToInputHistory(uri, input); + } + + UrlbarPrefs.set("autoFill.adaptiveHistory.enabled", useAdaptiveHistory); + + await triggerAutofillAndPickResult(userInput, autofilled); + + assertSearchTelemetryEmpty(histograms.search_hist); + assertTelemetryResults( + histograms, + expected, + 0, + UrlbarTestUtils.SELECTED_RESULT_METHODS.enter + ); + + UrlbarPrefs.clear("autoFill.adaptiveHistory.enabled"); + await PlacesTestUtils.clearInputHistory(); + await PlacesUtils.history.clear(); + } +}); + +// Checks about-page autofill (e.g., "about:about"). +add_task(async function about() { + let histograms = snapshotHistograms(); + await triggerAutofillAndPickResult("about:abou", "about:about"); + + assertSearchTelemetryEmpty(histograms.search_hist); + assertTelemetryResults( + histograms, + "autofill_about", + 0, + UrlbarTestUtils.SELECTED_RESULT_METHODS.enter + ); + + await PlacesUtils.history.clear(); +}); + +// Checks preloaded sites autofill. +add_task(async function preloaded() { + UrlbarPrefs.set("usepreloadedtopurls.enabled", true); + UrlbarPrefs.set("usepreloadedtopurls.expire_days", 100); + UrlbarProviderPreloadedSites.populatePreloadedSiteStorage([ + ["http://example.com/", "Example"], + ]); + + let histograms = snapshotHistograms(); + await triggerAutofillAndPickResult("example", "example.com/"); + + assertSearchTelemetryEmpty(histograms.search_hist); + assertTelemetryResults( + histograms, + "autofill_preloaded", + 0, + UrlbarTestUtils.SELECTED_RESULT_METHODS.enter + ); + + await PlacesUtils.history.clear(); + UrlbarPrefs.clear("usepreloadedtopurls.enabled"); + UrlbarPrefs.clear("usepreloadedtopurls.expire_days"); +}); + +// Checks the "other" fallback, which shouldn't normally happen. +add_task(async function other() { + let searchString = "exam"; + let autofilledValue = "example.com/"; + let provider = createOtherAutofillProvider(searchString, autofilledValue); + UrlbarProvidersManager.registerProvider(provider); + + let histograms = snapshotHistograms(); + await triggerAutofillAndPickResult(searchString, autofilledValue); + + assertSearchTelemetryEmpty(histograms.search_hist); + assertTelemetryResults( + histograms, + "autofill_other", + 0, + UrlbarTestUtils.SELECTED_RESULT_METHODS.enter + ); + + await PlacesUtils.history.clear(); + UrlbarProvidersManager.unregisterProvider(provider); +}); + +// Checks impression telemetry. +add_task(async function impression() { + const testData = [ + { + description: "Adaptive history autofill and pick it", + useAdaptiveHistory: true, + visitHistory: ["http://example.com/first", "http://example.com/second"], + inputHistory: [{ uri: "http://example.com/first", input: "exa" }], + userInput: "exa", + autofilled: "example.com/first", + expected: "autofill_adaptive", + }, + { + description: "Adaptive history autofill but pick another result", + useAdaptiveHistory: true, + visitHistory: ["http://example.com/first", "http://example.com/second"], + inputHistory: [{ uri: "http://example.com/first", input: "exa" }], + userInput: "exa", + urlToSelect: "http://example.com/second", + autofilled: "example.com/first", + expected: "autofill_adaptive", + }, + { + description: "Adaptive history autofill but not pick any result", + unpickResult: true, + useAdaptiveHistory: true, + visitHistory: ["http://example.com/first", "http://example.com/second"], + inputHistory: [{ uri: "http://example.com/first", input: "exa" }], + userInput: "exa", + autofilled: "example.com/first", + }, + { + description: "Origin autofill and pick it", + visitHistory: ["http://example.com/first", "http://example.com/second"], + userInput: "exa", + autofilled: "example.com/", + expected: "autofill_origin", + }, + { + description: "Origin autofill but pick another result", + visitHistory: ["http://example.com/first", "http://example.com/second"], + userInput: "exa", + urlToSelect: "http://example.com/second", + autofilled: "example.com/", + expected: "autofill_origin", + }, + { + description: "Origin autofill but not pick any result", + unpickResult: true, + visitHistory: ["http://example.com/first", "http://example.com/second"], + userInput: "exa", + autofilled: "example.com/", + }, + { + description: "URL autofill and pick it", + visitHistory: ["http://example.com/first", "http://example.com/second"], + userInput: "example.com/", + autofilled: "example.com/", + expected: "autofill_url", + }, + { + description: "URL autofill but pick another result", + visitHistory: ["http://example.com/first", "http://example.com/second"], + userInput: "example.com/", + urlToSelect: "http://example.com/second", + autofilled: "example.com/", + expected: "autofill_url", + }, + { + description: "URL autofill but not pick any result", + unpickResult: true, + visitHistory: ["http://example.com/first", "http://example.com/second"], + userInput: "example.com/", + autofilled: "example.com/", + }, + { + description: "about page autofill and pick it", + userInput: "about:a", + autofilled: "about:about", + expected: "autofill_about", + }, + { + description: "about page autofill but pick another result", + userInput: "about:a", + urlToSelect: "about:addons", + autofilled: "about:about", + expected: "autofill_about", + }, + { + description: "about page autofill but not pick any result", + unpickResult: true, + userInput: "about:a", + autofilled: "about:about", + }, + { + description: "Preloaded site autofill and pick it", + usePreloadedSite: true, + preloadedSites: [["http://example.com/", "Example"]], + userInput: "exa", + autofilled: "example.com/", + expected: "autofill_preloaded", + }, + { + description: "Preloaded site autofill but not pick any result", + unpickResult: true, + usePreloadedSite: true, + preloadedSites: [["http://example.com/", "Example"]], + userInput: "exa", + autofilled: "example.com/", + }, + { + description: "Other provider's autofill and pick it", + useOtherProvider: true, + userInput: "example", + autofilled: "example.com/", + expected: "autofill_other", + }, + { + description: "Other provider's autofill but not pick any result", + unpickResult: true, + useOtherProvider: true, + userInput: "example", + autofilled: "example.com/", + }, + ]; + + for (const { + description, + useAdaptiveHistory = false, + usePreloadedSite = false, + useOtherProvider = false, + unpickResult = false, + visitHistory, + inputHistory, + preloadedSites, + userInput, + select, + autofilled, + expected, + } of testData) { + info(description); + + UrlbarPrefs.set("autoFill.adaptiveHistory.enabled", useAdaptiveHistory); + if (usePreloadedSite) { + UrlbarPrefs.set("usepreloadedtopurls.enabled", true); + UrlbarPrefs.set("usepreloadedtopurls.expire_days", 100); + } + let otherProvider; + if (useOtherProvider) { + otherProvider = createOtherAutofillProvider(userInput, autofilled); + UrlbarProvidersManager.registerProvider(otherProvider); + } + + if (visitHistory) { + await PlacesTestUtils.addVisits(visitHistory); + } + if (inputHistory) { + for (const { uri, input } of inputHistory) { + await UrlbarUtils.addToInputHistory(uri, input); + } + } + if (preloadedSites) { + UrlbarProviderPreloadedSites.populatePreloadedSiteStorage(preloadedSites); + } + + await triggerAutofillAndPickResult( + userInput, + autofilled, + unpickResult, + select + ); + + const scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + if (unpickResult) { + TelemetryTestUtils.assertScalarUnset( + scalars, + "urlbar.impression.autofill_adaptive" + ); + TelemetryTestUtils.assertScalarUnset( + scalars, + "urlbar.impression.autofill_origin" + ); + TelemetryTestUtils.assertScalarUnset( + scalars, + "urlbar.impression.autofill_url" + ); + TelemetryTestUtils.assertScalarUnset( + scalars, + "urlbar.impression.autofill_about" + ); + } else { + TelemetryTestUtils.assertScalar( + scalars, + `urlbar.impression.${expected}`, + 1 + ); + } + + UrlbarPrefs.clear("autoFill.adaptiveHistory.enabled"); + UrlbarPrefs.clear("usepreloadedtopurls.enabled"); + UrlbarPrefs.clear("usepreloadedtopurls.expire_days"); + + if (otherProvider) { + UrlbarProvidersManager.unregisterProvider(otherProvider); + } + + await PlacesTestUtils.clearInputHistory(); + await PlacesUtils.history.clear(); + } +}); + +// Checks autofill deletion telemetry. +add_task(async function deletion() { + await PlacesTestUtils.addVisits(["http://example.com/"]); + + info("Delete autofilled value by DELETE key"); + await doDeletionTest({ + firstSearchString: "exa", + firstAutofilledValue: "example.com/", + trigger: () => { + EventUtils.synthesizeKey("KEY_Delete"); + Assert.equal(gURLBar.value, "exa"); + }, + expectedScalar: 1, + }); + + info("Delete autofilled value by BACKSPACE key"); + await doDeletionTest({ + firstSearchString: "exa", + firstAutofilledValue: "example.com/", + trigger: () => { + EventUtils.synthesizeKey("KEY_Backspace"); + Assert.equal(gURLBar.value, "exa"); + }, + expectedScalar: 1, + }); + + info("Delete autofilled value twice"); + await doDeletionTest({ + firstSearchString: "exa", + firstAutofilledValue: "example.com/", + trigger: () => { + // Delete autofilled string. + EventUtils.synthesizeKey("KEY_Delete"); + Assert.equal(gURLBar.value, "exa"); + + // Re-autofilling. + EventUtils.synthesizeKey("m"); + Assert.equal(gURLBar.value, "example.com/"); + Assert.equal(gURLBar.selectionStart, "exam".length); + Assert.equal(gURLBar.selectionEnd, "example.com/".length); + + // Delete autofilled string again. + EventUtils.synthesizeKey("KEY_Backspace"); + Assert.equal(gURLBar.value, "exam"); + }, + expectedScalar: 2, + }); + + info("Delete one char after unselecting autofilled string"); + await doDeletionTest({ + firstSearchString: "exa", + firstAutofilledValue: "example.com/", + trigger: () => { + // Cancel selection. + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + Assert.equal(gURLBar.selectionStart, "example.com/".length); + Assert.equal(gURLBar.selectionEnd, "example.com/".length); + + EventUtils.synthesizeKey("KEY_Backspace"); + Assert.equal(gURLBar.value, "example.com"); + }, + expectedScalar: 0, + }); + + info("Delete autofilled value after unselecting autofilled string"); + await doDeletionTest({ + firstSearchString: "exa", + firstAutofilledValue: "example.com/", + trigger: () => { + // Cancel selection. + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + Assert.equal(gURLBar.selectionStart, "example.com/".length); + Assert.equal(gURLBar.selectionEnd, "example.com/".length); + + // Delete autofilled string one by one. + for (let i = 0; i < "mple.com/".length; i++) { + EventUtils.synthesizeKey("KEY_Backspace"); + } + Assert.equal(gURLBar.value, "exa"); + }, + expectedScalar: 0, + }); + + info( + "Delete autofilled value after unselecting autofilled string then selecting them manually again" + ); + await doDeletionTest({ + firstSearchString: "exa", + firstAutofilledValue: "example.com/", + trigger: () => { + // Cancel selection. + const previousSelectionStart = gURLBar.selectionStart; + const previousSelectionEnd = gURLBar.selectionEnd; + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + Assert.equal(gURLBar.selectionStart, "example.com/".length); + Assert.equal(gURLBar.selectionEnd, "example.com/".length); + + // Select same range again. + gURLBar.selectionStart = previousSelectionStart; + gURLBar.selectionEnd = previousSelectionEnd; + + EventUtils.synthesizeKey("KEY_Backspace"); + Assert.equal(gURLBar.value, "exa"); + }, + expectedScalar: 1, + }); + + await PlacesUtils.history.clear(); +}); + +async function doDeletionTest({ + firstSearchString, + firstAutofilledValue, + trigger, + expectedScalar, +}) { + await BrowserTestUtils.withNewTab("about:blank", async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: firstSearchString, + fireInputEvent: true, + }); + const details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill, "Result is autofill"); + Assert.equal(gURLBar.value, firstAutofilledValue, "gURLBar.value"); + Assert.equal( + gURLBar.selectionStart, + firstSearchString.length, + "selectionStart" + ); + Assert.equal( + gURLBar.selectionEnd, + firstAutofilledValue.length, + "selectionEnd" + ); + + await trigger(); + + const scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + if (expectedScalar) { + TelemetryTestUtils.assertScalar( + scalars, + "urlbar.autofill_deletion", + expectedScalar + ); + } else { + TelemetryTestUtils.assertScalarUnset(scalars, "urlbar.autofill_deletion"); + } + + await UrlbarTestUtils.promisePopupClose(window); + }); +} 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..d4f4e77d57 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_dynamic.js @@ -0,0 +1,136 @@ +/* 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"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs", + UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs", + UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs", + UrlbarView: "resource:///modules/UrlbarView.sys.mjs", +}); + +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 { + resultMethodHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ), + }; +} + +function assertTelemetryResults(histograms, type, index, method) { + 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..28eae06a6f --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_extension.js @@ -0,0 +1,155 @@ +/* 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"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs", +}); + +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 { + resultMethodHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ), + search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"), + }; +} + +function assertTelemetryResults(histograms, type, index, method) { + TelemetryTestUtils.assertHistogram(histograms.resultMethodHist, method, 1); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + `urlbar.picked.${type}`, + index, + 1 + ); +} + +add_setup(async function () { + 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], + // 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_handoff.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_handoff.js new file mode 100644 index 0000000000..c2e1413a27 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_handoff.js @@ -0,0 +1,182 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { SearchSERPTelemetry } = ChromeUtils.importESModule( + "resource:///modules/SearchSERPTelemetry.sys.mjs" +); + +const TEST_PROVIDER_INFO = [ + { + telemetryId: "example", + searchPageRegexp: + /^https:\/\/example.com\/browser\/browser\/components\/search\/test\/browser\/searchTelemetry(?:Ad)?.html/, + queryParamName: "s", + codeParamName: "abc", + taggedCodes: ["ff"], + followOnParamNames: ["a"], + extraAdServersRegexps: [/^https:\/\/example\.com\/ad2?/], + }, +]; + +function getPageUrl(useAdPage = false) { + let page = useAdPage ? "searchTelemetryAd.html" : "searchTelemetry.html"; + return `https://example.com/browser/browser/components/search/test/browser/${page}`; +} + +// sharedData messages are only passed to the child on idle. Therefore +// we wait for a few idles to try and ensure the messages have been able +// to be passed across and handled. +async function waitForIdle() { + for (let i = 0; i < 10; i++) { + await new Promise(resolve => Services.tm.idleDispatchToMainThread(resolve)); + } +} + +add_setup(async function () { + SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO); + await waitForIdle(); + + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar", + true, + ], + ], + }); + + await SearchTestUtils.installSearchExtension( + { + search_url: getPageUrl(true), + search_url_get_params: "s={searchTerms}&abc=ff", + suggest_url: + "https://example.com/browser/browser/components/search/test/browser/searchSuggestionEngine.sjs", + suggest_url_get_params: "query={searchTerms}", + }, + { setAsDefault: true } + ); + + const oldCanRecord = Services.telemetry.canRecordExtended; + Services.telemetry.canRecordExtended = true; + Services.telemetry.setEventRecordingEnabled("navigation", true); + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + + Services.telemetry.canRecordExtended = oldCanRecord; + Services.telemetry.setEventRecordingEnabled("navigation", false); + + SearchSERPTelemetry.overrideSearchTelemetryForTests(); + + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + }); +}); + +add_task(async function test_search() { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + + const histogram = + TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"); + + info("Load about:newtab in new window"); + const newtab = "about:newtab"; + const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + BrowserTestUtils.loadURIString(tab.linkedBrowser, newtab); + await BrowserTestUtils.browserStopped(tab.linkedBrowser, newtab); + + info("Focus on search input in newtab content"); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const searchInput = content.document.querySelector(".fake-editable"); + searchInput.click(); + }); + + info("Search and wait the result"); + const onLoaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + EventUtils.synthesizeKey("q"); + EventUtils.synthesizeKey("VK_RETURN"); + await onLoaded; + + info("Check the telemetries"); + await assertHandoffResult(histogram); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_search_private_mode() { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + + const histogram = + TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"); + + info("Open private window"); + let privateWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + let tab = privateWindow.gBrowser.selectedTab; + + info("Focus on search input in newtab content"); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const searchInput = content.document.querySelector(".fake-editable"); + searchInput.click(); + }); + + info("Search and wait the result"); + const onLoaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + EventUtils.synthesizeKey("q", {}, privateWindow); + EventUtils.synthesizeKey("VK_RETURN", {}, privateWindow); + await onLoaded; + + info("Check the telemetries"); + await assertHandoffResult(histogram); + + await BrowserTestUtils.closeWindow(privateWindow); +}); + +async function assertHandoffResult(histogram) { + await assertScalars([ + ["browser.engagement.navigation.urlbar_handoff", "search_enter", 1], + ["browser.search.content.urlbar_handoff", "example:tagged:ff", 1], + ]); + await assertHistogram(histogram, [["other-Example.urlbar-handoff", 1]]); + TelemetryTestUtils.assertEvents( + [ + [ + "navigation", + "search", + "urlbar_handoff", + "enter", + { engine: "other-Example" }, + ], + ], + { category: "navigation", method: "search" } + ); +} + +async function assertHistogram(histogram, expectedResults) { + await TestUtils.waitForCondition(() => { + const snapshot = histogram.snapshot(); + return expectedResults.every(([key]) => key in snapshot); + }, "Wait until the histogram has expected keys"); + + for (const [key, value] of expectedResults) { + TelemetryTestUtils.assertKeyedHistogramSum(histogram, key, value); + } +} + +async function assertScalars(expectedResults) { + await TestUtils.waitForCondition(() => { + const scalars = TelemetryTestUtils.getProcessScalars("parent", true); + return expectedResults.every(([scalarName]) => scalarName in scalars); + }, "Wait until the scalars have expected keyed scalars"); + + const scalars = TelemetryTestUtils.getProcessScalars("parent", true); + + for (const [scalarName, key, value] of expectedResults) { + TelemetryTestUtils.assertKeyedScalar(scalars, scalarName, key, value); + } +} diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_persisted.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_persisted.js new file mode 100644 index 0000000000..904e774a2c --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_persisted.js @@ -0,0 +1,270 @@ +/* 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 file tests browser.engagement.navigation.urlbar_persisted and the + * event navigation.search.urlbar_persisted + */ + +"use strict"; + +const { SearchSERPTelemetry } = ChromeUtils.importESModule( + "resource:///modules/SearchSERPTelemetry.sys.mjs" +); + +const SCALAR_URLBAR_PERSISTED = + "browser.engagement.navigation.urlbar_persisted"; + +const SEARCH_STRING = "chocolate"; + +let testEngine; +add_setup(async () => { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", true]], + }); + + await SearchTestUtils.installSearchExtension( + { + name: "MozSearch", + search_url: "https://www.example.com/", + search_url_get_params: "q={searchTerms}&pc=fake_code", + }, + { setAsDefault: true } + ); + + testEngine = Services.search.getEngineByName("MozSearch"); + + // Enable event recording for the events. + Services.telemetry.setEventRecordingEnabled("navigation", true); + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + Services.telemetry.setEventRecordingEnabled("navigation", false); + }); +}); + +async function searchForString(searchString, tab) { + info(`Search for string: ${searchString}.`); + let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl( + testEngine, + searchString + ); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + expectedSearchUrl + ); + gURLBar.focus(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: searchString, + fireInputEvent: true, + }); + EventUtils.synthesizeKey("KEY_Enter"); + await browserLoadedPromise; + info("Finished loading search."); + return expectedSearchUrl; +} + +async function gotoUrl(url, tab) { + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + url + ); + BrowserTestUtils.loadURIString(tab.linkedBrowser, url); + await browserLoadedPromise; + info(`Loaded page: ${url}`); +} + +async function goBack(browser) { + let pageShowPromise = BrowserTestUtils.waitForContentEvent( + browser, + "pageshow" + ); + browser.goBack(); + await pageShowPromise; + info("Go back a page."); +} + +async function goForward(browser) { + let pageShowPromise = BrowserTestUtils.waitForContentEvent( + browser, + "pageshow" + ); + browser.goForward(); + await pageShowPromise; + info("Go forward a page."); +} + +function assertScalarSearchEnter(number) { + let scalars = TelemetryTestUtils.getProcessScalars("parent", true, true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + SCALAR_URLBAR_PERSISTED, + "search_enter", + number + ); +} + +function assertScalarDoesNotExist(scalar) { + let scalars = TelemetryTestUtils.getProcessScalars("parent", true, false); + Assert.ok(!(scalar in scalars), scalar + " must not be recorded."); +} + +function assertTelemetryEvents() { + TelemetryTestUtils.assertEvents( + [ + [ + "navigation", + "search", + "urlbar", + "enter", + { engine: "other-MozSearch" }, + ], + [ + "navigation", + "search", + "urlbar_persisted", + "enter", + { engine: "other-MozSearch" }, + ], + ], + { + category: "navigation", + method: "search", + } + ); +} + +// A user making a search after making a search should result +// in the telemetry being recorded. +add_task(async function search_after_search() { + let search_hist = + TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"); + + const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + await searchForString(SEARCH_STRING, tab); + + // Scalar should not exist from a blank page, only when a search + // is conducted from a default SERP. + await assertScalarDoesNotExist(SCALAR_URLBAR_PERSISTED); + + // After the first search, we should expect the SAP to change + // because the search term should show up on the SERP. + await searchForString(SEARCH_STRING, tab); + assertScalarSearchEnter(1); + + // Check search counts. + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.urlbar-persisted", + 1 + ); + + // Check events. + assertTelemetryEvents(); + + BrowserTestUtils.removeTab(tab); +}); + +// A user going to a tab that contains a SERP should +// trigger the telemetry when conducting a search. +add_task(async function switch_to_tab_and_search() { + let search_hist = + TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"); + + const tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser); + await searchForString(SEARCH_STRING, tab1); + + const tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser); + await gotoUrl("https://www.example.com/some-place", tab2); + + await BrowserTestUtils.switchTab(gBrowser, tab1); + await searchForString(SEARCH_STRING, tab1); + assertScalarSearchEnter(1); + + // Check search count. + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.urlbar-persisted", + 1 + ); + + // Check events. + assertTelemetryEvents(); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); + +// When a user reverts the Urlbar after the search terms persist, +// conducting another search should still be registered as a +// urlbar-persisted SAP. +add_task(async function handle_revert() { + let search_hist = + TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"); + + const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + await searchForString(SEARCH_STRING, tab); + + gURLBar.handleRevert(); + await searchForString(SEARCH_STRING, tab); + + assertScalarSearchEnter(1); + + // Check search count. + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.urlbar-persisted", + 1 + ); + + // Check events. + assertTelemetryEvents(); + + BrowserTestUtils.removeTab(tab); +}); + +// A user going back and forth in history should trigger +// urlbar-persisted telemetry when returning to a SERP +// and conducting a search. +add_task(async function back_and_forth() { + let search_hist = + TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"); + + const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + // Create three pages in history: a page, a SERP, and a page. + await gotoUrl("https://www.example.com/some-place", tab); + await searchForString(SEARCH_STRING, tab); + await gotoUrl("https://www.example.com/another-page", tab); + + // Go back to the SERP by using both back and forward. + await goBack(tab.linkedBrowser); + await goBack(tab.linkedBrowser); + await goForward(tab.linkedBrowser); + await assertScalarDoesNotExist(SCALAR_URLBAR_PERSISTED); + + // Then do a search. + await searchForString(SEARCH_STRING, tab); + assertScalarSearchEnter(1); + + // Check search count. + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.urlbar-persisted", + 1 + ); + + // Check events. + assertTelemetryEvents(); + + 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..26500033eb --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_places.js @@ -0,0 +1,270 @@ +/* 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" +); + +ChromeUtils.defineESModuleGetters(this, { + UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs", +}); + +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 { + resultMethodHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ), + search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"), + }; +} + +function assertTelemetryResults(histograms, type, index, method) { + TelemetryTestUtils.assertHistogram(histograms.resultMethodHist, method, 1); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + `urlbar.picked.${type}`, + index, + 1 + ); +} + +add_setup(async function () { + 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], + // 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/a/"); + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + assertSearchTelemetryEmpty(histograms.search_hist); + assertTelemetryResults( + histograms, + "visiturl", + 0, + UrlbarTestUtils.SELECTED_RESULT_METHODS.enter + ); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_quickactions.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_quickactions.js new file mode 100644 index 0000000000..b29807900b --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_quickactions.js @@ -0,0 +1,133 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This file tests urlbar telemetry for quickactions. + */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarProviderQuickActions: + "resource:///modules/UrlbarProviderQuickActions.sys.mjs", +}); + +let testActionCalled = 0; + +add_setup(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.quickactions", true], + ["browser.urlbar.quickactions.enabled", true], + ], + }); + + UrlbarProviderQuickActions.addAction("testaction", { + commands: ["testaction"], + label: "quickactions-downloads2", + onPick: () => testActionCalled++, + }); + + registerCleanupFunction(() => { + UrlbarProviderQuickActions.removeAction("testaction"); + }); +}); + +add_task(async function test() { + const histograms = snapshotHistograms(); + + // Do a search to show the quickaction. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "testaction", + waitForFocus, + fireInputEvent: true, + }); + + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_Enter"); + }); + + Assert.equal(testActionCalled, 1, "Test action was called"); + + TelemetryTestUtils.assertHistogram( + histograms.resultMethodHist, + UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection, + 1 + ); + + let scalars = TelemetryTestUtils.getProcessScalars("parent", true, true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + `urlbar.picked.quickaction`, + 1, + 1 + ); + + TelemetryTestUtils.assertKeyedScalar( + scalars, + "quickaction.picked", + "testaction-10", + 1 + ); + + TelemetryTestUtils.assertKeyedScalar( + scalars, + "quickaction.impression", + "testaction-10", + 1 + ); + + // Clean up for subsequent tests. + gURLBar.handleRevert(); +}); + +add_task(async function test_impressions() { + UrlbarProviderQuickActions.addAction("testaction2", { + commands: ["testaction2"], + label: "quickactions-downloads2", + onPick: () => {}, + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "testaction", + waitForFocus, + fireInputEvent: true, + }); + + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_Enter"); + }); + + let scalars = TelemetryTestUtils.getProcessScalars("parent", true, true); + + TelemetryTestUtils.assertKeyedScalar( + scalars, + "quickaction.impression", + `testaction-10`, + 1 + ); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "quickaction.impression", + `testaction2-10`, + 1 + ); + + UrlbarProviderQuickActions.removeAction("testaction2"); + gURLBar.handleRevert(); +}); + +function snapshotHistograms() { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + return { + resultMethodHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ), + }; +} 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..ffa3158f2b --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_remotetab.js @@ -0,0 +1,185 @@ +/* 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"; + +ChromeUtils.defineESModuleGetters(this, { + SyncedTabs: "resource://services-sync/SyncedTabs.sys.mjs", + UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs", +}); + +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 { + resultMethodHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ), + search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"), + }; +} + +function assertTelemetryResults(histograms, type, index, method) { + TelemetryTestUtils.assertHistogram(histograms.resultMethodHist, method, 1); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + `urlbar.picked.${type}`, + index, + 1 + ); +} + +add_setup(async function () { + 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], + // 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: Math.floor(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], {}))); + + // 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..7830102cf6 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_searchmode.js @@ -0,0 +1,592 @@ +/* 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"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarProviderTabToSearch: + "resource:///modules/UrlbarProviderTabToSearch.sys.mjs", +}); + +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_setup(async function () { + 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. + let suggestionEngine = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + "urlbarTelemetrySearchSuggestions.xml", + setAsDefault: true, + }); + suggestionEngine.alias = ENGINE_ALIAS; + engineDomain = suggestionEngine.searchUrlDomain; + engineName = suggestionEngine.name; + + // 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(); + + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + + // Clear historical search suggestions to avoid interference from previous + // tests. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.maxHistoricalSearchSuggestions", 0]], + }); + + // Make sure to restore the engine once we're done. + registerCleanupFunction(async function () { + Services.telemetry.canRecordExtended = oldCanRecord; + await PlacesUtils.history.clear(); + Services.telemetry.setEventRecordingEnabled("navigation", false); + }); +}); + +// 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 from a History +// menu. +add_task(async function test_historymenu() { + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + PlacesCommandHook.searchHistory(); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + entry: "historymenu", + }); + assertSearchModeScalars("historymenu", "history"); +}); + +// 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 that handoff-to-search-mode only occurs when suggestions are disabled +// in the Urlbar. +// 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.handoff(value, searchEngine) 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() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.searches", false]], + }); + 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); + await SpecialPowers.popPrefEnv(); +}); + +// 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), + fireInputEvent: true, + }); + 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_sponsored_topsites.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_sponsored_topsites.js new file mode 100644 index 0000000000..74d1fdb0db --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_sponsored_topsites.js @@ -0,0 +1,181 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + CONTEXTUAL_SERVICES_PING_TYPES: + "resource:///modules/PartnerLinkAttribution.sys.mjs", + NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs", + PartnerLinkAttribution: "resource:///modules/PartnerLinkAttribution.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(this, { + HttpServer: "resource://testing-common/httpd.js", +}); + +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/"; + +// This is used for "sendAttributionRequest" +var gHttpServer = null; +var gRequests = []; + +function submitHandler(request, response) { + gRequests.push(request); + response.setStatusLine(request.httpVersion, 200, "Ok"); +} + +// Spy for telemetry sender +let spy; + +add_setup(async function () { + sandbox = sinon.createSandbox(); + spy = sandbox.spy( + PartnerLinkAttribution._pingCentre, + "sendStructuredIngestionPing" + ); + + let topsitesAttribution = Services.prefs.getStringPref( + "browser.partnerlink.campaign.topsites" + ); + gHttpServer = new HttpServer(); + gHttpServer.registerPathHandler(`/cid/${topsitesAttribution}`, submitHandler); + gHttpServer.start(-1); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.sponsoredTopSites", true], + ["browser.urlbar.suggest.topsites", true], + ["browser.newtabpage.activity-stream.default.sites", EN_US_TOPSITES], + [ + "browser.partnerlink.attributionURL", + `http://localhost:${gHttpServer.identity.primaryPort}/cid/`, + ], + ], + }); + + await updateTopSites( + sites => sites && sites.length == EN_US_TOPSITES.split(",").length + ); + + registerCleanupFunction(async () => { + sandbox.restore(); + await gHttpServer.stop(); + gHttpServer = null; + }); +}); + +add_task(async function send_impression_and_click() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + let link = { + label: "test_label", + url: "http://example.com/", + sponsored_position: 1, + sendAttributionRequest: true, + sponsored_tile_id: 42, + sponsored_impression_url: "http://impression.test.com/", + sponsored_click_url: "http://click.test.com/", + }; + // Pin a sponsored TopSite to set up the test fixture + NewTabUtils.pinnedLinks.pin(link, 0); + + await updateTopSites(sites => sites && sites[0] && sites[0].isPinned); + + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + + await UrlbarTestUtils.promiseSearchComplete(window); + + // Select the first result and confirm it. + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + EventUtils.synthesizeKey("KEY_ArrowDown"); + + let loadPromise = BrowserTestUtils.waitForDocLoadAndStopIt( + result.url, + gBrowser.selectedBrowser + ); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + + Assert.ok( + spy.calledTwice, + "Should send an impression ping and a click ping" + ); + + // Validate the impression ping + let [payload, endpoint] = spy.firstCall.args; + Assert.ok( + endpoint.includes(CONTEXTUAL_SERVICES_PING_TYPES.TOPSITES_IMPRESSION), + "Should set the endpoint for TopSites impression" + ); + Assert.ok(!!payload.context_id, "Should set the context_id"); + Assert.equal(payload.advertiser, "test_label", "Should set the advertiser"); + Assert.equal( + payload.reporting_url, + "http://impression.test.com/", + "Should set the impression reporting URL" + ); + Assert.equal(payload.tile_id, 42, "Should set the tile_id"); + Assert.equal(payload.position, 1, "Should set the position"); + + // Validate the click ping + [payload, endpoint] = spy.secondCall.args; + Assert.ok( + endpoint.includes(CONTEXTUAL_SERVICES_PING_TYPES.TOPSITES_SELECTION), + "Should set the endpoint for TopSites click" + ); + Assert.ok(!!payload.context_id, "Should set the context_id"); + Assert.equal( + payload.reporting_url, + "http://click.test.com/", + "Should set the click reporting URL" + ); + Assert.equal(payload.tile_id, 42, "Should set the tile_id"); + Assert.equal(payload.position, 1, "Should set the position"); + + await UrlbarTestUtils.promisePopupClose(window, () => { + gURLBar.blur(); + }); + + NewTabUtils.pinnedLinks.unpin(link); + }); +}); + +add_task(async function zero_ping() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + spy.resetHistory(); + + // Reload the TopSites + await updateTopSites( + sites => sites && sites.length == EN_US_TOPSITES.split(",").length + ); + + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + + await UrlbarTestUtils.promiseSearchComplete(window); + + // Select the first result and confirm it. + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + EventUtils.synthesizeKey("KEY_ArrowDown"); + + let loadPromise = BrowserTestUtils.waitForDocLoadAndStopIt( + result.url, + gBrowser.selectedBrowser + ); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + + Assert.ok( + spy.notCalled, + "Should not send any ping if there is no sponsored Top Site" + ); + + await UrlbarTestUtils.promisePopupClose(window, () => { + gURLBar.blur(); + }); + }); +}); 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..35607d2f94 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tabtosearch.js @@ -0,0 +1,416 @@ +/* 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"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarProviderTabToSearch: + "resource:///modules/UrlbarProviderTabToSearch.sys.mjs", +}); + +function snapshotHistograms() { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + return { + resultMethodHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ), + search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"), + }; +} + +function assertTelemetryResults(histograms, type, index, method) { + 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 a tab-to-search result + * with the correct engine. + * + * @param {string} engineName + * The expected engine name. + * @param {boolean} [isOnboarding] + * If true, expects the tab-to-search result to be an onbarding result. + */ +async function checkForTabToSearchResult(engineName, isOnboarding) { + 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." + ); + if (isOnboarding) { + Assert.equal( + tabToSearchResult.payload.dynamicType, + "onboardTabToSearch", + "The tab-to-search result is an onboarding result." + ); + } else { + Assert.ok( + !tabToSearchResult.payload.dynamicType, + "The tab-to-search result should not be an onboarding result." + ); + } +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0]], + }); + + await SearchTestUtils.installSearchExtension({ + name: ENGINE_NAME, + search_url: `https://${ENGINE_DOMAIN}/`, + }); + + // Reset the enginesShown sets in case a previous test showed a tab-to-search + // result but did not end its engagement. + UrlbarProviderTabToSearch.enginesShown.regular.clear(); + UrlbarProviderTabToSearch.enginesShown.onboarding.clear(); + + // Enable local telemetry recording for the duration of the tests. + let oldCanRecord = Services.telemetry.canRecordExtended; + Services.telemetry.canRecordExtended = true; + + registerCleanupFunction(async () => { + Services.telemetry.canRecordExtended = oldCanRecord; + }); +}); + +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), + fireInputEvent: true, + }); + + 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(); + }); + + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); +}); + +add_task(async function impressions() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0]], + }); + await impressions_test(false); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function onboarding_impressions() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 3]], + }); + await impressions_test(true); + await SpecialPowers.popPrefEnv(); + delete UrlbarProviderTabToSearch.onboardingInteractionAtTime; +}); + +async function impressions_test(isOnboarding) { + await BrowserTestUtils.withNewTab("about:blank", async browser => { + const firstEngineHost = "example"; + let extension = await SearchTestUtils.installSearchExtension( + { + name: `${ENGINE_NAME}2`, + search_url: `https://${firstEngineHost}-2.com/`, + }, + { skipUnload: true } + ); + + 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 checkForTabToSearchResult(ENGINE_NAME, isOnboarding); + } + + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + let scalars = TelemetryTestUtils.getProcessScalars("parent", true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "urlbar.tips", + isOnboarding ? "tabtosearch_onboard-shown" : "tabtosearch-shown", + 1 + ); + TelemetryTestUtils.assertKeyedScalar( + scalars, + isOnboarding + ? "urlbar.tabtosearch.impressions_onboarding" + : "urlbar.tabtosearch.impressions", + // "other" is recorded as the engine name because we're not using a built-in engine. + "other", + 1 + ); + + info("Type through autofill to second engine hostname. Record impression."); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: firstEngineHost, + fireInputEvent: true, + }); + await checkForTabToSearchResult(ENGINE_NAME, isOnboarding); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: `${firstEngineHost}-`, + fireInputEvent: true, + }); + await checkForTabToSearchResult(`${ENGINE_NAME}2`, isOnboarding); + 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. + scalars = TelemetryTestUtils.getProcessScalars("parent", true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "urlbar.tips", + isOnboarding ? "tabtosearch_onboard-shown" : "tabtosearch-shown", + 3 + ); + TelemetryTestUtils.assertKeyedScalar( + scalars, + isOnboarding + ? "urlbar.tabtosearch.impressions_onboarding" + : "urlbar.tabtosearch.impressions", + "other", + 3 + ); + + info("Make a typo and return to autofill. Do not record impression."); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: `${firstEngineHost}-`, + fireInputEvent: true, + }); + await checkForTabToSearchResult(`${ENGINE_NAME}2`, isOnboarding); + 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 checkForTabToSearchResult(`${ENGINE_NAME}2`, isOnboarding); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + scalars = TelemetryTestUtils.getProcessScalars("parent", true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "urlbar.tips", + isOnboarding ? "tabtosearch_onboard-shown" : "tabtosearch-shown", + 4 + ); + TelemetryTestUtils.assertKeyedScalar( + scalars, + isOnboarding + ? "urlbar.tabtosearch.impressions_onboarding" + : "urlbar.tabtosearch.impressions", + "other", + 4 + ); + + info( + "Cancel then restart autofill. Continue to show the tab-to-search result." + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: `${firstEngineHost}-2`, + fireInputEvent: true, + }); + await checkForTabToSearchResult(`${ENGINE_NAME}2`, isOnboarding); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_Backspace"); + await searchPromise; + await checkForTabToSearchResult(`${ENGINE_NAME}2`, isOnboarding); + searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + // Type the "." from `example-2.com`. + EventUtils.synthesizeKey("."); + await searchPromise; + await checkForTabToSearchResult(`${ENGINE_NAME}2`, isOnboarding); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + scalars = TelemetryTestUtils.getProcessScalars("parent", true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "urlbar.tips", + isOnboarding ? "tabtosearch_onboard-shown" : "tabtosearch-shown", + 5 + ); + TelemetryTestUtils.assertKeyedScalar( + scalars, + isOnboarding + ? "urlbar.tabtosearch.impressions_onboarding" + : "urlbar.tabtosearch.impressions", + // "other" is recorded as the engine name because we're not using a built-in engine. + "other", + 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 checkForTabToSearchResult(`${ENGINE_NAME}2`, isOnboarding); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + scalars = TelemetryTestUtils.getProcessScalars("parent", true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "urlbar.tips", + isOnboarding ? "tabtosearch_onboard-shown" : "tabtosearch-shown", + 6 + ); + TelemetryTestUtils.assertKeyedScalar( + scalars, + isOnboarding + ? "urlbar.tabtosearch.impressions_onboarding" + : "urlbar.tabtosearch.impressions", + "other", + 6 + ); + + info( + "Open a result page and then autofill engine host. Record impression." + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: firstEngineHost, + fireInputEvent: true, + }); + await checkForTabToSearchResult(ENGINE_NAME, isOnboarding); + // 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 checkForTabToSearchResult(ENGINE_NAME, isOnboarding); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + // We clear the scalar this time. + scalars = TelemetryTestUtils.getProcessScalars("parent", true, true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "urlbar.tips", + isOnboarding ? "tabtosearch_onboard-shown" : "tabtosearch-shown", + 8 + ); + TelemetryTestUtils.assertKeyedScalar( + scalars, + isOnboarding + ? "urlbar.tabtosearch.impressions_onboarding" + : "urlbar.tabtosearch.impressions", + "other", + 8 + ); + + await PlacesUtils.history.clear(); + await extension.unload(); + }); +} 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..c234bc3ed8 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tip.js @@ -0,0 +1,130 @@ +/* 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"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarProvider: "resource:///modules/UrlbarUtils.sys.mjs", + UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs", + UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs", + UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs", +}); + +function snapshotHistograms() { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + return { + resultMethodHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ), + search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"), + }; +} + +function assertTelemetryResults(histograms, type, index, method) { + TelemetryTestUtils.assertHistogram(histograms.resultMethodHist, method, 1); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + `urlbar.picked.${type}`, + index, + 1 + ); +} + +add_setup(async function () { + 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, + { + helpUrl: "https://example.com/", + type: "test", + titleL10n: { id: "urlbar-search-tips-confirm" }, + buttons: [ + { + url: "https://example.com/", + l10n: { id: "urlbar-search-tips-confirm" }, + }, + ], + } + ), + { 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..12adb27caf --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_topsite.js @@ -0,0 +1,136 @@ +/* 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"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(this, { + AboutNewTab: "resource:///modules/AboutNewTab.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 { + resultMethodHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ), + search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"), + }; +} + +function assertTelemetryResults(histograms, type, index, method) { + 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.discoverystream.endpointSpocsClear", + "", + ], + ["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_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.topsites", true], + ["browser.newtabpage.activity-stream.default.sites", EN_US_TOPSITES], + ["browser.urlbar.suggest.quickactions", false], + ], + }); + 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_urlbar_telemetry_zeroPrefix.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_zeroPrefix.js new file mode 100644 index 0000000000..b331921553 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_zeroPrefix.js @@ -0,0 +1,266 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This file tests urlbar telemetry related to the zero-prefix view, i.e., when + * the search string is empty. + */ + +"use strict"; + +const HISTOGRAM_DWELL_TIME = "FX_URLBAR_ZERO_PREFIX_DWELL_TIME_MS"; +const SCALARS = { + ABANDONMENT: "urlbar.zeroprefix.abandonment", + ENGAGEMENT: "urlbar.zeroprefix.engagement", + EXPOSURE: "urlbar.zeroprefix.exposure", +}; + +add_setup(async function () { + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + Services.telemetry.clearScalars(); + + await SearchTestUtils.installSearchExtension({}, { setAsDefault: true }); + await updateTopSitesAndAwaitChanged(); +}); + +// zero prefix engagement +add_task(async function engagement() { + let dwellHistogram = + TelemetryTestUtils.getAndClearHistogram(HISTOGRAM_DWELL_TIME); + + await BrowserTestUtils.withNewTab("about:blank", async () => { + await showZeroPrefix(); + checkScalars({ + [SCALARS.EXPOSURE]: 1, + }); + checkAndClearHistogram(dwellHistogram, false); + + info("Finding row with result type URL"); + let foundURLRow = false; + let count = UrlbarTestUtils.getResultCount(window); + for (let i = 0; i < count && !foundURLRow; i++) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + let index = UrlbarTestUtils.getSelectedRowIndex(window); + Assert.equal(index, i, "The expected row index should be selected"); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + info(`Checked row at index ${i}, result type is: ${details.type}`); + if (details.type == UrlbarUtils.RESULT_TYPE.URL) { + foundURLRow = true; + } + } + Assert.ok(foundURLRow, "Should have found a row with result type URL"); + + let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + }); + + checkScalars({ + [SCALARS.ENGAGEMENT]: 1, + }); + checkAndClearHistogram(dwellHistogram, true); +}); + +// zero prefix abandonment +add_task(async function abandonment() { + let dwellHistogram = + TelemetryTestUtils.getAndClearHistogram(HISTOGRAM_DWELL_TIME); + + // Open and close the view twice. The second time the view will used a cached + // query context and that shouldn't interfere with telemetry. + for (let i = 0; i < 2; i++) { + await showZeroPrefix(); + checkScalars({ + [SCALARS.EXPOSURE]: 1, + }); + checkAndClearHistogram(dwellHistogram, false); + + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + checkScalars({ + [SCALARS.ABANDONMENT]: 1, + }); + dwellHistogram = checkAndClearHistogram(dwellHistogram, true); + } +}); + +// Shows the zero-prefix view, does some searches, then shows it again by doing +// a search for an empty string. +add_task(async function searches() { + let dwellHistogram = + TelemetryTestUtils.getAndClearHistogram(HISTOGRAM_DWELL_TIME); + + info("Show zero prefix"); + await showZeroPrefix(); + checkScalars({ + [SCALARS.EXPOSURE]: 1, + }); + checkAndClearHistogram(dwellHistogram, false); + + info("Search for 't'"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "t", + }); + checkScalars({}); + dwellHistogram = checkAndClearHistogram(dwellHistogram, true); + + info("Search for 'te'"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "te", + }); + checkScalars({}); + checkAndClearHistogram(dwellHistogram, false); + + info("Search for 't'"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "t", + }); + checkScalars({}); + checkAndClearHistogram(dwellHistogram, false); + + info("Search for ''"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + checkScalars({ + [SCALARS.EXPOSURE]: 1, + }); + checkAndClearHistogram(dwellHistogram, false); + + info("Blur urlbar and close view"); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + checkScalars({ + [SCALARS.ABANDONMENT]: 1, + }); + checkAndClearHistogram(dwellHistogram, true); +}); + +// A zero prefix engagement should not be recorded when the view isn't showing +// zero prefix. +add_task(async function notZeroPrefix_engagement() { + let dwellHistogram = + TelemetryTestUtils.getAndClearHistogram(HISTOGRAM_DWELL_TIME); + + await BrowserTestUtils.withNewTab("about:blank", async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + }); + + checkScalars({}); + checkAndClearHistogram(dwellHistogram, false); +}); + +// A zero prefix abandonment should not be recorded when the view isn't showing +// zero prefix. +add_task(async function notZeroPrefix_abandonment() { + let dwellHistogram = + TelemetryTestUtils.getAndClearHistogram(HISTOGRAM_DWELL_TIME); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + + checkScalars({}); + checkAndClearHistogram(dwellHistogram, false); +}); + +function checkScalars(expected) { + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + for (let scalar of Object.values(SCALARS)) { + if (expected.hasOwnProperty(scalar)) { + TelemetryTestUtils.assertScalar(scalars, scalar, expected[scalar]); + } else { + Assert.ok( + !scalars.hasOwnProperty(scalar), + "Scalar should not be recorded: " + scalar + ); + } + } +} + +function checkAndClearHistogram(histogram, expected) { + if (expected) { + Assert.deepEqual( + Object.values(histogram.snapshot().values).filter(v => v > 0), + [1], + "Dwell histogram should be updated" + ); + } else { + Assert.strictEqual( + histogram.snapshot().sum, + 0, + "Dwell histogram should not be updated" + ); + } + + return TelemetryTestUtils.getAndClearHistogram(histogram.name()); +} + +async function showZeroPrefix() { + let { promise, cleanup } = waitForQueryFinished(); + await SimpleTest.promiseFocus(window); + await UrlbarTestUtils.promisePopupOpen(window, () => + document.getElementById("Browser:OpenLocation").doCommand() + ); + await promise; + cleanup(); + + Assert.greater( + UrlbarTestUtils.getResultCount(window), + 0, + "There should be at least one row in the zero prefix view" + ); +} + +/** + * Returns a promise that's resolved on the next `onQueryFinished()`. It's + * important to wait for `onQueryFinished()` because that's when the view checks + * whether it's showing zero prefix. + * + * @returns {object} + * An object with the following properties: + * {Promise} promise + * Resolved when `onQueryFinished()` is called. + * {Function} cleanup + * This should be called to remove the listener. + */ +function waitForQueryFinished() { + let deferred = PromiseUtils.defer(); + let listener = { + onQueryFinished: () => deferred.resolve(), + }; + gURLBar.controller.addQueryListener(listener); + + return { + promise: deferred.promise, + cleanup() { + gURLBar.controller.removeQueryListener(listener); + }, + }; +} + +async function updateTopSitesAndAwaitChanged() { + let url = "http://mochi.test:8888/topsite"; + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits(url); + } + + info("Updating top sites and awaiting newtab-top-sites-changed"); + let changedPromise = TestUtils.topicObserved("newtab-top-sites-changed").then( + () => info("Observed newtab-top-sites-changed") + ); + await updateTopSites(sites => sites?.length); + await changedPromise; +} 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..9d3e922692 --- /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.loadURIString(deletedURLTab.linkedBrowser, testURL); + BrowserTestUtils.loadURIString(fullURLTab.linkedBrowser, testURL); + BrowserTestUtils.loadURIString(partialURLTab.linkedBrowser, testURL); + await Promise.all([loaded1, loaded2, loaded3]); + + testURL = BrowserUIUtils.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, + testURL, + "gURLBar.value should be testURL 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..f7a2721093 --- /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..c4053eaed7 --- /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_setup(async function () { + 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..0cf56f107d --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_view_resultTypes_display.js @@ -0,0 +1,317 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { SyncedTabs } = ChromeUtils.importESModule( + "resource://services-sync/SyncedTabs.sys.mjs" +); + +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_setup(async function () { + 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], + // Turn autofill off. + ["browser.urlbar.autoFill", false], + ], + }); + + await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME, + setAsDefault: true, + }); + + // 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.promiseNativeMouseEvent({ + type: "mousemove", + target: gURLBar.inputField, + offsetX: 0, + offsetY: 0, + }); +}); + +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: Math.floor(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 UrlbarProviderRemoteTabs. + 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_view_selectionByMouse.js b/browser/components/urlbar/tests/browser/browser_view_selectionByMouse.js new file mode 100644 index 0000000000..2de8439b58 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_view_selectionByMouse.js @@ -0,0 +1,607 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test selection on result view by mouse. + +ChromeUtils.defineESModuleGetters(this, { + UrlbarProviderQuickActions: + "resource:///modules/UrlbarProviderQuickActions.sys.mjs", +}); + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.quickactions.enabled", true], + ["browser.urlbar.suggest.quickactions", true], + ["browser.urlbar.shortcuts.quickactions", true], + ], + }); + + UrlbarTestUtils.disableResultMenuAutohide(window); + + await SearchTestUtils.installSearchExtension({}, { setAsDefault: true }); + + UrlbarProviderQuickActions.addAction("test-addons", { + commands: ["test-addons"], + label: "quickactions-addons", + onPick: () => + BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, "about:about"), + }); + UrlbarProviderQuickActions.addAction("test-downloads", { + commands: ["test-downloads"], + label: "quickactions-downloads2", + onPick: () => + BrowserTestUtils.loadURIString( + gBrowser.selectedBrowser, + "about:downloads" + ), + }); + + registerCleanupFunction(function () { + UrlbarProviderQuickActions.removeAction("test-addons"); + UrlbarProviderQuickActions.removeAction("test-downloads"); + }); +}); + +add_task(async function basic() { + const testData = [ + { + description: "Normal result to quick action button", + mousedown: ".urlbarView-row:nth-child(1) > .urlbarView-row-inner", + mouseup: ".urlbarView-quickaction-row[data-key=test-downloads]", + expected: "about:downloads", + }, + { + description: "Normal result to out of result", + mousedown: ".urlbarView-row:nth-child(1) > .urlbarView-row-inner", + mouseup: "body", + expected: false, + }, + { + description: "Quick action button to normal result", + mousedown: ".urlbarView-quickaction-row[data-key=test-addons]", + mouseup: ".urlbarView-row:nth-child(1)", + expected: "https://example.com/?q=test", + }, + { + description: "Quick action button to quick action button", + mousedown: ".urlbarView-quickaction-row[data-key=test-addons]", + mouseup: ".urlbarView-quickaction-row[data-key=test-downloads]", + expected: "about:downloads", + }, + { + description: "Quick action button to out of result", + mousedown: ".urlbarView-quickaction-row[data-key=test-downloads]", + mouseup: "body", + expected: false, + }, + ]; + + for (const { description, mousedown, mouseup, expected } of testData) { + info(description); + + await BrowserTestUtils.withNewTab("about:blank", async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "test", + window, + }); + let [downElement, upElement] = await waitForElements([ + mousedown, + mouseup, + ]); + + EventUtils.synthesizeMouseAtCenter(downElement, { + type: "mousedown", + }); + Assert.ok( + downElement.hasAttribute("selected"), + "Mousedown element should be selected after mousedown" + ); + + EventUtils.synthesizeMouseAtCenter(upElement, { type: "mouseup" }); + Assert.ok( + !downElement.hasAttribute("selected"), + "Mousedown element should not be selected after mouseup" + ); + Assert.ok( + !upElement.hasAttribute("selected"), + "Mouseup element should not be selected after mouseup" + ); + + if (expected) { + await BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + expected + ); + Assert.ok(true, "Expected page is opened"); + } + }); + } +}); + +add_task(async function outOfBrowser() { + const testData = [ + { + description: "Normal result to out of browser", + mousedown: ".urlbarView-row:nth-child(1) > .urlbarView-row-inner", + }, + { + description: "Normal result to out of result", + mousedown: ".urlbarView-row:nth-child(1) > .urlbarView-row-inner", + expected: false, + }, + { + description: "Quick action button to out of browser", + mousedown: ".urlbarView-quickaction-row[data-key=test-addons]", + }, + ]; + + for (const { description, mousedown } of testData) { + info(description); + + await BrowserTestUtils.withNewTab("about:blank", async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "test", + window, + }); + let [downElement] = await waitForElements([mousedown]); + + EventUtils.synthesizeMouseAtCenter(downElement, { + type: "mousedown", + }); + Assert.ok( + downElement.hasAttribute("selected"), + "Mousedown element should be selected after mousedown" + ); + + // Mouseup at out of browser. + EventUtils.synthesizeMouse(document.documentElement, -1, -1, { + type: "mouseup", + }); + + Assert.ok( + !downElement.hasAttribute("selected"), + "Mousedown element should not be selected after mouseup" + ); + }); + } +}); + +add_task(async function withSelectionByKeyboard() { + const testData = [ + { + description: "Select normal result, then click on out of result", + mousedown: "body", + mouseup: "body", + expected: { + selectedElementByKey: + "#urlbar-results .urlbarView-row > .urlbarView-row-inner[selected]", + selectedElementAfterMouseDown: + "#urlbar-results .urlbarView-row > .urlbarView-row-inner[selected]", + actionedPage: false, + }, + }, + { + description: "Select quick action button, then click on out of result", + arrowDown: 1, + mousedown: "body", + mouseup: "body", + expected: { + selectedElementByKey: + "#urlbar-results .urlbarView-quickaction-row[selected]", + selectedElementAfterMouseDown: + "#urlbar-results .urlbarView-quickaction-row[selected]", + actionedPage: false, + }, + }, + { + description: "Select normal result, then click on about:downloads", + mousedown: ".urlbarView-quickaction-row[data-key=test-downloads]", + mouseup: ".urlbarView-quickaction-row[data-key=test-downloads]", + expected: { + selectedElementByKey: + "#urlbar-results .urlbarView-row > .urlbarView-row-inner[selected]", + selectedElementAfterMouseDown: + ".urlbarView-quickaction-row[data-key=test-downloads]", + actionedPage: "about:downloads", + }, + }, + ]; + + for (const { + description, + arrowDown, + mousedown, + mouseup, + expected, + } of testData) { + info(description); + + await BrowserTestUtils.withNewTab("about:blank", async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "test", + window, + }); + let [downElement, upElement] = await waitForElements([ + mousedown, + mouseup, + ]); + + if (arrowDown) { + EventUtils.synthesizeKey( + "KEY_ArrowDown", + { repeat: arrowDown }, + window + ); + } + + let [selectedElementByKey] = await waitForElements([ + expected.selectedElementByKey, + ]); + Assert.ok( + selectedElementByKey.hasAttribute("selected"), + "selectedElementByKey should be selected after arrow down" + ); + + EventUtils.synthesizeMouseAtCenter(downElement, { + type: "mousedown", + }); + + if ( + expected.selectedElementByKey !== expected.selectedElementAfterMouseDown + ) { + let [selectedElementAfterMouseDown] = await waitForElements([ + expected.selectedElementAfterMouseDown, + ]); + Assert.ok( + selectedElementAfterMouseDown.hasAttribute("selected"), + "selectedElementAfterMouseDown should be selected after mousedown" + ); + Assert.ok( + !selectedElementByKey.hasAttribute("selected"), + "selectedElementByKey should not be selected after mousedown" + ); + } + + EventUtils.synthesizeMouseAtCenter(upElement, { + type: "mouseup", + }); + + if (expected.actionedPage) { + Assert.ok( + !selectedElementByKey.hasAttribute("selected"), + "selectedElementByKey should not be selected after page starts load" + ); + await BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + expected.actionedPage + ); + Assert.ok(true, "Expected page is opened"); + } else { + Assert.ok( + selectedElementByKey.hasAttribute("selected"), + "selectedElementByKey should remain selected" + ); + } + }); + } +}); + +add_task(async function withDnsFirstForSingleWordsPref() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.fixup.dns_first_for_single_words", true]], + }); + + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "https://example.org/", + title: "example", + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "ex", + window, + }); + + const details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + const target = details.element.action; + EventUtils.synthesizeMouseAtCenter(target, { type: "mousedown" }); + const onLoaded = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + "https://example.org/" + ); + EventUtils.synthesizeMouseAtCenter(target, { type: "mouseup" }); + await onLoaded; + Assert.ok(true, "Expected page is opened"); + + await PlacesUtils.bookmarks.eraseEverything(); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function buttons() { + let initialTabUrl = "https://example.com/initial"; + let mainResultUrl = "https://example.com/main"; + let mainResultHelpUrl = "https://example.com/help"; + let otherResultUrl = "https://example.com/other"; + + let searchString = "test"; + + // Add a provider with two results: The first has buttons and the second has a + // URL that should or shouldn't become the input's value when the block button + // in the first result is clicked, depending on the test. + let provider = new UrlbarTestUtils.TestProvider({ + priority: Infinity, + results: [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + url: mainResultUrl, + helpUrl: mainResultHelpUrl, + helpL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-learn-more-about-firefox-suggest" + : "firefox-suggest-urlbar-learn-more", + }, + isBlockable: true, + blockL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-dismiss-firefox-suggest" + : "firefox-suggest-urlbar-block", + }, + } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + url: otherResultUrl, + } + ), + ], + }); + + // Implement the provider's `onEngagement()` so it removes the result. + let onEngagementCallCount = 0; + provider.onEngagement = (isPrivate, state, queryContext, details) => { + onEngagementCallCount++; + queryContext.view.controller.removeResult(details.result); + }; + + UrlbarProvidersManager.registerProvider(provider); + + let assertBlockResultCalled = () => { + Assert.equal( + onEngagementCallCount, + 1, + "blockResult() should have been called once" + ); + onEngagementCallCount = 0; + + let rowUrls = []; + let rows = UrlbarTestUtils.getResultsContainer(window).children; + for (let row of rows) { + rowUrls.push(row.result.payload.url); + } + Assert.ok( + !rowUrls.includes(mainResultUrl), + "The main result should not be in the view after blocking it: " + + JSON.stringify(rowUrls) + ); + }; + let assertResultMenuOpen = () => { + Assert.equal( + gURLBar.view.resultMenu.state, + "showing", + "Result menu is showing" + ); + EventUtils.synthesizeKey("KEY_Escape"); + }; + + let testData = [ + { + description: UrlbarPrefs.get("resultMenu") + ? "Menu button to menu button" + : "Block button to block button", + mousedown: UrlbarPrefs.get("resultMenu") + ? ".urlbarView-row:nth-child(1) .urlbarView-button-menu" + : ".urlbarView-row:nth-child(1) .urlbarView-button-block", + afterMouseupCallback: UrlbarPrefs.get("resultMenu") + ? assertResultMenuOpen + : assertBlockResultCalled, + expected: { + mousedownSelected: false, + topSites: { + pageProxyState: "valid", + value: initialTabUrl, + }, + searchString: { + pageProxyState: "invalid", + value: searchString, + }, + }, + }, + { + skip: UrlbarPrefs.get("resultMenu"), + description: "Help button to help button", + mousedown: ".urlbarView-row:nth-child(1) .urlbarView-button-help", + expected: { + mousedownSelected: false, + url: mainResultHelpUrl, + newTab: true, + }, + }, + { + description: UrlbarPrefs.get("resultMenu") + ? "Row-inner to menu button" + : "Row-inner to block button", + mousedown: ".urlbarView-row:nth-child(1) > .urlbarView-row-inner", + mouseup: UrlbarPrefs.get("resultMenu") + ? ".urlbarView-row:nth-child(1) .urlbarView-button-menu" + : ".urlbarView-row:nth-child(1) .urlbarView-button-block", + afterMouseupCallback: UrlbarPrefs.get("resultMenu") + ? assertResultMenuOpen + : assertBlockResultCalled, + expected: { + mousedownSelected: true, + topSites: { + pageProxyState: "invalid", + value: UrlbarPrefs.get("resultMenu") ? initialTabUrl : otherResultUrl, + }, + searchString: { + pageProxyState: "invalid", + value: UrlbarPrefs.get("resultMenu") ? searchString : otherResultUrl, + }, + }, + }, + { + description: UrlbarPrefs.get("resultMenu") + ? "Menu button to row-inner" + : "Block button to row-inner", + mousedown: UrlbarPrefs.get("resultMenu") + ? ".urlbarView-row:nth-child(1) .urlbarView-button-menu" + : ".urlbarView-row:nth-child(1) .urlbarView-button-block", + mouseup: ".urlbarView-row:nth-child(1) > .urlbarView-row-inner", + expected: { + mousedownSelected: false, + url: mainResultUrl, + newTab: false, + }, + }, + ]; + + for (let showTopSites of [true, false]) { + for (let { + description, + mousedown, + mouseup, + expected, + afterMouseupCallback = null, + skip = false, + } of testData) { + if (skip) { + info( + `Skipping test with showTopSites = ${showTopSites}: ${description}` + ); + continue; + } + info(`Running test with showTopSites = ${showTopSites}: ${description}`); + mouseup ||= mousedown; + + await BrowserTestUtils.withNewTab(initialTabUrl, async () => { + Assert.equal( + gURLBar.getAttribute("pageproxystate"), + "valid", + "Sanity check: pageproxystate should be valid initially" + ); + Assert.equal( + gURLBar.value, + initialTabUrl, + "Sanity check: input.value should be the initial URL initially" + ); + + if (showTopSites) { + // Open the view and show top sites by performing the accel+L command. + await SimpleTest.promiseFocus(window); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + document.getElementById("Browser:OpenLocation").doCommand(); + await searchPromise; + } else { + // Do a search. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: searchString, + }); + } + + let [downElement, upElement] = await waitForElements([ + mousedown, + mouseup, + ]); + + // Mousedown and check the selection. + EventUtils.synthesizeMouseAtCenter(downElement, { + type: "mousedown", + }); + if (expected.mousedownSelected) { + Assert.ok( + downElement.hasAttribute("selected"), + "Mousedown element should be selected after mousedown" + ); + } else { + Assert.ok( + !downElement.hasAttribute("selected"), + "Mousedown element should not be selected after mousedown" + ); + } + + let loadPromise; + if (expected.url) { + loadPromise = expected.newTab + ? BrowserTestUtils.waitForNewTab(gBrowser, expected.url) + : BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + null, + expected.url + ); + } + + // Mouseup and check the selection. + EventUtils.synthesizeMouseAtCenter(upElement, { type: "mouseup" }); + Assert.ok( + !downElement.hasAttribute("selected"), + "Mousedown element should not be selected after mouseup" + ); + Assert.ok( + !upElement.hasAttribute("selected"), + "Mouseup element should not be selected after mouseup" + ); + + // If we expect a URL to load, we're done since the view will close and + // the input value will be set to the URL. + if (loadPromise) { + info("Waiting for URL to load: " + expected.url); + let tab = await loadPromise; + Assert.ok(true, "Expected URL loaded"); + if (expected.newTab) { + BrowserTestUtils.removeTab(tab); + } + return; + } + + if (afterMouseupCallback) { + await afterMouseupCallback(); + } + + let state = showTopSites ? expected.topSites : expected.searchString; + Assert.equal( + gURLBar.getAttribute("pageproxystate"), + state.pageProxyState, + "pageproxystate should be as expected" + ); + Assert.equal( + gURLBar.value, + state.value, + "input.value should be as expected" + ); + }); + } + } + + UrlbarProvidersManager.unregisterProvider(provider); +}); + +async function waitForElements(selectors) { + let elements; + await BrowserTestUtils.waitForCondition(() => { + elements = selectors.map(s => document.querySelector(s)); + return elements.every(e => e && BrowserTestUtils.is_visible(e)); + }, "Waiting for elements to become visible: " + JSON.stringify(selectors)); + return elements; +} 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..339a20d90e --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_whereToOpen.js @@ -0,0 +1,192 @@ +/* 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 @@ + + +Dummy test page + + + +

Dummy test page

+ + 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 @@ + +
Click me 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 @@ + + + +Try editing the URL bar + + + + + + 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 @@ +bug562649 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..4c41483944 --- /dev/null +++ b/browser/components/urlbar/tests/browser/head-common.js @@ -0,0 +1,156 @@ +ChromeUtils.defineESModuleGetters(this, { + PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs", + PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", + Preferences: "resource://gre/modules/Preferences.sys.mjs", + UrlbarProvider: "resource:///modules/UrlbarUtils.sys.mjs", + UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs", + UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs", + UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.sys.mjs", + UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(this, { + HttpServer: "resource://testing-common/httpd.js", +}); + +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.importESModule( + "resource://testing-common/UrlbarTestUtils.sys.mjs" + ); + module.init(this); + return module; +}); + +XPCOMUtils.defineLazyGetter(this, "SearchTestUtils", () => { + const { SearchTestUtils: module } = ChromeUtils.importESModule( + "resource://testing-common/SearchTestUtils.sys.mjs" + ); + 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; + } +} + +/** + * 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.discoverystream.endpointSpocsClear", + "", + ], + ["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"); +} + +/** + * Asserts a search term is in the url bar and state values are + * what they should be. + * + * @param {string} searchString + * String that should be matched in the url bar. + * @param {object | null} options + * Options for the assertions. + * @param {Window | null} options.window + * Window to use for tests. + * @param {string | null} options.pageProxyState + * The pageproxystate that should be expected. Defaults to "valid". + * @param {string | null} options.userTypedValue + * The userTypedValue that should be expected. Defaults to null. + */ +function assertSearchStringIsInUrlbar( + searchString, + { win = window, pageProxyState = "valid", userTypedValue = null } = {} +) { + Assert.equal( + win.gURLBar.value, + searchString, + `Search string should be the urlbar value.` + ); + Assert.equal( + win.gBrowser.selectedBrowser.searchTerms, + searchString, + `Search terms should match.` + ); + Assert.equal( + win.gBrowser.userTypedValue, + userTypedValue, + "userTypedValue should match." + ); + Assert.equal( + win.gURLBar.getAttribute("pageproxystate"), + pageProxyState, + "Pageproxystate should match." + ); +} diff --git a/browser/components/urlbar/tests/browser/head.js b/browser/components/urlbar/tests/browser/head.js new file mode 100644 index 0000000000..4d381320c9 --- /dev/null +++ b/browser/components/urlbar/tests/browser/head.js @@ -0,0 +1,125 @@ +/* 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"; + +ChromeUtils.defineESModuleGetters(this, { + ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs", + ExperimentFakes: "resource://testing-common/NimbusTestUtils.sys.mjs", + PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs", + PromptTestUtils: "resource://testing-common/PromptTestUtils.sys.mjs", + ResetProfile: "resource://gre/modules/ResetProfile.sys.mjs", + SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs", + TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs", + UrlbarController: "resource:///modules/UrlbarController.sys.mjs", + UrlbarQueryContext: "resource:///modules/UrlbarUtils.sys.mjs", + UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs", + UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.sys.mjs", + UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs", + UrlbarView: "resource:///modules/UrlbarView.sys.mjs", + sinon: "resource://testing-common/Sinon.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(this, { + AboutNewTab: "resource:///modules/AboutNewTab.jsm", + ObjectUtils: "resource://gre/modules/ObjectUtils.jsm", +}); + +XPCOMUtils.defineLazyGetter(this, "PlacesFrecencyRecalculator", () => { + return Cc["@mozilla.org/places/frecency-recalculator;1"].getService( + Ci.nsIObserver + ).wrappedJSObject; +}); + +let sandbox; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/components/urlbar/tests/browser/head-common.js", + this +); + +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"); +} + +/** + * 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; +} + +/** + * Opens the url bar context menu by synthesizing a click. + * Returns a menu item that is specified by an id. + * + * @param {string} anonid - Identifier of a menu item of the url bar context menu. + * @returns {string} - The element that has the corresponding identifier. + */ +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); +} + +/** + * Puts all CustomizableUI widgetry back to their default locations, and + * then fires the `aftercustomization` toolbox event so that UrlbarInput + * knows to reinitialize itself. + * + * @param {window} [win=window] + * The top-level browser window to fire the `aftercustomization` event in. + */ +function resetCUIAndReinitUrlbarInput(win = window) { + CustomizableUI.reset(); + CustomizableUI.dispatchToolboxEvent("aftercustomization", {}, win); +} 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 Binary files /dev/null and b/browser/components/urlbar/tests/browser/moz.png 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..5884a1d598 --- /dev/null +++ b/browser/components/urlbar/tests/browser/print_postdata.sjs @@ -0,0 +1,25 @@ +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 { + let body = new BinaryInputStream(request.bodyInputStream); + + let avail; + let bytes = []; + + while ((avail = body.available()) > 0) { + Array.prototype.push.apply(bytes, body.readByteArray(avail)); + } + + let 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..a3937b0e7a --- /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/redirect_to.sjs b/browser/components/urlbar/tests/browser/redirect_to.sjs new file mode 100644 index 0000000000..b52ebdc63e --- /dev/null +++ b/browser/components/urlbar/tests/browser/redirect_to.sjs @@ -0,0 +1,9 @@ +"use strict"; + +function handleRequest(request, response) { + // redirect_to.sjs?ctxmenu-image.png + // redirects to : ctxmenu-image.png + const redirectUrl = request.queryString; + response.setStatusLine(request.httpVersion, "302", "Found"); + response.setHeader("Location", redirectUrl, false); +} diff --git a/browser/components/urlbar/tests/browser/searchSuggestionEngine.sjs b/browser/components/urlbar/tests/browser/searchSuggestionEngine.sjs new file mode 100644 index 0000000000..145392fcf2 --- /dev/null +++ b/browser/components/urlbar/tests/browser/searchSuggestionEngine.sjs @@ -0,0 +1,57 @@ +/* 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 @@ + + + + +browser_searchSuggestionEngine searchSuggestionEngine.xml + + + + + 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 @@ + + + + +browser_searchSuggestionEngine2 searchSuggestionEngine2.xml + + + + + + + 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 @@ + + + + +browser_searchSuggestionEngineMany searchSuggestionEngineMany.xml + + + + + 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 @@ + + + + +searchSuggestionEngineSlow.xml + + + + + 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("Not so slow!"); + return; + } + response.processAsync(); + timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.init( + () => { + response.setHeader("Content-Type", "text/html", false); + response.write("This is a slow loading page."); + 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 @@ + + +browser_urlbar_telemetry urlbarTelemetrySearchSuggestions.xml + + + 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/engagementTelemetry/browser/browser.ini b/browser/components/urlbar/tests/engagementTelemetry/browser/browser.ini new file mode 100644 index 0000000000..762173562d --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser.ini @@ -0,0 +1,52 @@ +# 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 + head-exposure.js + head-groups.js + head-interaction.js + head-n_chars_n_words.js + head-sap.js + head-search_mode.js + ../../browser-tips/head.js + ../../ext/browser/head.js + ../../ext/api.js + ../../ext/schema.json +skip-if = + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure + +[browser_glean_telemetry_abandonment_groups.js] +[browser_glean_telemetry_abandonment_interaction.js] +[browser_glean_telemetry_abandonment_interaction_persisted_search_terms_disabled.js] +[browser_glean_telemetry_abandonment_interaction_persisted_search_terms_enabled.js] +[browser_glean_telemetry_abandonment_n_chars_n_words.js] +[browser_glean_telemetry_abandonment_sap.js] +[browser_glean_telemetry_abandonment_search_mode.js] +[browser_glean_telemetry_abandonment_tips.js] +[browser_glean_telemetry_engagement_edge_cases.js] +[browser_glean_telemetry_engagement_groups.js] +[browser_glean_telemetry_engagement_interaction.js] +[browser_glean_telemetry_engagement_interaction_persisted_search_terms_disabled.js] +[browser_glean_telemetry_engagement_interaction_persisted_search_terms_enabled.js] +[browser_glean_telemetry_engagement_n_chars_n_words.js] +[browser_glean_telemetry_engagement_sap.js] +[browser_glean_telemetry_engagement_search_mode.js] +[browser_glean_telemetry_engagement_selected_result.js] +support-files = + ../../../../search/test/browser/trendingSuggestionEngine.sjs +[browser_glean_telemetry_engagement_tips.js] +[browser_glean_telemetry_engagement_type.js] +[browser_glean_telemetry_exposure.js] +[browser_glean_telemetry_impression_groups.js] +[browser_glean_telemetry_impression_interaction.js] +[browser_glean_telemetry_impression_interaction_persisted_search_terms_disabled.js] +[browser_glean_telemetry_impression_interaction_persisted_search_terms_enabled.js] +[browser_glean_telemetry_impression_n_chars_n_words.js] +[browser_glean_telemetry_impression_preferences.js] +[browser_glean_telemetry_impression_sap.js] +[browser_glean_telemetry_impression_search_mode.js] +[browser_glean_telemetry_impression_timing.js] +[browser_glean_telemetry_record_preferences.js] diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_groups.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_groups.js new file mode 100644 index 0000000000..6341a21a1a --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_groups.js @@ -0,0 +1,202 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for the following data of abandonment telemetry. +// - groups +// - results +// - n_results + +add_setup(async function () { + await initGroupTest(); +}); + +add_task(async function heuristics() { + await doHeuristicsTest({ + trigger: () => doBlur(), + assert: () => + assertAbandonmentTelemetry([ + { groups: "heuristic", results: "search_engine" }, + ]), + }); +}); + +add_task(async function adaptive_history() { + await doAdaptiveHistoryTest({ + trigger: () => doBlur(), + assert: () => + assertAbandonmentTelemetry([ + { + groups: "heuristic,adaptive_history", + results: "search_engine,history", + n_results: 2, + }, + ]), + }); +}); + +add_task(async function search_history() { + await doSearchHistoryTest({ + trigger: () => doBlur(), + assert: () => + assertAbandonmentTelemetry([ + { + groups: "heuristic,search_history,search_history", + results: "search_engine,search_history,search_history", + n_results: 3, + }, + ]), + }); +}); + +add_task(async function search_suggest() { + await doSearchSuggestTest({ + trigger: () => doBlur(), + assert: () => + assertAbandonmentTelemetry([ + { + groups: "heuristic,search_suggest,search_suggest", + results: "search_engine,search_suggest,search_suggest", + n_results: 3, + }, + ]), + }); + + await doTailSearchSuggestTest({ + trigger: () => doBlur(), + assert: () => + assertAbandonmentTelemetry([ + { + groups: "heuristic,search_suggest", + results: "search_engine,search_suggest", + n_results: 2, + }, + ]), + }); +}); + +add_task(async function top_pick() { + await doTopPickTest({ + trigger: () => doBlur(), + assert: () => + assertAbandonmentTelemetry([ + { + groups: "heuristic,top_pick,search_suggest,search_suggest", + results: + "search_engine,rs_adm_sponsored,search_suggest,search_suggest", + n_results: 4, + }, + ]), + }); +}); + +add_task(async function top_site() { + await doTopSiteTest({ + trigger: () => doBlur(), + assert: () => + assertAbandonmentTelemetry([ + { + groups: "top_site,suggested_index", + results: "top_site,action", + n_results: 2, + }, + ]), + }); +}); + +add_task(async function remote_tab() { + await doRemoteTabTest({ + trigger: () => doBlur(), + assert: () => + assertAbandonmentTelemetry([ + { + groups: "heuristic,remote_tab", + results: "search_engine,remote_tab", + n_results: 2, + }, + ]), + }); +}); + +add_task(async function addon() { + await doAddonTest({ + trigger: () => doBlur(), + assert: () => + assertAbandonmentTelemetry([ + { + groups: "addon", + results: "addon", + n_results: 1, + }, + ]), + }); +}); + +add_task(async function general() { + await doGeneralBookmarkTest({ + trigger: () => doBlur(), + assert: () => + assertAbandonmentTelemetry([ + { + groups: "heuristic,suggested_index,general", + results: "search_engine,action,bookmark", + n_results: 3, + }, + ]), + }); + + await doGeneralHistoryTest({ + trigger: () => doBlur(), + assert: () => + assertAbandonmentTelemetry([ + { + groups: "heuristic,general", + results: "search_engine,history", + n_results: 2, + }, + ]), + }); +}); + +add_task(async function suggest() { + await doSuggestTest({ + trigger: () => doBlur(), + assert: () => + assertAbandonmentTelemetry([ + { + groups: "heuristic,suggest", + results: "search_engine,rs_adm_nonsponsored", + n_results: 2, + }, + ]), + }); +}); + +add_task(async function about_page() { + await doAboutPageTest({ + trigger: () => doBlur(), + assert: () => + assertAbandonmentTelemetry([ + { + groups: "heuristic,about_page,about_page", + results: "search_engine,history,history", + n_results: 3, + }, + ]), + }); +}); + +add_task(async function suggested_index() { + await doSuggestedIndexTest({ + trigger: () => doBlur(), + assert: () => + assertAbandonmentTelemetry([ + { + groups: "heuristic,suggested_index", + results: "search_engine,unit", + n_results: 2, + }, + ]), + }); +}); diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_interaction.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_interaction.js new file mode 100644 index 0000000000..0462833008 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_interaction.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for the following data of abandonment telemetry. +// - interaction + +add_setup(async function () { + await initInteractionTest(); +}); + +add_task(async function topsites() { + await doTopsitesTest({ + trigger: () => doBlur(), + assert: () => assertAbandonmentTelemetry([{ interaction: "topsites" }]), + }); +}); + +add_task(async function typed() { + await doTypedTest({ + trigger: () => doBlur(), + assert: () => assertAbandonmentTelemetry([{ interaction: "typed" }]), + }); + + await doTypedWithResultsPopupTest({ + trigger: () => doBlur(), + assert: () => assertAbandonmentTelemetry([{ interaction: "typed" }]), + }); +}); + +add_task(async function pasted() { + await doPastedTest({ + trigger: () => doBlur(), + assert: () => assertAbandonmentTelemetry([{ interaction: "pasted" }]), + }); + + await doPastedWithResultsPopupTest({ + trigger: () => doBlur(), + assert: () => assertAbandonmentTelemetry([{ interaction: "pasted" }]), + }); +}); + +add_task(async function topsite_search() { + // TODO: https://bugzilla.mozilla.org/show_bug.cgi?id=1804010 + // assertAbandonmentTelemetry([{ interaction: "topsite_search" }]); +}); + +add_task(async function returned_restarted_refined() { + await doReturnedRestartedRefinedTest({ + trigger: () => doBlur(), + assert: expected => + assertAbandonmentTelemetry([ + { interaction: "typed" }, + { interaction: expected }, + ]), + }); +}); diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_interaction_persisted_search_terms_disabled.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_interaction_persisted_search_terms_disabled.js new file mode 100644 index 0000000000..68799544b0 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_interaction_persisted_search_terms_disabled.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test abandonment telemetry with persisted search terms disabled. + +// Allow more time for Mac machines so they don't time out in verify mode. +if (AppConstants.platform == "macosx") { + requestLongerTimeout(3); +} + +add_setup(async function () { + await initInteractionTest(); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", false]], + }); +}); + +add_task(async function persisted_search_terms() { + await doPersistedSearchTermsTest({ + trigger: () => doBlur(), + assert: () => assertAbandonmentTelemetry([{ interaction: "typed" }]), + }); +}); + +add_task(async function persisted_search_terms_restarted_refined() { + await doPersistedSearchTermsRestartedRefinedTest({ + enabled: false, + trigger: () => doBlur(), + assert: expected => assertAbandonmentTelemetry([{ interaction: expected }]), + }); +}); + +add_task( + async function persisted_search_terms_restarted_refined_via_abandonment() { + await doPersistedSearchTermsRestartedRefinedViaAbandonmentTest({ + enabled: false, + trigger: () => doBlur(), + assert: expected => + assertAbandonmentTelemetry([ + { interaction: "typed" }, + { interaction: expected }, + ]), + }); + } +); diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_interaction_persisted_search_terms_enabled.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_interaction_persisted_search_terms_enabled.js new file mode 100644 index 0000000000..f0a217805f --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_interaction_persisted_search_terms_enabled.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test abandonment telemetry with persisted search terms enabled. + +// Allow more time for Mac machines so they don't time out in verify mode. +if (AppConstants.platform == "macosx") { + requestLongerTimeout(3); +} + +add_setup(async function () { + await initInteractionTest(); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.showSearchTerms.featureGate", true], + ["browser.urlbar.showSearchTerms.enabled", true], + ["browser.search.widget.inNavBar", false], + ], + }); +}); + +add_task(async function persisted_search_terms() { + await doPersistedSearchTermsTest({ + trigger: () => doBlur(), + assert: () => + assertAbandonmentTelemetry([{ interaction: "persisted_search_terms" }]), + }); +}); + +add_task(async function persisted_search_terms_restarted_refined() { + await doPersistedSearchTermsRestartedRefinedTest({ + enabled: true, + trigger: () => doBlur(), + assert: expected => assertAbandonmentTelemetry([{ interaction: expected }]), + }); +}); + +add_task( + async function persisted_search_terms_restarted_refined_via_abandonment() { + await doPersistedSearchTermsRestartedRefinedViaAbandonmentTest({ + enabled: true, + trigger: () => doBlur(), + assert: expected => + assertAbandonmentTelemetry([ + { interaction: "persisted_search_terms_restarted" }, + { interaction: expected }, + ]), + }); + } +); diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_n_chars_n_words.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_n_chars_n_words.js new file mode 100644 index 0000000000..7427db8cbf --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_n_chars_n_words.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for the following data of abandonment telemetry. +// - n_chars +// - n_words + +add_setup(async function () { + await initNCharsAndNWordsTest(); +}); + +add_task(async function n_chars() { + await doNCharsTest({ + trigger: () => doBlur(), + assert: nChars => assertAbandonmentTelemetry([{ n_chars: nChars }]), + }); + + await doNCharsWithOverMaxTextLengthCharsTest({ + trigger: () => doBlur(), + assert: nChars => assertAbandonmentTelemetry([{ n_chars: nChars }]), + }); +}); + +add_task(async function n_words() { + await doNWordsTest({ + trigger: () => doBlur(), + assert: nWords => assertAbandonmentTelemetry([{ n_words: nWords }]), + }); + + await doNWordsWithOverMaxTextLengthCharsTest({ + trigger: () => doBlur(), + assert: nWords => assertAbandonmentTelemetry([{ n_words: nWords }]), + }); +}); diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_sap.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_sap.js new file mode 100644 index 0000000000..3d0af65379 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_sap.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for the following data of abandonment telemetry. +// - sap + +add_setup(async function () { + await initSapTest(); +}); + +add_task(async function urlbar_newtab() { + await doUrlbarNewTabTest({ + trigger: () => doBlur(), + assert: () => assertAbandonmentTelemetry([{ sap: "urlbar_newtab" }]), + }); +}); + +add_task(async function urlbar() { + await doUrlbarTest({ + trigger: () => doBlur(), + assert: () => assertAbandonmentTelemetry([{ sap: "urlbar" }]), + }); +}); + +add_task(async function handoff() { + await doHandoffTest({ + trigger: () => doBlur(), + assert: () => assertAbandonmentTelemetry([{ sap: "handoff" }]), + }); +}); + +add_task(async function urlbar_addonpage() { + await doUrlbarAddonpageTest({ + trigger: () => doBlur(), + assert: () => assertAbandonmentTelemetry([{ sap: "urlbar_addonpage" }]), + }); +}); diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_search_mode.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_search_mode.js new file mode 100644 index 0000000000..7edcc47a30 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_search_mode.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for the following data of abandonment telemetry. +// - search_mode + +add_setup(async function () { + await initSearchModeTest(); +}); + +add_task(async function not_search_mode() { + await doNotSearchModeTest({ + trigger: () => doBlur(), + assert: () => assertAbandonmentTelemetry([{ search_mode: "" }]), + }); +}); + +add_task(async function search_engine() { + await doSearchEngineTest({ + trigger: () => doBlur(), + assert: () => + assertAbandonmentTelemetry([{ search_mode: "search_engine" }]), + }); +}); + +add_task(async function bookmarks() { + await doBookmarksTest({ + trigger: () => doBlur(), + assert: () => assertAbandonmentTelemetry([{ search_mode: "bookmarks" }]), + }); +}); + +add_task(async function history() { + await doHistoryTest({ + trigger: () => doBlur(), + assert: () => assertAbandonmentTelemetry([{ search_mode: "history" }]), + }); +}); + +add_task(async function tabs() { + await doTabTest({ + trigger: () => doBlur(), + assert: () => assertAbandonmentTelemetry([{ search_mode: "tabs" }]), + }); +}); + +add_task(async function actions() { + await doActionsTest({ + trigger: () => doBlur(), + assert: () => assertAbandonmentTelemetry([{ search_mode: "actions" }]), + }); +}); diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_tips.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_tips.js new file mode 100644 index 0000000000..f773b0fb28 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_tips.js @@ -0,0 +1,88 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for abandonment telemetry for tips using Glean. + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/components/urlbar/tests/browser-tips/head.js", + this +); + +add_setup(async function () { + Services.fog.setMetricsFeatureConfig( + JSON.stringify({ "urlbar.abandonment": false }) + ); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.searchTips.test.ignoreShowLimits", true], + ["browser.urlbar.showSearchTerms.featureGate", true], + ], + }); + const engine = await SearchTestUtils.promiseNewSearchEngine({ + url: "chrome://mochitests/content/browser/browser/components/urlbar/tests/browser/searchSuggestionEngine.xml", + }); + const originalDefaultEngine = await Services.search.getDefault(); + await Services.search.setDefault( + engine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + await Services.search.moveEngine(engine, 0); + + registerCleanupFunction(async function () { + Services.fog.setMetricsFeatureConfig("{}"); + await SpecialPowers.popPrefEnv(); + await Services.search.setDefault( + originalDefaultEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + resetSearchTipsProvider(); + }); +}); + +add_task(async function tip_persist() { + await doTest(async browser => { + await showPersistSearchTip("test"); + gURLBar.focus(); + await UrlbarTestUtils.promisePopupClose(window, () => { + gURLBar.blur(); + }); + + assertAbandonmentTelemetry([{ results: "tip_persist" }]); + }); +}); + +add_task(async function mouse_down_with_tip() { + await doTest(async browser => { + await showPersistSearchTip("test"); + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeMouseAtCenter(browser, {}); + }); + + assertAbandonmentTelemetry([{ results: "tip_persist" }]); + }); +}); + +add_task(async function mouse_down_without_tip() { + await doTest(async browser => { + EventUtils.synthesizeMouseAtCenter(browser, {}); + + assertAbandonmentTelemetry([]); + }); +}); + +async function showPersistSearchTip(word) { + await openPopup(word); + await doEnter(); + await BrowserTestUtils.waitForCondition(async () => { + for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) { + const detail = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + if (detail.result.payload?.type === "searchTip_persist") { + return true; + } + } + return false; + }); +} diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_edge_cases.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_edge_cases.js new file mode 100644 index 0000000000..d7b2e775b8 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_edge_cases.js @@ -0,0 +1,218 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test edge cases for engagement. + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/components/urlbar/tests/ext/browser/head.js", + this +); + +add_setup(async function () { + await setup(); +}); + +/** + * UrlbarProvider that does not add any result. + */ +class NoResponseTestProvider extends UrlbarTestUtils.TestProvider { + constructor() { + super({ name: "TestProviderNoResponse ", results: [] }); + this.#deferred = PromiseUtils.defer(); + } + + get type() { + return UrlbarUtils.PROVIDER_TYPE.HEURISTIC; + } + + async startQuery(context, addCallback) { + await this.#deferred.promise; + } + + done() { + this.#deferred.resolve(); + } + + #deferred = null; +} +const noResponseProvider = new NoResponseTestProvider(); + +/** + * UrlbarProvider that adds a heuristic result immediately as usual. + */ +class AnotherHeuristicProvider extends UrlbarTestUtils.TestProvider { + constructor({ results }) { + super({ name: "TestProviderAnotherHeuristic ", results }); + this.#deferred = PromiseUtils.defer(); + } + + get type() { + return UrlbarUtils.PROVIDER_TYPE.HEURISTIC; + } + + async startQuery(context, addCallback) { + for (const result of this._results) { + addCallback(this, result); + } + + this.#deferred.resolve(context); + } + + onQueryStarted() { + return this.#deferred.promise; + } + + #deferred = null; +} +const anotherHeuristicProvider = new AnotherHeuristicProvider({ + results: [ + Object.assign( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { url: "https://example.com/immediate" } + ), + { heuristic: true } + ), + ], +}); + +add_task(async function engagement_before_showing_results() { + await SpecialPowers.pushPrefEnv({ + // Avoid showing search tip. + set: [["browser.urlbar.tipShownCount.searchTip_onboard", 999]], + }); + + // Update chunkResultsDelayMs to delay the call to notifyResults. + const originalChuldResultDelayMs = + UrlbarProvidersManager._chunkResultsDelayMs; + UrlbarProvidersManager._chunkResultsDelayMs = 1000000; + + // Add a provider that waits forever in startQuery() to avoid fireing + // heuristicProviderTimer. + UrlbarProvidersManager.registerProvider(noResponseProvider); + + // Add a provider that add a result immediately as usual. + UrlbarProvidersManager.registerProvider(anotherHeuristicProvider); + + const cleanup = () => { + UrlbarProvidersManager.unregisterProvider(noResponseProvider); + UrlbarProvidersManager.unregisterProvider(anotherHeuristicProvider); + UrlbarProvidersManager._chunkResultsDelayMs = originalChuldResultDelayMs; + }; + registerCleanupFunction(cleanup); + + await doTest(async browser => { + // Try to show the results. + await UrlbarTestUtils.inputIntoURLBar(window, "exam"); + + // Wait until starting the query and filling expected results. + const context = await anotherHeuristicProvider.onQueryStarted(); + const query = UrlbarProvidersManager.queries.get(context); + await BrowserTestUtils.waitForCondition( + () => + query.unsortedResults.some( + r => r.providerName === "HeuristicFallback" + ) && + query.unsortedResults.some( + r => r.providerName === anotherHeuristicProvider.name + ) + ); + + // Type Enter key before showing any results. + await doEnter(); + + assertEngagementTelemetry([ + { + selected_result: "input_field", + selected_result_subtype: "", + provider: undefined, + results: "", + groups: "", + }, + ]); + + // Clear the pending query. + noResponseProvider.done(); + }); + + cleanup(); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function engagement_after_closing_results() { + const TRIGGERS = [ + () => EventUtils.synthesizeKey("KEY_Escape"), + () => + EventUtils.synthesizeMouseAtCenter( + document.getElementById("customizableui-special-spring2"), + {} + ), + ]; + + for (const trigger of TRIGGERS) { + await doTest(async browser => { + await openPopup("test"); + await UrlbarTestUtils.promisePopupClose(window, () => { + trigger(); + }); + Assert.equal( + gURLBar.value, + "test", + "The inputted text remains even if closing the results" + ); + // The tested trigger should not record abandonment event. + assertAbandonmentTelemetry([]); + + // Endgagement. + await doEnter(); + + assertEngagementTelemetry([ + { + selected_result: "input_field", + selected_result_subtype: "", + provider: undefined, + results: "", + groups: "", + }, + ]); + }); + } +}); + +add_task(async function enter_to_reload_current_url() { + await doTest(async browser => { + // Open a URL once. + await openPopup("https://example.com"); + await doEnter(); + + // Focus the urlbar. + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + await BrowserTestUtils.waitForCondition( + () => window.document.activeElement === gURLBar.inputField + ); + await UrlbarTestUtils.promiseSearchComplete(window); + + // Press Enter key to reload the page without selecting any suggestions. + await doEnter(); + + assertEngagementTelemetry([ + { + selected_result: "url", + selected_result_subtype: "", + provider: "HeuristicFallback", + results: "url", + groups: "heuristic", + }, + { + selected_result: "input_field", + selected_result_subtype: "", + provider: undefined, + results: "action", + groups: "suggested_index", + }, + ]); + }); +}); diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_groups.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_groups.js new file mode 100644 index 0000000000..9060835562 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_groups.js @@ -0,0 +1,259 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for the following data of engagement telemetry. +// - groups +// - results +// - n_results + +add_setup(async function () { + await initGroupTest(); +}); + +add_task(async function heuristics() { + await doHeuristicsTest({ + trigger: () => doEnter(), + assert: () => + assertEngagementTelemetry([ + { groups: "heuristic", results: "search_engine" }, + ]), + }); +}); + +add_task(async function adaptive_history() { + await doAdaptiveHistoryTest({ + trigger: () => doEnter(), + assert: () => + assertEngagementTelemetry([ + { + groups: "heuristic,adaptive_history", + results: "search_engine,history", + n_results: 2, + }, + ]), + }); +}); + +add_task(async function search_history() { + await doSearchHistoryTest({ + trigger: () => doEnter(), + assert: () => + assertEngagementTelemetry([ + { + groups: "heuristic,search_history,search_history", + results: "search_engine,search_history,search_history", + n_results: 3, + }, + ]), + }); +}); + +add_task(async function search_suggest() { + await doSearchSuggestTest({ + trigger: () => doEnter(), + assert: () => + assertEngagementTelemetry([ + { + groups: "heuristic,search_suggest,search_suggest", + results: "search_engine,search_suggest,search_suggest", + n_results: 3, + }, + ]), + }); + + await doTailSearchSuggestTest({ + trigger: () => doEnter(), + assert: () => + assertEngagementTelemetry([ + { + groups: "heuristic,search_suggest", + results: "search_engine,search_suggest", + n_results: 2, + }, + ]), + }); +}); + +add_task(async function top_pick() { + await doTopPickTest({ + trigger: () => doEnter(), + assert: () => + assertEngagementTelemetry([ + { + groups: "heuristic,top_pick,search_suggest,search_suggest", + results: + "search_engine,rs_adm_sponsored,search_suggest,search_suggest", + n_results: 4, + }, + ]), + }); +}); + +add_task(async function top_site() { + await doTopSiteTest({ + trigger: () => doEnter(), + assert: () => + assertEngagementTelemetry([ + { + groups: "top_site,suggested_index", + results: "top_site,action", + n_results: 2, + }, + ]), + }); +}); + +add_task(async function remote_tab() { + await doRemoteTabTest({ + trigger: () => doEnter(), + assert: () => + assertEngagementTelemetry([ + { + groups: "heuristic,remote_tab", + results: "search_engine,remote_tab", + n_results: 2, + }, + ]), + }); +}); + +add_task(async function addon() { + await doAddonTest({ + trigger: () => doEnter(), + assert: () => + assertEngagementTelemetry([ + { + groups: "addon", + results: "addon", + n_results: 1, + }, + ]), + }); +}); + +add_task(async function general() { + await doGeneralBookmarkTest({ + trigger: () => doEnter(), + assert: () => + assertEngagementTelemetry([ + { + groups: "heuristic,suggested_index,general", + results: "search_engine,action,bookmark", + n_results: 3, + }, + ]), + }); + + await doGeneralHistoryTest({ + trigger: () => doEnter(), + assert: () => + assertEngagementTelemetry([ + { + groups: "heuristic,general", + results: "search_engine,history", + n_results: 2, + }, + ]), + }); +}); + +add_task(async function suggest() { + await doSuggestTest({ + trigger: () => doEnter(), + assert: () => + assertEngagementTelemetry([ + { + groups: "heuristic,suggest", + results: "search_engine,rs_adm_nonsponsored", + n_results: 2, + }, + ]), + }); +}); + +add_task(async function about_page() { + await doAboutPageTest({ + trigger: () => doEnter(), + assert: () => + assertEngagementTelemetry([ + { + groups: "heuristic,about_page,about_page", + results: "search_engine,history,history", + n_results: 3, + }, + ]), + }); +}); + +add_task(async function suggested_index() { + await doSuggestedIndexTest({ + trigger: () => + SimpleTest.promiseClipboardChange("100 cm", () => { + EventUtils.synthesizeKey("KEY_Enter"); + }), + assert: () => + assertEngagementTelemetry([ + { + groups: "heuristic,suggested_index", + results: "search_engine,unit", + n_results: 2, + }, + ]), + }); +}); + +add_task(async function always_empty_if_drop_go() { + const expected = [ + { + engagement_type: "drop_go", + groups: "", + results: "", + n_results: 0, + }, + ]; + + await doTest(async browser => { + await doDropAndGo("example.com"); + + assertEngagementTelemetry(expected); + }); + + await doTest(async browser => { + // Open the results view once. + await showResultByArrowDown(); + await UrlbarTestUtils.promisePopupClose(window); + + await doDropAndGo("example.com"); + + assertEngagementTelemetry(expected); + }); +}); + +add_task(async function always_empty_if_paste_go() { + const expected = [ + { + engagement_type: "paste_go", + groups: "", + results: "", + n_results: 0, + }, + ]; + + await doTest(async browser => { + await doPasteAndGo("example.com"); + + assertEngagementTelemetry(expected); + }); + + await doTest(async browser => { + // Open the results view once. + await showResultByArrowDown(); + await UrlbarTestUtils.promisePopupClose(window); + + await doPasteAndGo("example.com"); + + assertEngagementTelemetry(expected); + }); +}); diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_interaction.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_interaction.js new file mode 100644 index 0000000000..9de0de8953 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_interaction.js @@ -0,0 +1,87 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for the following data of engagement telemetry. +// - interaction + +// Allow more time for Mac machines so they don't time out in verify mode. +if (AppConstants.platform == "macosx") { + requestLongerTimeout(3); +} + +add_setup(async function () { + await initInteractionTest(); +}); + +add_task(async function topsites() { + await doTopsitesTest({ + trigger: () => doEnter(), + assert: () => assertEngagementTelemetry([{ interaction: "topsites" }]), + }); +}); + +add_task(async function typed() { + await doTypedTest({ + trigger: () => doEnter(), + assert: () => assertEngagementTelemetry([{ interaction: "typed" }]), + }); + + await doTypedWithResultsPopupTest({ + trigger: () => doEnter(), + assert: () => assertEngagementTelemetry([{ interaction: "typed" }]), + }); +}); + +add_task(async function dropped() { + await doTest(async browser => { + await doDropAndGo("example.com"); + + assertEngagementTelemetry([{ interaction: "dropped" }]); + }); + + await doTest(async browser => { + await showResultByArrowDown(); + await doDropAndGo("example.com"); + + assertEngagementTelemetry([{ interaction: "dropped" }]); + }); +}); + +add_task(async function pasted() { + await doPastedTest({ + trigger: () => doEnter(), + assert: () => assertEngagementTelemetry([{ interaction: "pasted" }]), + }); + + await doPastedWithResultsPopupTest({ + trigger: () => doEnter(), + assert: () => assertEngagementTelemetry([{ interaction: "pasted" }]), + }); + + await doTest(async browser => { + await doPasteAndGo("www.example.com"); + + assertEngagementTelemetry([{ interaction: "pasted" }]); + }); + + await doTest(async browser => { + await showResultByArrowDown(); + await doPasteAndGo("www.example.com"); + + assertEngagementTelemetry([{ interaction: "pasted" }]); + }); +}); + +add_task(async function topsite_search() { + // TODO: https://bugzilla.mozilla.org/show_bug.cgi?id=1804010 + // assertEngagementTelemetry([{ interaction: "topsite_search" }]); +}); + +add_task(async function returned_restarted_refined() { + await doReturnedRestartedRefinedTest({ + trigger: () => doEnter(), + assert: expected => assertEngagementTelemetry([{ interaction: expected }]), + }); +}); diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_interaction_persisted_search_terms_disabled.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_interaction_persisted_search_terms_disabled.js new file mode 100644 index 0000000000..1bdb4f0b61 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_interaction_persisted_search_terms_disabled.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test engagement telemetry with persisted search terms disabled. + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/components/urlbar/tests/engagementTelemetry/browser/head-interaction.js", + this +); + +// Allow more time for Mac machines so they don't time out in verify mode. +if (AppConstants.platform == "macosx") { + requestLongerTimeout(3); +} + +add_setup(async function () { + await initInteractionTest(); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", false]], + }); +}); + +add_task(async function persisted_search_terms() { + await doPersistedSearchTermsTest({ + trigger: () => doEnter(), + assert: () => + assertEngagementTelemetry([ + { interaction: "typed" }, + { interaction: "typed" }, + ]), + }); +}); + +add_task(async function persisted_search_terms_restarted_refined() { + await doPersistedSearchTermsRestartedRefinedTest({ + enabled: false, + trigger: () => doEnter(), + assert: expected => + assertEngagementTelemetry([ + { interaction: "typed" }, + { interaction: expected }, + ]), + }); +}); + +add_task( + async function persisted_search_terms_restarted_refined_via_abandonment() { + await doPersistedSearchTermsRestartedRefinedViaAbandonmentTest({ + enabled: false, + trigger: () => doEnter(), + assert: expected => + assertEngagementTelemetry([ + { interaction: "typed" }, + { interaction: expected }, + ]), + }); + } +); diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_interaction_persisted_search_terms_enabled.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_interaction_persisted_search_terms_enabled.js new file mode 100644 index 0000000000..33a01fdd22 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_interaction_persisted_search_terms_enabled.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test engagement telemetry with persisted search terms enabled. + +// Allow more time for Mac machines so they don't time out in verify mode. +if (AppConstants.platform == "macosx") { + requestLongerTimeout(3); +} + +add_setup(async function () { + await initInteractionTest(); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.showSearchTerms.featureGate", true], + ["browser.urlbar.showSearchTerms.enabled", true], + ["browser.search.widget.inNavBar", false], + ], + }); +}); + +add_task(async function persisted_search_terms() { + await doPersistedSearchTermsTest({ + trigger: () => doEnter(), + assert: () => + assertEngagementTelemetry([ + { interaction: "typed" }, + { interaction: "persisted_search_terms" }, + ]), + }); +}); + +add_task(async function persisted_search_terms_restarted_refined() { + await doPersistedSearchTermsRestartedRefinedTest({ + enabled: true, + trigger: () => doEnter(), + assert: expected => + assertEngagementTelemetry([ + { interaction: "typed" }, + { interaction: expected }, + ]), + }); +}); + +add_task( + async function persisted_search_terms_restarted_refined_via_abandonment() { + await doPersistedSearchTermsRestartedRefinedViaAbandonmentTest({ + enabled: true, + trigger: () => doEnter(), + assert: expected => + assertEngagementTelemetry([ + { interaction: "typed" }, + { interaction: expected }, + ]), + }); + } +); diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_n_chars_n_words.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_n_chars_n_words.js new file mode 100644 index 0000000000..498ffd9532 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_n_chars_n_words.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for the following data of engagement telemetry. +// - n_chars +// - n_words + +add_setup(async function () { + await initNCharsAndNWordsTest(); +}); + +add_task(async function n_chars() { + await doNCharsTest({ + trigger: () => doEnter(), + assert: nChars => assertEngagementTelemetry([{ n_chars: nChars }]), + }); + + await doNCharsWithOverMaxTextLengthCharsTest({ + trigger: () => doEnter(), + assert: nChars => assertEngagementTelemetry([{ n_chars: nChars }]), + }); +}); + +add_task(async function n_words() { + await doNWordsTest({ + trigger: () => doEnter(), + assert: nWords => assertEngagementTelemetry([{ n_words: nWords }]), + }); + + await doNWordsWithOverMaxTextLengthCharsTest({ + trigger: () => doEnter(), + assert: nWords => assertEngagementTelemetry([{ n_words: nWords }]), + }); +}); diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_sap.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_sap.js new file mode 100644 index 0000000000..d361d70229 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_sap.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for the following data of engagement telemetry. +// - sap + +add_setup(async function () { + await initSapTest(); +}); + +add_task(async function urlbar() { + await doUrlbarTest({ + trigger: () => doEnter(), + assert: () => + assertEngagementTelemetry([{ sap: "urlbar_newtab" }, { sap: "urlbar" }]), + }); +}); + +add_task(async function handoff() { + await doHandoffTest({ + trigger: () => doEnter(), + assert: () => assertEngagementTelemetry([{ sap: "handoff" }]), + }); +}); + +add_task(async function urlbar_addonpage() { + await doUrlbarAddonpageTest({ + trigger: () => doEnter(), + assert: () => assertEngagementTelemetry([{ sap: "urlbar_addonpage" }]), + }); +}); diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_search_mode.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_search_mode.js new file mode 100644 index 0000000000..62f87f0664 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_search_mode.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for the following data of engagement telemetry. +// - search_mode + +add_setup(async function () { + await initSearchModeTest(); +}); + +add_task(async function not_search_mode() { + await doNotSearchModeTest({ + trigger: () => doEnter(), + assert: () => assertEngagementTelemetry([{ search_mode: "" }]), + }); +}); + +add_task(async function search_engine() { + await doSearchEngineTest({ + trigger: () => doEnter(), + assert: () => assertEngagementTelemetry([{ search_mode: "search_engine" }]), + }); +}); + +add_task(async function bookmarks() { + await doBookmarksTest({ + trigger: () => doEnter(), + assert: () => assertEngagementTelemetry([{ search_mode: "bookmarks" }]), + }); +}); + +add_task(async function history() { + await doHistoryTest({ + trigger: () => doEnter(), + assert: () => assertEngagementTelemetry([{ search_mode: "history" }]), + }); +}); + +add_task(async function tabs() { + await doTabTest({ + trigger: async () => { + const currentTab = gBrowser.selectedTab; + EventUtils.synthesizeKey("KEY_Enter"); + await BrowserTestUtils.waitForCondition( + () => gBrowser.selectedTab !== currentTab + ); + }, + assert: () => assertEngagementTelemetry([{ search_mode: "tabs" }]), + }); +}); + +add_task(async function actions() { + await doActionsTest({ + trigger: async () => { + const onLoad = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + doClickSubButton(".urlbarView-quickaction-row[data-key=addons]"); + await onLoad; + }, + assert: () => assertEngagementTelemetry([{ search_mode: "actions" }]), + }); +}); diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_selected_result.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_selected_result.js new file mode 100644 index 0000000000..c19c511ccc --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_selected_result.js @@ -0,0 +1,920 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for the following data of engagement telemetry. +// - selected_result +// - selected_result_subtype +// - provider +// - results + +// This test has many subtests and can time out in verify mode. +requestLongerTimeout(5); + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/components/urlbar/tests/ext/browser/head.js", + this +); + +add_setup(async function () { + await setup(); +}); + +add_task(async function selected_result_autofill_about() { + await doTest(async browser => { + await openPopup("about:about"); + await doEnter(); + + assertEngagementTelemetry([ + { + selected_result: "autofill_about", + selected_result_subtype: "", + provider: "Autofill", + results: "autofill_about", + }, + ]); + }); +}); + +add_task(async function selected_result_autofill_adaptive() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.autoFill.adaptiveHistory.enabled", true]], + }); + + await doTest(async browser => { + await PlacesTestUtils.addVisits("https://example.com/test"); + await UrlbarUtils.addToInputHistory("https://example.com/test", "exa"); + await openPopup("exa"); + await doEnter(); + + assertEngagementTelemetry([ + { + selected_result: "autofill_adaptive", + selected_result_subtype: "", + provider: "Autofill", + results: "autofill_adaptive", + }, + ]); + }); + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function selected_result_autofill_origin() { + await doTest(async browser => { + await PlacesTestUtils.addVisits("https://example.com/test"); + await openPopup("exa"); + await doEnter(); + + assertEngagementTelemetry([ + { + selected_result: "autofill_origin", + selected_result_subtype: "", + provider: "Autofill", + results: "autofill_origin,history", + }, + ]); + }); +}); + +add_task(async function selected_result_autofill_url() { + await doTest(async browser => { + await PlacesTestUtils.addVisits("https://example.com/test"); + await openPopup("https://example.com/test"); + await doEnter(); + + assertEngagementTelemetry([ + { + selected_result: "autofill_url", + selected_result_subtype: "", + provider: "Autofill", + results: "autofill_url", + }, + ]); + }); +}); + +add_task(async function selected_result_bookmark() { + await doTest(async browser => { + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "https://example.com/bookmark", + title: "bookmark", + }); + + await openPopup("bookmark"); + await selectRowByURL("https://example.com/bookmark"); + await doEnter(); + + assertEngagementTelemetry([ + { + selected_result: "bookmark", + selected_result_subtype: "", + provider: "Places", + results: "search_engine,action,bookmark", + }, + ]); + }); +}); + +add_task(async function selected_result_history() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.autoFill", false]], + }); + + await doTest(async browser => { + await PlacesTestUtils.addVisits("https://example.com/test"); + + await openPopup("example"); + await selectRowByURL("https://example.com/test"); + await doEnter(); + + assertEngagementTelemetry([ + { + selected_result: "history", + selected_result_subtype: "", + provider: "Places", + results: "search_engine,history", + }, + ]); + }); + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function selected_result_keyword() { + await doTest(async browser => { + await PlacesUtils.keywords.insert({ + keyword: "keyword", + url: "https://example.com/?q=%s", + }); + + await openPopup("keyword test"); + await doEnter(); + + assertEngagementTelemetry([ + { + selected_result: "keyword", + selected_result_subtype: "", + provider: "BookmarkKeywords", + results: "keyword", + }, + ]); + + await PlacesUtils.keywords.remove("keyword"); + }); +}); + +add_task(async function selected_result_search_engine() { + await doTest(async browser => { + await openPopup("x"); + await doEnter(); + + assertEngagementTelemetry([ + { + selected_result: "search_engine", + selected_result_subtype: "", + provider: "HeuristicFallback", + results: "search_engine", + }, + ]); + }); +}); + +add_task(async function selected_result_search_suggest() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.searches", true], + ["browser.urlbar.maxHistoricalSearchSuggestions", 2], + ], + }); + + await doTest(async browser => { + await openPopup("foo"); + await selectRowByURL("http://mochi.test:8888/?terms=foofoo"); + await doEnter(); + + assertEngagementTelemetry([ + { + selected_result: "search_suggest", + selected_result_subtype: "", + provider: "SearchSuggestions", + results: "search_engine,search_suggest,search_suggest", + }, + ]); + }); + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function selected_result_search_history() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.searches", true], + ["browser.urlbar.maxHistoricalSearchSuggestions", 2], + ], + }); + + await doTest(async browser => { + await UrlbarTestUtils.formHistory.add(["foofoo", "foobar"]); + + await openPopup("foo"); + await selectRowByURL("http://mochi.test:8888/?terms=foofoo"); + await doEnter(); + + assertEngagementTelemetry([ + { + selected_result: "search_history", + selected_result_subtype: "", + provider: "SearchSuggestions", + results: "search_engine,search_history,search_history", + }, + ]); + }); + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function selected_result_url() { + await doTest(async browser => { + await openPopup("https://example.com/"); + await doEnter(); + + assertEngagementTelemetry([ + { + selected_result: "url", + selected_result_subtype: "", + provider: "HeuristicFallback", + results: "url", + }, + ]); + }); +}); + +add_task(async function selected_result_action() { + await doTest(async browser => { + await showResultByArrowDown(); + await selectRowByProvider("quickactions"); + const onLoad = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + doClickSubButton(".urlbarView-quickaction-row[data-key=addons]"); + await onLoad; + + assertEngagementTelemetry([ + { + selected_result: "action", + selected_result_subtype: "addons", + provider: "quickactions", + results: "action", + }, + ]); + }); +}); + +add_task(async function selected_result_tab() { + const tab = BrowserTestUtils.addTab(gBrowser, "https://example.com/"); + + await doTest(async browser => { + await openPopup("example"); + await selectRowByProvider("Places"); + EventUtils.synthesizeKey("KEY_Enter"); + await BrowserTestUtils.waitForCondition(() => gBrowser.selectedTab === tab); + + assertEngagementTelemetry([ + { + selected_result: "tab", + selected_result_subtype: "", + provider: "Places", + results: "search_engine,search_suggest,search_suggest,tab", + }, + ]); + }); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function selected_result_remote_tab() { + const remoteTab = await loadRemoteTab("https://example.com"); + + await doTest(async browser => { + await openPopup("example"); + await selectRowByProvider("RemoteTabs"); + await doEnter(); + + assertEngagementTelemetry([ + { + selected_result: "remote_tab", + selected_result_subtype: "", + provider: "RemoteTabs", + results: "search_engine,remote_tab", + }, + ]); + }); + + await remoteTab.unload(); +}); + +add_task(async function selected_result_addon() { + const addon = loadOmniboxAddon({ keyword: "omni" }); + await addon.startup(); + + await doTest(async browser => { + await openPopup("omni test"); + await doEnter(); + + assertEngagementTelemetry([ + { + selected_result: "addon", + selected_result_subtype: "", + provider: "Omnibox", + results: "addon", + }, + ]); + }); + + await addon.unload(); +}); + +add_task(async function selected_result_tab_to_search() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0]], + }); + + await SearchTestUtils.installSearchExtension({ + name: "mozengine", + search_url: "https://mozengine/", + }); + + await doTest(async browser => { + for (let i = 0; i < 3; i++) { + await PlacesTestUtils.addVisits(["https://mozengine/"]); + } + + await openPopup("moze"); + await selectRowByProvider("TabToSearch"); + const onComplete = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_Enter"); + await onComplete; + + assertEngagementTelemetry([ + { + selected_result: "tab_to_search", + selected_result_subtype: "", + provider: "TabToSearch", + results: "search_engine,tab_to_search,history", + }, + ]); + }); + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function selected_result_top_site() { + await doTest(async browser => { + await addTopSites("https://example.com/"); + await showResultByArrowDown(); + await selectRowByURL("https://example.com/"); + await doEnter(); + + assertEngagementTelemetry([ + { + selected_result: "top_site", + selected_result_subtype: "", + provider: "UrlbarProviderTopSites", + results: "top_site,action", + }, + ]); + }); +}); + +add_task(async function selected_result_calc() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.calculator", true]], + }); + + await doTest(async browser => { + await openPopup("8*8"); + await selectRowByProvider("calculator"); + await SimpleTest.promiseClipboardChange("64", () => { + EventUtils.synthesizeKey("KEY_Enter"); + }); + + assertEngagementTelemetry([ + { + selected_result: "calc", + selected_result_subtype: "", + provider: "calculator", + results: "search_engine,calc", + }, + ]); + }); + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function selected_result_unit() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.unitConversion.enabled", true]], + }); + + await doTest(async browser => { + await openPopup("1m to cm"); + await selectRowByProvider("UnitConversion"); + await SimpleTest.promiseClipboardChange("100 cm", () => { + EventUtils.synthesizeKey("KEY_Enter"); + }); + + assertEngagementTelemetry([ + { + selected_result: "unit", + selected_result_subtype: "", + provider: "UnitConversion", + results: "search_engine,unit", + }, + ]); + }); + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function selected_result_site_specific_contextual_search() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.contextualSearch.enabled", true]], + }); + + await doTest(async browser => { + const extension = await SearchTestUtils.installSearchExtension( + { + name: "Contextual", + search_url: "https://example.com/browser", + }, + { skipUnload: true } + ); + const onLoaded = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + "https://example.com/" + ); + BrowserTestUtils.loadURIString( + gBrowser.selectedBrowser, + "https://example.com/" + ); + await onLoaded; + + await openPopup("search"); + await selectRowByProvider("UrlbarProviderContextualSearch"); + await doEnter(); + + assertEngagementTelemetry([ + { + selected_result: "site_specific_contextual_search", + selected_result_subtype: "", + provider: "UrlbarProviderContextualSearch", + results: "search_engine,site_specific_contextual_search", + }, + ]); + + await extension.unload(); + }); + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function selected_result_experimental_addon() { + const extension = await loadExtension({ + background: async () => { + browser.experiments.urlbar.addDynamicResultType("testDynamicType"); + browser.experiments.urlbar.addDynamicViewTemplate("testDynamicType", { + children: [ + { + name: "text", + tag: "span", + attributes: { + role: "button", + }, + }, + ], + }); + browser.urlbar.onBehaviorRequested.addListener(query => { + return "active"; + }, "testProvider"); + browser.urlbar.onResultsRequested.addListener(query => { + return [ + { + type: "dynamic", + source: "local", + payload: { + dynamicType: "testDynamicType", + }, + }, + ]; + }, "testProvider"); + browser.experiments.urlbar.onViewUpdateRequested.addListener(payload => { + return { + text: { + textContent: "This is a dynamic result.", + }, + }; + }, "testProvider"); + }, + }); + + await TestUtils.waitForCondition( + () => + UrlbarProvidersManager.getProvider("testProvider") && + UrlbarResult.getDynamicResultType("testDynamicType"), + "Waiting for provider and dynamic type to be registered" + ); + + await doTest(async browser => { + await openPopup("test"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Enter") + ); + + assertEngagementTelemetry([ + { + selected_result: "experimental_addon", + selected_result_subtype: "", + provider: "testProvider", + results: "search_engine,experimental_addon", + }, + ]); + }); + + await extension.unload(); +}); + +add_task(async function selected_result_adm_sponsored() { + const cleanupQuickSuggest = await ensureQuickSuggestInit(); + + await doTest(async browser => { + await openPopup("sponsored"); + await selectRowByURL("https://example.com/sponsored"); + await doEnter(); + + assertEngagementTelemetry([ + { + selected_result: "rs_adm_sponsored", + selected_result_subtype: "", + provider: "UrlbarProviderQuickSuggest", + results: "search_engine,rs_adm_sponsored", + }, + ]); + }); + + cleanupQuickSuggest(); +}); + +add_task(async function selected_result_adm_nonsponsored() { + const cleanupQuickSuggest = await ensureQuickSuggestInit(); + + await doTest(async browser => { + await openPopup("nonsponsored"); + await selectRowByURL("https://example.com/nonsponsored"); + await doEnter(); + + assertEngagementTelemetry([ + { + selected_result: "rs_adm_nonsponsored", + selected_result_subtype: "", + provider: "UrlbarProviderQuickSuggest", + results: "search_engine,rs_adm_nonsponsored", + }, + ]); + }); + + cleanupQuickSuggest(); +}); + +add_task(async function selected_result_input_field() { + const expected = [ + { + selected_result: "input_field", + selected_result_subtype: "", + provider: null, + results: "", + }, + ]; + + await doTest(async browser => { + await doDropAndGo("example.com"); + + assertEngagementTelemetry(expected); + }); + + await doTest(async browser => { + await doPasteAndGo("example.com"); + + assertEngagementTelemetry(expected); + }); +}); + +add_task(async function selected_result_weather() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.quickactions.enabled", false]], + }); + + const cleanupQuickSuggest = await ensureQuickSuggestInit(); + await MerinoTestUtils.initWeather(); + + await doTest(async browser => { + await openPopup(MerinoTestUtils.WEATHER_KEYWORD); + await selectRowByProvider("Weather"); + await doEnter(); + + assertEngagementTelemetry([ + { + selected_result: "weather", + selected_result_subtype: "", + provider: "Weather", + results: "search_engine,weather", + }, + ]); + }); + + cleanupQuickSuggest(); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function selected_result_navigational() { + const cleanupQuickSuggest = await ensureQuickSuggestInit({ + merinoSuggestions: [ + { + title: "Navigational suggestion", + url: "https://example.com/navigational-suggestion", + provider: "top_picks", + is_sponsored: false, + score: 0.25, + block_id: 0, + is_top_pick: true, + }, + ], + }); + + await doTest(async browser => { + await openPopup("only match the Merino suggestion"); + await selectRowByProvider("UrlbarProviderQuickSuggest"); + await doEnter(); + + assertEngagementTelemetry([ + { + selected_result: "merino_top_picks", + selected_result_subtype: "", + provider: "UrlbarProviderQuickSuggest", + results: "search_engine,merino_top_picks", + }, + ]); + }); + + cleanupQuickSuggest(); +}); + +add_task(async function selected_result_dynamic_wikipedia() { + const cleanupQuickSuggest = await ensureQuickSuggestInit({ + merinoSuggestions: [ + { + block_id: 1, + url: "https://example.com/dynamic-wikipedia", + title: "Dynamic Wikipedia suggestion", + click_url: "https://example.com/click", + impression_url: "https://example.com/impression", + advertiser: "dynamic-wikipedia", + provider: "wikipedia", + iab_category: "5 - Education", + }, + ], + }); + + await doTest(async browser => { + await openPopup("only match the Merino suggestion"); + await selectRowByProvider("UrlbarProviderQuickSuggest"); + await doEnter(); + + assertEngagementTelemetry([ + { + selected_result: "merino_wikipedia", + selected_result_subtype: "", + provider: "UrlbarProviderQuickSuggest", + results: "search_engine,merino_wikipedia", + }, + ]); + }); + + cleanupQuickSuggest(); +}); + +add_task(async function selected_result_search_shortcut_button() { + await doTest(async browser => { + const oneOffSearchButtons = UrlbarTestUtils.getOneOffSearchButtons(window); + await openPopup("x"); + Assert.ok(!oneOffSearchButtons.selectedButton); + + // Select oneoff button added for test in setup(). + for (;;) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + if (!oneOffSearchButtons.selectedButton) { + continue; + } + + if ( + oneOffSearchButtons.selectedButton.engine.name.includes( + "searchSuggestionEngine.xml" + ) + ) { + break; + } + } + + // Search immediately. + await doEnter({ shiftKey: true }); + + assertEngagementTelemetry([ + { + selected_result: "search_shortcut_button", + selected_result_subtype: "", + provider: null, + results: "search_engine", + }, + ]); + }); +}); + +add_task(async function selected_result_trending() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.searches", true], + ["browser.urlbar.trending.featureGate", true], + ["browser.urlbar.trending.requireSearchMode", false], + ["browser.urlbar.trending.maxResultsNoSearchMode", 1], + ["browser.urlbar.weather.featureGate", false], + ], + }); + + let defaultEngine = await Services.search.getDefault(); + let extension = await SearchTestUtils.installSearchExtension( + { + name: "mozengine", + search_url: "https://example.org/", + }, + { setAsDefault: true, skipUnload: true } + ); + + SearchTestUtils.useMockIdleService(); + await SearchTestUtils.updateRemoteSettingsConfig([ + { + webExtension: { id: "mozengine@tests.mozilla.org" }, + urls: { + trending: { + fullPath: + "https://example.com/browser/browser/components/search/test/browser/trendingSuggestionEngine.sjs", + query: "", + }, + }, + appliesTo: [{ included: { everywhere: true } }], + default: "yes", + }, + ]); + + await doTest(async browser => { + await openPopup(""); + await selectRowByProvider("SearchSuggestions"); + await doEnter(); + + assertEngagementTelemetry([ + { + selected_result: "trending_search", + selected_result_subtype: "", + provider: "SearchSuggestions", + results: "trending_search", + }, + ]); + }); + + await extension.unload(); + await Services.search.setDefault( + defaultEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + let settingsWritten = SearchTestUtils.promiseSearchNotification( + "write-settings-to-disk-complete" + ); + await SearchTestUtils.updateRemoteSettingsConfig(); + await settingsWritten; + await PlacesUtils.history.clear(); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function selected_result_trending_rich() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.richSuggestions.featureGate", true], + ["browser.urlbar.suggest.searches", true], + ["browser.urlbar.trending.featureGate", true], + ["browser.urlbar.trending.requireSearchMode", false], + ["browser.urlbar.trending.maxResultsNoSearchMode", 1], + ["browser.urlbar.weather.featureGate", false], + ], + }); + + let defaultEngine = await Services.search.getDefault(); + let extension = await SearchTestUtils.installSearchExtension( + { + name: "mozengine", + search_url: "https://example.org/", + }, + { setAsDefault: true, skipUnload: true } + ); + + SearchTestUtils.useMockIdleService(); + await SearchTestUtils.updateRemoteSettingsConfig([ + { + webExtension: { id: "mozengine@tests.mozilla.org" }, + urls: { + trending: { + fullPath: + "https://example.com/browser/browser/components/search/test/browser/trendingSuggestionEngine.sjs?richsuggestions=true", + query: "", + }, + }, + appliesTo: [{ included: { everywhere: true } }], + default: "yes", + }, + ]); + + await doTest(async browser => { + await openPopup(""); + await selectRowByProvider("SearchSuggestions"); + await doEnter(); + + assertEngagementTelemetry([ + { + selected_result: "trending_search_rich", + selected_result_subtype: "", + provider: "SearchSuggestions", + results: "trending_search_rich", + }, + ]); + }); + + await extension.unload(); + await Services.search.setDefault( + defaultEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + let settingsWritten = SearchTestUtils.promiseSearchNotification( + "write-settings-to-disk-complete" + ); + await SearchTestUtils.updateRemoteSettingsConfig(); + await settingsWritten; + await PlacesUtils.history.clear(); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function selected_result_addons() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.addons.featureGate", true], + ["browser.urlbar.suggest.searches", false], + ], + }); + + const cleanupQuickSuggest = await ensureQuickSuggestInit({ + merinoSuggestions: [ + { + provider: "amo", + icon: "https://example.com/good-addon.svg", + url: "https://example.com/good-addon", + title: "Good Addon", + description: "This is a good addon", + custom_details: { + amo: { + rating: "4.8", + number_of_ratings: "1234567", + guid: "good@addon", + }, + }, + is_top_pick: true, + }, + ], + }); + + await doTest(async browser => { + await openPopup("only match the Merino suggestion"); + await selectRowByProvider("UrlbarProviderQuickSuggest"); + await doEnter(); + + assertEngagementTelemetry([ + { + selected_result: "merino_amo", + selected_result_subtype: "", + provider: "UrlbarProviderQuickSuggest", + results: "search_engine,merino_amo", + }, + ]); + }); + + cleanupQuickSuggest(); + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_tips.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_tips.js new file mode 100644 index 0000000000..104e292788 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_tips.js @@ -0,0 +1,175 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for engagement telemetry for tips using Glean. + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/components/urlbar/tests/browser-tips/head.js", + this +); + +ChromeUtils.defineESModuleGetters(this, { + PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs", +}); + +add_setup(async function () { + makeProfileResettable(); + + Services.fog.setMetricsFeatureConfig( + JSON.stringify({ "urlbar.engagement": false }) + ); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.quickactions.enabled", false]], + }); + + registerCleanupFunction(async function () { + Services.fog.setMetricsFeatureConfig("{}"); + await SpecialPowers.popPrefEnv(); + }); +}); + +add_task(async function selected_result_tip() { + const testData = [ + { + type: "searchTip_onboard", + expected: "tip_onboard", + }, + { + type: "searchTip_persist", + expected: "tip_persist", + }, + { + type: "searchTip_redirect", + expected: "tip_redirect", + }, + { + type: "test", + expected: "tip_unknown", + }, + ]; + + for (const { type, expected } of testData) { + const deferred = PromiseUtils.defer(); + const provider = new UrlbarTestUtils.TestProvider({ + results: [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TIP, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + type, + helpUrl: "https://example.com/", + titleL10n: { id: "urlbar-search-tips-confirm" }, + buttons: [ + { + url: "https://example.com/", + l10n: { id: "urlbar-search-tips-confirm" }, + }, + ], + } + ), + ], + priority: 1, + onEngagement: () => { + deferred.resolve(); + }, + }); + UrlbarProvidersManager.registerProvider(provider); + + await doTest(async browser => { + await openPopup("example"); + await selectRowByType(type); + EventUtils.synthesizeKey("VK_RETURN"); + await deferred.promise; + + assertEngagementTelemetry([ + { + selected_result: expected, + results: expected, + }, + ]); + }); + + UrlbarProvidersManager.unregisterProvider(provider); + } +}); + +add_task(async function selected_result_intervention_clear() { + await doInterventionTest( + SEARCH_STRINGS.CLEAR, + "intervention_clear", + "chrome://browser/content/sanitize.xhtml", + [ + { + selected_result: "intervention_clear", + results: "search_engine,intervention_clear", + }, + ] + ); +}); + +add_task(async function selected_result_intervention_refresh() { + await doInterventionTest( + SEARCH_STRINGS.REFRESH, + "intervention_refresh", + "chrome://global/content/resetProfile.xhtml", + [ + { + selected_result: "intervention_refresh", + results: "search_engine,intervention_refresh", + }, + ] + ); +}); + +add_task(async function selected_result_intervention_update() { + // Updates are disabled for MSIX packages, this test is irrelevant for them. + if ( + AppConstants.platform === "win" && + Services.sysinfo.getProperty("hasWinPackageId") + ) { + return; + } + await UpdateUtils.setAppUpdateAutoEnabled(false); + await initUpdate({ queryString: "&noUpdates=1" }); + UrlbarProviderInterventions.checkForBrowserUpdate(true); + await processUpdateSteps([ + { + panelId: "checkingForUpdates", + checkActiveUpdate: null, + continueFile: CONTINUE_CHECK, + }, + { + panelId: "noUpdatesFound", + checkActiveUpdate: null, + continueFile: null, + }, + ]); + + await doInterventionTest( + SEARCH_STRINGS.UPDATE, + "intervention_update_refresh", + "chrome://global/content/resetProfile.xhtml", + [ + { + selected_result: "intervention_update", + results: "search_engine,intervention_update", + }, + ] + ); +}); + +async function doInterventionTest(keyword, type, dialog, expectedTelemetry) { + await doTest(async browser => { + await openPopup(keyword); + await selectRowByType(type); + const onDialog = BrowserTestUtils.promiseAlertDialog("cancel", dialog, { + isSubDialog: true, + }); + EventUtils.synthesizeKey("VK_RETURN"); + await onDialog; + + assertEngagementTelemetry(expectedTelemetry); + }); +} diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_type.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_type.js new file mode 100644 index 0000000000..b2b2233b52 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_type.js @@ -0,0 +1,151 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for the following data of engagement telemetry. +// - engagement_type + +add_setup(async function () { + await setup(); +}); + +add_task(async function engagement_type_click() { + await doTest(async browser => { + await openPopup("x"); + await doClick(); + + assertEngagementTelemetry([{ engagement_type: "click" }]); + }); +}); + +add_task(async function engagement_type_enter() { + await doTest(async browser => { + await openPopup("x"); + await doEnter(); + + assertEngagementTelemetry([{ engagement_type: "enter" }]); + }); +}); + +add_task(async function engagement_type_go_button() { + await doTest(async browser => { + await openPopup("x"); + EventUtils.synthesizeMouseAtCenter(gURLBar.goButton, {}); + + assertEngagementTelemetry([{ engagement_type: "go_button" }]); + }); +}); + +add_task(async function engagement_type_drop_go() { + await doTest(async browser => { + await doDropAndGo("example.com"); + + assertEngagementTelemetry([{ engagement_type: "drop_go" }]); + }); +}); + +add_task(async function engagement_type_paste_go() { + await doTest(async browser => { + await doPasteAndGo("www.example.com"); + + assertEngagementTelemetry([{ engagement_type: "paste_go" }]); + }); +}); + +add_task(async function engagement_type_dismiss() { + const cleanupQuickSuggest = await ensureQuickSuggestInit({ + // eslint-disable-next-line mozilla/valid-lazy + config: lazy.QuickSuggestTestUtils.BEST_MATCH_CONFIG, + }); + + for (const isBestMatchTest of [true, false]) { + const prefs = isBestMatchTest + ? [ + ["browser.urlbar.bestMatch.enabled", true], + ["browser.urlbar.bestMatch.blockingEnabled", true], + ["browser.urlbar.quicksuggest.blockingEnabled", false], + ] + : [ + ["browser.urlbar.bestMatch.enabled", false], + ["browser.urlbar.bestMatch.blockingEnabled", false], + ["browser.urlbar.quicksuggest.blockingEnabled", true], + ]; + await SpecialPowers.pushPrefEnv({ set: prefs }); + + await doTest(async browser => { + await openPopup("sponsored"); + + const originalResultCount = UrlbarTestUtils.getResultCount(window); + await selectRowByURL("https://example.com/sponsored"); + if (UrlbarPrefs.get("resultMenu")) { + UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "D"); + } else { + doClickSubButton(".urlbarView-button-block"); + } + await BrowserTestUtils.waitForCondition( + () => originalResultCount != UrlbarTestUtils.getResultCount(window) + ); + + assertEngagementTelemetry([{ engagement_type: "dismiss" }]); + + // The view should stay open after dismissing the result. Now pick the + // heuristic result. Another "click" engagement event should be recorded. + Assert.ok( + gURLBar.view.isOpen, + "View should remain open after dismissing result" + ); + await doClick(); + assertEngagementTelemetry([ + { engagement_type: "dismiss" }, + { engagement_type: "click", interaction: "typed" }, + ]); + }); + + await doTest(async browser => { + await openPopup("sponsored"); + + const originalResultCount = UrlbarTestUtils.getResultCount(window); + await selectRowByURL("https://example.com/sponsored"); + EventUtils.synthesizeKey("KEY_Delete", { shiftKey: true }); + await BrowserTestUtils.waitForCondition( + () => originalResultCount != UrlbarTestUtils.getResultCount(window) + ); + + assertEngagementTelemetry([{ engagement_type: "dismiss" }]); + }); + + await SpecialPowers.popPrefEnv(); + } + + cleanupQuickSuggest(); +}); + +add_task(async function engagement_type_help() { + const cleanupQuickSuggest = await ensureQuickSuggestInit(); + + for (const isBestMatchTest of [true, false]) { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.bestMatch.enabled", isBestMatchTest]], + }); + + await doTest(async browser => { + await openPopup("sponsored"); + await selectRowByURL("https://example.com/sponsored"); + const onTabOpened = BrowserTestUtils.waitForNewTab(gBrowser); + if (UrlbarPrefs.get("resultMenu")) { + UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "L"); + } else { + doClickSubButton(".urlbarView-button-help"); + } + const tab = await onTabOpened; + BrowserTestUtils.removeTab(tab); + + assertEngagementTelemetry([{ engagement_type: "help" }]); + }); + + await SpecialPowers.popPrefEnv(); + } + + cleanupQuickSuggest(); +}); diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_exposure.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_exposure.js new file mode 100644 index 0000000000..7529f29455 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_exposure.js @@ -0,0 +1,107 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const SPONSORED_QUERY = "sponsored"; +const NONSPONSORED_QUERY = "nonsponsored"; + +// test for exposure events +add_setup(async function () { + await initExposureTest(); +}); + +add_task(async function exposureSponsoredOnEngagement() { + await doExposureTest({ + prefs: [ + ["browser.urlbar.exposureResults", "rs_adm_sponsored"], + ["browser.urlbar.showExposureResults", true], + ], + query: SPONSORED_QUERY, + trigger: () => doClick(), + assert: () => assertExposureTelemetry([{ results: "rs_adm_sponsored" }]), + }); +}); + +add_task(async function exposureSponsoredOnAbandonment() { + await doExposureTest({ + prefs: [ + ["browser.urlbar.exposureResults", "rs_adm_sponsored"], + ["browser.urlbar.showExposureResults", true], + ], + query: SPONSORED_QUERY, + trigger: () => doBlur(), + assert: () => assertExposureTelemetry([{ results: "rs_adm_sponsored" }]), + }); +}); + +add_task(async function exposureFilter() { + await doExposureTest({ + prefs: [ + ["browser.urlbar.exposureResults", "rs_adm_sponsored"], + ["browser.urlbar.showExposureResults", false], + ], + query: SPONSORED_QUERY, + select: async () => { + // assert that the urlbar has no results + Assert.equal(await getResultByType("rs_adm_sponsored"), null); + }, + trigger: () => doBlur(), + assert: () => assertExposureTelemetry([{ results: "rs_adm_sponsored" }]), + }); +}); + +add_task(async function innerQueryExposure() { + await doExposureTest({ + prefs: [ + ["browser.urlbar.exposureResults", "rs_adm_sponsored"], + ["browser.urlbar.showExposureResults", true], + ], + query: NONSPONSORED_QUERY, + select: () => {}, + trigger: async () => { + // delete the old query + gURLBar.select(); + EventUtils.synthesizeKey("KEY_Backspace"); + await openPopup(SPONSORED_QUERY); + await defaultSelect(SPONSORED_QUERY); + await doClick(); + }, + assert: () => assertExposureTelemetry([{ results: "rs_adm_sponsored" }]), + }); +}); + +add_task(async function innerQueryInvertedExposure() { + await doExposureTest({ + prefs: [ + ["browser.urlbar.exposureResults", "rs_adm_sponsored"], + ["browser.urlbar.showExposureResults", true], + ], + query: SPONSORED_QUERY, + select: () => {}, + trigger: async () => { + // delete the old query + gURLBar.select(); + EventUtils.synthesizeKey("KEY_Backspace"); + await openPopup(NONSPONSORED_QUERY); + await defaultSelect(SPONSORED_QUERY); + await doClick(); + }, + assert: () => assertExposureTelemetry([{ results: "rs_adm_sponsored" }]), + }); +}); + +add_task(async function multipleProviders() { + await doExposureTest({ + prefs: [ + [ + "browser.urlbar.exposureResults", + "rs_adm_sponsored,rs_adm_nonsponsored", + ], + ["browser.urlbar.showExposureResults", true], + ], + query: NONSPONSORED_QUERY, + trigger: () => doClick(), + assert: () => assertExposureTelemetry([{ results: "rs_adm_nonsponsored" }]), + }); +}); diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_groups.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_groups.js new file mode 100644 index 0000000000..199460e312 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_groups.js @@ -0,0 +1,223 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for the following data of impression telemetry. +// - groups +// - results +// - n_results + +add_setup(async function () { + await initGroupTest(); + // Increase the pausing time to ensure to ready for all suggestions. + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "browser.urlbar.searchEngagementTelemetry.pauseImpressionIntervalMs", + 500, + ], + ], + }); +}); + +add_task(async function heuristics() { + await doHeuristicsTest({ + trigger: () => waitForPauseImpression(), + assert: () => + assertImpressionTelemetry([ + { reason: "pause", groups: "heuristic", results: "search_engine" }, + ]), + }); +}); + +add_task(async function adaptive_history() { + await doAdaptiveHistoryTest({ + trigger: () => waitForPauseImpression(), + assert: () => + assertImpressionTelemetry([ + { + reason: "pause", + groups: "heuristic,adaptive_history", + results: "search_engine,history", + n_results: 2, + }, + ]), + }); +}); + +add_task(async function search_history() { + await doSearchHistoryTest({ + trigger: () => waitForPauseImpression(), + assert: () => + assertImpressionTelemetry([ + { + reason: "pause", + groups: "heuristic,search_history,search_history", + results: "search_engine,search_history,search_history", + n_results: 3, + }, + ]), + }); +}); + +add_task(async function search_suggest() { + await doSearchSuggestTest({ + trigger: () => waitForPauseImpression(), + assert: () => + assertImpressionTelemetry([ + { + reason: "pause", + groups: "heuristic,search_suggest,search_suggest", + results: "search_engine,search_suggest,search_suggest", + n_results: 3, + }, + ]), + }); + + await doTailSearchSuggestTest({ + trigger: () => waitForPauseImpression(), + assert: () => + assertImpressionTelemetry([ + { + reason: "pause", + groups: "heuristic,search_suggest", + results: "search_engine,search_suggest", + n_results: 2, + }, + ]), + }); +}); + +add_task(async function top_pick() { + await doTopPickTest({ + trigger: () => waitForPauseImpression(), + assert: () => + assertImpressionTelemetry([ + { + reason: "pause", + groups: "heuristic,top_pick,search_suggest,search_suggest", + results: + "search_engine,rs_adm_sponsored,search_suggest,search_suggest", + n_results: 4, + }, + ]), + }); +}); + +add_task(async function top_site() { + await doTopSiteTest({ + trigger: () => waitForPauseImpression(), + assert: () => + assertImpressionTelemetry([ + { + reason: "pause", + groups: "top_site,suggested_index", + results: "top_site,action", + n_results: 2, + }, + ]), + }); +}); + +add_task(async function remote_tab() { + await doRemoteTabTest({ + trigger: () => waitForPauseImpression(), + assert: () => + assertImpressionTelemetry([ + { + reason: "pause", + groups: "heuristic,remote_tab", + results: "search_engine,remote_tab", + n_results: 2, + }, + ]), + }); +}); + +add_task(async function addon() { + await doAddonTest({ + trigger: () => waitForPauseImpression(), + assert: () => + assertImpressionTelemetry([ + { + reason: "pause", + groups: "addon", + results: "addon", + n_results: 1, + }, + ]), + }); +}); + +add_task(async function general() { + await doGeneralBookmarkTest({ + trigger: () => waitForPauseImpression(), + assert: () => + assertImpressionTelemetry([ + { + reason: "pause", + groups: "heuristic,suggested_index,general", + results: "search_engine,action,bookmark", + n_results: 3, + }, + ]), + }); + + await doGeneralHistoryTest({ + trigger: () => waitForPauseImpression(), + assert: () => + assertImpressionTelemetry([ + { + reason: "pause", + groups: "heuristic,general", + results: "search_engine,history", + n_results: 2, + }, + ]), + }); +}); + +add_task(async function suggest() { + await doSuggestTest({ + trigger: () => waitForPauseImpression(), + assert: () => + assertImpressionTelemetry([ + { + groups: "heuristic,suggest", + results: "search_engine,rs_adm_nonsponsored", + n_results: 2, + }, + ]), + }); +}); + +add_task(async function about_page() { + await doAboutPageTest({ + trigger: () => waitForPauseImpression(), + assert: () => + assertImpressionTelemetry([ + { + reason: "pause", + groups: "heuristic,about_page,about_page", + results: "search_engine,history,history", + n_results: 3, + }, + ]), + }); +}); + +add_task(async function suggested_index() { + await doSuggestedIndexTest({ + trigger: () => waitForPauseImpression(), + assert: () => + assertImpressionTelemetry([ + { + reason: "pause", + groups: "heuristic,suggested_index", + results: "search_engine,unit", + n_results: 2, + }, + ]), + }); +}); diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_interaction.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_interaction.js new file mode 100644 index 0000000000..f783bf766c --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_interaction.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for the following data of impression telemetry. +// - interaction + +add_setup(async function () { + await initInteractionTest(); +}); + +add_task(async function topsites() { + await doTopsitesTest({ + trigger: () => waitForPauseImpression(), + assert: () => + assertImpressionTelemetry([{ reason: "pause", interaction: "topsites" }]), + }); +}); + +add_task(async function typed() { + await doTypedTest({ + trigger: () => waitForPauseImpression(), + assert: () => + assertImpressionTelemetry([{ reason: "pause", interaction: "typed" }]), + }); + + await doTypedWithResultsPopupTest({ + trigger: () => waitForPauseImpression(), + assert: () => + assertImpressionTelemetry([{ reason: "pause", interaction: "typed" }]), + }); +}); + +add_task(async function pasted() { + await doPastedTest({ + trigger: () => waitForPauseImpression(), + assert: () => + assertImpressionTelemetry([{ reason: "pause", interaction: "pasted" }]), + }); + + await doPastedWithResultsPopupTest({ + trigger: () => waitForPauseImpression(), + assert: () => + assertImpressionTelemetry([{ reason: "pause", interaction: "pasted" }]), + }); +}); + +add_task(async function topsite_search() { + // TODO: https://bugzilla.mozilla.org/show_bug.cgi?id=1804010 + // assertImpressionTelemetry([{ interaction: "topsite_search" }]); +}); + +add_task(async function returned_restarted_refined() { + await doReturnedRestartedRefinedTest({ + trigger: () => waitForPauseImpression(), + assert: expected => + assertImpressionTelemetry([ + { reason: "pause" }, + { reason: "pause", interaction: expected }, + ]), + }); +}); diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_interaction_persisted_search_terms_disabled.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_interaction_persisted_search_terms_disabled.js new file mode 100644 index 0000000000..af7134b3a0 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_interaction_persisted_search_terms_disabled.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test impression telemetry with persisted search terms disabled. + +// Allow more time for Mac machines so they don't time out in verify mode. +if (AppConstants.platform == "macosx") { + requestLongerTimeout(3); +} + +add_setup(async function () { + await initInteractionTest(); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", false]], + }); +}); + +add_task(async function persisted_search_terms() { + await doPersistedSearchTermsTest({ + trigger: () => waitForPauseImpression(), + assert: () => + assertImpressionTelemetry([ + { reason: "pause" }, + { reason: "pause", interaction: "typed" }, + ]), + }); +}); + +add_task(async function persisted_search_terms_restarted_refined() { + await doPersistedSearchTermsRestartedRefinedTest({ + enabled: false, + trigger: () => waitForPauseImpression(), + assert: expected => + assertImpressionTelemetry([ + { reason: "pause" }, + { reason: "pause", interaction: expected }, + ]), + }); +}); + +add_task( + async function persisted_search_terms_restarted_refined_via_abandonment() { + await doPersistedSearchTermsRestartedRefinedViaAbandonmentTest({ + enabled: false, + trigger: () => waitForPauseImpression(), + assert: expected => + assertImpressionTelemetry([ + { reason: "pause" }, + { reason: "pause" }, + { reason: "pause", interaction: expected }, + ]), + }); + } +); diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_interaction_persisted_search_terms_enabled.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_interaction_persisted_search_terms_enabled.js new file mode 100644 index 0000000000..a29ff98b78 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_interaction_persisted_search_terms_enabled.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test impression telemetry with persisted search terms enabled. + +// Allow more time for Mac machines so they don't time out in verify mode. +if (AppConstants.platform == "macosx") { + requestLongerTimeout(3); +} + +add_setup(async function () { + await initInteractionTest(); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.showSearchTerms.featureGate", true], + ["browser.urlbar.showSearchTerms.enabled", true], + ["browser.search.widget.inNavBar", false], + ], + }); +}); + +add_task(async function interaction_persisted_search_terms() { + await doPersistedSearchTermsTest({ + trigger: () => waitForPauseImpression(), + assert: () => + assertImpressionTelemetry([ + { reason: "pause" }, + { reason: "pause", interaction: "persisted_search_terms" }, + ]), + }); +}); + +add_task(async function interaction_persisted_search_terms_restarted_refined() { + await doPersistedSearchTermsRestartedRefinedTest({ + enabled: true, + trigger: () => waitForPauseImpression(), + assert: expected => + assertImpressionTelemetry([ + { reason: "pause" }, + { reason: "pause", interaction: expected }, + ]), + }); +}); + +add_task( + async function interaction_persisted_search_terms_restarted_refined_via_abandonment() { + await doPersistedSearchTermsRestartedRefinedViaAbandonmentTest({ + enabled: true, + trigger: () => waitForPauseImpression(), + assert: expected => + assertImpressionTelemetry([ + { reason: "pause" }, + { reason: "pause" }, + { reason: "pause", interaction: expected }, + ]), + }); + } +); diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_n_chars_n_words.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_n_chars_n_words.js new file mode 100644 index 0000000000..528cc318e0 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_n_chars_n_words.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for the following data of impression telemetry. +// - n_chars +// - n_words + +add_setup(async function () { + await initNCharsAndNWordsTest(); +}); + +add_task(async function n_chars() { + await doNCharsTest({ + trigger: () => waitForPauseImpression(), + assert: nChars => + assertImpressionTelemetry([{ reason: "pause", n_chars: nChars }]), + }); + + await doNCharsWithOverMaxTextLengthCharsTest({ + trigger: () => waitForPauseImpression(), + assert: nChars => + assertImpressionTelemetry([{ reason: "pause", n_chars: nChars }]), + }); +}); + +add_task(async function n_words() { + await doNWordsTest({ + trigger: () => waitForPauseImpression(), + assert: nWords => + assertImpressionTelemetry([{ reason: "pause", n_words: nWords }]), + }); + + await doNWordsWithOverMaxTextLengthCharsTest({ + trigger: () => waitForPauseImpression(), + assert: nWords => + assertImpressionTelemetry([{ reason: "pause", n_words: nWords }]), + }); +}); diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_preferences.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_preferences.js new file mode 100644 index 0000000000..344e238e24 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_preferences.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the impression telemetry behavior with its preferences. + +add_setup(async function () { + await setup(); +}); + +add_task(async function pauseImpressionIntervalMs() { + const additionalInterval = 1000; + const originalInterval = UrlbarPrefs.get( + "searchEngagementTelemetry.pauseImpressionIntervalMs" + ); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "browser.urlbar.searchEngagementTelemetry.pauseImpressionIntervalMs", + originalInterval + additionalInterval, + ], + ], + }); + + await doTest(async browser => { + await openPopup("https://example.com"); + + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(r => setTimeout(r, originalInterval)); + await Services.fog.testFlushAllChildren(); + assertImpressionTelemetry([]); + + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(r => setTimeout(r, additionalInterval)); + await Services.fog.testFlushAllChildren(); + assertImpressionTelemetry([{ sap: "urlbar_newtab" }]); + }); + + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_sap.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_sap.js new file mode 100644 index 0000000000..482b906024 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_sap.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for the following data of impression telemetry. +// - sap + +add_setup(async function () { + await initSapTest(); +}); + +add_task(async function urlbar() { + await doUrlbarTest({ + trigger: () => waitForPauseImpression(), + assert: () => + assertImpressionTelemetry([ + { reason: "pause", sap: "urlbar_newtab" }, + { reason: "pause", sap: "urlbar" }, + ]), + }); +}); + +add_task(async function handoff() { + await doHandoffTest({ + trigger: () => waitForPauseImpression(), + assert: () => + assertImpressionTelemetry([{ reason: "pause", sap: "handoff" }]), + }); +}); + +add_task(async function urlbar_addonpage() { + await doUrlbarAddonpageTest({ + trigger: () => waitForPauseImpression(), + assert: () => + assertImpressionTelemetry([{ reason: "pause", sap: "urlbar_addonpage" }]), + }); +}); diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_search_mode.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_search_mode.js new file mode 100644 index 0000000000..727afa3cef --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_search_mode.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for the following data of impression telemetry. +// - search_mode + +add_setup(async function () { + await initSearchModeTest(); + // Increase the pausing time to ensure entering search mode. + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "browser.urlbar.searchEngagementTelemetry.pauseImpressionIntervalMs", + 1000, + ], + ], + }); +}); + +add_task(async function not_search_mode() { + await doNotSearchModeTest({ + trigger: () => waitForPauseImpression(), + assert: () => + assertImpressionTelemetry([{ reason: "pause", search_mode: "" }]), + }); +}); + +add_task(async function search_engine() { + await doSearchEngineTest({ + trigger: () => waitForPauseImpression(), + assert: () => + assertImpressionTelemetry([ + { reason: "pause", search_mode: "search_engine" }, + ]), + }); +}); + +add_task(async function bookmarks() { + await doBookmarksTest({ + trigger: () => waitForPauseImpression(), + assert: () => + assertImpressionTelemetry([ + { reason: "pause", search_mode: "bookmarks" }, + ]), + }); +}); + +add_task(async function history() { + await doHistoryTest({ + trigger: () => waitForPauseImpression(), + assert: () => + assertImpressionTelemetry([{ reason: "pause", search_mode: "history" }]), + }); +}); + +add_task(async function tabs() { + await doTabTest({ + trigger: () => waitForPauseImpression(), + assert: () => + assertImpressionTelemetry([{ reason: "pause", search_mode: "tabs" }]), + }); +}); + +add_task(async function actions() { + await doActionsTest({ + trigger: () => waitForPauseImpression(), + assert: () => + assertImpressionTelemetry([{ reason: "pause", search_mode: "actions" }]), + }); +}); diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_timing.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_timing.js new file mode 100644 index 0000000000..31f64996f3 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_timing.js @@ -0,0 +1,91 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for the taking timing for the impression telemetry. + +add_setup(async function () { + await setup(); +}); + +add_task(async function cancelImpressionTimerByEngagementEvent() { + const additionalInterval = 1000; + const originalInterval = UrlbarPrefs.get( + "searchEngagementTelemetry.pauseImpressionIntervalMs" + ); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "browser.urlbar.searchEngagementTelemetry.pauseImpressionIntervalMs", + originalInterval + additionalInterval, + ], + ], + }); + + for (const trigger of [doEnter, doBlur]) { + await doTest(async browser => { + await openPopup("https://example.com"); + await trigger(); + + // Check whether the impression timer was canceled. + await new Promise(r => + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + setTimeout(r, originalInterval + additionalInterval) + ); + assertImpressionTelemetry([]); + }); + } + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function cancelInpressionTimerByType() { + const originalInterval = UrlbarPrefs.get( + "searchEngagementTelemetry.pauseImpressionIntervalMs" + ); + + await doTest(async browser => { + await openPopup("x"); + await new Promise(r => + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + setTimeout(r, originalInterval / 10) + ); + assertImpressionTelemetry([]); + + EventUtils.synthesizeKey(" "); + EventUtils.synthesizeKey("z"); + await UrlbarTestUtils.promiseSearchComplete(window); + assertImpressionTelemetry([]); + await waitForPauseImpression(); + + assertImpressionTelemetry([{ n_chars: 3 }]); + }); +}); + +add_task(async function oneImpressionInOneSession() { + await doTest(async browser => { + await openPopup("x"); + await waitForPauseImpression(); + + // Sanity check. + assertImpressionTelemetry([{ n_chars: 1 }]); + + // Add a keyword to start new query. + EventUtils.synthesizeKey(" "); + EventUtils.synthesizeKey("z"); + await UrlbarTestUtils.promiseSearchComplete(window); + await waitForPauseImpression(); + + // No more taking impression telemetry. + assertImpressionTelemetry([{ n_chars: 1 }]); + + // Finish the current session. + await doEnter(); + + // Should take pause impression since new session started. + await openPopup("x z y"); + await waitForPauseImpression(); + assertImpressionTelemetry([{ n_chars: 1 }, { n_chars: 5 }]); + }); +}); diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_record_preferences.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_record_preferences.js new file mode 100644 index 0000000000..6760385841 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_record_preferences.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for preference telemetry. + +add_setup(async function () { + await Services.fog.testFlushAllChildren(); + Services.fog.testResetFOG(); + + // Create a new window in order to initialize TelemetryEvent of + // UrlbarController. + const win = await BrowserTestUtils.openNewBrowserWindow(); + registerCleanupFunction(async function () { + await BrowserTestUtils.closeWindow(win); + }); +}); + +add_task(async function prefMaxRichResults() { + Assert.equal( + Glean.urlbar.prefMaxResults.testGetValue(), + UrlbarPrefs.get("maxRichResults"), + "Record prefMaxResults when UrlbarController is initialized" + ); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.maxRichResults", 0]], + }); + Assert.equal( + Glean.urlbar.prefMaxResults.testGetValue(), + UrlbarPrefs.get("maxRichResults"), + "Record prefMaxResults when the maxRichResults pref is updated" + ); +}); + +add_task(async function prefSuggestTopsites() { + Assert.equal( + Glean.urlbar.prefSuggestTopsites.testGetValue(), + UrlbarPrefs.get("suggest.topsites"), + "Record prefSuggestTopsites when UrlbarController is initialized" + ); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.topsites", !UrlbarPrefs.get("suggest.topsites")], + ], + }); + Assert.equal( + Glean.urlbar.prefSuggestTopsites.testGetValue(), + UrlbarPrefs.get("suggest.topsites"), + "Record prefSuggestTopsites when the suggest.topsites pref is updated" + ); +}); diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/head-exposure.js b/browser/components/urlbar/tests/engagementTelemetry/browser/head-exposure.js new file mode 100644 index 0000000000..4ce2c6f869 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/head-exposure.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from head.js */ + +async function doExposureTest({ + prefs, + query, + trigger, + assert, + select = defaultSelect, +}) { + const cleanupQuickSuggest = await ensureQuickSuggestInit(); + await SpecialPowers.pushPrefEnv({ + set: prefs, + }); + + await doTest(async () => { + await openPopup(query); + await select(query); + + await trigger(); + await assert(); + }); + + await SpecialPowers.popPrefEnv(); + cleanupQuickSuggest(); +} + +async function defaultSelect(query) { + await selectRowByURL(`https://example.com/${query}`); +} + +async function getResultByType(provider) { + for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) { + const detail = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + const telemetryType = UrlbarUtils.searchEngagementTelemetryType( + detail.result + ); + if (telemetryType === provider) { + return detail.result; + } + } + return null; +} diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/head-groups.js b/browser/components/urlbar/tests/engagementTelemetry/browser/head-groups.js new file mode 100644 index 0000000000..b34221d749 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/head-groups.js @@ -0,0 +1,294 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from head.js */ + +async function doHeuristicsTest({ trigger, assert }) { + await doTest(async browser => { + await openPopup("x"); + + await trigger(); + await assert(); + }); +} + +async function doAdaptiveHistoryTest({ trigger, assert }) { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.autoFill", false]], + }); + + await doTest(async browser => { + await PlacesTestUtils.addVisits(["https://example.com/test"]); + await UrlbarUtils.addToInputHistory("https://example.com/test", "examp"); + + await openPopup("exa"); + await selectRowByURL("https://example.com/test"); + + await trigger(); + await assert(); + }); + + await SpecialPowers.popPrefEnv(); +} + +async function doSearchHistoryTest({ trigger, assert }) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.searches", true], + ["browser.urlbar.maxHistoricalSearchSuggestions", 2], + ], + }); + + await doTest(async browser => { + await UrlbarTestUtils.formHistory.add(["foofoo", "foobar"]); + + await openPopup("foo"); + await selectRowByURL("http://mochi.test:8888/?terms=foofoo"); + + await trigger(); + await assert(); + }); + + await SpecialPowers.popPrefEnv(); +} + +async function doSearchSuggestTest({ trigger, assert }) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.searches", true], + ["browser.urlbar.maxHistoricalSearchSuggestions", 2], + ], + }); + + await doTest(async browser => { + await openPopup("foo"); + await selectRowByURL("http://mochi.test:8888/?terms=foofoo"); + + await trigger(); + await assert(); + }); + + await SpecialPowers.popPrefEnv(); +} + +async function doTailSearchSuggestTest({ trigger, assert }) { + const cleanup = await _useTailSuggestionsEngine(); + + await doTest(async browser => { + await openPopup("hello"); + await selectRowByProvider("SearchSuggestions"); + + await trigger(); + await assert(); + }); + + await cleanup(); +} + +async function doTopPickTest({ trigger, assert }) { + const cleanupQuickSuggest = await ensureQuickSuggestInit({ + // eslint-disable-next-line mozilla/valid-lazy + config: lazy.QuickSuggestTestUtils.BEST_MATCH_CONFIG, + }); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.bestMatch.enabled", true]], + }); + + await doTest(async browser => { + await openPopup("sponsored"); + await selectRowByURL("https://example.com/sponsored"); + + await trigger(); + await assert(); + }); + + await SpecialPowers.popPrefEnv(); + cleanupQuickSuggest(); +} + +async function doTopSiteTest({ trigger, assert }) { + await doTest(async browser => { + await addTopSites("https://example.com/"); + + await showResultByArrowDown(); + await selectRowByURL("https://example.com/"); + + await trigger(); + await assert(); + }); +} + +async function doRemoteTabTest({ trigger, assert }) { + const remoteTab = await loadRemoteTab("https://example.com"); + + await doTest(async browser => { + await openPopup("example"); + await selectRowByProvider("RemoteTabs"); + + await trigger(); + await assert(); + }); + + await remoteTab.unload(); +} + +async function doAddonTest({ trigger, assert }) { + const addon = loadOmniboxAddon({ keyword: "omni" }); + await addon.startup(); + + await doTest(async browser => { + await openPopup("omni test"); + + await trigger(); + await assert(); + }); + + await addon.unload(); +} + +async function doGeneralBookmarkTest({ trigger, assert }) { + await doTest(async browser => { + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "https://example.com/bookmark", + title: "bookmark", + }); + + await openPopup("bookmark"); + await selectRowByURL("https://example.com/bookmark"); + + await trigger(); + await assert(); + }); +} + +async function doGeneralHistoryTest({ trigger, assert }) { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.autoFill", false]], + }); + + await doTest(async browser => { + await PlacesTestUtils.addVisits("https://example.com/test"); + + await openPopup("example"); + await selectRowByURL("https://example.com/test"); + + await trigger(); + await assert(); + }); + + await SpecialPowers.popPrefEnv(); +} + +async function doSuggestTest({ trigger, assert }) { + const cleanupQuickSuggest = await ensureQuickSuggestInit(); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.bestMatch.enabled", false]], + }); + + await doTest(async browser => { + await openPopup("nonsponsored"); + await selectRowByURL("https://example.com/nonsponsored"); + + await trigger(); + await assert(); + }); + + await SpecialPowers.popPrefEnv(); + cleanupQuickSuggest(); +} + +async function doAboutPageTest({ trigger, assert }) { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.maxRichResults", 3]], + }); + + await doTest(async browser => { + await openPopup("about:"); + await selectRowByURL("about:robots"); + + await trigger(); + await assert(); + }); + + await SpecialPowers.popPrefEnv(); +} + +async function doSuggestedIndexTest({ trigger, assert }) { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.unitConversion.enabled", true]], + }); + + await doTest(async browser => { + await openPopup("1m to cm"); + await selectRowByProvider("UnitConversion"); + + await trigger(); + await assert(); + }); + + await SpecialPowers.popPrefEnv(); +} + +/** + * Creates a search engine that returns tail suggestions and sets it as the + * default engine. + * + * @returns {Function} + * A cleanup function that will revert the default search engine and stop http + * server. + */ +async function _useTailSuggestionsEngine() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.suggest.enabled", true], + ["browser.urlbar.suggest.searches", true], + ["browser.urlbar.richSuggestions.tail", true], + ], + }); + + const engineName = "TailSuggestions"; + const httpServer = new HttpServer(); + httpServer.start(-1); + httpServer.registerPathHandler("/suggest", (req, resp) => { + const params = new URLSearchParams(req.queryString); + const searchStr = params.get("q"); + const suggestions = [ + searchStr, + [searchStr + "-tail"], + [], + { + "google:suggestdetail": [{ t: "-tail", mp: "… " }], + }, + ]; + resp.setHeader("Content-Type", "application/json", false); + resp.write(JSON.stringify(suggestions)); + }); + + await SearchTestUtils.installSearchExtension({ + name: engineName, + search_url: `http://localhost:${httpServer.identity.primaryPort}/search`, + suggest_url: `http://localhost:${httpServer.identity.primaryPort}/suggest`, + suggest_url_get_params: "?q={searchTerms}", + search_form: `http://localhost:${httpServer.identity.primaryPort}/search?q={searchTerms}`, + }); + + const tailEngine = Services.search.getEngineByName(engineName); + const originalEngine = await Services.search.getDefault(); + Services.search.setDefault( + tailEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + return async () => { + Services.search.setDefault( + originalEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + httpServer.stop(() => {}); + await SpecialPowers.popPrefEnv(); + }; +} diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/head-interaction.js b/browser/components/urlbar/tests/engagementTelemetry/browser/head-interaction.js new file mode 100644 index 0000000000..6bc0fd3b0e --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/head-interaction.js @@ -0,0 +1,238 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from head.js */ + +async function doTopsitesTest({ trigger, assert }) { + await doTest(async browser => { + await addTopSites("https://example.com/"); + + await showResultByArrowDown(); + await selectRowByURL("https://example.com/"); + + await trigger(); + await assert(); + }); +} + +async function doTypedTest({ trigger, assert }) { + await doTest(async browser => { + await openPopup("x"); + + await trigger(); + await assert(); + }); +} + +async function doTypedWithResultsPopupTest({ trigger, assert }) { + await doTest(async browser => { + await showResultByArrowDown(); + EventUtils.synthesizeKey("x"); + await UrlbarTestUtils.promiseSearchComplete(window); + + await trigger(); + await assert(); + }); +} + +async function doPastedTest({ trigger, assert }) { + await doTest(async browser => { + await doPaste("www.example.com"); + + await trigger(); + await assert(); + }); +} + +async function doPastedWithResultsPopupTest({ trigger, assert }) { + await doTest(async browser => { + await showResultByArrowDown(); + await doPaste("x"); + + await trigger(); + await assert(); + }); +} + +async function doReturnedRestartedRefinedTest({ trigger, assert }) { + const testData = [ + { + firstInput: "x", + // Just move the focus to the URL bar after blur. + secondInput: null, + expected: "returned", + }, + { + firstInput: "x", + secondInput: "x", + expected: "returned", + }, + { + firstInput: "x", + secondInput: "y", + expected: "restarted", + }, + { + firstInput: "x", + secondInput: "x y", + expected: "refined", + }, + { + firstInput: "x y", + secondInput: "x", + expected: "refined", + }, + ]; + + for (const { firstInput, secondInput, expected } of testData) { + await doTest(async browser => { + await openPopup(firstInput); + await waitForPauseImpression(); + await doBlur(); + + await UrlbarTestUtils.promisePopupOpen(window, () => { + document.getElementById("Browser:OpenLocation").doCommand(); + }); + if (secondInput) { + for (let i = 0; i < secondInput.length; i++) { + EventUtils.synthesizeKey(secondInput.charAt(i)); + } + } + await UrlbarTestUtils.promiseSearchComplete(window); + + await trigger(); + await assert(expected); + }); + } +} + +async function doPersistedSearchTermsTest({ trigger, assert }) { + await doTest(async browser => { + await openPopup("x"); + await waitForPauseImpression(); + await doEnter(); + + await openPopup("x"); + + await trigger(); + await assert(); + }); +} + +async function doPersistedSearchTermsRestartedRefinedTest({ + enabled, + trigger, + assert, +}) { + const testData = [ + { + firstInput: "x", + // Just move the focus to the URL bar after engagement. + secondInput: null, + expected: enabled ? "persisted_search_terms" : "topsites", + }, + { + firstInput: "x", + secondInput: "x", + expected: enabled ? "persisted_search_terms" : "typed", + }, + { + firstInput: "x", + secondInput: "y", + expected: enabled ? "persisted_search_terms_restarted" : "typed", + }, + { + firstInput: "x", + secondInput: "x y", + expected: enabled ? "persisted_search_terms_refined" : "typed", + }, + { + firstInput: "x y", + secondInput: "x", + expected: enabled ? "persisted_search_terms_refined" : "typed", + }, + ]; + + for (const { firstInput, secondInput, expected } of testData) { + await doTest(async browser => { + await openPopup(firstInput); + await waitForPauseImpression(); + await doEnter(); + + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeKey("l", { accelKey: true }); + }); + if (secondInput) { + for (let i = 0; i < secondInput.length; i++) { + EventUtils.synthesizeKey(secondInput.charAt(i)); + } + } + await UrlbarTestUtils.promiseSearchComplete(window); + + await trigger(); + await assert(expected); + }); + } +} + +async function doPersistedSearchTermsRestartedRefinedViaAbandonmentTest({ + enabled, + trigger, + assert, +}) { + const testData = [ + { + firstInput: "x", + // Just move the focus to the URL bar after blur. + secondInput: null, + expected: enabled ? "persisted_search_terms" : "returned", + }, + { + firstInput: "x", + secondInput: "x", + expected: enabled ? "persisted_search_terms" : "returned", + }, + { + firstInput: "x", + secondInput: "y", + expected: enabled ? "persisted_search_terms_restarted" : "restarted", + }, + { + firstInput: "x", + secondInput: "x y", + expected: enabled ? "persisted_search_terms_refined" : "refined", + }, + { + firstInput: "x y", + secondInput: "x", + expected: enabled ? "persisted_search_terms_refined" : "refined", + }, + ]; + + for (const { firstInput, secondInput, expected } of testData) { + await doTest(async browser => { + await openPopup("any search"); + await waitForPauseImpression(); + await doEnter(); + + await openPopup(firstInput); + await waitForPauseImpression(); + await doBlur(); + + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeKey("l", { accelKey: true }); + }); + if (secondInput) { + for (let i = 0; i < secondInput.length; i++) { + EventUtils.synthesizeKey(secondInput.charAt(i)); + } + } + await UrlbarTestUtils.promiseSearchComplete(window); + + await trigger(); + await assert(expected); + }); + } +} diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/head-n_chars_n_words.js b/browser/components/urlbar/tests/engagementTelemetry/browser/head-n_chars_n_words.js new file mode 100644 index 0000000000..6d4c61c7f0 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/head-n_chars_n_words.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from head.js */ + +async function doNCharsTest({ trigger, assert }) { + for (const input of ["x", "xx", "xx x", "xx x "]) { + await doTest(async browser => { + await openPopup(input); + + await trigger(); + await assert(input.length); + }); + } +} + +async function doNCharsWithOverMaxTextLengthCharsTest({ trigger, assert }) { + await doTest(async browser => { + let input = ""; + for (let i = 0; i < UrlbarUtils.MAX_TEXT_LENGTH * 2; i++) { + input += "x"; + } + await openPopup(input); + + await trigger(); + await assert(UrlbarUtils.MAX_TEXT_LENGTH * 2); + }); +} + +async function doNWordsTest({ trigger, assert }) { + for (const input of ["x", "xx", "xx x", "xx x "]) { + await doTest(async browser => { + await openPopup(input); + + await trigger(); + const splits = input.trim().split(" "); + await assert(splits.length); + }); + } +} + +async function doNWordsWithOverMaxTextLengthCharsTest({ trigger, assert }) { + await doTest(async browser => { + const word = "1234 "; + let input = ""; + while (input.length < UrlbarUtils.MAX_TEXT_LENGTH * 2) { + input += word; + } + await openPopup(input); + + await trigger(); + await assert(UrlbarUtils.MAX_TEXT_LENGTH / word.length); + }); +} diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/head-sap.js b/browser/components/urlbar/tests/engagementTelemetry/browser/head-sap.js new file mode 100644 index 0000000000..be4e852b1c --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/head-sap.js @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from head.js */ + +async function doUrlbarNewTabTest({ trigger, assert }) { + await doTest(async browser => { + await openPopup("x"); + + await trigger(); + await assert(); + }); +} + +async function doUrlbarTest({ trigger, assert }) { + await doTest(async browser => { + await openPopup("x"); + await waitForPauseImpression(); + await doEnter(); + await openPopup("y"); + + await trigger(); + await assert(); + }); +} + +async function doHandoffTest({ trigger, assert }) { + await doTest(async browser => { + BrowserTestUtils.loadURIString(browser, "about:newtab"); + await BrowserTestUtils.browserStopped(browser, "about:newtab"); + await SpecialPowers.spawn(browser, [], function () { + const searchInput = content.document.querySelector(".fake-editable"); + searchInput.click(); + }); + EventUtils.synthesizeKey("x"); + await UrlbarTestUtils.promiseSearchComplete(window); + + await trigger(); + await assert(); + }); +} + +async function doUrlbarAddonpageTest({ trigger, assert }) { + const extensionData = { + files: { + "page.html": "hello", + }, + }; + const extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + const extensionURL = `moz-extension://${extension.uuid}/page.html`; + + await doTest(async browser => { + const onLoad = BrowserTestUtils.browserLoaded(browser); + BrowserTestUtils.loadURIString(browser, extensionURL); + await onLoad; + await openPopup("x"); + + await trigger(); + await assert(); + }); + + await extension.unload(); +} diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/head-search_mode.js b/browser/components/urlbar/tests/engagementTelemetry/browser/head-search_mode.js new file mode 100644 index 0000000000..5c877da05f --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/head-search_mode.js @@ -0,0 +1,93 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from head.js */ + +async function doNotSearchModeTest({ trigger, assert }) { + await doTest(async browser => { + await openPopup("x"); + + await trigger(); + await assert(); + }); +} + +async function doSearchEngineTest({ trigger, assert }) { + await doTest(async browser => { + await openPopup("x"); + await UrlbarTestUtils.enterSearchMode(window); + + await trigger(); + await assert(); + }); +} + +async function doBookmarksTest({ trigger, assert }) { + await doTest(async browser => { + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "https://example.com/bookmark", + title: "bookmark", + }); + await openPopup("bookmark"); + await UrlbarTestUtils.enterSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + }); + await selectRowByURL("https://example.com/bookmark"); + + await trigger(); + await assert(); + }); +} + +async function doHistoryTest({ trigger, assert }) { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.autoFill", false]], + }); + + await doTest(async browser => { + await PlacesTestUtils.addVisits("https://example.com/test"); + await openPopup("example"); + await UrlbarTestUtils.enterSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + }); + await selectRowByURL("https://example.com/test"); + + await trigger(); + await assert(); + }); + + await SpecialPowers.popPrefEnv(); +} + +async function doTabTest({ trigger, assert }) { + const tab = BrowserTestUtils.addTab(gBrowser, "https://example.com/"); + + await doTest(async browser => { + await openPopup("example"); + await UrlbarTestUtils.enterSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.TABS, + }); + await selectRowByProvider("Places"); + + await trigger(); + await assert(); + }); + + BrowserTestUtils.removeTab(tab); +} + +async function doActionsTest({ trigger, assert }) { + await doTest(async browser => { + await openPopup("add"); + await UrlbarTestUtils.enterSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.ACTIONS, + }); + await selectRowByProvider("quickactions"); + + await trigger(); + await assert(); + }); +} diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/head.js b/browser/components/urlbar/tests/engagementTelemetry/browser/head.js new file mode 100644 index 0000000000..fa623deb5c --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/head.js @@ -0,0 +1,458 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/components/urlbar/tests/browser/head-common.js", + this +); + +ChromeUtils.defineESModuleGetters(this, { + QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs", +}); + +const lazy = {}; + +XPCOMUtils.defineLazyGetter(lazy, "QuickSuggestTestUtils", () => { + const { QuickSuggestTestUtils: module } = ChromeUtils.importESModule( + "resource://testing-common/QuickSuggestTestUtils.sys.mjs" + ); + module.init(this); + return module; +}); + +XPCOMUtils.defineLazyGetter(this, "MerinoTestUtils", () => { + const { MerinoTestUtils: module } = ChromeUtils.importESModule( + "resource://testing-common/MerinoTestUtils.sys.mjs" + ); + module.init(this); + return module; +}); + +ChromeUtils.defineESModuleGetters(lazy, { + UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs", + sinon: "resource://testing-common/Sinon.sys.mjs", +}); + +async function addTopSites(url) { + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits(url); + } + await updateTopSites(sites => { + return sites && sites[0] && sites[0].url == url; + }); +} + +function assertAbandonmentTelemetry(expectedExtraList) { + _assertGleanTelemetry("abandonment", expectedExtraList); +} + +function assertEngagementTelemetry(expectedExtraList) { + _assertGleanTelemetry("engagement", expectedExtraList); +} + +function assertImpressionTelemetry(expectedExtraList) { + _assertGleanTelemetry("impression", expectedExtraList); +} + +function assertExposureTelemetry(expectedExtraList) { + _assertGleanTelemetry("exposure", expectedExtraList); +} + +function _assertGleanTelemetry(telemetryName, expectedExtraList) { + const telemetries = Glean.urlbar[telemetryName].testGetValue() ?? []; + Assert.equal( + telemetries.length, + expectedExtraList.length, + "Telemetry event length matches expected event length." + ); + + for (let i = 0; i < telemetries.length; i++) { + const telemetry = telemetries[i]; + Assert.equal(telemetry.category, "urlbar"); + Assert.equal(telemetry.name, telemetryName); + + const expectedExtra = expectedExtraList[i]; + for (const key of Object.keys(expectedExtra)) { + Assert.equal( + telemetry.extra[key], + expectedExtra[key], + `${key} is correct` + ); + } + } +} + +async function ensureQuickSuggestInit({ + merinoSuggestions = undefined, + config = undefined, +} = {}) { + return lazy.QuickSuggestTestUtils.ensureQuickSuggestInit({ + config, + merinoSuggestions, + remoteSettingsResults: [ + { + type: "data", + attachment: [ + { + id: 1, + url: "https://example.com/sponsored", + title: "Sponsored suggestion", + keywords: ["sponsored"], + click_url: "https://example.com/click", + impression_url: "https://example.com/impression", + advertiser: "TestAdvertiser", + iab_category: "22 - Shopping", + }, + { + id: 2, + url: `https://example.com/nonsponsored`, + title: "Non-sponsored suggestion", + keywords: ["nonsponsored"], + click_url: "https://example.com/click", + impression_url: "https://example.com/impression", + advertiser: "TestAdvertiser", + iab_category: "5 - Education", + }, + ], + }, + { + type: "weather", + weather: MerinoTestUtils.WEATHER_RS_DATA, + }, + ], + }); +} + +async function doBlur() { + await UrlbarTestUtils.promisePopupClose(window, () => { + gURLBar.blur(); + }); +} + +async function doClick() { + const selected = UrlbarTestUtils.getSelectedRow(window); + const onLoad = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + EventUtils.synthesizeMouseAtCenter(selected, {}); + await onLoad; +} + +async function doClickSubButton(selector) { + const selected = UrlbarTestUtils.getSelectedElement(window); + const button = selected.closest(".urlbarView-row").querySelector(selector); + EventUtils.synthesizeMouseAtCenter(button, {}); +} + +async function doDropAndGo(data) { + const onLoad = BrowserTestUtils.browserLoaded(browser); + EventUtils.synthesizeDrop( + document.getElementById("back-button"), + gURLBar.inputField, + [[{ type: "text/plain", data }]], + "copy", + window + ); + await onLoad; +} + +async function doEnter(modifier = {}) { + const onLoad = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + EventUtils.synthesizeKey("KEY_Enter", modifier); + await onLoad; +} + +async function doPaste(data) { + await SimpleTest.promiseClipboardChange(data, () => { + clipboardHelper.copyString(data); + }); + + gURLBar.focus(); + gURLBar.select(); + document.commandDispatcher + .getControllerForCommand("cmd_paste") + .doCommand("cmd_paste"); + await UrlbarTestUtils.promiseSearchComplete(window); +} + +async function doPasteAndGo(data) { + await SimpleTest.promiseClipboardChange(data, () => { + clipboardHelper.copyString(data); + }); + const inputBox = gURLBar.querySelector("moz-input-box"); + const contextMenu = inputBox.menupopup; + const onPopup = BrowserTestUtils.waitForEvent(contextMenu, "popupshown"); + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, { + type: "contextmenu", + button: 2, + }); + await onPopup; + const onLoad = BrowserTestUtils.browserLoaded(browser); + const menuitem = inputBox.getMenuItem("paste-and-go"); + contextMenu.activateItem(menuitem); + await onLoad; +} + +async function doTest(testFn) { + await Services.fog.testFlushAllChildren(); + Services.fog.testResetFOG(); + // Enable recording telemetry for abandonment, engagement and impression. + Services.fog.setMetricsFeatureConfig( + JSON.stringify({ + "urlbar.abandonment": true, + "urlbar.engagement": true, + "urlbar.impression": true, + }) + ); + + gURLBar.controller.engagementEvent.reset(); + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesTestUtils.clearHistoryVisits(); + await PlacesTestUtils.clearInputHistory(); + await UrlbarTestUtils.formHistory.clear(window); + await QuickSuggest.blockedSuggestions.clear(); + await QuickSuggest.blockedSuggestions._test_readyPromise; + await updateTopSites(() => true); + + try { + await BrowserTestUtils.withNewTab(gBrowser, testFn); + } finally { + Services.fog.setMetricsFeatureConfig("{}"); + } +} + +async function initGroupTest() { + /* import-globals-from head-groups.js */ + Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/components/urlbar/tests/engagementTelemetry/browser/head-groups.js", + this + ); + await setup(); +} + +async function initInteractionTest() { + /* import-globals-from head-interaction.js */ + Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/components/urlbar/tests/engagementTelemetry/browser/head-interaction.js", + this + ); + await setup(); +} + +async function initNCharsAndNWordsTest() { + /* import-globals-from head-n_chars_n_words.js */ + Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/components/urlbar/tests/engagementTelemetry/browser/head-n_chars_n_words.js", + this + ); + await setup(); +} + +async function initSapTest() { + /* import-globals-from head-sap.js */ + Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/components/urlbar/tests/engagementTelemetry/browser/head-sap.js", + this + ); + await setup(); +} + +async function initSearchModeTest() { + /* import-globals-from head-search_mode.js */ + Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/components/urlbar/tests/engagementTelemetry/browser/head-search_mode.js", + this + ); + await setup(); +} + +async function initExposureTest() { + /* import-globals-from head-exposure.js */ + Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/components/urlbar/tests/engagementTelemetry/browser/head-exposure.js", + this + ); + await setup(); +} + +function loadOmniboxAddon({ keyword }) { + return ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["tabs"], + omnibox: { + keyword, + }, + }, + background() { + /* global browser */ + browser.omnibox.setDefaultSuggestion({ + description: "doit", + }); + browser.omnibox.onInputEntered.addListener(() => { + browser.tabs.update({ url: "https://example.com/" }); + }); + browser.omnibox.onInputChanged.addListener((text, suggest) => { + suggest([]); + }); + }, + }); +} + +async function loadRemoteTab(url) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.searches", false], + ["browser.urlbar.maxHistoricalSearchSuggestions", 0], + ["browser.urlbar.autoFill", false], + ["services.sync.username", "fake"], + ["services.sync.syncedTabs.showRemoteTabs", true], + ], + }); + + const REMOTE_TAB = { + id: "test", + type: "client", + lastModified: 1492201200, + name: "test", + clientType: "desktop", + tabs: [ + { + type: "tab", + title: "tesrt", + url, + icon: UrlbarUtils.ICON.DEFAULT, + client: "test", + lastUsed: Math.floor(Date.now() / 1000), + }, + ], + }; + + const sandbox = lazy.sinon.createSandbox(); + // eslint-disable-next-line no-undef + const syncedTabs = SyncedTabs; + const originalSyncedTabsInternal = syncedTabs._internal; + syncedTabs._internal = { + isConfiguredToSyncTabs: true, + hasSyncedThisSession: true, + getTabClients() { + return Promise.resolve([]); + }, + syncTabs() { + return Promise.resolve(); + }, + }; + const weaveXPCService = Cc["@mozilla.org/weave/service;1"].getService( + Ci.nsISupports + ).wrappedJSObject; + const oldWeaveServiceReady = weaveXPCService.ready; + weaveXPCService.ready = true; + sandbox + .stub(syncedTabs._internal, "getTabClients") + .callsFake(() => Promise.resolve(Cu.cloneInto([REMOTE_TAB], {}))); + + return { + async unload() { + sandbox.restore(); + weaveXPCService.ready = oldWeaveServiceReady; + syncedTabs._internal = originalSyncedTabsInternal; + // Reset internal cache in UrlbarProviderRemoteTabs. + Services.obs.notifyObservers(null, "weave:engine:sync:finish", "tabs"); + await SpecialPowers.popPrefEnv(); + }, + }; +} + +async function openPopup(input) { + await UrlbarTestUtils.promisePopupOpen(window, async () => { + await UrlbarTestUtils.inputIntoURLBar(window, input); + }); + await UrlbarTestUtils.promiseSearchComplete(window); +} + +async function selectRowByURL(url) { + for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) { + const detail = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + if (detail.url === url) { + UrlbarTestUtils.setSelectedRowIndex(window, i); + return; + } + } +} + +async function selectRowByProvider(provider) { + for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) { + const detail = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + if (detail.result.providerName === provider) { + UrlbarTestUtils.setSelectedRowIndex(window, i); + break; + } + } +} + +async function selectRowByType(type) { + for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) { + const detail = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + if (detail.result.payload.type === type) { + UrlbarTestUtils.setSelectedRowIndex(window, i); + return; + } + } +} + +async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.searchEngagementTelemetry.enabled", true], + ["browser.urlbar.quickactions.enabled", true], + ["browser.urlbar.quickactions.minimumSearchString", 0], + ["browser.urlbar.suggest.quickactions", true], + ["browser.urlbar.shortcuts.quickactions", true], + [ + "browser.urlbar.searchEngagementTelemetry.pauseImpressionIntervalMs", + 100, + ], + ], + }); + + const engine = await SearchTestUtils.promiseNewSearchEngine({ + url: "chrome://mochitests/content/browser/browser/components/urlbar/tests/browser/searchSuggestionEngine.xml", + }); + const originalDefaultEngine = await Services.search.getDefault(); + await Services.search.setDefault( + engine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + await Services.search.moveEngine(engine, 0); + + registerCleanupFunction(async function () { + await SpecialPowers.popPrefEnv(); + await Services.search.setDefault( + originalDefaultEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + }); +} + +async function setupNimbus(variables) { + return lazy.UrlbarTestUtils.initNimbusFeature(variables); +} + +async function showResultByArrowDown() { + gURLBar.value = ""; + gURLBar.select(); + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeKey("KEY_ArrowDown"); + }); + await UrlbarTestUtils.promiseSearchComplete(window); +} + +async function waitForPauseImpression() { + await new Promise(r => + setTimeout( + r, + UrlbarPrefs.get("searchEngagementTelemetry.pauseImpressionIntervalMs") + ) + ); + await Services.fog.testFlushAllChildren(); +} diff --git a/browser/components/urlbar/tests/ext/api.js b/browser/components/urlbar/tests/ext/api.js new file mode 100644 index 0000000000..77da790190 --- /dev/null +++ b/browser/components/urlbar/tests/ext/api.js @@ -0,0 +1,260 @@ +/* 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.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + Preferences: "resource://gre/modules/Preferences.sys.mjs", + UrlbarProviderExtension: + "resource:///modules/UrlbarProviderExtension.sys.mjs", + UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs", + UrlbarView: "resource:///modules/UrlbarView.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(this, { + BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.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.sys.mjs 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..8d11a88066 --- /dev/null +++ b/browser/components/urlbar/tests/ext/browser/head.js @@ -0,0 +1,253 @@ +/* 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"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/components/urlbar/tests/browser/head-common.js", + this +); + +ChromeUtils.defineESModuleGetters(this, { + Preferences: "resource://gre/modules/Preferences.sys.mjs", +}); + +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_setup(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 {object} options + * Options object + * @param {Function} options.background + * This function is serialized and becomes the background script. + * @param {object} [options.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.ok( + false, + `"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..ced5deddaa --- /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/quicksuggest/MerinoTestUtils.sys.mjs b/browser/components/urlbar/tests/quicksuggest/MerinoTestUtils.sys.mjs new file mode 100644 index 0000000000..7763881b4f --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/MerinoTestUtils.sys.mjs @@ -0,0 +1,765 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs", + QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs", + TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs", + UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs", +}); + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +// The following properties and methods are copied from the test scope to the +// test utils object so they can be easily accessed. Be careful about assuming a +// particular property will be defined because depending on the scope -- browser +// test or xpcshell test -- some may not be. +const TEST_SCOPE_PROPERTIES = [ + "Assert", + "EventUtils", + "info", + "registerCleanupFunction", +]; + +const SEARCH_PARAMS = { + CLIENT_VARIANTS: "client_variants", + PROVIDERS: "providers", + QUERY: "q", + SEQUENCE_NUMBER: "seq", + SESSION_ID: "sid", +}; + +const REQUIRED_SEARCH_PARAMS = [ + SEARCH_PARAMS.QUERY, + SEARCH_PARAMS.SEQUENCE_NUMBER, + SEARCH_PARAMS.SESSION_ID, +]; + +// We set the client timeout to a large value to avoid intermittent failures in +// CI, especially TV tests, where the Merino fetch unexpectedly doesn't finish +// before the default timeout. +const CLIENT_TIMEOUT_MS = 2000; + +const HISTOGRAM_LATENCY = "FX_URLBAR_MERINO_LATENCY_MS"; +const HISTOGRAM_RESPONSE = "FX_URLBAR_MERINO_RESPONSE"; + +// Maps from string labels of the `FX_URLBAR_MERINO_RESPONSE` histogram to their +// numeric values. +const RESPONSE_HISTOGRAM_VALUES = { + success: 0, + timeout: 1, + network_error: 2, + http_error: 3, + no_suggestion: 4, +}; + +const WEATHER_KEYWORD = "weather"; + +const WEATHER_RS_DATA = { + keywords: [WEATHER_KEYWORD], +}; + +const WEATHER_SUGGESTION = { + title: "Weather for San Francisco", + url: "https://example.com/weather", + provider: "accuweather", + is_sponsored: false, + score: 0.2, + icon: null, + city_name: "San Francisco", + current_conditions: { + url: "https://example.com/weather-current-conditions", + summary: "Mostly cloudy", + icon_id: 6, + temperature: { c: 15.5, f: 60.0 }, + }, + forecast: { + url: "https://example.com/weather-forecast", + summary: "Pleasant Saturday", + high: { c: 21.1, f: 70.0 }, + low: { c: 13.9, f: 57.0 }, + }, +}; + +// We set the weather suggestion fetch interval to an absurdly large value so it +// absolutely will not fire during tests. +const WEATHER_FETCH_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours + +/** + * Test utils for Merino. + */ +class _MerinoTestUtils { + /** + * Initializes the utils. + * + * @param {object} scope + * The global JS scope where tests are being run. This allows the instance + * to access test helpers like `Assert` that are available in the scope. + */ + init(scope) { + if (!scope) { + throw new Error("MerinoTestUtils.init() must be called with a scope"); + } + + this.#initDepth++; + scope.info?.("MerinoTestUtils init: Depth is now " + this.#initDepth); + + for (let p of TEST_SCOPE_PROPERTIES) { + this[p] = scope[p]; + } + // If you add other properties to `this`, null them in `uninit()`. + + if (!this.#server) { + this.#server = new MockMerinoServer(scope); + } + lazy.UrlbarPrefs.set("merino.timeoutMs", CLIENT_TIMEOUT_MS); + scope.registerCleanupFunction?.(() => { + scope.info?.("MerinoTestUtils cleanup function"); + this.uninit(); + }); + } + + /** + * Uninitializes the utils. If they were created with a test scope that + * defines `registerCleanupFunction()`, you don't need to call this yourself + * because it will automatically be called as a cleanup function. Otherwise + * you'll need to call this. + */ + uninit() { + this.#initDepth--; + this.info?.("MerinoTestUtils uninit: Depth is now " + this.#initDepth); + + if (this.#initDepth) { + this.info?.("MerinoTestUtils uninit: Bailing because depth > 0"); + return; + } + this.info?.("MerinoTestUtils uninit: Now uninitializing"); + + for (let p of TEST_SCOPE_PROPERTIES) { + this[p] = null; + } + this.#server.uninit(); + this.#server = null; + lazy.UrlbarPrefs.clear("merino.timeoutMs"); + } + + /** + * @returns {object} + * The names of URL search params. + */ + get SEARCH_PARAMS() { + return SEARCH_PARAMS; + } + + /** + * @returns {string} + * The weather keyword in `WEATHER_RS_DATA`. Can be used as a search string + * to match the weather suggestion. + */ + get WEATHER_KEYWORD() { + return WEATHER_KEYWORD; + } + + /** + * @returns {object} + * Default remote settings data that sets up `WEATHER_KEYWORD` as the + * keyword for the weather suggestion. + */ + get WEATHER_RS_DATA() { + return { ...WEATHER_RS_DATA }; + } + + /** + * @returns {object} + * A mock weather suggestion. + */ + get WEATHER_SUGGESTION() { + return WEATHER_SUGGESTION; + } + + /** + * @returns {MockMerinoServer} + * The mock Merino server. The server isn't started until its `start()` + * method is called. + */ + get server() { + return this.#server; + } + + /** + * Clears the Merino-related histograms and returns them. + * + * @param {object} options + * Options + * @param {string} options.extraLatency + * The name of another latency histogram you expect to be updated. + * @param {string} options.extraResponse + * The name of another response histogram you expect to be updated. + * @returns {object} + * An object of histograms: `{ latency, response }` + * `latency` and `response` are both arrays of Histogram objects. + */ + getAndClearHistograms({ + extraLatency = undefined, + extraResponse = undefined, + } = {}) { + let histograms = { + latency: [ + lazy.TelemetryTestUtils.getAndClearHistogram(HISTOGRAM_LATENCY), + ], + response: [ + lazy.TelemetryTestUtils.getAndClearHistogram(HISTOGRAM_RESPONSE), + ], + }; + if (extraLatency) { + histograms.latency.push( + lazy.TelemetryTestUtils.getAndClearHistogram(extraLatency) + ); + } + if (extraResponse) { + histograms.response.push( + lazy.TelemetryTestUtils.getAndClearHistogram(extraResponse) + ); + } + return histograms; + } + + /** + * Asserts the Merino-related histograms are updated as expected. Clears the + * histograms before returning. + * + * @param {object} options + * Options object + * @param {MerinoClient} options.client + * The relevant `MerinoClient` instance. This is used to check the latency + * stopwatch. + * @param {object} options.histograms + * The histograms object returned from `getAndClearHistograms()`. + * @param {string} options.response + * The expected string label for the `response` histogram. If the histogram + * should not be recorded, pass null. + * @param {boolean} options.latencyRecorded + * Whether the latency histogram is expected to contain a value. + * @param {boolean} options.latencyStopwatchRunning + * Whether the latency stopwatch is expected to be running. + */ + checkAndClearHistograms({ + client, + histograms, + response, + latencyRecorded, + latencyStopwatchRunning = false, + }) { + // Check the response histograms. + if (response) { + this.Assert.ok( + RESPONSE_HISTOGRAM_VALUES.hasOwnProperty(response), + "Sanity check: Expected response is valid: " + response + ); + for (let histogram of histograms.response) { + lazy.TelemetryTestUtils.assertHistogram( + histogram, + RESPONSE_HISTOGRAM_VALUES[response], + 1 + ); + } + } else { + for (let histogram of histograms.response) { + this.Assert.strictEqual( + histogram.snapshot().sum, + 0, + "Response histogram not updated: " + histogram.name() + ); + } + } + + // Check the latency histograms. + if (latencyRecorded) { + // There should be a single value across all buckets. + for (let histogram of histograms.latency) { + this.Assert.deepEqual( + Object.values(histogram.snapshot().values).filter(v => v > 0), + [1], + "Latency histogram updated: " + histogram.name() + ); + } + } else { + for (let histogram of histograms.latency) { + this.Assert.strictEqual( + histogram.snapshot().sum, + 0, + "Latency histogram not updated: " + histogram.name() + ); + } + } + + // Check the latency stopwatch. + this.Assert.equal( + TelemetryStopwatch.running( + HISTOGRAM_LATENCY, + client._test_latencyStopwatchInstance + ), + latencyStopwatchRunning, + "Latency stopwatch running as expected" + ); + + // Clear histograms. + for (let histogramArray of Object.values(histograms)) { + for (let histogram of histogramArray) { + histogram.clear(); + } + } + } + + /** + * Initializes the quick suggest weather feature and mock Merino server. + */ + async initWeather() { + await this.server.start(); + this.server.response.body.suggestions = [WEATHER_SUGGESTION]; + + lazy.QuickSuggest.weather._test_fetchIntervalMs = WEATHER_FETCH_INTERVAL_MS; + + // Enabling weather will trigger a fetch. Wait for it to finish so the + // suggestion is ready when this function returns. + let fetchPromise = lazy.QuickSuggest.weather.waitForFetches(); + lazy.UrlbarPrefs.set("weather.featureGate", true); + lazy.UrlbarPrefs.set("suggest.weather", true); + await fetchPromise; + + this.Assert.equal( + lazy.QuickSuggest.weather._test_pendingFetchCount, + 0, + "No pending fetches after awaiting initial fetch" + ); + + this.registerCleanupFunction?.(async () => { + lazy.UrlbarPrefs.clear("weather.featureGate"); + lazy.UrlbarPrefs.clear("suggest.weather"); + lazy.QuickSuggest.weather._test_fetchIntervalMs = -1; + }); + } + + #initDepth = 0; + #server = null; +} + +/** + * A mock Merino server with useful helper methods. + */ +class MockMerinoServer { + /** + * Until `start()` is called the server isn't started and `this.url` is null. + * + * @param {object} scope + * The global JS scope where tests are being run. This allows the instance + * to access test helpers like `Assert` that are available in the scope. + */ + constructor(scope) { + scope.info?.("MockMerinoServer constructor"); + + for (let p of TEST_SCOPE_PROPERTIES) { + this[p] = scope[p]; + } + + let path = "/merino"; + this.#httpServer = new HttpServer(); + this.#httpServer.registerPathHandler(path, (req, resp) => + this.#handleRequest(req, resp) + ); + this.#baseURL = new URL("http://localhost/"); + this.#baseURL.pathname = path; + + this.reset(); + } + + /** + * Uninitializes the server. + */ + uninit() { + this.info?.("MockMerinoServer uninit"); + for (let p of TEST_SCOPE_PROPERTIES) { + this[p] = null; + } + } + + /** + * @returns {nsIHttpServer} + * The underlying HTTP server. + */ + get httpServer() { + return this.#httpServer; + } + + /** + * @returns {URL} + * The server's endpoint URL or null if the server isn't running. + */ + get url() { + return this.#url; + } + + /** + * @returns {Array} + * Array of received nsIHttpRequest objects. Requests are continually + * collected, and the list can be cleared with `reset()`. + */ + get requests() { + return this.#requests; + } + + /** + * @returns {object} + * An object that describes the response that the server will return. Can be + * modified or set to a different object to change the response. Can be + * reset to the default reponse by calling `reset()`. For details see + * `makeDefaultResponse()` and `#handleRequest()`. In summary: + * + * { + * status, + * contentType, + * delay, + * body: { + * request_id, + * suggestions, + * }, + * } + */ + get response() { + return this.#response; + } + set response(value) { + this.#response = value; + } + + /** + * Starts the server and sets `this.url`. If the server was created with a + * test scope that defines `registerCleanupFunction()`, you don't need to call + * `stop()` yourself because it will automatically be called as a cleanup + * function. Otherwise you'll need to call `stop()`. + */ + async start() { + if (this.#url) { + return; + } + + this.info("MockMerinoServer starting"); + + this.#httpServer.start(-1); + this.#url = new URL(this.#baseURL); + this.#url.port = this.#httpServer.identity.primaryPort; + + this._originalEndpointURL = lazy.UrlbarPrefs.get("merino.endpointURL"); + lazy.UrlbarPrefs.set("merino.endpointURL", this.#url.toString()); + + this.registerCleanupFunction?.(() => this.stop()); + + // Wait for the server to actually start serving. In TV tests, where the + // server is created over and over again, sometimes it doesn't seem to be + // ready after being recreated even after `#httpServer.start()` is called. + this.info("MockMerinoServer waiting to start serving..."); + this.reset(); + let suggestion; + while (!suggestion) { + let response = await fetch(this.#url); + let body = await response?.json(); + suggestion = body?.suggestions?.[0]; + } + this.reset(); + this.info("MockMerinoServer is now serving"); + } + + /** + * Stops the server and cleans up other state. + */ + async stop() { + if (!this.#url) { + return; + } + + // `uninit()` may have already been called by this point and removed + // `this.info()`, so don't assume it's defined. + this.info?.("MockMerinoServer stopping"); + + // Cancel delayed-response timers and resolve their promises. Otherwise, if + // a test awaits this method before finishing, it will hang until the timers + // fire and allow the server to send the responses. + this.#cancelDelayedResponses(); + + await this.#httpServer.stop(); + this.#url = null; + lazy.UrlbarPrefs.set("merino.endpointURL", this._originalEndpointURL); + + this.info?.("MockMerinoServer is now stopped"); + } + + /** + * Returns a new object that describes the default response the server will + * return. + * + * @returns {object} + */ + makeDefaultResponse() { + return { + status: 200, + contentType: "application/json", + body: { + request_id: "request_id", + suggestions: [ + { + provider: "adm", + full_keyword: "full_keyword", + title: "title", + url: "url", + icon: null, + impression_url: "impression_url", + click_url: "click_url", + block_id: 1, + advertiser: "advertiser", + is_sponsored: true, + score: 1, + }, + ], + }, + }; + } + + /** + * Clears the received requests and sets the response to the default. + */ + reset() { + this.#requests = []; + this.response = this.makeDefaultResponse(); + this.#cancelDelayedResponses(); + } + + /** + * Asserts a given list of requests has been received. Clears the list of + * received requests before returning. + * + * @param {Array} expected + * The expected requests. Each item should be an object: `{ params }` + */ + checkAndClearRequests(expected) { + let actual = this.requests.map(req => { + let params = new URLSearchParams(req.queryString); + return { params: Object.fromEntries(params) }; + }); + + this.info("Checking requests"); + this.info("actual: " + JSON.stringify(actual)); + this.info("expect: " + JSON.stringify(expected)); + + // Check the request count. + this.Assert.equal(actual.length, expected.length, "Expected request count"); + if (actual.length != expected.length) { + return; + } + + // Check each request. + for (let i = 0; i < actual.length; i++) { + let a = actual[i]; + let e = expected[i]; + this.info("Checking requests at index " + i); + this.info("actual: " + JSON.stringify(a)); + this.info("expect: " + JSON.stringify(e)); + + // Check required search params. + for (let p of REQUIRED_SEARCH_PARAMS) { + this.Assert.ok( + a.params.hasOwnProperty(p), + "Required param is present in actual request: " + p + ); + if (p != SEARCH_PARAMS.SESSION_ID) { + this.Assert.ok( + e.params.hasOwnProperty(p), + "Required param is present in expected request: " + p + ); + } + } + + // If the expected request doesn't include a session ID, then: + if (!e.params.hasOwnProperty(SEARCH_PARAMS.SESSION_ID)) { + if (e.params[SEARCH_PARAMS.SEQUENCE_NUMBER] == 0 || i == 0) { + // If its sequence number is zero, then copy the actual request's + // sequence number to the expected request. As a convenience, do the + // same if this is the first request. + e.params[SEARCH_PARAMS.SESSION_ID] = + a.params[SEARCH_PARAMS.SESSION_ID]; + } else { + // Otherwise this is not the first request in the session and + // therefore the session ID should be the same as the ID in the + // previous expected request. + e.params[SEARCH_PARAMS.SESSION_ID] = + expected[i - 1].params[SEARCH_PARAMS.SESSION_ID]; + } + } + + this.Assert.deepEqual(a, e, "Expected request at index " + i); + + let actualSessionID = a.params[SEARCH_PARAMS.SESSION_ID]; + this.Assert.ok(actualSessionID, "Session ID exists"); + this.Assert.ok( + /^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/i.test(actualSessionID), + "Session ID is a UUID" + ); + } + + this.#requests = []; + } + + /** + * Temporarily creates the conditions for a network error. Any Merino fetches + * that occur during the callback will fail with a network error. + * + * @param {Function} callback + * Callback function. + */ + async withNetworkError(callback) { + // Set the endpoint to a valid, unreachable URL. + let originalURL = lazy.UrlbarPrefs.get("merino.endpointURL"); + lazy.UrlbarPrefs.set( + "merino.endpointURL", + "http://localhost/valid-but-unreachable-url" + ); + + // Set the timeout high enough that the network error exception will happen + // first. On Mac and Linux the fetch naturally times out fairly quickly but + // on Windows it seems to take 5s, so set our artificial timeout to 10s. + let originalTimeout = lazy.UrlbarPrefs.get("merino.timeoutMs"); + lazy.UrlbarPrefs.set("merino.timeoutMs", 10000); + + await callback(); + + lazy.UrlbarPrefs.set("merino.endpointURL", originalURL); + lazy.UrlbarPrefs.set("merino.timeoutMs", originalTimeout); + } + + /** + * Returns a promise that will resolve when the next request is received. + * + * @returns {Promise} + */ + waitForNextRequest() { + if (!this.#nextRequestDeferred) { + this.#nextRequestDeferred = lazy.PromiseUtils.defer(); + } + return this.#nextRequestDeferred.promise; + } + + /** + * nsIHttpServer request handler. + * + * @param {nsIHttpRequest} httpRequest + * Request. + * @param {nsIHttpResponse} httpResponse + * Response. + */ + #handleRequest(httpRequest, httpResponse) { + this.info( + "MockMerinoServer received request with query string: " + + JSON.stringify(httpRequest.queryString) + ); + this.info( + "MockMerinoServer replying with response: " + + JSON.stringify(this.response) + ); + + // Add the request to the list of received requests. + this.#requests.push(httpRequest); + + // Resolve promises waiting on the next request. + this.#nextRequestDeferred?.resolve(); + this.#nextRequestDeferred = null; + + // Now set up and finish the response. + httpResponse.processAsync(); + + let { response } = this; + + let finishResponse = () => { + let status = response.status || 200; + httpResponse.setStatusLine("", status, status); + + let contentType = response.contentType || "application/json"; + httpResponse.setHeader("Content-Type", contentType, false); + + if (typeof response.body == "string") { + httpResponse.write(response.body); + } else if (response.body) { + httpResponse.write(JSON.stringify(response.body)); + } + + httpResponse.finish(); + }; + + if (typeof response.delay != "number") { + finishResponse(); + return; + } + + // Set up a timer to wait until the delay elapses. Since we called + // `httpResponse.processAsync()`, we need to be careful to always finish the + // response, even if the timer is canceled. Otherwise the server will hang + // when we try to stop it at the end of the test. When an `nsITimer` is + // canceled, its callback is *not* called. Therefore we set up a race + // between the timer's callback and a deferred promise. If the timer is + // canceled, resolving the deferred promise will resolve the race, and the + // response can then be finished. + + let delayedResponseID = this.#nextDelayedResponseID++; + this.info( + "MockMerinoServer delaying response: " + + JSON.stringify({ delayedResponseID, delay: response.delay }) + ); + + let deferred = lazy.PromiseUtils.defer(); + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + let record = { timer, resolve: deferred.resolve }; + this.#delayedResponseRecords.add(record); + + // Don't await this promise. + Promise.race([ + deferred.promise, + new Promise(resolve => { + timer.initWithCallback( + resolve, + response.delay, + Ci.nsITimer.TYPE_ONE_SHOT + ); + }), + ]).then(() => { + this.info( + "MockMerinoServer done delaying response: " + + JSON.stringify({ delayedResponseID }) + ); + deferred.resolve(); + this.#delayedResponseRecords.delete(record); + finishResponse(); + }); + } + + /** + * Cancels the timers for delayed responses and resolves their promises. + */ + #cancelDelayedResponses() { + for (let { timer, resolve } of this.#delayedResponseRecords) { + timer.cancel(); + resolve(); + } + this.#delayedResponseRecords.clear(); + } + + #httpServer = null; + #url = null; + #baseURL = null; + #response = null; + #requests = []; + #nextRequestDeferred = null; + #nextDelayedResponseID = 0; + #delayedResponseRecords = new Set(); +} + +export var MerinoTestUtils = new _MerinoTestUtils(); diff --git a/browser/components/urlbar/tests/quicksuggest/QuickSuggestTestUtils.sys.mjs b/browser/components/urlbar/tests/quicksuggest/QuickSuggestTestUtils.sys.mjs new file mode 100644 index 0000000000..34da73e847 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/QuickSuggestTestUtils.sys.mjs @@ -0,0 +1,1017 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-disable mozilla/valid-lazy */ + +import { + CONTEXTUAL_SERVICES_PING_TYPES, + PartnerLinkAttribution, +} from "resource:///modules/PartnerLinkAttribution.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs", + ExperimentFakes: "resource://testing-common/NimbusTestUtils.sys.mjs", + NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", + QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs", + QuickSuggestRemoteSettings: + "resource:///modules/urlbar/private/QuickSuggestRemoteSettings.sys.mjs", + RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", + SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs", + TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs", + TestUtils: "resource://testing-common/TestUtils.sys.mjs", + UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs", + UrlbarProviderQuickSuggest: + "resource:///modules/UrlbarProviderQuickSuggest.sys.mjs", + UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs", + UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs", + setTimeout: "resource://gre/modules/Timer.sys.mjs", + sinon: "resource://testing-common/Sinon.sys.mjs", +}); + +let gTestScope; + +// Test utils singletons need special handling. Since they are uninitialized in +// cleanup functions, they must be re-initialized on each new test. That does +// not happen automatically inside system modules like this one because system +// module lifetimes are the app's lifetime, unlike individual browser chrome and +// xpcshell tests. +Object.defineProperty(lazy, "UrlbarTestUtils", { + get: () => { + if (!lazy._UrlbarTestUtils) { + const { UrlbarTestUtils: module } = ChromeUtils.importESModule( + "resource://testing-common/UrlbarTestUtils.sys.mjs" + ); + module.init(gTestScope); + gTestScope.registerCleanupFunction(() => { + // Make sure the utils are re-initialized during the next test. + lazy._UrlbarTestUtils = null; + }); + lazy._UrlbarTestUtils = module; + } + return lazy._UrlbarTestUtils; + }, +}); + +// Test utils singletons need special handling. Since they are uninitialized in +// cleanup functions, they must be re-initialized on each new test. That does +// not happen automatically inside system modules like this one because system +// module lifetimes are the app's lifetime, unlike individual browser chrome and +// xpcshell tests. +Object.defineProperty(lazy, "MerinoTestUtils", { + get: () => { + if (!lazy._MerinoTestUtils) { + const { MerinoTestUtils: module } = ChromeUtils.importESModule( + "resource://testing-common/MerinoTestUtils.sys.mjs" + ); + module.init(gTestScope); + gTestScope.registerCleanupFunction(() => { + // Make sure the utils are re-initialized during the next test. + lazy._MerinoTestUtils = null; + }); + lazy._MerinoTestUtils = module; + } + return lazy._MerinoTestUtils; + }, +}); + +const DEFAULT_CONFIG = {}; + +const BEST_MATCH_CONFIG = { + best_match: { + blocked_suggestion_ids: [], + min_search_string_length: 4, + }, +}; + +const DEFAULT_PING_PAYLOADS = { + [CONTEXTUAL_SERVICES_PING_TYPES.QS_BLOCK]: { + advertiser: "testadvertiser", + block_id: 1, + context_id: () => actual => !!actual, + iab_category: "22 - Shopping", + improve_suggest_experience_checked: false, + match_type: "firefox-suggest", + request_id: null, + source: "remote-settings", + }, + [CONTEXTUAL_SERVICES_PING_TYPES.QS_SELECTION]: { + advertiser: "testadvertiser", + block_id: 1, + context_id: () => actual => !!actual, + improve_suggest_experience_checked: false, + match_type: "firefox-suggest", + reporting_url: "https://example.com/click", + request_id: null, + source: "remote-settings", + }, + [CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION]: { + advertiser: "testadvertiser", + block_id: 1, + context_id: () => actual => !!actual, + improve_suggest_experience_checked: false, + is_clicked: false, + match_type: "firefox-suggest", + reporting_url: "https://example.com/impression", + request_id: null, + source: "remote-settings", + }, +}; + +// The following properties and methods are copied from the test scope to the +// test utils object so they can be easily accessed. Be careful about assuming a +// particular property will be defined because depending on the scope -- browser +// test or xpcshell test -- some may not be. +const TEST_SCOPE_PROPERTIES = [ + "Assert", + "EventUtils", + "info", + "registerCleanupFunction", +]; + +/** + * Mock RemoteSettings. + * + * @param {object} options + * Options object + * @param {object} options.config + * Dummy config in the RemoteSettings. + * @param {Array} options.data + * Dummy data in the RemoteSettings. + */ +class MockRemoteSettings { + constructor({ config = DEFAULT_CONFIG, data = [] }) { + this.#config = config; + this.#data = data; + + // Make a stub for "get" function to return dummy data. + const rs = lazy.RemoteSettings("quicksuggest"); + this.#sandbox = lazy.sinon.createSandbox(); + this.#sandbox.stub(rs, "get").callsFake(async query => { + return query.filters.type === "configuration" + ? [{ configuration: this.#config }] + : this.#data.filter(r => r.type === query.filters.type); + }); + + // Make a stub for "download" in attachments. + this.#sandbox.stub(rs.attachments, "download").callsFake(async record => { + if (!record.attachment) { + throw new Error("No attachmet in the record"); + } + const encoder = new TextEncoder(); + return { + buffer: encoder.encode(JSON.stringify(record.attachment)), + }; + }); + } + + async sync() { + if (!lazy.QuickSuggestRemoteSettings.rs) { + // There are no registered features that use remote settings. + return; + } + + // Observe config-set event to recognize that the config is synced. + const onConfigSync = new Promise(resolve => { + lazy.QuickSuggestRemoteSettings.emitter.once("config-set", resolve); + }); + + // Make a stub for each feature to recognize that the features are synced. + const features = lazy.QuickSuggestRemoteSettings.features; + const onFeatureSyncs = features.map(feature => { + return new Promise(resolve => { + const stub = this.#sandbox + .stub(feature, "onRemoteSettingsSync") + .callsFake(async (...args) => { + // Call and wait for the original function. + await stub.wrappedMethod.apply(feature, args); + stub.restore(); + resolve(); + }); + }); + }); + + // Force to sync. + const rs = lazy.RemoteSettings("quicksuggest"); + rs.emit("sync"); + + // Wait for sync. + await Promise.all([onConfigSync, ...onFeatureSyncs]); + } + + /* + * Update the config and data in RemoteSettings. If the config or the data are + * undefined, use the current one. + * + * @param {object} options + * Options object + * @param {object} options.config + * Dummy config in the RemoteSettings. + * @param {Array} options.data + * Dummy data in the RemoteSettings. + */ + async update({ config = this.#config, data = this.#data }) { + this.#config = config; + this.#data = data; + + await this.sync(); + } + + cleanup() { + this.#sandbox.restore(); + } + + #config = null; + #data = null; + #sandbox = null; +} + +/** + * Test utils for quick suggest. + */ +class _QuickSuggestTestUtils { + /** + * Initializes the utils. + * + * @param {object} scope + * The global JS scope where tests are being run. This allows the instance + * to access test helpers like `Assert` that are available in the scope. + */ + init(scope) { + if (!scope) { + throw new Error("QuickSuggestTestUtils() must be called with a scope"); + } + gTestScope = scope; + for (let p of TEST_SCOPE_PROPERTIES) { + this[p] = scope[p]; + } + // If you add other properties to `this`, null them in `uninit()`. + + Services.telemetry.clearScalars(); + + scope.registerCleanupFunction?.(() => this.uninit()); + } + + /** + * Uninitializes the utils. If they were created with a test scope that + * defines `registerCleanupFunction()`, you don't need to call this yourself + * because it will automatically be called as a cleanup function. Otherwise + * you'll need to call this. + */ + uninit() { + gTestScope = null; + for (let p of TEST_SCOPE_PROPERTIES) { + this[p] = null; + } + Services.telemetry.clearScalars(); + } + + get DEFAULT_CONFIG() { + // Return a clone so callers can modify it. + return Cu.cloneInto(DEFAULT_CONFIG, this); + } + + get BEST_MATCH_CONFIG() { + // Return a clone so callers can modify it. + return Cu.cloneInto(BEST_MATCH_CONFIG, this); + } + + /** + * Waits for quick suggest initialization to finish, ensures its data will not + * be updated again during the test, and also optionally sets it up with mock + * suggestions. + * + * @param {object} options + * Options object + * @param {Array} options.remoteSettingsResults + * Array of remote settings result objects. If not given, no suggestions + * will be present in remote settings. + * @param {Array} options.merinoSuggestions + * Array of Merino suggestion objects. If given, this function will start + * the mock Merino server and set `quicksuggest.dataCollection.enabled` to + * true so that `UrlbarProviderQuickSuggest` will fetch suggestions from it. + * Otherwise Merino will not serve suggestions, but you can still set up + * Merino without using this function by using `MerinoTestUtils` directly. + * @param {object} options.config + * The quick suggest configuration object. + * @returns {Function} + * A cleanup function. You only need to call this function if you're in a + * browser chrome test and you did not also call `init`. You can ignore it + * otherwise. + */ + async ensureQuickSuggestInit({ + remoteSettingsResults, + merinoSuggestions = null, + config = DEFAULT_CONFIG, + } = {}) { + this.#mockRemoteSettings = new MockRemoteSettings({ + config, + data: remoteSettingsResults, + }); + + this.info?.("ensureQuickSuggestInit calling QuickSuggest.init()"); + lazy.QuickSuggest.init(); + + // Sync with current data. + await this.#mockRemoteSettings.sync(); + + // Set up Merino. + if (merinoSuggestions) { + this.info?.("ensureQuickSuggestInit setting up Merino server"); + await lazy.MerinoTestUtils.server.start(); + lazy.MerinoTestUtils.server.response.body.suggestions = merinoSuggestions; + lazy.UrlbarPrefs.set("quicksuggest.dataCollection.enabled", true); + this.info?.("ensureQuickSuggestInit done setting up Merino server"); + } + + let cleanup = async () => { + this.info?.("ensureQuickSuggestInit starting cleanup"); + this.#mockRemoteSettings.cleanup(); + if (merinoSuggestions) { + lazy.UrlbarPrefs.clear("quicksuggest.dataCollection.enabled"); + } + this.info?.("ensureQuickSuggestInit finished cleanup"); + }; + this.registerCleanupFunction?.(cleanup); + + return cleanup; + } + + /** + * Clears the current remote settings data and adds a new set of data. + * This can be used to add remote settings data after + * `ensureQuickSuggestInit()` has been called. + * + * @param {Array} data + * Array of remote settings data objects. + */ + async setRemoteSettingsResults(data) { + await this.#mockRemoteSettings.update({ data }); + } + + /** + * Sets the quick suggest configuration. You should call this again with + * `DEFAULT_CONFIG` before your test finishes. See also `withConfig()`. + * + * @param {object} config + * The config to be applied. See + */ + async setConfig(config) { + await this.#mockRemoteSettings.update({ config }); + } + + /** + * Sets the quick suggest configuration, calls your callback, and restores the + * previous configuration. + * + * @param {object} options + * The options object. + * @param {object} options.config + * The configuration that should be used with the callback + * @param {Function} options.callback + * Will be called with the configuration applied + * + * @see {@link setConfig} + */ + async withConfig({ config, callback }) { + let original = lazy.QuickSuggestRemoteSettings.config; + await this.setConfig(config); + await callback(); + await this.setConfig(original); + } + + /** + * Sets the Firefox Suggest scenario and waits for prefs to be updated. + * + * @param {string} scenario + * Pass falsey to reset the scenario to the default. + */ + async setScenario(scenario) { + // If we try to set the scenario before a previous update has finished, + // `updateFirefoxSuggestScenario` will bail, so wait. + await this.waitForScenarioUpdated(); + await lazy.UrlbarPrefs.updateFirefoxSuggestScenario({ scenario }); + } + + /** + * Waits for any prior scenario update to finish. + */ + async waitForScenarioUpdated() { + await lazy.TestUtils.waitForCondition( + () => !lazy.UrlbarPrefs.updatingFirefoxSuggestScenario, + "Waiting for updatingFirefoxSuggestScenario to be false" + ); + } + + /** + * Asserts a result is a quick suggest result. + * + * @param {object} [options] + * The options object. + * @param {string} options.url + * The expected URL. At least one of `url` and `originalUrl` must be given. + * @param {string} options.originalUrl + * The expected original URL (the URL with an unreplaced timestamp + * template). At least one of `url` and `originalUrl` must be given. + * @param {object} options.window + * The window that should be used for this assertion + * @param {number} [options.index] + * The expected index of the quick suggest result. Pass -1 to use the index + * of the last result. + * @param {boolean} [options.isSponsored] + * Whether the result is expected to be sponsored. + * @param {boolean} [options.isBestMatch] + * Whether the result is expected to be a best match. + * @returns {result} + * The quick suggest result. + */ + async assertIsQuickSuggest({ + url, + originalUrl, + window, + index = -1, + isSponsored = true, + isBestMatch = false, + } = {}) { + this.Assert.ok( + url || originalUrl, + "At least one of url and originalUrl is specified" + ); + + if (index < 0) { + let resultCount = lazy.UrlbarTestUtils.getResultCount(window); + if (isBestMatch) { + index = 1; + this.Assert.greater( + resultCount, + 1, + "Sanity check: Result count should be > 1" + ); + } else { + index = resultCount - 1; + this.Assert.greater( + resultCount, + 0, + "Sanity check: Result count should be > 0" + ); + } + } + + let details = await lazy.UrlbarTestUtils.getDetailsOfResultAt( + window, + index + ); + let { result } = details; + + this.info?.( + `Checking actual result at index ${index}: ` + JSON.stringify(result) + ); + + this.Assert.equal( + result.providerName, + "UrlbarProviderQuickSuggest", + "Result provider name is UrlbarProviderQuickSuggest" + ); + this.Assert.equal(details.type, lazy.UrlbarUtils.RESULT_TYPE.URL); + this.Assert.equal(details.isSponsored, isSponsored, "Result isSponsored"); + if (url) { + this.Assert.equal(details.url, url, "Result URL"); + } + if (originalUrl) { + this.Assert.equal( + result.payload.originalUrl, + originalUrl, + "Result original URL" + ); + } + + this.Assert.equal(!!result.isBestMatch, isBestMatch, "Result isBestMatch"); + + let { row } = details.element; + + let sponsoredElement = isBestMatch + ? row._elements.get("bottom") + : row._elements.get("action"); + this.Assert.ok(sponsoredElement, "Result sponsored label element exists"); + this.Assert.equal( + sponsoredElement.textContent, + isSponsored ? "Sponsored" : "", + "Result sponsored label" + ); + + this.Assert.equal( + result.payload.helpUrl, + lazy.QuickSuggest.HELP_URL, + "Result helpURL" + ); + + if (lazy.UrlbarPrefs.get("resultMenu")) { + this.Assert.ok( + row._buttons.get("menu"), + "The menu button should be present" + ); + } else { + let helpButton = row._buttons.get("help"); + this.Assert.ok(helpButton, "The help button should be present"); + + let blockButton = row._buttons.get("block"); + if (!isBestMatch) { + this.Assert.equal( + !!blockButton, + lazy.UrlbarPrefs.get("quickSuggestBlockingEnabled"), + "The block button is present iff quick suggest blocking is enabled" + ); + } else { + this.Assert.equal( + !!blockButton, + lazy.UrlbarPrefs.get("bestMatchBlockingEnabled"), + "The block button is present iff best match blocking is enabled" + ); + } + } + + return details; + } + + /** + * Asserts a result is not a quick suggest result. + * + * @param {object} window + * The window that should be used for this assertion + * @param {number} index + * The index of the result. + */ + async assertIsNotQuickSuggest(window, index) { + let details = await lazy.UrlbarTestUtils.getDetailsOfResultAt( + window, + index + ); + this.Assert.notEqual( + details.result.providerName, + "UrlbarProviderQuickSuggest", + `Result at index ${index} is not provided by UrlbarProviderQuickSuggest` + ); + } + + /** + * Asserts that none of the results are quick suggest results. + * + * @param {object} window + * The window that should be used for this assertion + */ + async assertNoQuickSuggestResults(window) { + for (let i = 0; i < lazy.UrlbarTestUtils.getResultCount(window); i++) { + await this.assertIsNotQuickSuggest(window, i); + } + } + + /** + * Checks the values of all the quick suggest telemetry keyed scalars and, + * if provided, other non-quick-suggest keyed scalars. Scalar values are all + * assumed to be 1. + * + * @param {object} expectedKeysByScalarName + * Maps scalar names to keys that are expected to be recorded. The value for + * each key is assumed to be 1. If you expect a scalar to be incremented, + * include it in this object; otherwise, don't include it. + */ + assertScalars(expectedKeysByScalarName) { + let scalars = lazy.TelemetryTestUtils.getProcessScalars( + "parent", + true, + true + ); + + // Check all quick suggest scalars. + expectedKeysByScalarName = { ...expectedKeysByScalarName }; + for (let scalarName of Object.values( + lazy.UrlbarProviderQuickSuggest.TELEMETRY_SCALARS + )) { + if (scalarName in expectedKeysByScalarName) { + lazy.TelemetryTestUtils.assertKeyedScalar( + scalars, + scalarName, + expectedKeysByScalarName[scalarName], + 1 + ); + delete expectedKeysByScalarName[scalarName]; + } else { + this.Assert.ok( + !(scalarName in scalars), + "Scalar should not be present: " + scalarName + ); + } + } + + // Check any other remaining scalars that were passed in. + for (let [scalarName, key] of Object.entries(expectedKeysByScalarName)) { + lazy.TelemetryTestUtils.assertKeyedScalar(scalars, scalarName, key, 1); + } + } + + /** + * Checks quick suggest telemetry events. This is the same as + * `TelemetryTestUtils.assertEvents()` except it filters in only quick suggest + * events by default. If you are expecting events that are not in the quick + * suggest category, use `TelemetryTestUtils.assertEvents()` directly or pass + * in a filter override for `category`. + * + * @param {Array} expectedEvents + * List of expected telemetry events. + * @param {object} filterOverrides + * Extra properties to set in the filter object. + * @param {object} options + * The options object to pass to `TelemetryTestUtils.assertEvents()`. + */ + assertEvents(expectedEvents, filterOverrides = {}, options = undefined) { + lazy.TelemetryTestUtils.assertEvents( + expectedEvents, + { + category: lazy.QuickSuggest.TELEMETRY_EVENT_CATEGORY, + ...filterOverrides, + }, + options + ); + } + + /** + * Creates a `sinon.sandbox` and `sinon.spy` that can be used to instrument + * the quick suggest custom telemetry pings. If `init` was called with a test + * scope where `registerCleanupFunction` is defined, the sandbox will + * automically be restored at the end of the test. + * + * @returns {object} + * An object: { sandbox, spy, spyCleanup } + * `spyCleanup` is a cleanup function that should be called if you're in a + * browser chrome test and you did not also call `init`, or if you need to + * remove the spy before the test ends for some other reason. You can ignore + * it otherwise. + */ + createTelemetryPingSpy() { + let sandbox = lazy.sinon.createSandbox(); + let spy = sandbox.spy( + PartnerLinkAttribution._pingCentre, + "sendStructuredIngestionPing" + ); + let spyCleanup = () => sandbox.restore(); + this.registerCleanupFunction?.(spyCleanup); + return { sandbox, spy, spyCleanup }; + } + + /** + * Asserts that custom telemetry pings are recorded in the order they appear + * in the given `pings` array and that no other pings are recorded. + * + * @param {object} spy + * A `sinon.spy` object. See `createTelemetryPingSpy()`. This method resets + * the spy before returning. + * @param {Array} pings + * The expected pings in the order they are expected to be recorded. Each + * item in this array should be an object: `{ type, payload }` + * + * {string} type + * The ping's expected type, one of the `CONTEXTUAL_SERVICES_PING_TYPES` + * values. + * {object} payload + * The ping's expected payload. For convenience, you can leave out + * properties whose values are expected to be the default values defined + * in `DEFAULT_PING_PAYLOADS`. + */ + assertPings(spy, pings) { + let calls = spy.getCalls(); + this.Assert.equal( + calls.length, + pings.length, + "Expected number of ping calls" + ); + + for (let i = 0; i < pings.length; i++) { + let ping = pings[i]; + this.info?.( + `Checking ping at index ${i}, expected is: ` + JSON.stringify(ping) + ); + + // Add default properties to the expected payload for any that aren't + // already defined. + let { type, payload } = ping; + let defaultPayload = DEFAULT_PING_PAYLOADS[type]; + this.Assert.ok( + defaultPayload, + `Sanity check: Default payload exists for type: ${type}` + ); + payload = { ...defaultPayload, ...payload }; + + // Check the endpoint URL. + let call = calls[i]; + let endpointURL = call.args[1]; + this.Assert.ok( + endpointURL.includes(type), + `Endpoint URL corresponds to the expected ping type: ${type}` + ); + + // Check the payload. + let actualPayload = call.args[0]; + this._assertPingPayload(actualPayload, payload); + } + + spy.resetHistory(); + } + + /** + * Helper for checking contextual services ping payloads. + * + * @param {object} actualPayload + * The actual payload in the ping. + * @param {object} expectedPayload + * An object describing the expected payload. Non-function values in this + * object are checked for equality against the corresponding actual payload + * values. Function values are called and passed the corresponding actual + * values and should return true if the actual values are correct. + */ + _assertPingPayload(actualPayload, expectedPayload) { + this.info?.( + "Checking ping payload. Actual: " + + JSON.stringify(actualPayload) + + " -- Expected (excluding function properties): " + + JSON.stringify(expectedPayload) + ); + + this.Assert.equal( + Object.entries(actualPayload).length, + Object.entries(expectedPayload).length, + "Payload has expected number of properties" + ); + + for (let [key, expectedValue] of Object.entries(expectedPayload)) { + let actualValue = actualPayload[key]; + if (typeof expectedValue == "function") { + this.Assert.ok(expectedValue(actualValue), "Payload property: " + key); + } else { + this.Assert.equal( + actualValue, + expectedValue, + "Payload property: " + key + ); + } + } + } + + /** + * Asserts that URLs in a result's payload have the timestamp template + * substring replaced with real timestamps. + * + * @param {UrlbarResult} result The results to check + * @param {object} urls + * An object that contains the expected payload properties with template + * substrings. For example: + * ```js + * { + * url: "http://example.com/foo-%YYYYMMDDHH%", + * sponsoredClickUrl: "http://example.com/bar-%YYYYMMDDHH%", + * } + * ``` + */ + assertTimestampsReplaced(result, urls) { + let { TIMESTAMP_TEMPLATE, TIMESTAMP_LENGTH } = lazy.QuickSuggest; + + // Parse the timestamp strings from each payload property and save them in + // `urls[key].timestamp`. + urls = { ...urls }; + for (let [key, url] of Object.entries(urls)) { + let index = url.indexOf(TIMESTAMP_TEMPLATE); + this.Assert.ok( + index >= 0, + `Timestamp template ${TIMESTAMP_TEMPLATE} is in URL ${url} for key ${key}` + ); + let value = result.payload[key]; + this.Assert.ok(value, "Key is in result payload: " + key); + let timestamp = value.substring(index, index + TIMESTAMP_LENGTH); + + // Set `urls[key]` to an object that's helpful in the logged info message + // below. + urls[key] = { url, value, timestamp }; + } + + this.info?.("Parsed timestamps: " + JSON.stringify(urls)); + + // Make a set of unique timestamp strings. There should only be one. + let { timestamp } = Object.values(urls)[0]; + this.Assert.deepEqual( + [...new Set(Object.values(urls).map(o => o.timestamp))], + [timestamp], + "There's only one unique timestamp string" + ); + + // Parse the parts of the timestamp string. + let year = timestamp.slice(0, -6); + let month = timestamp.slice(-6, -4); + let day = timestamp.slice(-4, -2); + let hour = timestamp.slice(-2); + let date = new Date(year, month - 1, day, hour); + + // The timestamp should be no more than two hours in the past. Typically it + // will be the same as the current hour, but since its resolution is in + // terms of hours and it's possible the test may have crossed over into a + // new hour as it was running, allow for the previous hour. + this.Assert.less( + Date.now() - 2 * 60 * 60 * 1000, + date.getTime(), + "Timestamp is within the past two hours" + ); + } + + /** + * Calls a callback while enrolled in a mock Nimbus experiment. The experiment + * is automatically unenrolled and cleaned up after the callback returns. + * + * @param {object} options + * Options for the mock experiment. + * @param {Function} options.callback + * The callback to call while enrolled in the mock experiment. + * @param {object} options.options + * See {@link enrollExperiment}. + */ + async withExperiment({ callback, ...options }) { + let doExperimentCleanup = await this.enrollExperiment(options); + await callback(); + await doExperimentCleanup(); + } + + /** + * Enrolls in a mock Nimbus experiment. + * + * @param {object} options + * Options for the mock experiment. + * @param {object} [options.valueOverrides] + * Values for feature variables. + * @returns {Promise} + * The experiment cleanup function (async). + */ + async enrollExperiment({ valueOverrides = {} }) { + this.info?.("Awaiting ExperimentAPI.ready"); + await lazy.ExperimentAPI.ready(); + + // Wait for any prior scenario updates to finish. If updates are ongoing, + // UrlbarPrefs will ignore the Nimbus update when the experiment is + // installed. This shouldn't be a problem in practice because in reality + // scenario updates are triggered only on app startup and Nimbus + // enrollments, but tests can trigger lots of updates back to back. + await this.waitForScenarioUpdated(); + + let doExperimentCleanup = + await lazy.ExperimentFakes.enrollWithFeatureConfig({ + enabled: true, + featureId: "urlbar", + value: valueOverrides, + }); + + // Wait for the pref updates triggered by the experiment enrollment. + this.info?.("Awaiting update after enrolling in experiment"); + await this.waitForScenarioUpdated(); + + return async () => { + this.info?.("Awaiting experiment cleanup"); + await doExperimentCleanup(); + + // The same pref updates will be triggered by unenrollment, so wait for + // them again. + this.info?.("Awaiting update after unenrolling in experiment"); + await this.waitForScenarioUpdated(); + }; + } + + /** + * Clears the Nimbus exposure event. + */ + async clearExposureEvent() { + // Exposure event recording is queued to the idle thread, so wait for idle + // before we start so any events from previous tasks will have been recorded + // and won't interfere with this task. + await new Promise(resolve => Services.tm.idleDispatchToMainThread(resolve)); + + Services.telemetry.clearEvents(); + lazy.NimbusFeatures.urlbar._didSendExposureEvent = false; + lazy.QuickSuggest._recordedExposureEvent = false; + } + + /** + * Asserts the Nimbus exposure event is recorded or not as expected. + * + * @param {boolean} expectedRecorded + * Whether the event is expected to be recorded. + */ + async assertExposureEvent(expectedRecorded) { + this.Assert.equal( + lazy.QuickSuggest._recordedExposureEvent, + expectedRecorded, + "_recordedExposureEvent is correct" + ); + + let filter = { + category: "normandy", + method: "expose", + object: "nimbus_experiment", + }; + + let expectedEvents = []; + if (expectedRecorded) { + expectedEvents.push({ + ...filter, + extra: { + branchSlug: "control", + featureId: "urlbar", + }, + }); + } + + // The event recording is queued to the idle thread when the search starts, + // so likewise queue the assert to idle instead of doing it immediately. + await new Promise(resolve => { + Services.tm.idleDispatchToMainThread(() => { + lazy.TelemetryTestUtils.assertEvents(expectedEvents, filter); + resolve(); + }); + }); + } + + /** + * Sets the app's locales, calls your callback, and resets locales. + * + * @param {Array} locales + * An array of locale strings. The entire array will be set as the available + * locales, and the first locale in the array will be set as the requested + * locale. + * @param {Function} callback + * The callback to be called with the {@link locales} set. This function can + * be async. + */ + async withLocales(locales, callback) { + let promiseChanges = async desiredLocales => { + this.info?.( + "Changing locales from " + + JSON.stringify(Services.locale.requestedLocales) + + " to " + + JSON.stringify(desiredLocales) + ); + + if (desiredLocales[0] == Services.locale.requestedLocales[0]) { + // Nothing happens when the locale doesn't actually change. + return; + } + + this.info?.("Waiting for intl:requested-locales-changed"); + await lazy.TestUtils.topicObserved("intl:requested-locales-changed"); + this.info?.("Observed intl:requested-locales-changed"); + + // Wait for the search service to reload engines. Otherwise tests can fail + // in strange ways due to internal search service state during shutdown. + // It won't always reload engines but it's hard to tell in advance when it + // won't, so also set a timeout. + this.info?.("Waiting for TOPIC_SEARCH_SERVICE"); + await Promise.race([ + lazy.TestUtils.topicObserved( + lazy.SearchUtils.TOPIC_SEARCH_SERVICE, + (subject, data) => { + this.info?.("Observed TOPIC_SEARCH_SERVICE with data: " + data); + return data == "engines-reloaded"; + } + ), + new Promise(resolve => { + lazy.setTimeout(() => { + this.info?.("Timed out waiting for TOPIC_SEARCH_SERVICE"); + resolve(); + }, 2000); + }), + ]); + + this.info?.("Done waiting for locale changes"); + }; + + let available = Services.locale.availableLocales; + let requested = Services.locale.requestedLocales; + + let newRequested = locales.slice(0, 1); + let promise = promiseChanges(newRequested); + Services.locale.availableLocales = locales; + Services.locale.requestedLocales = newRequested; + await promise; + + this.Assert.equal( + Services.locale.appLocaleAsBCP47, + locales[0], + "App locale is now " + locales[0] + ); + + await callback(); + + promise = promiseChanges(requested); + Services.locale.availableLocales = available; + Services.locale.requestedLocales = requested; + await promise; + } + + #mockRemoteSettings = null; +} + +export var QuickSuggestTestUtils = new _QuickSuggestTestUtils(); diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser.ini b/browser/components/urlbar/tests/quicksuggest/browser/browser.ini new file mode 100644 index 0000000000..a29ef67770 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser.ini @@ -0,0 +1,37 @@ +# 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 + searchSuggestionEngine.xml + searchSuggestionEngine.sjs + subdialog.xhtml + +[browser_quicksuggest.js] +[browser_quicksuggest_addons.js] +[browser_quicksuggest_block.js] +[browser_quicksuggest_configuration.js] +[browser_quicksuggest_indexes.js] +skip-if = + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +[browser_quicksuggest_merinoSessions.js] +[browser_quicksuggest_onboardingDialog.js] +skip-if = + os == 'linux' && bits == 64 # Bug 1773830 +[browser_telemetry_dynamicWikipedia.js] +tags = search-telemetry +[browser_telemetry_impressionEdgeCases.js] +tags = search-telemetry +[browser_telemetry_navigationalSuggestions.js] +tags = search-telemetry +[browser_telemetry_nonsponsored.js] +tags = search-telemetry +[browser_telemetry_other.js] +tags = search-telemetry +[browser_telemetry_sponsored.js] +tags = search-telemetry +[browser_telemetry_weather.js] +tags = search-telemetry +[browser_weather.js] diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest.js new file mode 100644 index 0000000000..d11f6d6386 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest.js @@ -0,0 +1,88 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests browser quick suggestions. + */ + +const TEST_URL = "http://example.com/quicksuggest"; + +const REMOTE_SETTINGS_RESULTS = [ + { + id: 1, + url: `${TEST_URL}?q=frabbits`, + title: "frabbits", + keywords: ["fra", "frab"], + click_url: "http://click.reporting.test.com/", + impression_url: "http://impression.reporting.test.com/", + advertiser: "TestAdvertiser", + }, + { + id: 2, + url: `${TEST_URL}?q=nonsponsored`, + title: "Non-Sponsored", + keywords: ["nonspon"], + click_url: "http://click.reporting.test.com/nonsponsored", + impression_url: "http://impression.reporting.test.com/nonsponsored", + advertiser: "TestAdvertiserNonSponsored", + iab_category: "5 - Education", + }, +]; + +add_setup(async function () { + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + await UrlbarTestUtils.formHistory.clear(); + + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + remoteSettingsResults: [ + { + type: "data", + attachment: REMOTE_SETTINGS_RESULTS, + }, + ], + }); +}); + +// Tests a sponsored result and keyword highlighting. +add_task(async function sponsored() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "fra", + }); + await QuickSuggestTestUtils.assertIsQuickSuggest({ + window, + index: 1, + isSponsored: true, + url: `${TEST_URL}?q=frabbits`, + }); + let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1); + Assert.equal( + row.querySelector(".urlbarView-title").firstChild.textContent, + "fra", + "The part of the keyword that matches users input is not bold." + ); + Assert.equal( + row.querySelector(".urlbarView-title > strong").textContent, + "b", + "The auto completed section of the keyword is bolded." + ); + await UrlbarTestUtils.promisePopupClose(window); +}); + +// Tests a non-sponsored result. +add_task(async function nonSponsored() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "nonspon", + }); + await QuickSuggestTestUtils.assertIsQuickSuggest({ + window, + index: 1, + isSponsored: false, + url: `${TEST_URL}?q=nonsponsored`, + }); + await UrlbarTestUtils.promisePopupClose(window); +}); diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_addons.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_addons.js new file mode 100644 index 0000000000..e0b75d0e9b --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_addons.js @@ -0,0 +1,560 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for addon suggestions. + +const TEST_MERINO_SUGGESTIONS = [ + { + provider: "amo", + icon: "https://example.com/first.svg", + url: "https://example.com/first-addon", + title: "First Addon", + description: "This is a first addon", + custom_details: { + amo: { + rating: "5", + number_of_ratings: "1234567", + guid: "first@addon", + }, + }, + is_top_pick: true, + }, + { + provider: "amo", + icon: "https://example.com/second.png", + url: "https://example.com/second-addon", + title: "Second Addon", + description: "This is a second addon", + custom_details: { + amo: { + rating: "4.5", + number_of_ratings: "123", + guid: "second@addon", + }, + }, + is_sponsored: true, + is_top_pick: false, + }, + { + provider: "amo", + icon: "https://example.com/third.svg", + url: "https://example.com/third-addon", + title: "Third Addon", + description: "This is a third addon", + custom_details: { + amo: { + rating: "0", + number_of_ratings: "0", + guid: "third@addon", + }, + }, + is_top_pick: false, + }, + { + provider: "amo", + icon: "https://example.com/fourth.svg", + url: "https://example.com/fourth-addon", + title: "Fourth Addon", + description: "This is a fourth addon", + custom_details: { + amo: { + rating: "4", + number_of_ratings: "4", + guid: "fourth@addon", + }, + }, + }, +]; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.quicksuggest.enabled", true], + ["browser.urlbar.quicksuggest.remoteSettings.enabled", false], + ["browser.urlbar.bestMatch.enabled", true], + ], + }); + + await SearchTestUtils.installSearchExtension({}, { setAsDefault: true }); + + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + merinoSuggestions: TEST_MERINO_SUGGESTIONS, + }); +}); + +add_task(async function basic() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.addons.featureGate", true]], + }); + + for (const merinoSuggestion of TEST_MERINO_SUGGESTIONS) { + MerinoTestUtils.server.response.body.suggestions = [merinoSuggestion]; + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "only match the Merino suggestion", + }); + Assert.equal(UrlbarTestUtils.getResultCount(window), 2); + + const { element, result } = await UrlbarTestUtils.getDetailsOfResultAt( + window, + 1 + ); + const row = element.row; + const icon = row.querySelector(".urlbarView-dynamic-addons-icon"); + Assert.equal(icon.src, merinoSuggestion.icon); + const url = row.querySelector(".urlbarView-dynamic-addons-url"); + Assert.equal(url.textContent, merinoSuggestion.url); + const title = row.querySelector(".urlbarView-dynamic-addons-title"); + Assert.equal(title.textContent, merinoSuggestion.title); + const description = row.querySelector( + ".urlbarView-dynamic-addons-description" + ); + Assert.equal(description.textContent, merinoSuggestion.description); + const reviews = row.querySelector(".urlbarView-dynamic-addons-reviews"); + Assert.equal( + reviews.textContent, + `${new Intl.NumberFormat().format( + Number(merinoSuggestion.custom_details.amo.number_of_ratings) + )} reviews` + ); + + const isTopPick = merinoSuggestion.is_top_pick ?? true; + if (isTopPick) { + Assert.equal(result.suggestedIndex, 1); + } else if (merinoSuggestion.is_sponsored) { + Assert.equal( + result.suggestedIndex, + UrlbarPrefs.get("quickSuggestSponsoredIndex") + ); + } else { + Assert.equal( + result.suggestedIndex, + UrlbarPrefs.get("quickSuggestNonSponsoredIndex") + ); + } + + const onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + merinoSuggestion.url + ); + EventUtils.synthesizeMouseAtCenter(row, {}); + await onLoad; + Assert.ok(true, "Expected page is loaded"); + + await PlacesUtils.history.clear(); + } + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function ratings() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.addons.featureGate", true]], + }); + + const testRating = [ + "0", + "0.24", + "0.25", + "0.74", + "0.75", + "1", + "1.24", + "1.25", + "1.74", + "1.75", + "2", + "2.24", + "2.25", + "2.74", + "2.75", + "3", + "3.24", + "3.25", + "3.74", + "3.75", + "4", + "4.24", + "4.25", + "4.74", + "4.75", + "5", + ]; + const baseMerinoSuggestion = JSON.parse( + JSON.stringify(TEST_MERINO_SUGGESTIONS[0]) + ); + + for (const rating of testRating) { + baseMerinoSuggestion.custom_details.amo.rating = rating; + MerinoTestUtils.server.response.body.suggestions = [baseMerinoSuggestion]; + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "only match the Merino suggestion", + }); + Assert.equal(UrlbarTestUtils.getResultCount(window), 2); + + const { element } = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + + const ratingElements = element.row.querySelectorAll( + ".urlbarView-dynamic-addons-rating" + ); + Assert.equal(ratingElements.length, 5); + + for (let i = 0; i < ratingElements.length; i++) { + const ratingElement = ratingElements[i]; + + const distanceToFull = Number(rating) - i; + let fill = "full"; + if (distanceToFull < 0.25) { + fill = "empty"; + } else if (distanceToFull < 0.75) { + fill = "half"; + } + Assert.equal(ratingElement.getAttribute("fill"), fill); + } + } + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function disable() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.addons.featureGate", false]], + }); + + // Restore AdmWikipedia suggestions. + MerinoTestUtils.server.reset(); + // Add one Addon suggestion that is higher score than AdmWikipedia. + MerinoTestUtils.server.response.body.suggestions.push( + Object.assign({}, TEST_MERINO_SUGGESTIONS[0], { score: 2 }) + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "only match the Merino suggestion", + }); + Assert.equal(UrlbarTestUtils.getResultCount(window), 2); + + const { result } = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.payload.telemetryType, "adm_sponsored"); + + MerinoTestUtils.server.response.body.suggestions = TEST_MERINO_SUGGESTIONS; + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function resultMenu_showLessFrequently() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.addons.featureGate", true], + ["browser.urlbar.addons.showLessFrequentlyCount", 0], + ], + }); + + const cleanUpNimbus = await UrlbarTestUtils.initNimbusFeature({ + addonsShowLessFrequentlyCap: 3, + }); + + // Sanity check. + Assert.equal(UrlbarPrefs.get("addonsShowLessFrequentlyCap"), 3); + Assert.equal(UrlbarPrefs.get("addons.showLessFrequentlyCount"), 0); + + await doShowLessFrequently({ + input: "aaa b", + expected: { + isSuggestionShown: true, + isMenuItemShown: true, + }, + }); + Assert.equal(UrlbarPrefs.get("addons.showLessFrequentlyCount"), 1); + + await doShowLessFrequently({ + input: "aaa b", + expected: { + isSuggestionShown: true, + isMenuItemShown: true, + }, + }); + Assert.equal(UrlbarPrefs.get("addons.showLessFrequentlyCount"), 2); + + await doShowLessFrequently({ + input: "aaa b", + expected: { + isSuggestionShown: true, + isMenuItemShown: true, + }, + }); + Assert.equal(UrlbarPrefs.get("addons.showLessFrequentlyCount"), 3); + + await doShowLessFrequently({ + input: "aaa b", + expected: { + // The suggestion should not display since addons.showLessFrequentlyCount + // is 3 and the substring (" b") after the first word ("aaa") is 2 chars + // long. + isSuggestionShown: false, + }, + }); + + await doShowLessFrequently({ + input: "aaa bb", + expected: { + // The suggestion should display, but item should not shown since the + // addons.showLessFrequentlyCount reached to addonsShowLessFrequentlyCap + // already. + isSuggestionShown: true, + isMenuItemShown: false, + }, + }); + + await cleanUpNimbus(); + await SpecialPowers.popPrefEnv(); +}); + +// Tests the "Not interested" result menu dismissal command. +add_task(async function resultMenu_notInterested() { + await doDismissTest("not_interested"); +}); + +// Tests the "Not relevant" result menu dismissal command. +add_task(async function notRelevant() { + await doDismissTest("not_relevant"); +}); + +add_task(async function rowLabel() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.addons.featureGate", true]], + }); + + const testCases = [ + { + bestMatch: true, + expected: "Firefox extension", + }, + { + bestMatch: false, + expected: "Firefox Suggest", + }, + ]; + + for (const { bestMatch, expected } of testCases) { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.bestMatch.enabled", bestMatch]], + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "only match the Merino suggestion", + }); + Assert.equal(UrlbarTestUtils.getResultCount(window), 2); + + const { element } = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + const row = element.row; + Assert.equal(row.getAttribute("label"), expected); + + await SpecialPowers.popPrefEnv(); + } + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function treatmentB() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.addons.featureGate", true]], + }); + + const cleanUpNimbus = await UrlbarTestUtils.initNimbusFeature({ + addonsUITreatment: "b", + }); + // Sanity check. + Assert.equal(UrlbarPrefs.get("addonsUITreatment"), "b"); + + const merinoSuggestion = TEST_MERINO_SUGGESTIONS[0]; + MerinoTestUtils.server.response.body.suggestions = [merinoSuggestion]; + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "only match the Merino suggestion", + }); + Assert.equal(UrlbarTestUtils.getResultCount(window), 2); + + const { element } = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + const row = element.row; + const icon = row.querySelector(".urlbarView-dynamic-addons-icon"); + Assert.equal(icon.src, merinoSuggestion.icon); + const url = row.querySelector(".urlbarView-dynamic-addons-url"); + Assert.equal(url.textContent, merinoSuggestion.url); + const title = row.querySelector(".urlbarView-dynamic-addons-title"); + Assert.equal(title.textContent, merinoSuggestion.title); + const description = row.querySelector( + ".urlbarView-dynamic-addons-description" + ); + Assert.equal(description.textContent, merinoSuggestion.description); + const ratingContainer = row.querySelector( + ".urlbarView-dynamic-addons-ratingContainer" + ); + Assert.ok(BrowserTestUtils.is_hidden(ratingContainer)); + const reviews = row.querySelector(".urlbarView-dynamic-addons-reviews"); + Assert.equal(reviews.textContent, "Recommended"); + + await cleanUpNimbus(); + await SpecialPowers.popPrefEnv(); +}); + +async function doShowLessFrequently({ input, expected }) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: input, + }); + + if (!expected.isSuggestionShown) { + for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) { + const details = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + Assert.notEqual( + details.result.payload.dynamicType, + "addons", + `Addons suggestion should be absent (checking index ${i})` + ); + } + + return; + } + + const resultIndex = 1; + const details = await UrlbarTestUtils.getDetailsOfResultAt( + window, + resultIndex + ); + Assert.equal( + details.result.payload.dynamicType, + "addons", + `Addons suggestion should be present at expected index after ${input} search` + ); + + // Click the command. + try { + await UrlbarTestUtils.openResultMenuAndClickItem( + window, + "show_less_frequently", + { + resultIndex, + } + ); + Assert.ok(expected.isMenuItemShown); + Assert.ok( + gURLBar.view.isOpen, + "The view should remain open clicking the command" + ); + Assert.ok( + details.element.row.hasAttribute("feedback-acknowledgment"), + "Row should have feedback acknowledgment after clicking command" + ); + } catch (e) { + Assert.ok(!expected.isMenuItemShown); + Assert.ok( + !details.element.row.hasAttribute("feedback-acknowledgment"), + "Row should not have feedback acknowledgment after clicking command" + ); + Assert.equal( + e.message, + "Menu item not found for command: show_less_frequently" + ); + } + + await UrlbarTestUtils.promisePopupClose(window); +} + +async function doDismissTest(command) { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.addons.featureGate", true]], + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "123", + }); + + const resultCount = UrlbarTestUtils.getResultCount(window); + const resultIndex = 1; + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex); + Assert.equal( + details.result.payload.dynamicType, + "addons", + "Addons suggestion should be present" + ); + + // Sanity check. + Assert.ok(UrlbarPrefs.get("suggest.addons")); + + // Click the command. + await UrlbarTestUtils.openResultMenuAndClickItem( + window, + ["[data-l10n-id=firefox-suggest-command-dont-show-this]", command], + { resultIndex, openByMouse: true } + ); + + Assert.ok( + !UrlbarPrefs.get("suggest.addons"), + "suggest.addons pref should be set to false after dismissal" + ); + + // The row should be a tip now. + Assert.ok(gURLBar.view.isOpen, "The view should remain open after dismissal"); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + resultCount, + "The result count should not haved changed after dismissal" + ); + details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex); + Assert.equal( + details.type, + UrlbarUtils.RESULT_TYPE.TIP, + "Row should be a tip after dismissal" + ); + Assert.equal( + details.result.payload.type, + "dismissalAcknowledgment", + "Tip type should be dismissalAcknowledgment" + ); + Assert.ok( + !details.element.row.hasAttribute("feedback-acknowledgment"), + "Row should not have feedback acknowledgment after dismissal" + ); + + // Get the dismissal acknowledgment's "Got it" button and click it. + let gotItButton = UrlbarTestUtils.getButtonForResultIndex( + window, + "0", + resultIndex + ); + Assert.ok(gotItButton, "Row should have a 'Got it' button"); + EventUtils.synthesizeMouseAtCenter(gotItButton, {}, window); + + // The view should remain open and the tip row should be gone. + Assert.ok( + gURLBar.view.isOpen, + "The view should remain open clicking the 'Got it' button" + ); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + resultCount - 1, + "The result count should be one less after clicking 'Got it' button" + ); + for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) { + details = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + Assert.ok( + details.type != UrlbarUtils.RESULT_TYPE.TIP && + details.result.payload.dynamicType !== "addons", + "Tip result and addon result should not be present" + ); + } + + await UrlbarTestUtils.promisePopupClose(window); + + await SpecialPowers.popPrefEnv(); + UrlbarPrefs.clear("suggest.addons"); +} diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_block.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_block.js new file mode 100644 index 0000000000..081818c02b --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_block.js @@ -0,0 +1,445 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests blocking quick suggest results, including best matches. See also: +// +// browser_bestMatch.js +// Includes tests for blocking best match rows independent of quick suggest, +// especially the superficial UI part that should be common to all types of +// best matches + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + CONTEXTUAL_SERVICES_PING_TYPES: + "resource:///modules/PartnerLinkAttribution.sys.mjs", +}); + +const { TELEMETRY_SCALARS } = UrlbarProviderQuickSuggest; +const { TIMESTAMP_TEMPLATE } = QuickSuggest; + +// Include the timestamp template in the suggestion URLs so we can make sure +// their original URLs with the unreplaced templates are blocked and not their +// URLs with timestamps. +const REMOTE_SETTINGS_RESULTS = [ + { + id: 1, + url: `https://example.com/sponsored?t=${TIMESTAMP_TEMPLATE}`, + title: "Sponsored suggestion", + keywords: ["sponsored"], + click_url: "https://example.com/click", + impression_url: "https://example.com/impression", + advertiser: "TestAdvertiser", + iab_category: "22 - Shopping", + }, + { + id: 2, + url: `https://example.com/nonsponsored?t=${TIMESTAMP_TEMPLATE}`, + title: "Non-sponsored suggestion", + keywords: ["nonsponsored"], + click_url: "https://example.com/click", + impression_url: "https://example.com/impression", + advertiser: "TestAdvertiser", + iab_category: "5 - Education", + }, +]; + +// Spy for the custom impression/click sender +let spy; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.bestMatch.blockingEnabled", true], + ["browser.urlbar.quicksuggest.blockingEnabled", true], + ], + }); + + ({ spy } = QuickSuggestTestUtils.createTelemetryPingSpy()); + + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + await UrlbarTestUtils.formHistory.clear(); + + await QuickSuggest.blockedSuggestions._test_readyPromise; + await QuickSuggest.blockedSuggestions.clear(); + + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + remoteSettingsResults: [ + { + type: "data", + attachment: REMOTE_SETTINGS_RESULTS, + }, + ], + config: QuickSuggestTestUtils.BEST_MATCH_CONFIG, + }); +}); + +/** + * Adds a test task that runs the given callback with combinations of the + * following: + * + * - Best match disabled and enabled + * - Each result in `REMOTE_SETTINGS_RESULTS` + * + * @param {Function} fn + * The callback function. It's passed: `{ isBestMatch, suggestion }` + */ +function add_combo_task(fn) { + let taskFn = async () => { + for (let isBestMatch of [false, true]) { + UrlbarPrefs.set("bestMatch.enabled", isBestMatch); + for (let result of REMOTE_SETTINGS_RESULTS) { + info(`Running ${fn.name}: ${JSON.stringify({ isBestMatch, result })}`); + await fn({ isBestMatch, result }); + } + UrlbarPrefs.clear("bestMatch.enabled"); + } + }; + Object.defineProperty(taskFn, "name", { value: fn.name }); + add_task(taskFn); +} + +// Picks the block button with the keyboard. +add_combo_task(async function basic_keyboard({ result, isBestMatch }) { + await doBasicBlockTest({ + result, + isBestMatch, + block: async () => { + if (UrlbarPrefs.get("resultMenu")) { + await UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "D", { + resultIndex: 1, + }); + } else { + // TAB twice to select the block button: once to select the main + // part of the row, once to select the block button. + EventUtils.synthesizeKey("KEY_Tab", { repeat: 2 }); + EventUtils.synthesizeKey("KEY_Enter"); + } + }, + }); +}); + +// Picks the block button with the mouse. +add_combo_task(async function basic_mouse({ result, isBestMatch }) { + await doBasicBlockTest({ + result, + isBestMatch, + block: async () => { + if (UrlbarPrefs.get("resultMenu")) { + await UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "D", { + resultIndex: 1, + openByMouse: true, + }); + } else { + EventUtils.synthesizeMouseAtCenter( + UrlbarTestUtils.getButtonForResultIndex(window, "block", 1), + {} + ); + } + }, + }); +}); + +// Uses the key shortcut to block a suggestion. +add_combo_task(async function basic_keyShortcut({ result, isBestMatch }) { + await doBasicBlockTest({ + result, + isBestMatch, + block: () => { + // Arrow down once to select the row. + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_Delete", { shiftKey: true }); + }, + }); +}); + +async function doBasicBlockTest({ result, isBestMatch, block }) { + spy.resetHistory(); + + // Do a search that triggers the suggestion. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: result.keywords[0], + }); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "Two rows are present after searching (heuristic + suggestion)" + ); + + let isSponsored = result.keywords[0] == "sponsored"; + await QuickSuggestTestUtils.assertIsQuickSuggest({ + window, + isBestMatch, + isSponsored, + originalUrl: result.url, + }); + + // Block the suggestion. + await block(); + + // The row should have been removed. + Assert.ok( + UrlbarTestUtils.isPopupOpen(window), + "View remains open after blocking result" + ); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 1, + "Only one row after blocking suggestion" + ); + await QuickSuggestTestUtils.assertNoQuickSuggestResults(window); + + // The URL should be blocked. + Assert.ok( + await QuickSuggest.blockedSuggestions.has(result.url), + "Suggestion is blocked" + ); + + // Check telemetry scalars. + let index = 2; + let scalars = {}; + if (isSponsored) { + scalars[TELEMETRY_SCALARS.IMPRESSION_SPONSORED] = index; + scalars[TELEMETRY_SCALARS.BLOCK_SPONSORED] = index; + } else { + scalars[TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED] = index; + scalars[TELEMETRY_SCALARS.BLOCK_NONSPONSORED] = index; + } + if (isBestMatch) { + if (isSponsored) { + scalars = { + ...scalars, + [TELEMETRY_SCALARS.IMPRESSION_SPONSORED_BEST_MATCH]: index, + [TELEMETRY_SCALARS.BLOCK_SPONSORED_BEST_MATCH]: index, + }; + } else { + scalars = { + ...scalars, + [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED_BEST_MATCH]: index, + [TELEMETRY_SCALARS.BLOCK_NONSPONSORED_BEST_MATCH]: index, + }; + } + } + QuickSuggestTestUtils.assertScalars(scalars); + + // Check the engagement event. + let match_type = isBestMatch ? "best-match" : "firefox-suggest"; + QuickSuggestTestUtils.assertEvents([ + { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "engagement", + object: "block", + extra: { + match_type, + position: String(index), + suggestion_type: isSponsored ? "sponsored" : "nonsponsored", + }, + }, + ]); + + // Check the custom telemetry pings. + QuickSuggestTestUtils.assertPings(spy, [ + { + type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION, + payload: { + match_type, + block_id: result.id, + is_clicked: false, + position: index, + }, + }, + { + type: CONTEXTUAL_SERVICES_PING_TYPES.QS_BLOCK, + payload: { + match_type, + block_id: result.id, + iab_category: result.iab_category, + position: index, + }, + }, + ]); + + await UrlbarTestUtils.promisePopupClose(window); + await QuickSuggest.blockedSuggestions.clear(); +} + +// Blocks multiple suggestions one after the other. +add_task(async function blockMultiple() { + for (let isBestMatch of [false, true]) { + UrlbarPrefs.set("bestMatch.enabled", isBestMatch); + info(`Testing with best match enabled: ${isBestMatch}`); + + for (let i = 0; i < REMOTE_SETTINGS_RESULTS.length; i++) { + // Do a search that triggers the i'th suggestion. + let { keywords, url } = REMOTE_SETTINGS_RESULTS[i]; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: keywords[0], + }); + await QuickSuggestTestUtils.assertIsQuickSuggest({ + window, + isBestMatch, + originalUrl: url, + isSponsored: keywords[0] == "sponsored", + }); + + // Block it. + if (UrlbarPrefs.get("resultMenu")) { + await UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "D", { + resultIndex: 1, + }); + } else { + EventUtils.synthesizeKey("KEY_Tab", { repeat: 2 }); + EventUtils.synthesizeKey("KEY_Enter"); + } + Assert.ok( + await QuickSuggest.blockedSuggestions.has(url), + "Suggestion is blocked after picking block button" + ); + + // Make sure all previous suggestions remain blocked and no other + // suggestions are blocked yet. + for (let j = 0; j < REMOTE_SETTINGS_RESULTS.length; j++) { + Assert.equal( + await QuickSuggest.blockedSuggestions.has( + REMOTE_SETTINGS_RESULTS[j].url + ), + j <= i, + `Suggestion at index ${j} is blocked or not as expected` + ); + } + } + + await UrlbarTestUtils.promisePopupClose(window); + await QuickSuggest.blockedSuggestions.clear(); + UrlbarPrefs.clear("bestMatch.enabled"); + } +}); + +// Tests with blocking disabled for both best matches and non-best-matches. +add_combo_task(async function disabled_both({ result, isBestMatch }) { + await doDisabledTest({ + result, + isBestMatch, + quickSuggestBlockingEnabled: false, + bestMatchBlockingEnabled: false, + }); +}); + +// Tests with blocking disabled only for non-best-matches. +add_combo_task(async function disabled_quickSuggest({ result, isBestMatch }) { + await doDisabledTest({ + result, + isBestMatch, + quickSuggestBlockingEnabled: false, + bestMatchBlockingEnabled: true, + }); +}); + +// Tests with blocking disabled only for best matches. +add_combo_task(async function disabled_bestMatch({ result, isBestMatch }) { + await doDisabledTest({ + result, + isBestMatch, + quickSuggestBlockingEnabled: true, + bestMatchBlockingEnabled: false, + }); +}); + +async function doDisabledTest({ + result, + isBestMatch, + bestMatchBlockingEnabled, + quickSuggestBlockingEnabled, +}) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.bestMatch.blockingEnabled", bestMatchBlockingEnabled], + [ + "browser.urlbar.quicksuggest.blockingEnabled", + quickSuggestBlockingEnabled, + ], + ], + }); + + // Do a search to show a suggestion. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: result.keywords[0], + }); + let expectedResultCount = 2; + Assert.equal( + UrlbarTestUtils.getResultCount(window), + expectedResultCount, + "Two rows are present after searching (heuristic + suggestion)" + ); + let details = await QuickSuggestTestUtils.assertIsQuickSuggest({ + window, + isBestMatch, + originalUrl: result.url, + isSponsored: result.keywords[0] == "sponsored", + }); + + // Arrow down to select the suggestion and press the key shortcut to block. + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_Delete", { shiftKey: true }); + Assert.ok( + UrlbarTestUtils.isPopupOpen(window), + "View remains open after trying to block result" + ); + + if ( + (isBestMatch && !bestMatchBlockingEnabled) || + (!isBestMatch && !quickSuggestBlockingEnabled) + ) { + // Blocking is disabled. The key shortcut shouldn't have done anything. + if (!UrlbarPrefs.get("resultMenu")) { + Assert.ok( + !details.element.row._buttons.get("block"), + "Block button is not present" + ); + } + Assert.equal( + UrlbarTestUtils.getResultCount(window), + expectedResultCount, + "Same number of results after key shortcut" + ); + await QuickSuggestTestUtils.assertIsQuickSuggest({ + window, + isBestMatch, + originalUrl: result.url, + isSponsored: result.keywords[0] == "sponsored", + }); + Assert.ok( + !(await QuickSuggest.blockedSuggestions.has(result.url)), + "Suggestion is not blocked" + ); + } else { + // Blocking is enabled. The suggestion should have been blocked. + if (!UrlbarPrefs.get("resultMenu")) { + Assert.ok( + details.element.row._buttons.get("block"), + "Block button is present" + ); + } + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 1, + "Only one row after blocking suggestion" + ); + await QuickSuggestTestUtils.assertNoQuickSuggestResults(window); + Assert.ok( + await QuickSuggest.blockedSuggestions.has(result.url), + "Suggestion is blocked" + ); + await QuickSuggest.blockedSuggestions.clear(); + } + + await UrlbarTestUtils.promisePopupClose(window); + await SpecialPowers.popPrefEnv(); +} diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_configuration.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_configuration.js new file mode 100644 index 0000000000..066ffecd51 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_configuration.js @@ -0,0 +1,2101 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests QuickSuggest configurations. + */ + +ChromeUtils.defineESModuleGetters(this, { + EnterprisePolicyTesting: + "resource://testing-common/EnterprisePolicyTesting.sys.mjs", + sinon: "resource://testing-common/Sinon.sys.mjs", +}); + +// We use this pref in enterprise preference policy tests. We specifically use a +// pref that's sticky and exposed in the UI to make sure it can be set properly. +const POLICY_PREF = "suggest.quicksuggest.nonsponsored"; + +let gDefaultBranch = Services.prefs.getDefaultBranch("browser.urlbar."); +let gUserBranch = Services.prefs.getBranch("browser.urlbar."); + +add_setup(async function () { + await QuickSuggestTestUtils.ensureQuickSuggestInit(); +}); + +// Makes sure `QuickSuggest._updateFeatureState()` is called when the +// `browser.urlbar.quicksuggest.enabled` pref is changed. +add_task(async function test_updateFeatureState_pref() { + Assert.ok( + UrlbarPrefs.get("quicksuggest.enabled"), + "Sanity check: quicksuggest.enabled is true by default" + ); + + let sandbox = sinon.createSandbox(); + let spy = sandbox.spy(QuickSuggest, "_updateFeatureState"); + + UrlbarPrefs.set("quicksuggest.enabled", false); + Assert.equal( + spy.callCount, + 1, + "_updateFeatureState called once after changing pref" + ); + + UrlbarPrefs.clear("quicksuggest.enabled"); + Assert.equal( + spy.callCount, + 2, + "_updateFeatureState called again after clearing pref" + ); + + sandbox.restore(); +}); + +// Makes sure `QuickSuggest._updateFeatureState()` is called when a Nimbus +// experiment is installed and uninstalled. +add_task(async function test_updateFeatureState_experiment() { + let sandbox = sinon.createSandbox(); + let spy = sandbox.spy(QuickSuggest, "_updateFeatureState"); + + await QuickSuggestTestUtils.withExperiment({ + callback: () => { + Assert.equal( + spy.callCount, + 1, + "_updateFeatureState called once after installing experiment" + ); + }, + }); + + Assert.equal( + spy.callCount, + 2, + "_updateFeatureState called again after uninstalling experiment" + ); + + sandbox.restore(); +}); + +add_task(async function test_indexes() { + await QuickSuggestTestUtils.withExperiment({ + valueOverrides: { + quickSuggestNonSponsoredIndex: 99, + quickSuggestSponsoredIndex: -1337, + }, + callback: () => { + Assert.equal( + UrlbarPrefs.get("quickSuggestNonSponsoredIndex"), + 99, + "quickSuggestNonSponsoredIndex" + ); + Assert.equal( + UrlbarPrefs.get("quickSuggestSponsoredIndex"), + -1337, + "quickSuggestSponsoredIndex" + ); + }, + }); +}); + +add_task(async function test_merino() { + await QuickSuggestTestUtils.withExperiment({ + valueOverrides: { + merinoEnabled: true, + merinoEndpointURL: "http://example.com/test_merino_config", + merinoClientVariants: "test-client-variants", + merinoProviders: "test-providers", + }, + callback: () => { + Assert.equal(UrlbarPrefs.get("merinoEnabled"), true, "merinoEnabled"); + Assert.equal( + UrlbarPrefs.get("merinoEndpointURL"), + "http://example.com/test_merino_config", + "merinoEndpointURL" + ); + Assert.equal( + UrlbarPrefs.get("merinoClientVariants"), + "test-client-variants", + "merinoClientVariants" + ); + Assert.equal( + UrlbarPrefs.get("merinoProviders"), + "test-providers", + "merinoProviders" + ); + }, + }); +}); + +add_task(async function test_scenario_online() { + await doBasicScenarioTest("online", { + urlbarPrefs: { + // prefs + "quicksuggest.scenario": "online", + "quicksuggest.enabled": true, + "quicksuggest.dataCollection.enabled": false, + "quicksuggest.shouldShowOnboardingDialog": true, + "suggest.quicksuggest.nonsponsored": true, + "suggest.quicksuggest.sponsored": true, + + // Nimbus variables + quickSuggestScenario: "online", + quickSuggestEnabled: true, + quickSuggestShouldShowOnboardingDialog: true, + }, + defaults: [ + { + name: "browser.urlbar.quicksuggest.enabled", + value: true, + }, + { + name: "browser.urlbar.quicksuggest.dataCollection.enabled", + value: false, + }, + { + name: "browser.urlbar.quicksuggest.shouldShowOnboardingDialog", + value: true, + }, + { + name: "browser.urlbar.suggest.quicksuggest.nonsponsored", + value: true, + }, + { + name: "browser.urlbar.suggest.quicksuggest.sponsored", + value: true, + }, + ], + }); +}); + +add_task(async function test_scenario_offline() { + await doBasicScenarioTest("offline", { + urlbarPrefs: { + // prefs + "quicksuggest.scenario": "offline", + "quicksuggest.enabled": true, + "quicksuggest.dataCollection.enabled": false, + "quicksuggest.shouldShowOnboardingDialog": false, + "suggest.quicksuggest.nonsponsored": true, + "suggest.quicksuggest.sponsored": true, + + // Nimbus variables + quickSuggestScenario: "offline", + quickSuggestEnabled: true, + quickSuggestShouldShowOnboardingDialog: false, + }, + defaults: [ + { + name: "browser.urlbar.quicksuggest.enabled", + value: true, + }, + { + name: "browser.urlbar.quicksuggest.dataCollection.enabled", + value: false, + }, + { + name: "browser.urlbar.quicksuggest.shouldShowOnboardingDialog", + value: false, + }, + { + name: "browser.urlbar.suggest.quicksuggest.nonsponsored", + value: true, + }, + { + name: "browser.urlbar.suggest.quicksuggest.sponsored", + value: true, + }, + ], + }); +}); + +add_task(async function test_scenario_history() { + await doBasicScenarioTest("history", { + urlbarPrefs: { + // prefs + "quicksuggest.scenario": "history", + "quicksuggest.enabled": false, + + // Nimbus variables + quickSuggestScenario: "history", + quickSuggestEnabled: false, + }, + defaults: [ + { + name: "browser.urlbar.quicksuggest.enabled", + value: false, + }, + ], + }); +}); + +async function doBasicScenarioTest(scenario, expectedPrefs) { + await QuickSuggestTestUtils.withExperiment({ + valueOverrides: { + quickSuggestScenario: scenario, + }, + callback: () => { + // Pref updates should always settle down by the time enrollment is done. + Assert.ok( + !UrlbarPrefs.updatingFirefoxSuggestPrefs, + "updatingFirefoxSuggestPrefs is false" + ); + + assertScenarioPrefs(expectedPrefs); + }, + }); + + // Similarly, pref updates should always settle down by the time unenrollment + // is done. + Assert.ok( + !UrlbarPrefs.updatingFirefoxSuggestPrefs, + "updatingFirefoxSuggestPrefs is false" + ); + + assertDefaultScenarioPrefs(); +} + +function assertScenarioPrefs({ urlbarPrefs, defaults }) { + for (let [name, value] of Object.entries(urlbarPrefs)) { + Assert.equal(UrlbarPrefs.get(name), value, `UrlbarPrefs.get("${name}")`); + } + + let prefs = Services.prefs.getDefaultBranch(""); + for (let { name, getter, value } of defaults) { + Assert.equal( + prefs[getter || "getBoolPref"](name), + value, + `Default branch pref: ${name}` + ); + } +} + +function assertDefaultScenarioPrefs() { + assertScenarioPrefs({ + urlbarPrefs: { + "quicksuggest.scenario": "offline", + "quicksuggest.enabled": true, + "quicksuggest.dataCollection.enabled": false, + "quicksuggest.shouldShowOnboardingDialog": false, + "suggest.quicksuggest.nonsponsored": true, + "suggest.quicksuggest.sponsored": true, + + // No Nimbus variables since they're only available when an experiment is + // installed. + }, + defaults: [ + { + name: "browser.urlbar.quicksuggest.enabled", + value: true, + }, + { + name: "browser.urlbar.quicksuggest.dataCollection.enabled", + value: false, + }, + { + name: "browser.urlbar.quicksuggest.shouldShowOnboardingDialog", + value: false, + }, + { + name: "browser.urlbar.suggest.quicksuggest.nonsponsored", + value: true, + }, + { + name: "browser.urlbar.suggest.quicksuggest.sponsored", + value: true, + }, + ], + }); +} + +function clearOnboardingPrefs() { + UrlbarPrefs.clear("suggest.quicksuggest.nonsponsored"); + UrlbarPrefs.clear("suggest.quicksuggest.sponsored"); + UrlbarPrefs.clear("quicksuggest.dataCollection.enabled"); + UrlbarPrefs.clear("quicksuggest.shouldShowOnboardingDialog"); + UrlbarPrefs.clear("quicksuggest.showedOnboardingDialog"); + UrlbarPrefs.clear("quicksuggest.seenRestarts"); +} + +// The following tasks test Nimbus enrollments + +// Initial state: +// * History (quick suggest feature disabled) +// +// Enrollment: +// * History +// +// Expected: +// * All history prefs set on the default branch +add_task(async function () { + await checkEnrollments({ + initialPrefsToSet: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.history, + }, + valueOverrides: { + quickSuggestScenario: "history", + }, + expectedPrefs: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.history, + }, + }); +}); + +// Initial state: +// * History (quick suggest feature disabled) +// +// Enrollment: +// * Offline +// +// Expected: +// * All offline prefs set on the default branch +add_task(async function () { + await checkEnrollments({ + initialPrefsToSet: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.history, + }, + valueOverrides: { + quickSuggestScenario: "offline", + }, + expectedPrefs: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline, + }, + }); +}); + +// Initial state: +// * History (quick suggest feature disabled) +// +// Enrollment: +// * Online +// +// Expected: +// * All online prefs set on the default branch +add_task(async function () { + await checkEnrollments({ + initialPrefsToSet: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.history, + }, + valueOverrides: { + quickSuggestScenario: "online", + }, + expectedPrefs: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online, + }, + }); +}); + +// The following tasks test OFFLINE TO OFFLINE + +// Initial state: +// * Offline (suggestions on and data collection off by default) +// * User did not override any defaults +// +// Enrollment: +// * Offline +// +// Expected: +// * All offline prefs set on the default branch +add_task(async function () { + await checkEnrollments({ + initialPrefsToSet: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline, + }, + valueOverrides: { + quickSuggestScenario: "offline", + }, + expectedPrefs: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline, + }, + }); +}); + +// Initial state: +// * Offline (suggestions on and data collection off by default) +// * Non-sponsored suggestions: user left on +// * Sponsored suggestions: user turned off +// * Data collection: user left off +// +// Enrollment: +// * Offline +// +// Expected: +// * Non-sponsored suggestions: remain on +// * Sponsored suggestions: remain off +// * Data collection: remains off +add_task(async function () { + await checkEnrollments({ + initialPrefsToSet: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline, + userBranch: { + "suggest.quicksuggest.sponsored": false, + }, + }, + valueOverrides: { + quickSuggestScenario: "offline", + }, + expectedPrefs: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline, + userBranch: { + "suggest.quicksuggest.sponsored": false, + }, + }, + }); +}); + +// Initial state: +// * Offline (suggestions on and data collection off by default) +// * Non-sponsored suggestions: user turned off +// * Sponsored suggestions: user left on +// * Data collection: user left off +// +// Enrollment: +// * Offline +// +// Expected: +// * Non-sponsored suggestions: remain off +// * Sponsored suggestions: remain on +// * Data collection: remains off +add_task(async function () { + await checkEnrollments({ + initialPrefsToSet: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + }, + }, + valueOverrides: { + quickSuggestScenario: "offline", + }, + expectedPrefs: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + }, + }, + }); +}); + +// Initial state: +// * Offline (suggestions on and data collection off by default) +// * Non-sponsored suggestions: user turned off +// * Sponsored suggestions: user turned off +// * Data collection: user left off +// +// Enrollment: +// * Offline +// +// Expected: +// * Non-sponsored suggestions: remain off +// * Sponsored suggestions: remain off +// * Data collection: remains off +add_task(async function () { + await checkEnrollments({ + initialPrefsToSet: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + }, + }, + valueOverrides: { + quickSuggestScenario: "offline", + }, + expectedPrefs: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + }, + }, + }); +}); + +// Initial state: +// * Offline (suggestions on and data collection off by default) +// * Non-sponsored suggestions: user left on +// * Sponsored suggestions: user left on +// * Data collection: user turned on +// +// Enrollment: +// * Offline +// +// Expected: +// * Non-sponsored suggestions: remain on +// * Sponsored suggestions: remain on +// * Data collection: remains on +add_task(async function () { + await checkEnrollments({ + initialPrefsToSet: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline, + userBranch: { + "quicksuggest.dataCollection.enabled": true, + }, + }, + valueOverrides: { + quickSuggestScenario: "offline", + }, + expectedPrefs: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline, + userBranch: { + "quicksuggest.dataCollection.enabled": true, + }, + }, + }); +}); + +// Initial state: +// * Offline (suggestions on and data collection off by default) +// * Non-sponsored suggestions: user turned off +// * Sponsored suggestions: user turned off +// * Data collection: user turned on +// +// Enrollment: +// * Offline +// +// Expected: +// * Non-sponsored suggestions: remain off +// * Sponsored suggestions: remain off +// * Data collection: remains on +add_task(async function () { + await checkEnrollments({ + initialPrefsToSet: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + "quicksuggest.dataCollection.enabled": true, + }, + }, + valueOverrides: { + quickSuggestScenario: "offline", + }, + expectedPrefs: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + "quicksuggest.dataCollection.enabled": true, + }, + }, + }); +}); + +// The following tasks test OFFLINE TO ONLINE + +// Initial state: +// * Offline (suggestions on and data collection off by default) +// * User did not override any defaults +// +// Enrollment: +// * Online +// +// Expected: +// * All online prefs set on the default branch +add_task(async function () { + await checkEnrollments({ + initialPrefsToSet: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline, + }, + valueOverrides: { + quickSuggestScenario: "online", + }, + expectedPrefs: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online, + }, + }); +}); + +// Initial state: +// * Offline (suggestions on and data collection off by default) +// * Non-sponsored suggestions: user left on +// * Sponsored suggestions: user turned off +// * Data collection: user left off +// +// Enrollment: +// * Online +// +// Expected: +// * Non-sponsored suggestions: remain on +// * Sponsored suggestions: remain off +// * Data collection: remains off +add_task(async function () { + await checkEnrollments({ + initialPrefsToSet: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline, + userBranch: { + "suggest.quicksuggest.sponsored": false, + }, + }, + valueOverrides: { + quickSuggestScenario: "online", + }, + expectedPrefs: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.sponsored": false, + }, + }, + }); +}); + +// Initial state: +// * Offline (suggestions on and data collection off by default) +// * Non-sponsored suggestions: user turned off +// * Sponsored suggestions: user left on +// * Data collection: user left off +// +// Enrollment: +// * Online +// +// Expected: +// * Non-sponsored suggestions: remain off +// * Sponsored suggestions: remain on +// * Data collection: remains off +add_task(async function () { + await checkEnrollments({ + initialPrefsToSet: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + }, + }, + valueOverrides: { + quickSuggestScenario: "online", + }, + expectedPrefs: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + }, + }, + }); +}); + +// Initial state: +// * Offline (suggestions on and data collection off by default) +// * Non-sponsored suggestions: user turned off +// * Sponsored suggestions: user turned off +// * Data collection: user left off +// +// Enrollment: +// * Online +// +// Expected: +// * Non-sponsored suggestions: remain off +// * Sponsored suggestions: remain off +// * Data collection: remains off +add_task(async function () { + await checkEnrollments({ + initialPrefsToSet: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + }, + }, + valueOverrides: { + quickSuggestScenario: "online", + }, + expectedPrefs: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + }, + }, + }); +}); + +// Initial state: +// * Offline (suggestions on and data collection off by default) +// * Non-sponsored suggestions: user left on +// * Sponsored suggestions: user left on +// * Data collection: user turned on +// +// Enrollment: +// * Online +// +// Expected: +// * Non-sponsored suggestions: remain on +// * Sponsored suggestions: remain on +// * Data collection: remains on +add_task(async function () { + await checkEnrollments({ + initialPrefsToSet: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline, + userBranch: { + "quicksuggest.dataCollection.enabled": true, + }, + }, + valueOverrides: { + quickSuggestScenario: "online", + }, + expectedPrefs: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online, + userBranch: { + "quicksuggest.dataCollection.enabled": true, + }, + }, + }); +}); + +// Initial state: +// * Offline (suggestions on and data collection off by default) +// * Non-sponsored suggestions: user left on +// * Sponsored suggestions: user turned off +// * Data collection: user turned on +// +// Enrollment: +// * Online +// +// Expected: +// * Non-sponsored suggestions: remain on +// * Sponsored suggestions: remain off +// * Data collection: remains on +add_task(async function () { + await checkEnrollments({ + initialPrefsToSet: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline, + userBranch: { + "suggest.quicksuggest.sponsored": false, + "quicksuggest.dataCollection.enabled": true, + }, + }, + valueOverrides: { + quickSuggestScenario: "online", + }, + expectedPrefs: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.sponsored": false, + "quicksuggest.dataCollection.enabled": true, + }, + }, + }); +}); + +// Initial state: +// * Offline (suggestions on and data collection off by default) +// * Non-sponsored suggestions: user turned off +// * Sponsored suggestions: user left on +// * Data collection: user turned on +// +// Enrollment: +// * Online +// +// Expected: +// * Non-sponsored suggestions: remain off +// * Sponsored suggestions: remain on +// * Data collection: remains on +add_task(async function () { + await checkEnrollments({ + initialPrefsToSet: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + "quicksuggest.dataCollection.enabled": true, + }, + }, + valueOverrides: { + quickSuggestScenario: "online", + }, + expectedPrefs: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + "quicksuggest.dataCollection.enabled": true, + }, + }, + }); +}); + +// Initial state: +// * Offline (suggestions on and data collection off by default) +// * Non-sponsored suggestions: user turned off +// * Sponsored suggestions: user turned off +// * Data collection: user turned on +// +// Enrollment: +// * Online +// +// Expected: +// * Non-sponsored suggestions: remain off +// * Sponsored suggestions: remain off +// * Data collection: remains on +add_task(async function () { + await checkEnrollments({ + initialPrefsToSet: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + "quicksuggest.dataCollection.enabled": true, + }, + }, + valueOverrides: { + quickSuggestScenario: "online", + }, + expectedPrefs: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + "quicksuggest.dataCollection.enabled": true, + }, + }, + }); +}); + +// The following tasks test ONLINE TO ONLINE + +// Initial state: +// * Online (suggestions on and data collection off by default) +// * User did not override any defaults +// +// Enrollment: +// * Online +// +// Expected: +// * All online prefs set on the default branch +add_task(async function () { + await checkEnrollments({ + initialPrefsToSet: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online, + }, + valueOverrides: { + quickSuggestScenario: "online", + }, + expectedPrefs: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online, + }, + }); +}); + +// Initial state: +// * Online (suggestions on and data collection off by default) +// * Non-sponsored suggestions: user left on +// * Sponsored suggestions: user turned off +// * Data collection: user left off +// +// Enrollment: +// * Online +// +// Expected: +// * Non-sponsored suggestions: remain on +// * Sponsored suggestions: remain off +// * Data collection: remains off +add_task(async function () { + await checkEnrollments({ + initialPrefsToSet: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.sponsored": false, + }, + }, + valueOverrides: { + quickSuggestScenario: "online", + }, + expectedPrefs: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.sponsored": false, + }, + }, + }); +}); + +// Initial state: +// * Online (suggestions on and data collection off by default) +// * Non-sponsored suggestions: user turned off +// * Sponsored suggestions: user left on +// * Data collection: user left off +// +// Enrollment: +// * Online +// +// Expected: +// * Non-sponsored suggestions: remain off +// * Sponsored suggestions: remain on +// * Data collection: remains off +add_task(async function () { + await checkEnrollments({ + initialPrefsToSet: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + }, + }, + valueOverrides: { + quickSuggestScenario: "online", + }, + expectedPrefs: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + }, + }, + }); +}); + +// Initial state: +// * Online (suggestions on and data collection off by default) +// * Non-sponsored suggestions: user turned off +// * Sponsored suggestions: user turned off +// * Data collection: user left off +// +// Enrollment: +// * Online +// +// Expected: +// * Non-sponsored suggestions: remain off +// * Sponsored suggestions: remain off +// * Data collection: remains off +add_task(async function () { + await checkEnrollments({ + initialPrefsToSet: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + }, + }, + valueOverrides: { + quickSuggestScenario: "online", + }, + expectedPrefs: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + }, + }, + }); +}); + +// Initial state: +// * Online (suggestions on and data collection off by default) +// * Non-sponsored suggestions: user left on +// * Sponsored suggestions: user left on +// * Data collection: user turned on +// +// Enrollment: +// * Online +// +// Expected: +// * Non-sponsored suggestions: remain on +// * Sponsored suggestions: remain on +// * Data collection: remains on +add_task(async function () { + await checkEnrollments({ + initialPrefsToSet: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online, + userBranch: { + "quicksuggest.dataCollection.enabled": true, + }, + }, + valueOverrides: { + quickSuggestScenario: "online", + }, + expectedPrefs: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online, + userBranch: { + "quicksuggest.dataCollection.enabled": true, + }, + }, + }); +}); + +// Initial state: +// * Online (suggestions on and data collection off by default) +// * Non-sponsored suggestions: user left on +// * Sponsored suggestions: user turned off +// * Data collection: user turned on +// +// Enrollment: +// * Online +// +// Expected: +// * Non-sponsored suggestions: remain on +// * Sponsored suggestions: remain off +// * Data collection: remains on +add_task(async function () { + await checkEnrollments({ + initialPrefsToSet: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.sponsored": false, + "quicksuggest.dataCollection.enabled": true, + }, + }, + valueOverrides: { + quickSuggestScenario: "online", + }, + expectedPrefs: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.sponsored": false, + "quicksuggest.dataCollection.enabled": true, + }, + }, + }); +}); + +// Initial state: +// * Online (suggestions on and data collection off by default) +// * Non-sponsored suggestions: user turned off +// * Sponsored suggestions: user left on +// * Data collection: user turned on +// +// Enrollment: +// * Online +// +// Expected: +// * Non-sponsored suggestions: remain off +// * Sponsored suggestions: remain on +// * Data collection: remains on +add_task(async function () { + await checkEnrollments({ + initialPrefsToSet: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + "quicksuggest.dataCollection.enabled": true, + }, + }, + valueOverrides: { + quickSuggestScenario: "online", + }, + expectedPrefs: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + "quicksuggest.dataCollection.enabled": true, + }, + }, + }); +}); + +// Initial state: +// * Online (suggestions on and data collection off by default) +// * Non-sponsored suggestions: user turned off +// * Sponsored suggestions: user turned off +// * Data collection: user turned on +// +// Enrollment: +// * Online +// +// Expected: +// * Non-sponsored suggestions: remain off +// * Sponsored suggestions: remain off +// * Data collection: remains on +add_task(async function () { + await checkEnrollments({ + initialPrefsToSet: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + "quicksuggest.dataCollection.enabled": true, + }, + }, + valueOverrides: { + quickSuggestScenario: "online", + }, + expectedPrefs: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + "quicksuggest.dataCollection.enabled": true, + }, + }, + }); +}); + +// The following tasks test scenarios in conjunction with individual Nimbus +// variables + +// Initial state: +// * Offline (suggestions on and data collection off by default) +// * User did not override any defaults +// +// Enrollment: +// * Offline +// * Sponsored suggestions individually forced on +// +// Expected: +// * Sponsored suggestions: on +add_task(async function () { + await checkEnrollments({ + initialPrefsToSet: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline, + }, + valueOverrides: { + quickSuggestScenario: "offline", + quickSuggestSponsoredEnabled: true, + }, + expectedPrefs: { + defaultBranch: { + ...UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline, + "suggest.quicksuggest.sponsored": true, + }, + }, + }); +}); + +// Initial state: +// * Offline (suggestions on and data collection off by default) +// * Sponsored suggestions: user turned off +// +// Enrollment: +// * Offline +// * Sponsored suggestions individually forced on +// +// Expected: +// * Sponsored suggestions: remain off +add_task(async function () { + await checkEnrollments({ + initialPrefsToSet: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline, + userBranch: { + "suggest.quicksuggest.sponsored": false, + }, + }, + valueOverrides: { + quickSuggestScenario: "offline", + quickSuggestSponsoredEnabled: true, + }, + expectedPrefs: { + defaultBranch: { + ...UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline, + "suggest.quicksuggest.sponsored": true, + }, + userBranch: { + "suggest.quicksuggest.sponsored": false, + }, + }, + }); +}); + +// Initial state: +// * Offline (suggestions on and data collection off by default) +// * User did not override any defaults +// +// Enrollment: +// * Offline +// * Data collection individually forced on +// +// Expected: +// * Data collection: on +add_task(async function () { + await checkEnrollments({ + initialPrefsToSet: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline, + }, + valueOverrides: { + quickSuggestScenario: "offline", + quickSuggestDataCollectionEnabled: true, + }, + expectedPrefs: { + defaultBranch: { + ...UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline, + "quicksuggest.dataCollection.enabled": true, + }, + }, + }); +}); + +// Initial state: +// * Offline (suggestions on and data collection off by default) +// * Data collection: user turned off (it's off by default, so this simulates +// when the user toggled it on and then back off) +// +// Enrollment: +// * Offline +// * Data collection individually forced on +// +// Expected: +// * Data collection: remains off +add_task(async function () { + await checkEnrollments({ + initialPrefsToSet: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline, + userBranch: { + "quicksuggest.dataCollection.enabled": false, + }, + }, + valueOverrides: { + quickSuggestScenario: "offline", + quickSuggestDataCollectionEnabled: true, + }, + expectedPrefs: { + defaultBranch: { + ...UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline, + "quicksuggest.dataCollection.enabled": true, + }, + userBranch: { + "quicksuggest.dataCollection.enabled": false, + }, + }, + }); +}); + +// Initial state: +// * Online (suggestions on and data collection off by default) +// * User did not override any defaults +// +// Enrollment: +// * Online +// * Sponsored suggestions individually forced off +// +// Expected: +// * Sponsored suggestions: off +add_task(async function () { + await checkEnrollments({ + initialPrefsToSet: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online, + }, + valueOverrides: { + quickSuggestScenario: "online", + quickSuggestSponsoredEnabled: false, + }, + expectedPrefs: { + defaultBranch: { + ...UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online, + "suggest.quicksuggest.sponsored": false, + }, + }, + }); +}); + +// Initial state: +// * Online (suggestions on and data collection off by default) +// * Sponsored suggestions: user turned on (they're on by default, so this +// simulates when the user toggled them off and then back on) +// +// Enrollment: +// * Online +// * Sponsored suggestions individually forced off +// +// Expected: +// * Sponsored suggestions: remain on +add_task(async function () { + await checkEnrollments({ + initialPrefsToSet: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.sponsored": true, + }, + }, + valueOverrides: { + quickSuggestScenario: "online", + quickSuggestSponsoredEnabled: false, + }, + expectedPrefs: { + defaultBranch: { + ...UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online, + "suggest.quicksuggest.sponsored": false, + }, + userBranch: { + "suggest.quicksuggest.sponsored": true, + }, + }, + }); +}); + +// Initial state: +// * Online (suggestions on and data collection off by default) +// * User did not override any defaults +// +// Enrollment: +// * Online +// * Data collection individually forced on +// +// Expected: +// * Data collection: on +add_task(async function () { + await checkEnrollments({ + initialPrefsToSet: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online, + }, + valueOverrides: { + quickSuggestScenario: "online", + quickSuggestDataCollectionEnabled: true, + }, + expectedPrefs: { + defaultBranch: { + ...UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online, + "quicksuggest.dataCollection.enabled": true, + }, + }, + }); +}); + +// Initial state: +// * Online (suggestions on and data collection off by default) +// * Data collection: user turned off (it's off by default, so this simulates +// when the user toggled it on and then back off) +// +// Enrollment: +// * Online +// * Data collection individually forced on +// +// Expected: +// * Data collection: remains off +add_task(async function () { + await checkEnrollments({ + initialPrefsToSet: { + defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online, + userBranch: { + "quicksuggest.dataCollection.enabled": false, + }, + }, + valueOverrides: { + quickSuggestScenario: "online", + quickSuggestDataCollectionEnabled: true, + }, + expectedPrefs: { + defaultBranch: { + ...UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online, + "quicksuggest.dataCollection.enabled": true, + }, + userBranch: { + "quicksuggest.dataCollection.enabled": false, + }, + }, + }); +}); + +// The following tasks test individual Nimbus variables without scenarios + +// Initial state: +// * Suggestions on by default and user left them on +// +// 1. First enrollment: +// * Suggestions forced off +// +// Expected: +// * Suggestions off +// +// 2. User turns on suggestions +// 3. Second enrollment: +// * Suggestions forced off again +// +// Expected: +// * Suggestions remain on +add_task(async function () { + await checkEnrollments([ + { + initialPrefsToSet: { + defaultBranch: { + "suggest.quicksuggest.nonsponsored": true, + "suggest.quicksuggest.sponsored": true, + }, + }, + valueOverrides: { + quickSuggestNonSponsoredEnabled: false, + quickSuggestSponsoredEnabled: false, + }, + expectedPrefs: { + defaultBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + }, + }, + }, + { + initialPrefsToSet: { + userBranch: { + "suggest.quicksuggest.nonsponsored": true, + "suggest.quicksuggest.sponsored": true, + }, + }, + valueOverrides: { + quickSuggestNonSponsoredEnabled: false, + quickSuggestSponsoredEnabled: false, + }, + expectedPrefs: { + defaultBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + }, + userBranch: { + "suggest.quicksuggest.nonsponsored": true, + "suggest.quicksuggest.sponsored": true, + }, + }, + }, + ]); +}); + +// Initial state: +// * Suggestions on by default but user turned them off +// +// Enrollment: +// * Suggestions forced on +// +// Expected: +// * Suggestions remain off +add_task(async function () { + await checkEnrollments({ + initialPrefsToSet: { + defaultBranch: { + "suggest.quicksuggest.nonsponsored": true, + "suggest.quicksuggest.sponsored": true, + }, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + }, + }, + valueOverrides: { + quickSuggestNonSponsoredEnabled: true, + quickSuggestSponsoredEnabled: true, + }, + expectedPrefs: { + defaultBranch: { + "suggest.quicksuggest.nonsponsored": true, + "suggest.quicksuggest.sponsored": true, + }, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + }, + }, + }); +}); + +// Initial state: +// * Suggestions off by default and user left them off +// +// 1. First enrollment: +// * Suggestions forced on +// +// Expected: +// * Suggestions on +// +// 2. User turns off suggestions +// 3. Second enrollment: +// * Suggestions forced on again +// +// Expected: +// * Suggestions remain off +add_task(async function () { + await checkEnrollments([ + { + initialPrefsToSet: { + defaultBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + }, + }, + valueOverrides: { + quickSuggestNonSponsoredEnabled: true, + quickSuggestSponsoredEnabled: true, + }, + expectedPrefs: { + defaultBranch: { + "suggest.quicksuggest.nonsponsored": true, + "suggest.quicksuggest.sponsored": true, + }, + }, + }, + { + initialPrefsToSet: { + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + }, + }, + valueOverrides: { + quickSuggestNonSponsoredEnabled: true, + quickSuggestSponsoredEnabled: true, + }, + expectedPrefs: { + defaultBranch: { + "suggest.quicksuggest.nonsponsored": true, + "suggest.quicksuggest.sponsored": true, + }, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + }, + }, + }, + ]); +}); + +// Initial state: +// * Suggestions off by default but user turned them on +// +// Enrollment: +// * Suggestions forced off +// +// Expected: +// * Suggestions remain on +add_task(async function () { + await checkEnrollments({ + initialPrefsToSet: { + defaultBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + }, + userBranch: { + "suggest.quicksuggest.nonsponsored": true, + "suggest.quicksuggest.sponsored": true, + }, + }, + valueOverrides: { + quickSuggestNonSponsoredEnabled: false, + quickSuggestSponsoredEnabled: false, + }, + expectedPrefs: { + defaultBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + }, + userBranch: { + "suggest.quicksuggest.nonsponsored": true, + "suggest.quicksuggest.sponsored": true, + }, + }, + }); +}); + +// Initial state: +// * Data collection on by default and user left them on +// +// 1. First enrollment: +// * Data collection forced off +// +// Expected: +// * Data collection off +// +// 2. User turns on data collection +// 3. Second enrollment: +// * Data collection forced off again +// +// Expected: +// * Data collection remains on +add_task(async function () { + await checkEnrollments( + [ + { + initialPrefsToSet: { + defaultBranch: { + "quicksuggest.dataCollection.enabled": true, + }, + }, + valueOverrides: { + quickSuggestDataCollectionEnabled: false, + }, + expectedPrefs: { + defaultBranch: { + "quicksuggest.dataCollection.enabled": false, + }, + }, + }, + ], + [ + { + initialPrefsToSet: { + userBranch: { + "quicksuggest.dataCollection.enabled": true, + }, + }, + valueOverrides: { + quickSuggestDataCollectionEnabled: false, + }, + expectedPrefs: { + defaultBranch: { + "quicksuggest.dataCollection.enabled": false, + }, + userBranch: { + "quicksuggest.dataCollection.enabled": true, + }, + }, + }, + ] + ); +}); + +// Initial state: +// * Data collection on by default but user turned it off +// +// Enrollment: +// * Data collection forced on +// +// Expected: +// * Data collection remains off +add_task(async function () { + await checkEnrollments({ + initialPrefsToSet: { + defaultBranch: { + "quicksuggest.dataCollection.enabled": true, + }, + userBranch: { + "quicksuggest.dataCollection.enabled": false, + }, + }, + valueOverrides: { + quickSuggestDataCollectionEnabled: true, + }, + expectedPrefs: { + defaultBranch: { + "quicksuggest.dataCollection.enabled": true, + }, + userBranch: { + "quicksuggest.dataCollection.enabled": false, + }, + }, + }); +}); + +// Initial state: +// * Data collection off by default and user left it off +// +// 1. First enrollment: +// * Data collection forced on +// +// Expected: +// * Data collection on +// +// 2. User turns off data collection +// 3. Second enrollment: +// * Data collection forced on again +// +// Expected: +// * Data collection remains off +add_task(async function () { + await checkEnrollments( + [ + { + initialPrefsToSet: { + defaultBranch: { + "quicksuggest.dataCollection.enabled": false, + }, + }, + valueOverrides: { + quickSuggestDataCollectionEnabled: true, + }, + expectedPrefs: { + defaultBranch: { + "quicksuggest.dataCollection.enabled": true, + }, + }, + }, + ], + [ + { + initialPrefsToSet: { + userBranch: { + "quicksuggest.dataCollection.enabled": false, + }, + }, + valueOverrides: { + quickSuggestDataCollectionEnabled: true, + }, + expectedPrefs: { + defaultBranch: { + "quicksuggest.dataCollection.enabled": false, + }, + userBranch: { + "quicksuggest.dataCollection.enabled": false, + }, + }, + }, + ] + ); +}); + +// Initial state: +// * Data collection off by default but user turned it on +// +// Enrollment: +// * Data collection forced off +// +// Expected: +// * Data collection remains on +add_task(async function () { + await checkEnrollments({ + initialPrefsToSet: { + defaultBranch: { + "quicksuggest.dataCollection.enabled": false, + }, + userBranch: { + "quicksuggest.dataCollection.enabled": true, + }, + }, + valueOverrides: { + quickSuggestDataCollectionEnabled: false, + }, + expectedPrefs: { + defaultBranch: { + "quicksuggest.dataCollection.enabled": false, + }, + userBranch: { + "quicksuggest.dataCollection.enabled": true, + }, + }, + }); +}); + +/** + * Tests one or more enrollments. Sets an initial set of prefs on the default + * and/or user branches, enrolls in a mock Nimbus experiment, checks expected + * pref values, unenrolls, and finally checks prefs again. + * + * The given `options` value may be an object as described below or an array of + * such objects, one per enrollment. + * + * @param {object} options + * Function options. + * @param {object} options.initialPrefsToSet + * An object: { userBranch, defaultBranch } + * `userBranch` and `defaultBranch` are objects that map pref names (relative + * to `browser.urlbar`) to values. These prefs will be set on the appropriate + * branch before enrollment. Both `userBranch` and `defaultBranch` are + * optional. + * @param {object} options.valueOverrides + * The `valueOverrides` object passed to the mock experiment. It should map + * Nimbus variable names to values. + * @param {object} options.expectedPrefs + * Preferences that should be set after enrollment. It has the same shape as + * `options.initialPrefsToSet`. + */ +async function checkEnrollments(options) { + info("Testing: " + JSON.stringify(options)); + + let enrollments; + if (Array.isArray(options)) { + enrollments = options; + } else { + enrollments = [options]; + } + + // Do each enrollment. + for (let i = 0; i < enrollments.length; i++) { + info( + `Starting setup for enrollment ${i}: ` + JSON.stringify(enrollments[i]) + ); + + let { initialPrefsToSet, valueOverrides, expectedPrefs } = enrollments[i]; + + // Set initial prefs. + UrlbarPrefs._updatingFirefoxSuggestScenario = true; + let { defaultBranch: initialDefaultBranch, userBranch: initialUserBranch } = + initialPrefsToSet; + initialDefaultBranch = initialDefaultBranch || {}; + initialUserBranch = initialUserBranch || {}; + for (let name of Object.keys(initialDefaultBranch)) { + // Clear user-branch values on the default prefs so the defaults aren't + // masked. + gUserBranch.clearUserPref(name); + } + for (let [branch, prefs] of [ + [gDefaultBranch, initialDefaultBranch], + [gUserBranch, initialUserBranch], + ]) { + for (let [name, value] of Object.entries(prefs)) { + branch.setBoolPref(name, value); + } + } + UrlbarPrefs._updatingFirefoxSuggestScenario = false; + + let { + defaultBranch: expectedDefaultBranch, + userBranch: expectedUserBranch, + } = expectedPrefs; + expectedDefaultBranch = expectedDefaultBranch || {}; + expectedUserBranch = expectedUserBranch || {}; + + // Install the experiment. + info(`Installing experiment for enrollment ${i}`); + await QuickSuggestTestUtils.withExperiment({ + valueOverrides, + callback: () => { + info(`Installed experiment for enrollment ${i}, now checking prefs`); + + // Check expected pref values. Store expected effective values as we go + // so we can check them afterward. For a given pref, the expected + // effective value is the user value, or if there's not a user value, + // the default value. + let expectedEffectivePrefs = {}; + for (let [branch, prefs, branchType] of [ + [gDefaultBranch, expectedDefaultBranch, "default"], + [gUserBranch, expectedUserBranch, "user"], + ]) { + for (let [name, value] of Object.entries(prefs)) { + expectedEffectivePrefs[name] = value; + Assert.equal( + branch.getBoolPref(name), + value, + `Pref ${name} on ${branchType} branch` + ); + if (branch == gUserBranch) { + Assert.ok( + gUserBranch.prefHasUserValue(name), + `Pref ${name} is on user branch` + ); + } + } + } + for (let name of Object.keys(initialDefaultBranch)) { + if (!expectedUserBranch.hasOwnProperty(name)) { + Assert.ok( + !gUserBranch.prefHasUserValue(name), + `Pref ${name} is not on user branch` + ); + } + } + for (let [name, value] of Object.entries(expectedEffectivePrefs)) { + Assert.equal( + UrlbarPrefs.get(name), + value, + `Pref ${name} effective value` + ); + } + + info(`Uninstalling experiment for enrollment ${i}`); + }, + }); + + info(`Uninstalled experiment for enrollment ${i}, now checking prefs`); + + // Check expected effective values after unenrollment. The expected + // effective value for a pref at this point is the value on the user branch, + // or if there's not a user value, the original value on the default branch + // before enrollment. This assumes the default values reflect the offline + // scenario (the case for the U.S. region). + let effectivePrefs = Object.assign( + {}, + UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline + ); + for (let [name, value] of Object.entries(expectedUserBranch)) { + effectivePrefs[name] = value; + } + for (let [name, value] of Object.entries(effectivePrefs)) { + Assert.equal( + UrlbarPrefs.get(name), + value, + `Pref ${name} effective value after unenrolling` + ); + } + + // Clean up. + UrlbarPrefs._updatingFirefoxSuggestScenario = true; + for (let name of Object.keys(expectedUserBranch)) { + UrlbarPrefs.clear(name); + } + UrlbarPrefs._updatingFirefoxSuggestScenario = false; + } +} + +// The following tasks test enterprise preference policies + +// Preference policy test for the following: +// * Status: locked +// * Value: false +add_task(async function () { + await doPolicyTest({ + prefPolicy: { + Status: "locked", + Value: false, + }, + expectedDefault: false, + expectedUser: undefined, + expectedLocked: true, + }); +}); + +// Preference policy test for the following: +// * Status: locked +// * Value: true +add_task(async function () { + await doPolicyTest({ + prefPolicy: { + Status: "locked", + Value: true, + }, + expectedDefault: true, + expectedUser: undefined, + expectedLocked: true, + }); +}); + +// Preference policy test for the following: +// * Status: default +// * Value: false +add_task(async function () { + await doPolicyTest({ + prefPolicy: { + Status: "default", + Value: false, + }, + expectedDefault: false, + expectedUser: undefined, + expectedLocked: false, + }); +}); + +// Preference policy test for the following: +// * Status: default +// * Value: true +add_task(async function () { + await doPolicyTest({ + prefPolicy: { + Status: "default", + Value: true, + }, + expectedDefault: true, + expectedUser: undefined, + expectedLocked: false, + }); +}); + +// Preference policy test for the following: +// * Status: user +// * Value: false +add_task(async function () { + await doPolicyTest({ + prefPolicy: { + Status: "user", + Value: false, + }, + expectedDefault: true, + expectedUser: false, + expectedLocked: false, + }); +}); + +// Preference policy test for the following: +// * Status: user +// * Value: true +add_task(async function () { + await doPolicyTest({ + prefPolicy: { + Status: "user", + Value: true, + }, + expectedDefault: true, + // Because the pref is sticky, it's true on the user branch even though it's + // also true on the default branch. Sticky prefs retain their user-branch + // values even when they're the same as their default-branch values. + expectedUser: true, + expectedLocked: false, + }); +}); + +/** + * This tests an enterprise preference policy with one of the quick suggest + * sticky prefs (defined by `POLICY_PREF`). Pref policies should apply to the + * quick suggest sticky prefs just as they do to non-sticky prefs. + * + * @param {object} options + * Options object. + * @param {object} options.prefPolicy + * An object `{ Status, Value }` that will be included in the policy. + * @param {boolean} options.expectedDefault + * The expected default-branch pref value after setting the policy. + * @param {boolean} options.expectedUser + * The expected user-branch pref value after setting the policy or undefined + * if the pref should not exist on the user branch. + * @param {boolean} options.expectedLocked + * Whether the pref is expected to be locked after setting the policy. + */ +async function doPolicyTest({ + prefPolicy, + expectedDefault, + expectedUser, + expectedLocked, +}) { + info( + "Starting pref policy test: " + + JSON.stringify({ + prefPolicy, + expectedDefault, + expectedUser, + expectedLocked, + }) + ); + + let pref = POLICY_PREF; + + // Check initial state. + Assert.ok( + gDefaultBranch.getBoolPref(pref), + `${pref} is initially true on default branch (assuming en-US)` + ); + Assert.ok( + !gUserBranch.prefHasUserValue(pref), + `${pref} does not have initial user value` + ); + + // Set up the policy. + await EnterprisePolicyTesting.setupPolicyEngineWithJson({ + policies: { + Preferences: { + [`browser.urlbar.${pref}`]: prefPolicy, + }, + }, + }); + Assert.equal( + Services.policies.status, + Ci.nsIEnterprisePolicies.ACTIVE, + "Policy engine is active" + ); + + // Check the default branch. + Assert.equal( + gDefaultBranch.getBoolPref(pref), + expectedDefault, + `${pref} has expected default-branch value after setting policy` + ); + + // Check the user branch. + Assert.equal( + gUserBranch.prefHasUserValue(pref), + expectedUser !== undefined, + `${pref} is on user branch as expected after setting policy` + ); + if (expectedUser !== undefined) { + Assert.equal( + gUserBranch.getBoolPref(pref), + expectedUser, + `${pref} has expected user-branch value after setting policy` + ); + } + + // Check the locked state. + Assert.equal( + gDefaultBranch.prefIsLocked(pref), + expectedLocked, + `${pref} is locked as expected after setting policy` + ); + + // Clean up. + await EnterprisePolicyTesting.setupPolicyEngineWithJson(""); + Assert.equal( + Services.policies.status, + Ci.nsIEnterprisePolicies.INACTIVE, + "Policy engine is inactive" + ); + + gDefaultBranch.unlockPref(pref); + gUserBranch.clearUserPref(pref); + await QuickSuggestTestUtils.setScenario(null); + + Assert.ok( + !gDefaultBranch.prefIsLocked(pref), + `${pref} is not locked after cleanup` + ); + Assert.ok( + gDefaultBranch.getBoolPref(pref), + `${pref} is true on default branch after cleanup (assuming en-US)` + ); + Assert.ok( + !gUserBranch.prefHasUserValue(pref), + `${pref} does not have user value after cleanup` + ); +} diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_indexes.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_indexes.js new file mode 100644 index 0000000000..282e1a2ba0 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_indexes.js @@ -0,0 +1,425 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests the configurable indexes of sponsored and non-sponsored ("Firefox +// Suggest") quick suggest results. + +"use strict"; + +const SUGGESTIONS_FIRST_PREF = "browser.urlbar.showSearchSuggestionsFirst"; +const SUGGESTIONS_PREF = "browser.urlbar.suggest.searches"; + +const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml"; +const MAX_RESULTS = UrlbarPrefs.get("maxRichResults"); + +const SPONSORED_INDEX_PREF = "browser.urlbar.quicksuggest.sponsoredIndex"; +const NON_SPONSORED_INDEX_PREF = + "browser.urlbar.quicksuggest.nonSponsoredIndex"; + +const SPONSORED_SEARCH_STRING = "frabbits"; +const NON_SPONSORED_SEARCH_STRING = "nonspon"; + +const TEST_URL = "http://example.com/quicksuggest"; + +const REMOTE_SETTINGS_RESULTS = [ + { + id: 1, + url: `${TEST_URL}?q=${SPONSORED_SEARCH_STRING}`, + title: "frabbits", + keywords: [SPONSORED_SEARCH_STRING], + click_url: "http://click.reporting.test.com/", + impression_url: "http://impression.reporting.test.com/", + advertiser: "TestAdvertiser", + }, + { + id: 2, + url: `${TEST_URL}?q=${NON_SPONSORED_SEARCH_STRING}`, + title: "Non-Sponsored", + keywords: [NON_SPONSORED_SEARCH_STRING], + click_url: "http://click.reporting.test.com/nonsponsored", + impression_url: "http://impression.reporting.test.com/nonsponsored", + advertiser: "TestAdvertiserNonSponsored", + iab_category: "5 - Education", + }, +]; + +add_setup(async function () { + // This test intermittently times out on Mac TV WebRender. + if (AppConstants.platform == "macosx") { + requestLongerTimeout(3); + } + + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + await UrlbarTestUtils.formHistory.clear(); + + // Add a mock engine so we don't hit the network. + await SearchTestUtils.installSearchExtension({}, { setAsDefault: true }); + + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + remoteSettingsResults: [ + { + type: "data", + attachment: REMOTE_SETTINGS_RESULTS, + }, + ], + }); + + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + }); +}); + +// Tests with history only +add_task(async function noSuggestions() { + await doTestPermutations(({ withHistory, generalIndex }) => ({ + expectedResultCount: withHistory ? MAX_RESULTS : 2, + expectedIndex: generalIndex == 0 || !withHistory ? 1 : MAX_RESULTS - 1, + })); +}); + +// Tests with suggestions followed by history +add_task(async function suggestionsFirst() { + await SpecialPowers.pushPrefEnv({ + set: [[SUGGESTIONS_FIRST_PREF, true]], + }); + await withSuggestions(async () => { + await doTestPermutations(({ withHistory, generalIndex }) => ({ + expectedResultCount: withHistory ? MAX_RESULTS : 4, + expectedIndex: generalIndex == 0 || !withHistory ? 3 : MAX_RESULTS - 1, + })); + }); + await SpecialPowers.popPrefEnv(); +}); + +// Tests with history followed by suggestions +add_task(async function suggestionsLast() { + await SpecialPowers.pushPrefEnv({ + set: [[SUGGESTIONS_FIRST_PREF, false]], + }); + await withSuggestions(async () => { + await doTestPermutations(({ withHistory, generalIndex }) => ({ + expectedResultCount: withHistory ? MAX_RESULTS : 4, + expectedIndex: generalIndex == 0 || !withHistory ? 1 : MAX_RESULTS - 3, + })); + }); + await SpecialPowers.popPrefEnv(); +}); + +// Tests with history only plus a suggestedIndex result with a resultSpan +add_task(async function otherSuggestedIndex_noSuggestions() { + await doSuggestedIndexTest([ + // heuristic + { heuristic: true }, + // TestProvider result + { suggestedIndex: 1, resultSpan: 2 }, + // history + { type: UrlbarUtils.RESULT_TYPE.URL }, + { type: UrlbarUtils.RESULT_TYPE.URL }, + { type: UrlbarUtils.RESULT_TYPE.URL }, + { type: UrlbarUtils.RESULT_TYPE.URL }, + { type: UrlbarUtils.RESULT_TYPE.URL }, + { type: UrlbarUtils.RESULT_TYPE.URL }, + // quick suggest + { + type: UrlbarUtils.RESULT_TYPE.URL, + providerName: UrlbarProviderQuickSuggest.name, + }, + ]); +}); + +// Tests with suggestions followed by history plus a suggestedIndex result with +// a resultSpan +add_task(async function otherSuggestedIndex_suggestionsFirst() { + await SpecialPowers.pushPrefEnv({ + set: [[SUGGESTIONS_FIRST_PREF, true]], + }); + await withSuggestions(async () => { + await doSuggestedIndexTest([ + // heuristic + { heuristic: true }, + // TestProvider result + { suggestedIndex: 1, resultSpan: 2 }, + // search suggestions + { + type: UrlbarUtils.RESULT_TYPE.SEARCH, + payload: { suggestion: SPONSORED_SEARCH_STRING + "foo" }, + }, + { + type: UrlbarUtils.RESULT_TYPE.SEARCH, + payload: { suggestion: SPONSORED_SEARCH_STRING + "bar" }, + }, + // history + { type: UrlbarUtils.RESULT_TYPE.URL }, + { type: UrlbarUtils.RESULT_TYPE.URL }, + { type: UrlbarUtils.RESULT_TYPE.URL }, + { type: UrlbarUtils.RESULT_TYPE.URL }, + // quick suggest + { + type: UrlbarUtils.RESULT_TYPE.URL, + providerName: UrlbarProviderQuickSuggest.name, + }, + ]); + }); + await SpecialPowers.popPrefEnv(); +}); + +// Tests with history followed by suggestions plus a suggestedIndex result with +// a resultSpan +add_task(async function otherSuggestedIndex_suggestionsLast() { + await SpecialPowers.pushPrefEnv({ + set: [[SUGGESTIONS_FIRST_PREF, false]], + }); + await withSuggestions(async () => { + await doSuggestedIndexTest([ + // heuristic + { heuristic: true }, + // TestProvider result + { suggestedIndex: 1, resultSpan: 2 }, + // history + { type: UrlbarUtils.RESULT_TYPE.URL }, + { type: UrlbarUtils.RESULT_TYPE.URL }, + { type: UrlbarUtils.RESULT_TYPE.URL }, + { type: UrlbarUtils.RESULT_TYPE.URL }, + // quick suggest + { + type: UrlbarUtils.RESULT_TYPE.URL, + providerName: UrlbarProviderQuickSuggest.name, + }, + // search suggestions + { + type: UrlbarUtils.RESULT_TYPE.SEARCH, + payload: { suggestion: SPONSORED_SEARCH_STRING + "foo" }, + }, + { + type: UrlbarUtils.RESULT_TYPE.SEARCH, + payload: { suggestion: SPONSORED_SEARCH_STRING + "bar" }, + }, + ]); + }); + await SpecialPowers.popPrefEnv(); +}); + +/** + * A test provider that returns one result with a suggestedIndex and resultSpan. + */ +class TestProvider extends UrlbarTestUtils.TestProvider { + constructor() { + super({ + results: [ + Object.assign( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { url: "http://example.com/test" } + ), + { + suggestedIndex: 1, + resultSpan: 2, + } + ), + ], + }); + } +} + +/** + * Does a round of test permutations. + * + * @param {Function} callback + * For each permutation, this will be called with the arguments of `doTest()`, + * and it should return an object with the appropriate values of + * `expectedResultCount` and `expectedIndex`. + */ +async function doTestPermutations(callback) { + for (let isSponsored of [true, false]) { + for (let withHistory of [true, false]) { + for (let generalIndex of [0, -1]) { + let opts = { + isSponsored, + withHistory, + generalIndex, + }; + await doTest(Object.assign(opts, callback(opts))); + } + } + } +} + +/** + * Does one test run. + * + * @param {object} options + * Options for the test. + * @param {boolean} options.isSponsored + * True to use a sponsored result, false to use a non-sponsored result. + * @param {boolean} options.withHistory + * True to run with a bunch of history, false to run with no history. + * @param {number} options.generalIndex + * The value to set as the relevant index pref, i.e., the index within the + * general group of the quick suggest result. + * @param {number} options.expectedResultCount + * The expected total result count for sanity checking. + * @param {number} options.expectedIndex + * The expected index of the quick suggest result in the whole results list. + */ +async function doTest({ + isSponsored, + withHistory, + generalIndex, + expectedResultCount, + expectedIndex, +}) { + info( + "Running test with options: " + + JSON.stringify({ + isSponsored, + withHistory, + generalIndex, + expectedResultCount, + expectedIndex, + }) + ); + + // Set the index pref. + let indexPref = isSponsored ? SPONSORED_INDEX_PREF : NON_SPONSORED_INDEX_PREF; + await SpecialPowers.pushPrefEnv({ + set: [[indexPref, generalIndex]], + }); + + // Add history. + if (withHistory) { + await addHistory(); + } + + // Do a search. + let value = isSponsored + ? SPONSORED_SEARCH_STRING + : NON_SPONSORED_SEARCH_STRING; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value, + }); + + // Check the result count and quick suggest result. + Assert.equal( + UrlbarTestUtils.getResultCount(window), + expectedResultCount, + "Expected result count" + ); + await QuickSuggestTestUtils.assertIsQuickSuggest({ + window, + isSponsored, + index: expectedIndex, + url: isSponsored + ? `${TEST_URL}?q=${SPONSORED_SEARCH_STRING}` + : `${TEST_URL}?q=${NON_SPONSORED_SEARCH_STRING}`, + }); + + await UrlbarTestUtils.promisePopupClose(window); + await PlacesUtils.history.clear(); + await SpecialPowers.popPrefEnv(); +} + +/** + * Adds history that matches the sponsored and non-sponsored search strings. + */ +async function addHistory() { + for (let i = 0; i < MAX_RESULTS; i++) { + await PlacesTestUtils.addVisits([ + "http://example.com/" + SPONSORED_SEARCH_STRING + i, + "http://example.com/" + NON_SPONSORED_SEARCH_STRING + i, + ]); + } +} + +/** + * Adds a search engine that provides suggestions, calls your callback, and then + * removes the engine. + * + * @param {Function} callback + * Your callback function. + */ +async function withSuggestions(callback) { + await SpecialPowers.pushPrefEnv({ + set: [[SUGGESTIONS_PREF, true]], + }); + let engine = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME, + }); + let oldDefaultEngine = await Services.search.getDefault(); + await Services.search.setDefault( + engine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + try { + await callback(engine); + } finally { + await Services.search.setDefault( + oldDefaultEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + await Services.search.removeEngine(engine); + await SpecialPowers.popPrefEnv(); + } +} + +/** + * Registers a test provider that returns a result with a suggestedIndex and + * resultSpan and asserts the given expected results match the actual results. + * + * @param {Array} expectedProps + * See `checkResults()`. + */ +async function doSuggestedIndexTest(expectedProps) { + await addHistory(); + let provider = new TestProvider(); + UrlbarProvidersManager.registerProvider(provider); + + let context = await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: SPONSORED_SEARCH_STRING, + }); + checkResults(context.results, expectedProps); + await UrlbarTestUtils.promisePopupClose(window); + + UrlbarProvidersManager.unregisterProvider(provider); + await PlacesUtils.history.clear(); +} + +/** + * Asserts the given actual and expected results match. + * + * @param {Array} actualResults + * Array of actual results. + * @param {Array} expectedProps + * Array of expected result-like objects. Only the properties defined in each + * of these objects are compared against the corresponding actual result. + */ +function checkResults(actualResults, expectedProps) { + Assert.equal( + actualResults.length, + expectedProps.length, + "Expected result count" + ); + + let actualProps = actualResults.map((actual, i) => { + if (expectedProps.length <= i) { + return actual; + } + let props = {}; + let expected = expectedProps[i]; + for (let [key, expectedValue] of Object.entries(expected)) { + if (key != "payload") { + props[key] = actual[key]; + } else { + props.payload = {}; + for (let pkey of Object.keys(expectedValue)) { + props.payload[pkey] = actual.payload[pkey]; + } + } + } + return props; + }); + Assert.deepEqual(actualProps, expectedProps); +} diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_merinoSessions.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_merinoSessions.js new file mode 100644 index 0000000000..050ea31e12 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_merinoSessions.js @@ -0,0 +1,142 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// End-to-end browser smoke test for Merino sessions. More comprehensive tests +// are in test_quicksuggest_merinoSessions.js. This test essentially makes sure +// engagements occur as expected when interacting with the urlbar. If you need +// to add tests that do not depend on a new definition of "engagement", consider +// adding them to test_quicksuggest_merinoSessions.js instead. + +"use strict"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.merino.enabled", true], + ["browser.urlbar.quicksuggest.remoteSettings.enabled", false], + ["browser.urlbar.quicksuggest.dataCollection.enabled", true], + ], + }); + + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + await UrlbarTestUtils.formHistory.clear(); + + // Install a mock default engine so we don't hit the network. + await SearchTestUtils.installSearchExtension({}, { setAsDefault: true }); + + await MerinoTestUtils.server.start(); +}); + +// In a single engagement, all requests should use the same session ID and the +// sequence number should be incremented. +add_task(async function singleEngagement() { + for (let i = 0; i < 3; i++) { + let searchString = "search" + i; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: searchString, + fireInputEvent: true, + }); + + MerinoTestUtils.server.checkAndClearRequests([ + { + params: { + [MerinoTestUtils.SEARCH_PARAMS.QUERY]: searchString, + [MerinoTestUtils.SEARCH_PARAMS.SEQUENCE_NUMBER]: i, + }, + }, + ]); + } + + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); +}); + +// In a single engagement, all requests should use the same session ID and the +// sequence number should be incremented. This task closes the panel between +// searches but keeps the input focused, so the engagement should not end. +add_task(async function singleEngagement_panelClosed() { + for (let i = 0; i < 3; i++) { + let searchString = "search" + i; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: searchString, + fireInputEvent: true, + }); + + MerinoTestUtils.server.checkAndClearRequests([ + { + params: { + [MerinoTestUtils.SEARCH_PARAMS.QUERY]: searchString, + [MerinoTestUtils.SEARCH_PARAMS.SEQUENCE_NUMBER]: i, + }, + }, + ]); + + EventUtils.synthesizeKey("KEY_Escape"); + Assert.ok(!UrlbarTestUtils.isPopupOpen(window), "Panel is closed"); + Assert.ok(gURLBar.focused, "Input remains focused"); + } + + // End the engagement to reset the session for the next test. + gURLBar.blur(); +}); + +// New engagements should not use the same session ID as previous engagements +// and the sequence number should be reset. This task completes each engagement +// successfully. +add_task(async function manyEngagements_engagement() { + for (let i = 0; i < 3; i++) { + // Open a new tab since we'll load the mock default search engine page. + await BrowserTestUtils.withNewTab("about:blank", async () => { + let searchString = "search" + i; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: searchString, + fireInputEvent: true, + }); + + MerinoTestUtils.server.checkAndClearRequests([ + { + params: { + [MerinoTestUtils.SEARCH_PARAMS.QUERY]: searchString, + [MerinoTestUtils.SEARCH_PARAMS.SEQUENCE_NUMBER]: 0, + }, + }, + ]); + + // Press enter on the heuristic result to load the search engine page and + // complete the engagement. + let loadPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser + ); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + }); + } +}); + +// New engagements should not use the same session ID as previous engagements +// and the sequence number should be reset. This task abandons each engagement. +add_task(async function manyEngagements_abandonment() { + for (let i = 0; i < 3; i++) { + let searchString = "search" + i; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: searchString, + fireInputEvent: true, + }); + + MerinoTestUtils.server.checkAndClearRequests([ + { + params: { + [MerinoTestUtils.SEARCH_PARAMS.QUERY]: searchString, + [MerinoTestUtils.SEARCH_PARAMS.SEQUENCE_NUMBER]: 0, + }, + }, + ]); + + // Blur the urlbar to abandon the engagement. + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + } +}); diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_onboardingDialog.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_onboardingDialog.js new file mode 100644 index 0000000000..92c3c7c95d --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_onboardingDialog.js @@ -0,0 +1,1596 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests the buttons in the onboarding dialog for quick suggest/Firefox Suggest. + */ + +ChromeUtils.defineESModuleGetters(this, { + TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.sys.mjs", +}); + +const OTHER_DIALOG_URI = getRootDirectory(gTestPath) + "subdialog.xhtml"; + +// Default-branch pref values in the offline scenario. +const OFFLINE_DEFAULT_PREFS = { + "suggest.quicksuggest.nonsponsored": true, + "suggest.quicksuggest.sponsored": true, + "quicksuggest.dataCollection.enabled": false, +}; + +let gDefaultBranch = Services.prefs.getDefaultBranch("browser.urlbar."); +let gUserBranch = Services.prefs.getBranch("browser.urlbar."); + +// Allow more time for Mac and Linux machines so they don't time out in verify mode. +if (AppConstants.platform === "macosx") { + requestLongerTimeout(4); +} else if (AppConstants.platform === "linux") { + requestLongerTimeout(2); +} + +// Whether the tab key can move the focus. On macOS with full keyboard access +// disabled (which is default), this will be false. See `canTabMoveFocus`. +let gCanTabMoveFocus; +add_setup(async function () { + gCanTabMoveFocus = await canTabMoveFocus(); +}); + +// When the user has already enabled the data-collection pref, the dialog should +// not appear. +add_task(async function dataCollectionAlreadyEnabled() { + setDialogPrereqPrefs(); + UrlbarPrefs.set("quicksuggest.dataCollection.enabled", true); + + info("Calling maybeShowOnboardingDialog"); + let showed = await QuickSuggest.maybeShowOnboardingDialog(); + Assert.ok(!showed, "The dialog was not shown"); + + UrlbarPrefs.clear("quicksuggest.dataCollection.enabled"); +}); + +// When the current tab is about:welcome, the dialog should not appear. +add_task(async function aboutWelcome() { + setDialogPrereqPrefs(); + await BrowserTestUtils.withNewTab("about:welcome", async () => { + info("Calling maybeShowOnboardingDialog"); + let showed = await QuickSuggest.maybeShowOnboardingDialog(); + Assert.ok(!showed, "The dialog was not shown"); + }); +}); + +// The Escape key should dismiss the dialog without opting in. This task tests +// when Escape is pressed while the focus is inside the dialog. +add_task(async function escKey_focusInsideDialog() { + await doDialogTest({ + callback: async () => { + const { maybeShowPromise } = await showOnboardingDialog({ + skipIntroduction: true, + }); + + const tabCount = gBrowser.tabs.length; + Assert.ok( + document.activeElement.classList.contains("dialogFrame"), + "dialogFrame is focused in the browser window" + ); + + info("Close the dialog"); + EventUtils.synthesizeKey("KEY_Escape"); + + await maybeShowPromise; + + Assert.equal( + gBrowser.currentURI.spec, + "about:blank", + "Nothing loaded in the current tab" + ); + Assert.equal(gBrowser.tabs.length, tabCount, "No news tabs were opened"); + }, + onboardingDialogVersion: JSON.stringify({ version: 1 }), + onboardingDialogChoice: "dismiss_2", + expectedUserBranchPrefs: { + "quicksuggest.dataCollection.enabled": false, + }, + telemetryEvents: [ + { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "data_collect_toggled", + object: "disabled", + }, + { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "opt_in_dialog", + object: "dismiss_2", + }, + ], + }); +}); + +// The Escape key should dismiss the dialog without opting in. This task tests +// when Escape is pressed while the focus is outside the dialog. +add_task(async function escKey_focusOutsideDialog() { + await doDialogTest({ + callback: async () => { + const { maybeShowPromise } = await showOnboardingDialog({ + skipIntroduction: true, + }); + + document.documentElement.focus(); + Assert.ok( + !document.activeElement.classList.contains("dialogFrame"), + "dialogFrame is not focused in the browser window" + ); + + info("Close the dialog"); + EventUtils.synthesizeKey("KEY_Escape"); + + await maybeShowPromise; + }, + onboardingDialogVersion: JSON.stringify({ version: 1 }), + onboardingDialogChoice: "dismiss_2", + expectedUserBranchPrefs: { + "quicksuggest.dataCollection.enabled": false, + }, + telemetryEvents: [ + { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "data_collect_toggled", + object: "disabled", + }, + { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "opt_in_dialog", + object: "dismiss_2", + }, + ], + }); +}); + +// The Escape key should dismiss the dialog without opting in when another +// dialog is queued and shown before the onboarding. This task dismisses the +// other dialog by pressing the Escape key. +add_task(async function escKey_queued_esc() { + await doQueuedEscKeyTest("KEY_Escape"); +}); + +// The Escape key should dismiss the dialog without opting in when another +// dialog is queued and shown before the onboarding. This task dismisses the +// other dialog by pressing the Enter key. +add_task(async function escKey_queued_enter() { + await doQueuedEscKeyTest("KEY_Enter"); +}); + +async function doQueuedEscKeyTest(otherDialogKey) { + await doDialogTest({ + callback: async () => { + // Create promises that will resolve when each dialog is opened. + let uris = [OTHER_DIALOG_URI, QuickSuggest.ONBOARDING_URI]; + let [otherOpenedPromise, onboardingOpenedPromise] = uris.map(uri => + TestUtils.topicObserved( + "subdialog-loaded", + contentWin => contentWin.document.documentURI == uri + ).then(async ([contentWin]) => { + if (contentWin.document.readyState != "complete") { + await BrowserTestUtils.waitForEvent(contentWin, "load"); + } + }) + ); + + info("Queuing dialogs for opening"); + let otherClosedPromise = gDialogBox.open(OTHER_DIALOG_URI); + let onboardingClosedPromise = QuickSuggest.maybeShowOnboardingDialog(); + + info("Waiting for the other dialog to open"); + await otherOpenedPromise; + + info(`Pressing ${otherDialogKey} and waiting for other dialog to close`); + EventUtils.synthesizeKey(otherDialogKey); + await otherClosedPromise; + + info("Waiting for the onboarding dialog to open"); + await onboardingOpenedPromise; + + info("Pressing Escape and waiting for onboarding dialog to close"); + EventUtils.synthesizeKey("KEY_Escape"); + await onboardingClosedPromise; + }, + onboardingDialogVersion: JSON.stringify({ version: 1 }), + onboardingDialogChoice: "dismiss_1", + expectedUserBranchPrefs: { + "quicksuggest.dataCollection.enabled": false, + }, + telemetryEvents: [ + { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "data_collect_toggled", + object: "disabled", + }, + { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "opt_in_dialog", + object: "dismiss_1", + }, + ], + }); +} + +// Tests `dismissed_other` by closing the dialog programmatically. +add_task(async function dismissed_other_on_introduction() { + await doDialogTest({ + callback: async () => { + const { maybeShowPromise } = await showOnboardingDialog(); + gDialogBox._dialog.close(); + await maybeShowPromise; + }, + onboardingDialogVersion: JSON.stringify({ version: 1 }), + onboardingDialogChoice: "dismiss_1", + expectedUserBranchPrefs: { + "quicksuggest.dataCollection.enabled": false, + }, + telemetryEvents: [ + { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "data_collect_toggled", + object: "disabled", + }, + { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "opt_in_dialog", + object: "dismiss_1", + }, + ], + }); +}); + +// The default is to wait for no browser restarts to show the onboarding dialog +// on the first restart. This tests that we can override it by configuring the +// `showOnboardingDialogOnNthRestart` +add_task(async function nimbus_override_wait_after_n_restarts() { + UrlbarPrefs.clear("quicksuggest.shouldShowOnboardingDialog"); + UrlbarPrefs.clear("quicksuggest.showedOnboardingDialog"); + UrlbarPrefs.clear("quicksuggest.seenRestarts", 0); + + await QuickSuggestTestUtils.withExperiment({ + valueOverrides: { + quickSuggestScenario: "online", + // Wait for 1 browser restart + quickSuggestShowOnboardingDialogAfterNRestarts: 1, + }, + callback: async () => { + let prefPromise = TestUtils.waitForPrefChange( + "browser.urlbar.quicksuggest.showedOnboardingDialog", + value => value === true + ).then(() => info("Saw pref change")); + + // Simulate 2 restarts. this function is only called by BrowserGlue + // on startup, the first restart would be where MR1 was shown then + // we will show onboarding the 2nd restart after that. + info("Simulating first restart"); + await QuickSuggest.maybeShowOnboardingDialog(); + + info("Simulating second restart"); + const dialogPromise = BrowserTestUtils.promiseAlertDialogOpen( + null, + QuickSuggest.ONBOARDING_URI, + { isSubDialog: true } + ); + const maybeShowPromise = QuickSuggest.maybeShowOnboardingDialog(); + const win = await dialogPromise; + if (win.document.readyState != "complete") { + await BrowserTestUtils.waitForEvent(win, "load"); + } + // Close dialog. + EventUtils.synthesizeKey("KEY_Escape"); + + info("Waiting for maybeShowPromise and pref change"); + await Promise.all([maybeShowPromise, prefPromise]); + }, + }); +}); + +add_task(async function nimbus_skip_onboarding_dialog() { + UrlbarPrefs.clear("quicksuggest.shouldShowOnboardingDialog"); + UrlbarPrefs.clear("quicksuggest.showedOnboardingDialog"); + UrlbarPrefs.clear("quicksuggest.seenRestarts", 0); + + await QuickSuggestTestUtils.withExperiment({ + valueOverrides: { + quickSuggestScenario: "online", + quickSuggestShouldShowOnboardingDialog: false, + }, + callback: async () => { + // Simulate 3 restarts. + for (let i = 0; i < 3; i++) { + info(`Simulating restart ${i + 1}`); + await QuickSuggest.maybeShowOnboardingDialog(); + } + Assert.ok( + !Services.prefs.getBoolPref( + "browser.urlbar.quicksuggest.showedOnboardingDialog", + false + ), + "The showed onboarding dialog pref should not be set" + ); + }, + }); +}); + +add_task(async function nimbus_exposure_event() { + const testData = [ + { + experimentType: "modal", + expectedRecorded: true, + }, + { + experimentType: "best-match", + expectedRecorded: false, + }, + { + expectedRecorded: false, + }, + ]; + + for (const { experimentType, expectedRecorded } of testData) { + info(`Nimbus exposure event test for type:[${experimentType}]`); + UrlbarPrefs.clear("quicksuggest.shouldShowOnboardingDialog"); + UrlbarPrefs.clear("quicksuggest.showedOnboardingDialog"); + UrlbarPrefs.clear("quicksuggest.seenRestarts", 0); + + await QuickSuggestTestUtils.clearExposureEvent(); + + await QuickSuggestTestUtils.withExperiment({ + valueOverrides: { + quickSuggestScenario: "online", + experimentType, + }, + callback: async () => { + info("Calling showOnboardingDialog"); + const { maybeShowPromise } = await showOnboardingDialog(); + EventUtils.synthesizeKey("KEY_Escape"); + await maybeShowPromise; + + info("Check the event"); + await QuickSuggestTestUtils.assertExposureEvent(expectedRecorded); + }, + }); + } +}); + +const LOGO_TYPE = { + FIREFOX: 1, + MAGGLASS: 2, + ANIMATION_MAGGLASS: 3, +}; + +const VARIATION_TEST_DATA = [ + { + name: "A", + introductionSection: { + logoType: LOGO_TYPE.ANIMATION_MAGGLASS, + l10n: { + onboardingNext: "firefox-suggest-onboarding-introduction-next-button-1", + "introduction-title": "firefox-suggest-onboarding-introduction-title-1", + }, + visibility: { + "#onboardingLearnMoreOnIntroduction": false, + ".description-section": false, + ".pager": true, + }, + defaultFocusOrder: [ + "onboardingNext", + "onboardingClose", + "onboardingNext", + ], + actions: ["onboardingClose", "onboardingNext"], + }, + mainSection: { + logoType: LOGO_TYPE.MAGGLASS, + l10n: { + "main-title": "firefox-suggest-onboarding-main-title-1", + "main-description": "firefox-suggest-onboarding-main-description-1", + "main-accept-option-label": + "firefox-suggest-onboarding-main-accept-option-label", + "main-accept-option-description": + "firefox-suggest-onboarding-main-accept-option-description-1", + "main-reject-option-label": + "firefox-suggest-onboarding-main-reject-option-label", + "main-reject-option-description": + "firefox-suggest-onboarding-main-reject-option-description-1", + }, + visibility: { + "#main-privacy-first": false, + ".description-section #onboardingLearnMore": false, + ".accept #onboardingLearnMore": true, + ".pager": true, + }, + defaultFocusOrder: [ + "onboardingNext", + "onboardingAccept", + "onboardingLearnMore", + "onboardingReject", + "onboardingSkipLink", + "onboardingAccept", + ], + acceptFocusOrder: [ + "onboardingAccept", + "onboardingLearnMore", + "onboardingSubmit", + "onboardingSkipLink", + "onboardingAccept", + ], + rejectFocusOrder: [ + "onboardingReject", + "onboardingSubmit", + "onboardingSkipLink", + "onboardingLearnMore", + "onboardingReject", + ], + actions: [ + "onboardingAccept", + "onboardingReject", + "onboardingSkipLink", + "onboardingLearnMore", + ], + }, + }, + { + // We don't need to test the focus order and actions because the layout of + // variation B-H is as same as A. + name: "B", + introductionSection: { + logoType: LOGO_TYPE.ANIMATION_MAGGLASS, + l10n: { + onboardingNext: "firefox-suggest-onboarding-introduction-next-button-1", + "introduction-title": "firefox-suggest-onboarding-introduction-title-2", + }, + visibility: { + "#onboardingLearnMoreOnIntroduction": false, + ".description-section": false, + ".pager": true, + }, + }, + mainSection: { + logoType: LOGO_TYPE.MAGGLASS, + l10n: { + "main-title": "firefox-suggest-onboarding-main-title-2", + "main-description": "firefox-suggest-onboarding-main-description-2", + "main-accept-option-label": + "firefox-suggest-onboarding-main-accept-option-label", + "main-accept-option-description": + "firefox-suggest-onboarding-main-accept-option-description-1", + "main-reject-option-label": + "firefox-suggest-onboarding-main-reject-option-label", + "main-reject-option-description": + "firefox-suggest-onboarding-main-reject-option-description-1", + }, + visibility: { + "#main-privacy-first": false, + ".description-section #onboardingLearnMore": false, + ".accept #onboardingLearnMore": true, + ".pager": true, + }, + }, + }, + { + name: "C", + introductionSection: { + logoType: LOGO_TYPE.FIREFOX, + l10n: { + onboardingNext: "firefox-suggest-onboarding-introduction-next-button-1", + "introduction-title": "firefox-suggest-onboarding-introduction-title-3", + }, + visibility: { + "#onboardingLearnMoreOnIntroduction": false, + ".description-section": false, + ".pager": true, + }, + }, + mainSection: { + logoType: LOGO_TYPE.FIREFOX, + l10n: { + "main-title": "firefox-suggest-onboarding-main-title-3", + "main-description": "firefox-suggest-onboarding-main-description-3", + "main-accept-option-label": + "firefox-suggest-onboarding-main-accept-option-label", + "main-accept-option-description": + "firefox-suggest-onboarding-main-accept-option-description-1", + "main-reject-option-label": + "firefox-suggest-onboarding-main-reject-option-label", + "main-reject-option-description": + "firefox-suggest-onboarding-main-reject-option-description-1", + }, + visibility: { + "#main-privacy-first": false, + ".description-section #onboardingLearnMore": false, + ".accept #onboardingLearnMore": true, + ".pager": true, + }, + }, + }, + { + name: "D", + introductionSection: { + logoType: LOGO_TYPE.ANIMATION_MAGGLASS, + l10n: { + onboardingNext: "firefox-suggest-onboarding-introduction-next-button-1", + "introduction-title": "firefox-suggest-onboarding-introduction-title-4", + }, + visibility: { + "#onboardingLearnMoreOnIntroduction": false, + ".description-section": false, + ".pager": true, + }, + }, + mainSection: { + logoType: LOGO_TYPE.MAGGLASS, + l10n: { + "main-title": "firefox-suggest-onboarding-main-title-4", + "main-description": "firefox-suggest-onboarding-main-description-4", + "main-accept-option-label": + "firefox-suggest-onboarding-main-accept-option-label", + "main-accept-option-description": + "firefox-suggest-onboarding-main-accept-option-description-2", + "main-reject-option-label": + "firefox-suggest-onboarding-main-reject-option-label", + "main-reject-option-description": + "firefox-suggest-onboarding-main-reject-option-description-2", + }, + visibility: { + "#main-privacy-first": false, + ".description-section #onboardingLearnMore": false, + ".accept #onboardingLearnMore": true, + ".pager": true, + }, + }, + }, + { + name: "E", + introductionSection: { + logoType: LOGO_TYPE.FIREFOX, + l10n: { + onboardingNext: "firefox-suggest-onboarding-introduction-next-button-1", + "introduction-title": "firefox-suggest-onboarding-introduction-title-5", + }, + visibility: { + "#onboardingLearnMoreOnIntroduction": false, + ".description-section": false, + ".pager": true, + }, + }, + mainSection: { + logoType: LOGO_TYPE.FIREFOX, + l10n: { + "main-title": "firefox-suggest-onboarding-main-title-5", + "main-description": "firefox-suggest-onboarding-main-description-5", + "main-accept-option-label": + "firefox-suggest-onboarding-main-accept-option-label", + "main-accept-option-description": + "firefox-suggest-onboarding-main-accept-option-description-2", + "main-reject-option-label": + "firefox-suggest-onboarding-main-reject-option-label", + "main-reject-option-description": + "firefox-suggest-onboarding-main-reject-option-description-2", + }, + visibility: { + "#main-privacy-first": false, + ".description-section #onboardingLearnMore": false, + ".accept #onboardingLearnMore": true, + ".pager": true, + }, + }, + }, + { + name: "F", + introductionSection: { + logoType: LOGO_TYPE.ANIMATION_MAGGLASS, + l10n: { + onboardingNext: "firefox-suggest-onboarding-introduction-next-button-2", + "introduction-title": "firefox-suggest-onboarding-introduction-title-6", + }, + visibility: { + "#onboardingLearnMoreOnIntroduction": false, + ".description-section": false, + ".pager": true, + }, + }, + mainSection: { + logoType: LOGO_TYPE.MAGGLASS, + l10n: { + "main-title": "firefox-suggest-onboarding-main-title-6", + "main-description": "firefox-suggest-onboarding-main-description-6", + "main-accept-option-label": + "firefox-suggest-onboarding-main-accept-option-label", + "main-accept-option-description": + "firefox-suggest-onboarding-main-accept-option-description-2", + "main-reject-option-label": + "firefox-suggest-onboarding-main-reject-option-label", + "main-reject-option-description": + "firefox-suggest-onboarding-main-reject-option-description-2", + }, + visibility: { + "#main-privacy-first": false, + ".description-section #onboardingLearnMore": false, + ".accept #onboardingLearnMore": true, + ".pager": true, + }, + }, + }, + { + name: "G", + introductionSection: { + logoType: LOGO_TYPE.ANIMATION_MAGGLASS, + l10n: { + onboardingNext: "firefox-suggest-onboarding-introduction-next-button-1", + "introduction-title": "firefox-suggest-onboarding-introduction-title-7", + }, + visibility: { + "#onboardingLearnMoreOnIntroduction": false, + ".description-section": false, + ".pager": true, + }, + }, + mainSection: { + logoType: LOGO_TYPE.MAGGLASS, + l10n: { + "main-title": "firefox-suggest-onboarding-main-title-7", + "main-description": "firefox-suggest-onboarding-main-description-7", + "main-accept-option-label": + "firefox-suggest-onboarding-main-accept-option-label", + "main-accept-option-description": + "firefox-suggest-onboarding-main-accept-option-description-2", + "main-reject-option-label": + "firefox-suggest-onboarding-main-reject-option-label", + "main-reject-option-description": + "firefox-suggest-onboarding-main-reject-option-description-2", + }, + visibility: { + "#main-privacy-first": true, + ".description-section #onboardingLearnMore": false, + ".accept #onboardingLearnMore": true, + ".pager": true, + }, + }, + }, + { + name: "H", + introductionSection: { + logoType: LOGO_TYPE.FIREFOX, + l10n: { + onboardingNext: "firefox-suggest-onboarding-introduction-next-button-1", + "introduction-title": "firefox-suggest-onboarding-introduction-title-2", + }, + visibility: { + "#onboardingLearnMoreOnIntroduction": false, + ".description-section": false, + ".pager": true, + }, + }, + mainSection: { + logoType: LOGO_TYPE.FIREFOX, + l10n: { + "main-title": "firefox-suggest-onboarding-main-title-8", + "main-description": "firefox-suggest-onboarding-main-description-8", + "main-accept-option-label": + "firefox-suggest-onboarding-main-accept-option-label", + "main-accept-option-description": + "firefox-suggest-onboarding-main-accept-option-description-1", + "main-reject-option-label": + "firefox-suggest-onboarding-main-reject-option-label", + "main-reject-option-description": + "firefox-suggest-onboarding-main-reject-option-description-1", + }, + visibility: { + "#main-privacy-first": false, + ".description-section #onboardingLearnMore": false, + ".accept #onboardingLearnMore": true, + ".pager": true, + }, + }, + }, + { + name: "100-A", + introductionSection: { + logoType: LOGO_TYPE.FIREFOX, + l10n: { + onboardingNext: "firefox-suggest-onboarding-introduction-next-button-3", + "introduction-title": "firefox-suggest-onboarding-main-title-9", + }, + visibility: { + "#onboardingLearnMoreOnIntroduction": true, + ".description-section": true, + ".pager": true, + }, + defaultFocusOrder: [ + "onboardingNext", + "onboardingLearnMoreOnIntroduction", + "onboardingClose", + "onboardingNext", + ], + actions: [ + "onboardingClose", + "onboardingNext", + "onboardingLearnMoreOnIntroduction", + ], + }, + mainSection: { + logoType: LOGO_TYPE.FIREFOX, + l10n: { + "main-title": "firefox-suggest-onboarding-main-title-9", + "main-description": "firefox-suggest-onboarding-main-description-9", + "main-accept-option-label": + "firefox-suggest-onboarding-main-accept-option-label-2", + "main-accept-option-description": + "firefox-suggest-onboarding-main-accept-option-description-3", + "main-reject-option-label": + "firefox-suggest-onboarding-main-reject-option-label-2", + "main-reject-option-description": + "firefox-suggest-onboarding-main-reject-option-description-3", + }, + visibility: { + "#main-privacy-first": true, + ".description-section #onboardingLearnMore": true, + ".accept #onboardingLearnMore": false, + ".pager": false, + }, + defaultFocusOrder: [ + "onboardingNext", + "onboardingLearnMore", + "onboardingAccept", + "onboardingReject", + "onboardingSkipLink", + "onboardingLearnMore", + ], + acceptFocusOrder: [ + "onboardingAccept", + "onboardingSubmit", + "onboardingSkipLink", + "onboardingLearnMore", + "onboardingAccept", + ], + rejectFocusOrder: [ + "onboardingReject", + "onboardingSubmit", + "onboardingSkipLink", + "onboardingLearnMore", + "onboardingReject", + ], + actions: [ + "onboardingAccept", + "onboardingReject", + "onboardingSkipLink", + "onboardingLearnMore", + ], + }, + }, + { + name: "100-B", + mainSection: { + logoType: LOGO_TYPE.FIREFOX, + l10n: { + "main-title": "firefox-suggest-onboarding-main-title-9", + "main-description": "firefox-suggest-onboarding-main-description-9", + "main-accept-option-label": + "firefox-suggest-onboarding-main-accept-option-label-2", + "main-accept-option-description": + "firefox-suggest-onboarding-main-accept-option-description-3", + "main-reject-option-label": + "firefox-suggest-onboarding-main-reject-option-label-2", + "main-reject-option-description": + "firefox-suggest-onboarding-main-reject-option-description-3", + }, + visibility: { + "#main-privacy-first": true, + ".description-section #onboardingLearnMore": true, + ".accept #onboardingLearnMore": false, + ".pager": false, + }, + // Layout of 100-B is same as 100-A, but since there is no the introduction + // pane, only the default focus order on the main pane is a bit diffrence. + defaultFocusOrder: [ + "onboardingLearnMore", + "onboardingAccept", + "onboardingReject", + "onboardingSkipLink", + "onboardingLearnMore", + ], + }, + }, +]; + +/** + * This test checks for differences due to variations in logo type, l10n text, + * element visibility, order of focus, actions, etc. The designation is on + * VARIATION_TEST_DATA. The items that can be specified are below. + * + * name: Specify the variation name. + * + * The following items are specified for each section. + * (introductionSection, mainSection). + * + * logoType: + * Specify the expected logo type. Please refer to LOGO_TYPE about the type. + * + * l10n: + * Specify the expected l10n id applied to elements. + * + * visibility: + * Specify the expected visibility of elements. The way to specify the element + * is using selector. + * + * defaultFocusOrder: + * Specify the expected focus order right after the section is appeared. The + * way to specify the element is using id. + * + * acceptFocusOrder: + * Specify the expected focus order after selecting accept option. + * + * rejectFocusOrder: + * Specify the expected focus order after selecting reject option. + * + * actions: + * Specify the action we want to verify such as clicking the close button. The + * available actions are below. + * - onboardingClose: + * Action of the close button “x” by mouse/keyboard. + * - onboardingNext: + * Action of the next button that transits from the introduction section to + * the main section by mouse/keyboard. + * - onboardingAccept: + * Action of the submit button by mouse/keyboard after selecting accept + * option by mouse/keyboard. + * - onboardingReject: + * Action of the submit button by mouse/keyboard after selecting reject + * option by mouse/keyboard. + * - onboardingSkipLink: + * Action of the skip link by mouse/keyboard. + * - onboardingLearnMore: + * Action of the learn more link by mouse/keyboard. + * - onboardingLearnMoreOnIntroduction: + * Action of the learn more link on the introduction section by + * mouse/keyboard. + */ +add_task(async function variation_test() { + for (const variation of VARIATION_TEST_DATA) { + info(`Test for variation [${variation.name}]`); + + info("Do layout test"); + await doLayoutTest(variation); + + for (const action of variation.introductionSection?.actions || []) { + info( + `${action} test on the introduction section for variation [${variation.name}]` + ); + await this[action](variation); + } + + for (const action of variation.mainSection?.actions || []) { + info( + `${action} test on the main section for variation [${variation.name}]` + ); + await this[action](variation, !!variation.introductionSection); + } + } +}); + +async function doLayoutTest(variation) { + UrlbarPrefs.clear("quicksuggest.shouldShowOnboardingDialog"); + UrlbarPrefs.clear("quicksuggest.showedOnboardingDialog"); + UrlbarPrefs.clear("quicksuggest.seenRestarts", 0); + + await QuickSuggestTestUtils.withExperiment({ + valueOverrides: { + quickSuggestScenario: "online", + quickSuggestOnboardingDialogVariation: variation.name, + }, + callback: async () => { + info("Calling showOnboardingDialog"); + const { win, maybeShowPromise } = await showOnboardingDialog(); + + const introductionSection = win.document.getElementById( + "introduction-section" + ); + const mainSection = win.document.getElementById("main-section"); + + if (variation.introductionSection) { + info("Check the section visibility"); + Assert.ok(BrowserTestUtils.is_visible(introductionSection)); + Assert.ok(BrowserTestUtils.is_hidden(mainSection)); + + info("Check the introduction section"); + await assertSection(introductionSection, variation.introductionSection); + + info("Transition to the main section"); + win.document.getElementById("onboardingNext").click(); + await BrowserTestUtils.waitForCondition( + () => + BrowserTestUtils.is_hidden(introductionSection) && + BrowserTestUtils.is_visible(mainSection) + ); + } else { + info("Check the section visibility"); + Assert.ok(BrowserTestUtils.is_hidden(introductionSection)); + Assert.ok(BrowserTestUtils.is_visible(mainSection)); + } + + info("Check the main section"); + await assertSection(mainSection, variation.mainSection); + + info("Close the dialog"); + EventUtils.synthesizeKey("KEY_Escape", {}, win); + await maybeShowPromise; + }, + }); +} + +async function assertSection(sectionElement, expectedSection) { + info("Check the logo"); + assertLogo(sectionElement, expectedSection.logoType); + + info("Check the l10n"); + assertL10N(sectionElement, expectedSection.l10n); + + info("Check the visibility"); + assertVisibility(sectionElement, expectedSection.visibility); + + if (!gCanTabMoveFocus) { + Assert.ok(true, "Tab key can't move focus, skipping test for focus order"); + return; + } + + if (expectedSection.defaultFocusOrder) { + info("Check the default focus order"); + assertFocusOrder(sectionElement, expectedSection.defaultFocusOrder); + } + + if (expectedSection.acceptFocusOrder) { + info("Check the focus order after selecting accept option"); + sectionElement.querySelector("#onboardingAccept").focus(); + EventUtils.synthesizeKey("VK_SPACE", {}, sectionElement.ownerGlobal); + assertFocusOrder(sectionElement, expectedSection.acceptFocusOrder); + } + + if (expectedSection.rejectFocusOrder) { + info("Check the focus order after selecting reject option"); + sectionElement.querySelector("#onboardingReject").focus(); + EventUtils.synthesizeKey("VK_SPACE", {}, sectionElement.ownerGlobal); + assertFocusOrder(sectionElement, expectedSection.rejectFocusOrder); + } +} + +function assertLogo(sectionElement, expectedLogoType) { + let expectedLogoImage; + switch (expectedLogoType) { + case LOGO_TYPE.FIREFOX: { + expectedLogoImage = 'url("chrome://branding/content/about-logo.svg")'; + break; + } + case LOGO_TYPE.MAGGLASS: { + expectedLogoImage = + 'url("chrome://browser/content/urlbar/quicksuggestOnboarding_magglass.svg")'; + break; + } + case LOGO_TYPE.ANIMATION_MAGGLASS: { + const mediaQuery = sectionElement.ownerGlobal.matchMedia( + "(prefers-reduced-motion: no-preference)" + ); + expectedLogoImage = mediaQuery.matches + ? 'url("chrome://browser/content/urlbar/quicksuggestOnboarding_magglass_animation.svg")' + : 'url("chrome://browser/content/urlbar/quicksuggestOnboarding_magglass.svg")'; + break; + } + default: { + Assert.ok(false, `Unexpected image type ${expectedLogoType}`); + break; + } + } + + const logo = sectionElement.querySelector(".logo"); + Assert.ok(BrowserTestUtils.is_visible(logo)); + const logoImage = + sectionElement.ownerGlobal.getComputedStyle(logo).backgroundImage; + Assert.equal(logoImage, expectedLogoImage); +} + +function assertL10N(sectionElement, expectedL10N) { + for (const [id, l10n] of Object.entries(expectedL10N)) { + const element = sectionElement.querySelector("#" + id); + Assert.equal(element.getAttribute("data-l10n-id"), l10n); + } +} + +function assertVisibility(sectionElement, expectedVisibility) { + for (const [selector, visibility] of Object.entries(expectedVisibility)) { + const element = sectionElement.querySelector(selector); + if (visibility) { + Assert.ok(BrowserTestUtils.is_visible(element)); + } else { + if (!element) { + Assert.ok(true); + return; + } + Assert.ok(BrowserTestUtils.is_hidden(element)); + } + } +} + +function assertFocusOrder(sectionElement, expectedFocusOrder) { + const win = sectionElement.ownerGlobal; + + // Check initial active element. + Assert.equal(win.document.activeElement.id, expectedFocusOrder[0]); + + for (const next of expectedFocusOrder.slice(1)) { + EventUtils.synthesizeKey("KEY_Tab", {}, win); + Assert.equal(win.document.activeElement.id, next); + } +} + +async function onboardingClose(variation) { + await doActionTest({ + callback: async (win, userAction, maybeShowPromise) => { + info("Check the status of the close button"); + const closeButton = win.document.getElementById("onboardingClose"); + Assert.ok(BrowserTestUtils.is_visible(closeButton)); + Assert.equal(closeButton.getAttribute("title"), "Close"); + + info("Commit the close button"); + userAction(closeButton); + + info("Waiting for maybeShowOnboardingDialog to finish"); + await maybeShowPromise; + }, + variation, + onboardingDialogVersion: JSON.stringify({ + version: 1, + variation: variation.name.toLowerCase(), + }), + onboardingDialogChoice: "close_1", + expectedUserBranchPrefs: { + "quicksuggest.dataCollection.enabled": false, + }, + telemetryEvents: [ + { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "data_collect_toggled", + object: "disabled", + }, + { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "opt_in_dialog", + object: "close_1", + }, + ], + }); +} + +async function onboardingNext(variation) { + await doActionTest({ + callback: async (win, userAction, maybeShowPromise) => { + info("Check the status of the next button"); + const nextButton = win.document.getElementById("onboardingNext"); + Assert.ok(BrowserTestUtils.is_visible(nextButton)); + + info("Commit the next button"); + userAction(nextButton); + + const introductionSection = win.document.getElementById( + "introduction-section" + ); + const mainSection = win.document.getElementById("main-section"); + await BrowserTestUtils.waitForCondition( + () => + BrowserTestUtils.is_hidden(introductionSection) && + BrowserTestUtils.is_visible(mainSection), + "Wait for the transition" + ); + + info("Exit"); + EventUtils.synthesizeKey("KEY_Escape", {}, win); + + info("Waiting for maybeShowOnboardingDialog to finish"); + await maybeShowPromise; + }, + variation, + onboardingDialogVersion: JSON.stringify({ + version: 1, + variation: variation.name.toLowerCase(), + }), + onboardingDialogChoice: "dismiss_2", + expectedUserBranchPrefs: { + "quicksuggest.dataCollection.enabled": false, + }, + telemetryEvents: [ + { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "data_collect_toggled", + object: "disabled", + }, + { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "opt_in_dialog", + object: "dismiss_2", + }, + ], + }); +} + +async function onboardingAccept(variation, skipIntroduction) { + await doActionTest({ + callback: async (win, userAction, maybeShowPromise) => { + info("Check the status of the accept option and submit button"); + const acceptOption = win.document.getElementById("onboardingAccept"); + const submitButton = win.document.getElementById("onboardingSubmit"); + Assert.ok(acceptOption); + Assert.ok(submitButton.disabled); + + info("Select the accept option"); + userAction(acceptOption); + + info("Commit the submit button"); + Assert.ok(!submitButton.disabled); + userAction(submitButton); + + info("Waiting for maybeShowOnboardingDialog to finish"); + await maybeShowPromise; + }, + variation, + skipIntroduction, + onboardingDialogVersion: JSON.stringify({ + version: 1, + variation: variation.name.toLowerCase(), + }), + onboardingDialogChoice: "accept_2", + expectedUserBranchPrefs: { + "quicksuggest.onboardingDialogVersion": JSON.stringify({ version: 1 }), + "quicksuggest.dataCollection.enabled": true, + }, + telemetryEvents: [ + { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "data_collect_toggled", + object: "enabled", + }, + { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "opt_in_dialog", + object: "accept_2", + }, + ], + }); +} + +async function onboardingReject(variation, skipIntroduction) { + await doActionTest({ + callback: async (win, userAction, maybeShowPromise) => { + info("Check the status of the reject option and submit button"); + const rejectOption = win.document.getElementById("onboardingReject"); + const submitButton = win.document.getElementById("onboardingSubmit"); + Assert.ok(rejectOption); + Assert.ok(submitButton.disabled); + + info("Select the reject option"); + userAction(rejectOption); + + info("Commit the submit button"); + Assert.ok(!submitButton.disabled); + userAction(submitButton); + + info("Waiting for maybeShowOnboardingDialog to finish"); + await maybeShowPromise; + }, + variation, + skipIntroduction, + onboardingDialogVersion: JSON.stringify({ + version: 1, + variation: variation.name.toLowerCase(), + }), + onboardingDialogChoice: "reject_2", + expectedUserBranchPrefs: { + "quicksuggest.dataCollection.enabled": false, + }, + telemetryEvents: [ + { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "data_collect_toggled", + object: "disabled", + }, + { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "opt_in_dialog", + object: "reject_2", + }, + ], + }); +} + +async function onboardingSkipLink(variation, skipIntroduction) { + await doActionTest({ + callback: async (win, userAction, maybeShowPromise) => { + info("Check the status of the skip link"); + const skipLink = win.document.getElementById("onboardingSkipLink"); + Assert.ok(BrowserTestUtils.is_visible(skipLink)); + + info("Commit the skip link"); + const tabCount = gBrowser.tabs.length; + userAction(skipLink); + + info("Waiting for maybeShowOnboardingDialog to finish"); + await maybeShowPromise; + + info("Check the current tab status"); + Assert.equal( + gBrowser.currentURI.spec, + "about:blank", + "Nothing loaded in the current tab" + ); + Assert.equal(gBrowser.tabs.length, tabCount, "No news tabs were opened"); + }, + variation, + skipIntroduction, + onboardingDialogVersion: JSON.stringify({ + version: 1, + variation: variation.name.toLowerCase(), + }), + onboardingDialogChoice: "not_now_2", + expectedUserBranchPrefs: { + "quicksuggest.dataCollection.enabled": false, + }, + telemetryEvents: [ + { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "data_collect_toggled", + object: "disabled", + }, + { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "opt_in_dialog", + object: "not_now_2", + }, + ], + }); +} + +async function onboardingLearnMore(variation, skipIntroduction) { + await doLearnMoreTest( + variation, + skipIntroduction, + "onboardingLearnMore", + "learn_more_2" + ); +} + +async function onboardingLearnMoreOnIntroduction(variation, skipIntroduction) { + await doLearnMoreTest( + variation, + skipIntroduction, + "onboardingLearnMoreOnIntroduction", + "learn_more_1" + ); +} + +async function doLearnMoreTest(variation, skipIntroduction, target, telemetry) { + await doActionTest({ + callback: async (win, userAction, maybeShowPromise) => { + info("Check the status of the learn more link"); + const learnMoreLink = win.document.getElementById(target); + Assert.ok(BrowserTestUtils.is_visible(learnMoreLink)); + + info("Commit the learn more link"); + const loadPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + QuickSuggest.HELP_URL + ).then(tab => { + info("Saw new tab"); + return tab; + }); + userAction(learnMoreLink); + + info("Waiting for maybeShowOnboardingDialog to finish"); + await maybeShowPromise; + + info("Waiting for new tab"); + let tab = await loadPromise; + + info("Check the current tab status"); + Assert.equal(gBrowser.selectedTab, tab, "Current tab is the new tab"); + Assert.equal( + gBrowser.currentURI.spec, + QuickSuggest.HELP_URL, + "Current tab is the support page" + ); + BrowserTestUtils.removeTab(tab); + }, + variation, + skipIntroduction, + onboardingDialogChoice: telemetry, + onboardingDialogVersion: JSON.stringify({ + version: 1, + variation: variation.name.toLowerCase(), + }), + expectedUserBranchPrefs: { + "quicksuggest.dataCollection.enabled": false, + }, + telemetryEvents: [ + { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "data_collect_toggled", + object: "disabled", + }, + { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "opt_in_dialog", + object: telemetry, + }, + ], + }); +} + +async function doActionTest({ + variation, + skipIntroduction, + callback, + onboardingDialogVersion, + onboardingDialogChoice, + expectedUserBranchPrefs, + telemetryEvents, +}) { + const userClick = target => { + info("Click on the target"); + target.click(); + }; + const userEnter = target => { + target.focus(); + if (target.type === "radio") { + info("Space on the target"); + EventUtils.synthesizeKey("VK_SPACE", {}, target.ownerGlobal); + } else { + info("Enter on the target"); + EventUtils.synthesizeKey("KEY_Enter", {}, target.ownerGlobal); + } + }; + + for (const userAction of [userClick, userEnter]) { + UrlbarPrefs.clear("quicksuggest.shouldShowOnboardingDialog"); + UrlbarPrefs.clear("quicksuggest.showedOnboardingDialog"); + UrlbarPrefs.clear("quicksuggest.seenRestarts", 0); + + await QuickSuggestTestUtils.withExperiment({ + valueOverrides: { + quickSuggestScenario: "online", + quickSuggestOnboardingDialogVariation: variation.name, + }, + callback: async () => { + await doDialogTest({ + callback: async () => { + info("Calling showOnboardingDialog"); + const { win, maybeShowPromise } = await showOnboardingDialog({ + skipIntroduction, + }); + + await callback(win, userAction, maybeShowPromise); + }, + onboardingDialogVersion, + onboardingDialogChoice, + expectedUserBranchPrefs, + telemetryEvents, + }); + }, + }); + } +} + +async function doDialogTest({ + callback, + onboardingDialogVersion, + onboardingDialogChoice, + telemetryEvents, + expectedUserBranchPrefs, +}) { + setDialogPrereqPrefs(); + + // Set initial prefs on the default branch. + let initialDefaultBranch = OFFLINE_DEFAULT_PREFS; + let originalDefaultBranch = {}; + for (let [name, value] of Object.entries(initialDefaultBranch)) { + originalDefaultBranch = gDefaultBranch.getBoolPref(name); + gDefaultBranch.setBoolPref(name, value); + gUserBranch.clearUserPref(name); + } + + // Setting the prefs just now triggered telemetry events, so clear them + // before calling the callback. + Services.telemetry.clearEvents(); + + // Call the callback, which should trigger the dialog and interact with it. + await BrowserTestUtils.withNewTab("about:blank", async () => { + await callback(); + }); + + // Now check all pref values on the default and user branches. + for (let [name, value] of Object.entries(initialDefaultBranch)) { + Assert.equal( + gDefaultBranch.getBoolPref(name), + value, + "Default-branch value for pref did not change after modal: " + name + ); + + let effectiveValue; + if (name in expectedUserBranchPrefs) { + effectiveValue = expectedUserBranchPrefs[name]; + Assert.equal( + gUserBranch.getBoolPref(name), + effectiveValue, + "User-branch value for pref has expected value: " + name + ); + } else { + effectiveValue = value; + Assert.ok( + !gUserBranch.prefHasUserValue(name), + "User-branch value for pref does not exist: " + name + ); + } + + // For good measure, check the value returned by UrlbarPrefs. + Assert.equal( + UrlbarPrefs.get(name), + effectiveValue, + "Effective value for pref is correct: " + name + ); + } + + Assert.equal( + UrlbarPrefs.get("quicksuggest.onboardingDialogVersion"), + onboardingDialogVersion, + "onboardingDialogVersion" + ); + Assert.equal( + UrlbarPrefs.get("quicksuggest.onboardingDialogChoice"), + onboardingDialogChoice, + "onboardingDialogChoice" + ); + Assert.equal( + TelemetryEnvironment.currentEnvironment.settings.userPrefs[ + "browser.urlbar.quicksuggest.onboardingDialogChoice" + ], + onboardingDialogChoice, + "onboardingDialogChoice is correct in TelemetryEnvironment" + ); + + QuickSuggestTestUtils.assertEvents(telemetryEvents); + + Assert.ok( + UrlbarPrefs.get("quicksuggest.showedOnboardingDialog"), + "quicksuggest.showedOnboardingDialog is true after showing dialog" + ); + + // Clean up. + for (let [name, value] of Object.entries(originalDefaultBranch)) { + gDefaultBranch.setBoolPref(name, value); + } + for (let name of Object.keys(expectedUserBranchPrefs)) { + gUserBranch.clearUserPref(name); + } +} + +/** + * Show onbaording dialog. + * + * @param {object} [options] + * The object options. + * @param {boolean} [options.skipIntroduction] + * If true, return dialog with skipping the introduction section. + * @returns {{ window, maybeShowPromise: Promise }} + * win: window object of the dialog. + * maybeShowPromise: Promise of QuickSuggest.maybeShowOnboardingDialog(). + */ +async function showOnboardingDialog({ skipIntroduction } = {}) { + const dialogPromise = BrowserTestUtils.promiseAlertDialogOpen( + null, + QuickSuggest.ONBOARDING_URI, + { isSubDialog: true } + ); + + const maybeShowPromise = QuickSuggest.maybeShowOnboardingDialog(); + + const win = await dialogPromise; + if (win.document.readyState != "complete") { + await BrowserTestUtils.waitForEvent(win, "load"); + } + + // Wait until all listers on onboarding dialog are ready. + await window._quicksuggestOnboardingReady; + + if (!skipIntroduction) { + return { win, maybeShowPromise }; + } + + // Trigger the transition by pressing Enter on the Next button. + EventUtils.synthesizeKey("KEY_Enter"); + + const introductionSection = win.document.getElementById( + "introduction-section" + ); + const mainSection = win.document.getElementById("main-section"); + + await BrowserTestUtils.waitForCondition( + () => + BrowserTestUtils.is_hidden(introductionSection) && + BrowserTestUtils.is_visible(mainSection) + ); + + return { win, maybeShowPromise }; +} + +/** + * Sets all the required prefs for showing the onboarding dialog except for the + * prefs that are set when the dialog is accepted. + */ +function setDialogPrereqPrefs() { + UrlbarPrefs.set("quicksuggest.shouldShowOnboardingDialog", true); + UrlbarPrefs.set("quicksuggest.showedOnboardingDialog", false); +} + +/** + * This is a real hacky way of determining whether the tab key can move focus. + * Windows and Linux both support it but macOS does not unless full keyboard + * access is enabled, so practically this is only useful on macOS. Gecko seems + * to know whether full keyboard access is enabled because it affects focus in + * Firefox and some code in nsXULElement.cpp and other places mention it, but + * there doesn't seem to be a way to access that information from JS. There is + * `Services.focus.elementIsFocusable`, but it returns true regardless of + * whether full access is enabled. + * + * So what we do here is open the dialog and synthesize a tab key. If the focus + * doesn't change, then we assume moving the focus via the tab key is not + * supported. + * + * Why not just always skip the focus tasks on Mac? Because individual + * developers (like the one writing this comment) may be running macOS with full + * keyboard access enabled and want to actually run the tasks on their machines. + * + * @returns {boolean} + */ +async function canTabMoveFocus() { + if (AppConstants.platform != "macosx") { + return true; + } + + let canMove = false; + await doDialogTest({ + callback: async () => { + const { win, maybeShowPromise } = await showOnboardingDialog({ + skipIntroduction: true, + }); + + let doc = win.document; + doc.getElementById("onboardingAccept").focus(); + EventUtils.synthesizeKey("KEY_Tab"); + + // Whether or not the focus can move to the link. + canMove = doc.activeElement.id === "onboardingLearnMore"; + + EventUtils.synthesizeKey("KEY_Escape"); + await maybeShowPromise; + }, + onboardingDialogVersion: JSON.stringify({ version: 1 }), + onboardingDialogChoice: "dismiss_2", + expectedUserBranchPrefs: { + "quicksuggest.dataCollection.enabled": false, + }, + telemetryEvents: [ + { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "data_collect_toggled", + object: "disabled", + }, + { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "opt_in_dialog", + object: "dismiss_2", + }, + ], + }); + + return canMove; +} diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_dynamicWikipedia.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_dynamicWikipedia.js new file mode 100644 index 0000000000..ece6239953 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_dynamicWikipedia.js @@ -0,0 +1,114 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This file tests primary telemetry for dynamic Wikipedia suggestions. + */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + CONTEXTUAL_SERVICES_PING_TYPES: + "resource:///modules/PartnerLinkAttribution.sys.mjs", +}); + +const { TELEMETRY_SCALARS } = UrlbarProviderQuickSuggest; + +const MERINO_SUGGESTION = { + block_id: 1, + url: "https://example.com/dynamic-wikipedia", + title: "Dynamic Wikipedia suggestion", + click_url: "https://example.com/click", + impression_url: "https://example.com/impression", + advertiser: "dynamic-wikipedia", + provider: "wikipedia", + iab_category: "5 - Education", +}; + +const suggestion_type = "dynamic-wikipedia"; +const match_type = "firefox-suggest"; +const index = 1; +const position = index + 1; + +add_setup(async function () { + await setUpTelemetryTest({ + merinoSuggestions: [MERINO_SUGGESTION], + }); +}); + +add_task(async function () { + await doTelemetryTest({ + index, + suggestion: MERINO_SUGGESTION, + // impression-only + impressionOnly: { + scalars: { + [TELEMETRY_SCALARS.IMPRESSION_DYNAMIC_WIKIPEDIA]: position, + }, + event: { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "engagement", + object: "impression_only", + extra: { + suggestion_type, + match_type, + position: position.toString(), + }, + }, + }, + selectables: { + // click + "urlbarView-row-inner": { + scalars: { + [TELEMETRY_SCALARS.IMPRESSION_DYNAMIC_WIKIPEDIA]: position, + [TELEMETRY_SCALARS.CLICK_DYNAMIC_WIKIPEDIA]: position, + "urlbar.picked.dynamic_wikipedia": index.toString(), + }, + event: { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "engagement", + object: "click", + extra: { + suggestion_type, + match_type, + position: position.toString(), + }, + }, + }, + // block + "urlbarView-button-block": { + scalars: { + [TELEMETRY_SCALARS.IMPRESSION_DYNAMIC_WIKIPEDIA]: position, + [TELEMETRY_SCALARS.BLOCK_DYNAMIC_WIKIPEDIA]: position, + }, + event: { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "engagement", + object: "block", + extra: { + suggestion_type, + match_type, + position: position.toString(), + }, + }, + }, + // help + "urlbarView-button-help": { + scalars: { + [TELEMETRY_SCALARS.IMPRESSION_DYNAMIC_WIKIPEDIA]: position, + [TELEMETRY_SCALARS.HELP_DYNAMIC_WIKIPEDIA]: position, + }, + event: { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "engagement", + object: "help", + extra: { + suggestion_type, + match_type, + position: position.toString(), + }, + }, + }, + }, + }); +}); diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_impressionEdgeCases.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_impressionEdgeCases.js new file mode 100644 index 0000000000..c52d22a886 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_impressionEdgeCases.js @@ -0,0 +1,477 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This file tests abandonment and edge cases related to impressions. + */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + CONTEXTUAL_SERVICES_PING_TYPES: + "resource:///modules/PartnerLinkAttribution.sys.mjs", + UrlbarView: "resource:///modules/UrlbarView.sys.mjs", + sinon: "resource://testing-common/Sinon.sys.mjs", +}); + +const { TELEMETRY_SCALARS } = UrlbarProviderQuickSuggest; + +const REMOTE_SETTINGS_RESULTS = [ + { + id: 1, + url: "https://example.com/sponsored", + title: "Sponsored suggestion", + keywords: ["sponsored"], + click_url: "https://example.com/click", + impression_url: "https://example.com/impression", + advertiser: "testadvertiser", + }, + { + id: 2, + url: "https://example.com/nonsponsored", + title: "Non-sponsored suggestion", + keywords: ["nonsponsored"], + click_url: "https://example.com/click", + impression_url: "https://example.com/impression", + advertiser: "testadvertiser", + iab_category: "5 - Education", + }, +]; + +const SPONSORED_RESULT = REMOTE_SETTINGS_RESULTS[0]; + +// Spy for the custom impression/click sender +let spy; + +add_setup(async function () { + ({ spy } = QuickSuggestTestUtils.createTelemetryPingSpy()); + + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + await UrlbarTestUtils.formHistory.clear(); + + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + + // Add a mock engine so we don't hit the network. + await SearchTestUtils.installSearchExtension({}, { setAsDefault: true }); + + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + remoteSettingsResults: [ + { + type: "data", + attachment: REMOTE_SETTINGS_RESULTS, + }, + ], + }); +}); + +// Makes sure impression telemetry is not recorded when the urlbar engagement is +// abandoned. +add_task(async function abandonment() { + Services.telemetry.clearEvents(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "sponsored", + fireInputEvent: true, + }); + await QuickSuggestTestUtils.assertIsQuickSuggest({ + window, + url: SPONSORED_RESULT.url, + }); + await UrlbarTestUtils.promisePopupClose(window, () => { + gURLBar.blur(); + }); + QuickSuggestTestUtils.assertScalars({}); + QuickSuggestTestUtils.assertEvents([]); + QuickSuggestTestUtils.assertPings(spy, []); +}); + +// Makes sure impression telemetry is not recorded when a quick suggest result +// is not present. +add_task(async function noQuickSuggestResult() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + Services.telemetry.clearEvents(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "noImpression_noQuickSuggestResult", + fireInputEvent: true, + }); + await QuickSuggestTestUtils.assertNoQuickSuggestResults(window); + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Enter"); + }); + QuickSuggestTestUtils.assertScalars({}); + QuickSuggestTestUtils.assertEvents([]); + QuickSuggestTestUtils.assertPings(spy, []); + }); + await PlacesUtils.history.clear(); +}); + +// When a quick suggest result is added to the view but hidden during the view +// update, impression telemetry should not be recorded for it. +add_task(async function hiddenRow() { + Services.telemetry.clearEvents(); + + // Increase the timeout of the remove-stale-rows timer so that it doesn't + // interfere with this task. + let originalRemoveStaleRowsTimeout = UrlbarView.removeStaleRowsTimeout; + UrlbarView.removeStaleRowsTimeout = 30000; + registerCleanupFunction(() => { + UrlbarView.removeStaleRowsTimeout = originalRemoveStaleRowsTimeout; + }); + + // Set up a test provider that doesn't add any results until we resolve its + // `finishQueryPromise`. For the first search below, it will add many search + // suggestions. + let maxCount = UrlbarPrefs.get("maxRichResults"); + let results = []; + for (let i = 0; i < maxCount; i++) { + results.push( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.SEARCH, + UrlbarUtils.RESULT_SOURCE.SEARCH, + { + engine: "Example", + suggestion: "suggestion " + i, + lowerCaseSuggestion: "suggestion " + i, + query: "test", + } + ) + ); + } + let provider = new DelayingTestProvider({ results }); + UrlbarProvidersManager.registerProvider(provider); + + // Open a new tab since we'll load a page below. + let tab = await BrowserTestUtils.openNewForegroundTab({ gBrowser }); + + // Do a normal search and allow the test provider to finish. + provider.finishQueryPromise = Promise.resolve(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + fireInputEvent: true, + }); + + // Sanity check the rows. After the heuristic, the remaining rows should be + // the search results added by the test provider. + Assert.equal( + UrlbarTestUtils.getResultCount(window), + maxCount, + "Row count after first search" + ); + for (let i = 1; i < maxCount; i++) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "Expected result type at index " + i + ); + Assert.equal( + result.source, + UrlbarUtils.RESULT_SOURCE.SEARCH, + "Expected result source at index " + i + ); + } + + // Now set up a second search that triggers a quick suggest result. Add a + // mutation listener to the view so we can tell when the quick suggest row is + // added. + let mutationPromise = new Promise(resolve => { + let observer = new MutationObserver(mutations => { + let rows = UrlbarTestUtils.getResultsContainer(window).children; + for (let row of rows) { + if (row.result.providerName == "UrlbarProviderQuickSuggest") { + observer.disconnect(); + resolve(row); + return; + } + } + }); + observer.observe(UrlbarTestUtils.getResultsContainer(window), { + childList: true, + }); + }); + + // Set the test provider's `finishQueryPromise` to a promise that doesn't + // resolve. That will prevent the search from completing, which will prevent + // the view from removing stale rows and showing the quick suggest row. + let resolveQuery; + provider.finishQueryPromise = new Promise( + resolve => (resolveQuery = resolve) + ); + + // Start the second search but don't wait for it to finish. + gURLBar.focus(); + let queryPromise = UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: REMOTE_SETTINGS_RESULTS[0].keywords[0], + fireInputEvent: true, + }); + + // Wait for the quick suggest row to be added to the view. It should be hidden + // because (a) quick suggest results have a `suggestedIndex`, and rows with + // suggested indexes can't replace rows without suggested indexes, and (b) the + // view already contains the maximum number of rows due to the first search. + // It should remain hidden until the search completes or the remove-stale-rows + // timer fires. Next, we'll hit enter, which will cancel the search and close + // the view, so the row should never appear. + let quickSuggestRow = await mutationPromise; + Assert.ok( + BrowserTestUtils.is_hidden(quickSuggestRow), + "Quick suggest row is hidden" + ); + + // Hit enter to pick the heuristic search result. This will cancel the search + // and notify the quick suggest provider that an engagement occurred. + let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Enter"); + }); + await loadPromise; + + // Resolve the test provider's promise finally. + resolveQuery(); + await queryPromise; + + // The quick suggest provider added a result but it wasn't visible in the + // view. No impression telemetry should be recorded for it. + QuickSuggestTestUtils.assertScalars({}); + QuickSuggestTestUtils.assertEvents([]); + QuickSuggestTestUtils.assertPings(spy, []); + + BrowserTestUtils.removeTab(tab); + UrlbarProvidersManager.unregisterProvider(provider); + UrlbarView.removeStaleRowsTimeout = originalRemoveStaleRowsTimeout; +}); + +// When a quick suggest result has not been added to the view, impression +// telemetry should not be recorded for it even if it's the result most recently +// returned by the provider. +add_task(async function notAddedToView() { + Services.telemetry.clearEvents(); + + // Open a new tab since we'll load a page. + await BrowserTestUtils.withNewTab("about:blank", async () => { + // Do an initial search that doesn't match any suggestions to make sure + // there aren't any quick suggest results in the view to start. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "this doesn't match anything", + fireInputEvent: true, + }); + await QuickSuggestTestUtils.assertNoQuickSuggestResults(window); + await UrlbarTestUtils.promisePopupClose(window); + + // Now do a search for a suggestion and hit enter after the provider adds it + // but before it appears in the view. + await doEngagementWithoutAddingResultToView( + REMOTE_SETTINGS_RESULTS[0].keywords[0] + ); + + // The quick suggest provider added a result but it wasn't visible in the + // view, and no other quick suggest results were visible in the view. No + // impression telemetry should be recorded. + QuickSuggestTestUtils.assertScalars({}); + QuickSuggestTestUtils.assertEvents([]); + QuickSuggestTestUtils.assertPings(spy, []); + }); +}); + +// When a quick suggest result is visible in the view, impression telemetry +// should be recorded for it even if it's not the result most recently returned +// by the provider. +add_task(async function previousResultStillVisible() { + Services.telemetry.clearEvents(); + + // Open a new tab since we'll load a page. + await BrowserTestUtils.withNewTab("about:blank", async () => { + // Do a search for the first suggestion. + let firstSuggestion = REMOTE_SETTINGS_RESULTS[0]; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: firstSuggestion.keywords[0], + fireInputEvent: true, + }); + + let index = 1; + await QuickSuggestTestUtils.assertIsQuickSuggest({ + window, + index, + url: firstSuggestion.url, + }); + + // Without closing the view, do a second search for the second suggestion + // and hit enter after the provider adds it but before it appears in the + // view. + await doEngagementWithoutAddingResultToView( + REMOTE_SETTINGS_RESULTS[1].keywords[0], + index + ); + + // An impression for the first suggestion should be recorded since it's + // still visible in the view, not the second suggestion. + QuickSuggestTestUtils.assertScalars({ + [TELEMETRY_SCALARS.IMPRESSION_SPONSORED]: index + 1, + }); + QuickSuggestTestUtils.assertEvents([ + { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "engagement", + object: "impression_only", + extra: { + match_type: "firefox-suggest", + position: String(index + 1), + suggestion_type: "sponsored", + }, + }, + ]); + QuickSuggestTestUtils.assertPings(spy, [ + { + type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION, + payload: { + improve_suggest_experience_checked: false, + block_id: firstSuggestion.id, + is_clicked: false, + match_type: "firefox-suggest", + position: index + 1, + }, + }, + ]); + }); +}); + +/** + * Does a search that causes the quick suggest provider to return a result + * without adding it to the view and then hits enter to load a SERP and create + * an engagement. + * + * @param {string} searchString + * The search string. + * @param {number} previousResultIndex + * If the view is already open and showing a quick suggest result, pass its + * index here. Otherwise pass -1. + */ +async function doEngagementWithoutAddingResultToView( + searchString, + previousResultIndex = -1 +) { + // Set the timeout of the chunk timer to a really high value so that it will + // not fire. The view updates when the timer fires, which we specifically want + // to avoid here. + let originalChunkDelayMs = UrlbarProvidersManager._chunkResultsDelayMs; + UrlbarProvidersManager._chunkResultsDelayMs = 30000; + registerCleanupFunction(() => { + UrlbarProvidersManager._chunkResultsDelayMs = originalChunkDelayMs; + }); + + // Stub `UrlbarProviderQuickSuggest.getPriority()` to return Infinity. + let sandbox = sinon.createSandbox(); + let getPriorityStub = sandbox.stub(UrlbarProviderQuickSuggest, "getPriority"); + getPriorityStub.returns(Infinity); + + // Spy on `UrlbarProviderQuickSuggest.onEngagement()`. + let onEngagementSpy = sandbox.spy(UrlbarProviderQuickSuggest, "onEngagement"); + + let sandboxCleanup = () => { + getPriorityStub?.restore(); + getPriorityStub = null; + sandbox?.restore(); + sandbox = null; + }; + registerCleanupFunction(sandboxCleanup); + + // In addition to setting the chunk timeout to a large value above, in order + // to prevent the view from updating there also needs to be a heuristic + // provider that takes a long time to add results. Set one up that doesn't add + // any results until we resolve its `finishQueryPromise`. Set its priority to + // Infinity too so that only it and the quick suggest provider will be active. + let provider = new DelayingTestProvider({ + results: [], + priority: Infinity, + type: UrlbarUtils.PROVIDER_TYPE.HEURISTIC, + }); + UrlbarProvidersManager.registerProvider(provider); + + let resolveQuery; + provider.finishQueryPromise = new Promise(r => (resolveQuery = r)); + + // Add a query listener so we can grab the query context. + let context; + let queryListener = { + onQueryStarted: c => (context = c), + }; + gURLBar.controller.addQueryListener(queryListener); + + // Do a search but don't wait for it to finish. + gURLBar.focus(); + UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: searchString, + fireInputEvent: true, + }); + + // Wait for the quick suggest provider to add its result to `context.unsortedResults`. + let result = await TestUtils.waitForCondition(() => { + let query = UrlbarProvidersManager.queries.get(context); + return query?.unsortedResults.find( + r => r.providerName == "UrlbarProviderQuickSuggest" + ); + }, "Waiting for quick suggest result to be added to context.unsortedResults"); + + gURLBar.controller.removeQueryListener(queryListener); + + // The view should not have updated, so the result's `rowIndex` should still + // have its initial value of -1. + Assert.equal(result.rowIndex, -1, "result.rowIndex is still -1"); + + // If there's a result from the previous query, assert it's still in the + // view. Otherwise assume that the view should be closed. These are mostly + // sanity checks because they should only fail if the telemetry assertions + // below also fail. + if (previousResultIndex >= 0) { + let rows = gURLBar.view.panel.querySelector(".urlbarView-results"); + Assert.equal( + rows.children[previousResultIndex].result.providerName, + "UrlbarProviderQuickSuggest", + "Result already in view is a quick suggest" + ); + } else { + Assert.ok(!gURLBar.view.isOpen, "View is closed"); + } + + // Hit enter to load a SERP for the search string. This should notify the + // quick suggest provider that an engagement occurred. + let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Enter"); + }); + await loadPromise; + + let engagementCalls = onEngagementSpy.getCalls().filter(call => { + let state = call.args[1]; + return state == "engagement"; + }); + Assert.equal(engagementCalls.length, 1, "One engagement occurred"); + + // Clean up. + resolveQuery(); + UrlbarProvidersManager.unregisterProvider(provider); + UrlbarProvidersManager._chunkResultsDelayMs = originalChunkDelayMs; + sandboxCleanup(); +} + +/** + * A test provider that doesn't finish `startQuery()` until `finishQueryPromise` + * is resolved. + */ +class DelayingTestProvider extends UrlbarTestUtils.TestProvider { + finishQueryPromise = null; + async startQuery(context, addCallback) { + for (let result of this._results) { + addCallback(this, result); + } + await this.finishQueryPromise; + } +} diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_navigationalSuggestions.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_navigationalSuggestions.js new file mode 100644 index 0000000000..defb9bb76d --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_navigationalSuggestions.js @@ -0,0 +1,353 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This file tests primary telemetry for navigational suggestions, a.k.a. + * navigational top picks. + */ + +"use strict"; + +const { TELEMETRY_SCALARS } = UrlbarProviderQuickSuggest; + +const MERINO_SUGGESTION = { + title: "Navigational suggestion", + url: "https://example.com/navigational-suggestion", + provider: "top_picks", + is_sponsored: false, + score: 0.25, + block_id: 0, + is_top_pick: true, +}; + +const suggestion_type = "navigational"; +const index = 1; +const position = index + 1; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + // `bestMatch.enabled` must be set to show nav suggestions with the best + // match UI treatment. + ["browser.urlbar.bestMatch.enabled", true], + // Disable tab-to-search since like best match it's also shown with + // `suggestedIndex` = 1. + ["browser.urlbar.suggest.engines", false], + ], + }); + + await setUpTelemetryTest({ + merinoSuggestions: [MERINO_SUGGESTION], + }); +}); + +// Clicks the heuristic when a nav suggestion is not matched +add_task(async function notMatched_clickHeuristic() { + await doTest({ + suggestion: null, + shouldBeShown: false, + pickRowIndex: 0, + scalars: { + [TELEMETRY_SCALARS.IMPRESSION_NAV_NOTMATCHED]: "search_engine", + [TELEMETRY_SCALARS.CLICK_NAV_NOTMATCHED]: "search_engine", + }, + events: [], + }); +}); + +// Clicks a non-heuristic row when a nav suggestion is not matched +add_task(async function notMatched_clickOther() { + await PlacesTestUtils.addVisits("http://mochi.test:8888/example"); + await doTest({ + suggestion: null, + shouldBeShown: false, + pickRowIndex: 1, + scalars: { + [TELEMETRY_SCALARS.IMPRESSION_NAV_NOTMATCHED]: "search_engine", + }, + events: [], + }); +}); + +// Clicks the heuristic when a nav suggestion is shown +add_task(async function shown_clickHeuristic() { + await doTest({ + suggestion: MERINO_SUGGESTION, + shouldBeShown: true, + pickRowIndex: 0, + scalars: { + [TELEMETRY_SCALARS.IMPRESSION_NAV_SHOWN]: "search_engine", + [TELEMETRY_SCALARS.CLICK_NAV_SHOWN_HEURISTIC]: "search_engine", + }, + events: [ + { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "engagement", + object: "impression_only", + extra: { + suggestion_type, + match_type: "best-match", + position: position.toString(), + source: "merino", + }, + }, + ], + }); +}); + +// Clicks the nav suggestion +add_task(async function shown_clickNavSuggestion() { + await doTest({ + suggestion: MERINO_SUGGESTION, + shouldBeShown: true, + pickRowIndex: index, + scalars: { + [TELEMETRY_SCALARS.IMPRESSION_NAV_SHOWN]: "search_engine", + [TELEMETRY_SCALARS.CLICK_NAV_SHOWN_NAV]: "search_engine", + "urlbar.picked.navigational": "1", + }, + events: [ + { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "engagement", + object: "click", + extra: { + suggestion_type, + match_type: "best-match", + position: position.toString(), + source: "merino", + }, + }, + ], + }); +}); + +// Clicks a non-heuristic non-nav-suggestion row when the nav suggestion is +// shown +add_task(async function shown_clickOther() { + await PlacesTestUtils.addVisits("http://mochi.test:8888/example"); + await doTest({ + suggestion: MERINO_SUGGESTION, + shouldBeShown: true, + pickRowIndex: 2, + scalars: { + [TELEMETRY_SCALARS.IMPRESSION_NAV_SHOWN]: "search_engine", + }, + events: [ + { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "engagement", + object: "impression_only", + extra: { + suggestion_type, + match_type: "best-match", + position: position.toString(), + source: "merino", + }, + }, + ], + }); +}); + +// Clicks the heuristic when it dupes the nav suggestion +add_task(async function duped_clickHeuristic() { + // Add enough visits to example.com so it autofills. + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits("https://example.com/"); + } + + // Set the nav suggestion's URL to the same URL, example.com. + let suggestion = { + ...MERINO_SUGGESTION, + url: "https://example.com/", + }; + + await doTest({ + suggestion, + shouldBeShown: false, + pickRowIndex: 0, + scalars: { + [TELEMETRY_SCALARS.IMPRESSION_NAV_SUPERCEDED]: "autofill_origin", + [TELEMETRY_SCALARS.CLICK_NAV_SUPERCEDED]: "autofill_origin", + }, + events: [], + }); +}); + +// Clicks a non-heuristic row when the heuristic dupes the nav suggestion +add_task(async function duped_clickOther() { + // Add enough visits to example.com so it autofills. + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits("https://example.com/"); + } + + // Set the nav suggestion's URL to the same URL, example.com. + let suggestion = { + ...MERINO_SUGGESTION, + url: "https://example.com/", + }; + + // Add a visit to another URL so it appears in the search below. + await PlacesTestUtils.addVisits("https://example.com/some-other-url"); + + await doTest({ + suggestion, + shouldBeShown: false, + pickRowIndex: 1, + scalars: { + [TELEMETRY_SCALARS.IMPRESSION_NAV_SUPERCEDED]: "autofill_origin", + }, + events: [], + }); +}); + +// Telemetry specific to nav suggestions should not be recorded when the +// `recordNavigationalSuggestionTelemetry` Nimbus variable is false. +add_task(async function recordNavigationalSuggestionTelemetry_false() { + await doTest({ + valueOverrides: { + recordNavigationalSuggestionTelemetry: false, + }, + suggestion: MERINO_SUGGESTION, + shouldBeShown: true, + pickRowIndex: index, + scalars: {}, + events: [ + // The legacy engagement event should still be recorded as it is for all + // quick suggest results. + { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "engagement", + object: "click", + extra: { + suggestion_type, + match_type: "best-match", + position: position.toString(), + source: "merino", + }, + }, + ], + }); +}); + +// Telemetry specific to nav suggestions should not be recorded when the +// `recordNavigationalSuggestionTelemetry` Nimbus variable is left out. +add_task(async function recordNavigationalSuggestionTelemetry_undefined() { + await doTest({ + valueOverrides: {}, + suggestion: MERINO_SUGGESTION, + shouldBeShown: true, + pickRowIndex: index, + scalars: {}, + events: [ + // The legacy engagement event should still be recorded as it is for all + // quick suggest results. + { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "engagement", + object: "click", + extra: { + suggestion_type, + match_type: "best-match", + position: position.toString(), + source: "merino", + }, + }, + ], + }); +}); + +/** + * Does the following: + * + * 1. Sets up a Merino nav suggestion + * 2. Enrolls in a Nimbus experiment with the specified variables + * 3. Does a search + * 4. Makes sure the nav suggestion is or isn't shown as expected + * 5. Clicks a specified row + * 6. Makes sure the expected telemetry is recorded + * + * @param {object} options + * Options object + * @param {object} options.suggestion + * The nav suggestion or null if Merino shouldn't serve one. + * @param {boolean} options.shouldBeShown + * Whether the nav suggestion is expected to be shown. + * @param {number} options.pickRowIndex + * The index of the row to pick. + * @param {object} options.scalars + * An object that specifies the nav suggest keyed scalars that are expected to + * be recorded. + * @param {Array} options.events + * An object that specifies the legacy engagement events that are expected to + * be recorded. + * @param {object} options.valueOverrides + * The Nimbus variables to use. + */ +async function doTest({ + suggestion, + shouldBeShown, + pickRowIndex, + scalars, + events, + valueOverrides = { + recordNavigationalSuggestionTelemetry: true, + }, +}) { + MerinoTestUtils.server.response.body.suggestions = suggestion + ? [suggestion] + : []; + + Services.telemetry.clearEvents(); + let { spy, spyCleanup } = QuickSuggestTestUtils.createTelemetryPingSpy(); + + await QuickSuggestTestUtils.withExperiment({ + valueOverrides, + callback: async () => { + await BrowserTestUtils.withNewTab("about:blank", async () => { + gURLBar.focus(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "example", + fireInputEvent: true, + }); + + if (shouldBeShown) { + await QuickSuggestTestUtils.assertIsQuickSuggest({ + window, + index, + url: suggestion.url, + isBestMatch: true, + isSponsored: false, + }); + } else { + await QuickSuggestTestUtils.assertNoQuickSuggestResults(window); + } + + let loadPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser + ); + if (pickRowIndex > 0) { + info("Arrowing down to row index " + pickRowIndex); + EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: pickRowIndex }); + } + info("Pressing Enter and waiting for page load"); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + }); + }, + }); + + info("Checking scalars"); + QuickSuggestTestUtils.assertScalars(scalars); + + info("Checking events"); + QuickSuggestTestUtils.assertEvents(events); + + info("Checking pings"); + QuickSuggestTestUtils.assertPings(spy, []); + + await spyCleanup(); + await PlacesUtils.history.clear(); + MerinoTestUtils.server.response.body.suggestions = [MERINO_SUGGESTION]; +} diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_nonsponsored.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_nonsponsored.js new file mode 100644 index 0000000000..c7ddcdd2ce --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_nonsponsored.js @@ -0,0 +1,368 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This file tests primary telemetry for nonsponsored suggestions. + */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + CONTEXTUAL_SERVICES_PING_TYPES: + "resource:///modules/PartnerLinkAttribution.sys.mjs", +}); + +const { TELEMETRY_SCALARS } = UrlbarProviderQuickSuggest; + +const REMOTE_SETTINGS_RESULT = { + id: 1, + url: "https://example.com/nonsponsored", + title: "Non-sponsored suggestion", + keywords: ["nonsponsored"], + click_url: "https://example.com/click", + impression_url: "https://example.com/impression", + advertiser: "testadvertiser", + iab_category: "5 - Education", +}; + +const suggestion_type = "nonsponsored"; +const index = 1; +const position = index + 1; + +add_setup(async function () { + await setUpTelemetryTest({ + remoteSettingsResults: [ + { + type: "data", + attachment: [REMOTE_SETTINGS_RESULT], + }, + ], + }); +}); + +// nonsponsored +add_task(async function nonsponsored() { + let match_type = "firefox-suggest"; + + // Make sure `improve_suggest_experience_checked` is recorded correctly + // depending on the value of the related pref. + for (let improve_suggest_experience_checked of [false, true]) { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "browser.urlbar.quicksuggest.dataCollection.enabled", + improve_suggest_experience_checked, + ], + ], + }); + await doTelemetryTest({ + index, + suggestion: REMOTE_SETTINGS_RESULT, + // impression-only + impressionOnly: { + scalars: { + [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED]: position, + }, + event: { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "engagement", + object: "impression_only", + extra: { + suggestion_type, + match_type, + position: position.toString(), + }, + }, + ping: { + type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION, + payload: { + match_type, + position, + improve_suggest_experience_checked, + is_clicked: false, + block_id: REMOTE_SETTINGS_RESULT.id, + advertiser: REMOTE_SETTINGS_RESULT.advertiser, + }, + }, + }, + selectables: { + // click + "urlbarView-row-inner": { + scalars: { + [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED]: position, + [TELEMETRY_SCALARS.CLICK_NONSPONSORED]: position, + }, + event: { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "engagement", + object: "click", + extra: { + suggestion_type, + match_type, + position: position.toString(), + }, + }, + pings: [ + { + type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION, + payload: { + match_type, + position, + improve_suggest_experience_checked, + is_clicked: true, + block_id: REMOTE_SETTINGS_RESULT.id, + advertiser: REMOTE_SETTINGS_RESULT.advertiser, + }, + }, + { + type: CONTEXTUAL_SERVICES_PING_TYPES.QS_SELECTION, + payload: { + match_type, + position, + improve_suggest_experience_checked, + block_id: REMOTE_SETTINGS_RESULT.id, + advertiser: REMOTE_SETTINGS_RESULT.advertiser, + }, + }, + ], + }, + // block + "urlbarView-button-block": { + scalars: { + [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED]: position, + [TELEMETRY_SCALARS.BLOCK_NONSPONSORED]: position, + }, + event: { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "engagement", + object: "block", + extra: { + suggestion_type, + match_type, + position: position.toString(), + }, + }, + pings: [ + { + type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION, + payload: { + match_type, + position, + improve_suggest_experience_checked, + is_clicked: false, + block_id: REMOTE_SETTINGS_RESULT.id, + advertiser: REMOTE_SETTINGS_RESULT.advertiser, + }, + }, + { + type: CONTEXTUAL_SERVICES_PING_TYPES.QS_BLOCK, + payload: { + match_type, + position, + improve_suggest_experience_checked, + block_id: REMOTE_SETTINGS_RESULT.id, + advertiser: REMOTE_SETTINGS_RESULT.advertiser, + iab_category: REMOTE_SETTINGS_RESULT.iab_category, + }, + }, + ], + }, + // help + "urlbarView-button-help": { + scalars: { + [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED]: position, + [TELEMETRY_SCALARS.HELP_NONSPONSORED]: position, + }, + event: { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "engagement", + object: "help", + extra: { + suggestion_type, + match_type, + position: position.toString(), + }, + }, + pings: [ + { + type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION, + payload: { + match_type, + position, + improve_suggest_experience_checked, + is_clicked: false, + block_id: REMOTE_SETTINGS_RESULT.id, + advertiser: REMOTE_SETTINGS_RESULT.advertiser, + }, + }, + ], + }, + }, + }); + await SpecialPowers.popPrefEnv(); + } +}); + +// nonsponsored best match +add_task(async function nonsponsoredBestMatch() { + let match_type = "best-match"; + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.bestMatch.enabled", true]], + }); + await QuickSuggestTestUtils.setConfig( + QuickSuggestTestUtils.BEST_MATCH_CONFIG + ); + await doTelemetryTest({ + index, + suggestion: REMOTE_SETTINGS_RESULT, + // impression-only + impressionOnly: { + scalars: { + [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED]: position, + [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED_BEST_MATCH]: position, + }, + event: { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "engagement", + object: "impression_only", + extra: { + suggestion_type, + match_type, + position: position.toString(), + }, + }, + ping: { + type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION, + payload: { + match_type, + position, + is_clicked: false, + improve_suggest_experience_checked: false, + block_id: REMOTE_SETTINGS_RESULT.id, + advertiser: REMOTE_SETTINGS_RESULT.advertiser, + }, + }, + }, + selectables: { + // click + "urlbarView-row-inner": { + scalars: { + [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED]: position, + [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED_BEST_MATCH]: position, + [TELEMETRY_SCALARS.CLICK_NONSPONSORED]: position, + [TELEMETRY_SCALARS.CLICK_NONSPONSORED_BEST_MATCH]: position, + }, + event: { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "engagement", + object: "click", + extra: { + suggestion_type, + match_type, + position: position.toString(), + }, + }, + pings: [ + { + type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION, + payload: { + match_type, + position, + is_clicked: true, + improve_suggest_experience_checked: false, + block_id: REMOTE_SETTINGS_RESULT.id, + advertiser: REMOTE_SETTINGS_RESULT.advertiser, + }, + }, + { + type: CONTEXTUAL_SERVICES_PING_TYPES.QS_SELECTION, + payload: { + match_type, + position, + improve_suggest_experience_checked: false, + block_id: REMOTE_SETTINGS_RESULT.id, + advertiser: REMOTE_SETTINGS_RESULT.advertiser, + }, + }, + ], + }, + // block + "urlbarView-button-block": { + scalars: { + [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED]: position, + [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED_BEST_MATCH]: position, + [TELEMETRY_SCALARS.BLOCK_NONSPONSORED]: position, + [TELEMETRY_SCALARS.BLOCK_NONSPONSORED_BEST_MATCH]: position, + }, + event: { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "engagement", + object: "block", + extra: { + suggestion_type, + match_type, + position: position.toString(), + }, + }, + pings: [ + { + type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION, + payload: { + match_type, + position, + is_clicked: false, + improve_suggest_experience_checked: false, + block_id: REMOTE_SETTINGS_RESULT.id, + advertiser: REMOTE_SETTINGS_RESULT.advertiser, + }, + }, + { + type: CONTEXTUAL_SERVICES_PING_TYPES.QS_BLOCK, + payload: { + match_type, + position, + improve_suggest_experience_checked: false, + block_id: REMOTE_SETTINGS_RESULT.id, + advertiser: REMOTE_SETTINGS_RESULT.advertiser, + iab_category: REMOTE_SETTINGS_RESULT.iab_category, + }, + }, + ], + }, + // help + "urlbarView-button-help": { + scalars: { + [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED]: position, + [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED_BEST_MATCH]: position, + [TELEMETRY_SCALARS.HELP_NONSPONSORED]: position, + [TELEMETRY_SCALARS.HELP_NONSPONSORED_BEST_MATCH]: position, + }, + event: { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "engagement", + object: "help", + extra: { + suggestion_type, + match_type, + position: position.toString(), + }, + }, + pings: [ + { + type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION, + payload: { + match_type, + position, + is_clicked: false, + improve_suggest_experience_checked: false, + block_id: REMOTE_SETTINGS_RESULT.id, + advertiser: REMOTE_SETTINGS_RESULT.advertiser, + }, + }, + ], + }, + }, + }); + await QuickSuggestTestUtils.setConfig(QuickSuggestTestUtils.DEFAULT_CONFIG); + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_other.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_other.js new file mode 100644 index 0000000000..d151ca81ad --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_other.js @@ -0,0 +1,409 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This file tests ancillary quick suggest telemetry, i.e., telemetry that's not + * strongly related to showing suggestions in the urlbar. + */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.sys.mjs", +}); + +const REMOTE_SETTINGS_RESULTS = [ + { + id: 1, + url: "https://example.com/sponsored", + title: "Sponsored suggestion", + keywords: ["sponsored"], + click_url: "https://example.com/click", + impression_url: "https://example.com/impression", + advertiser: "testadvertiser", + }, +]; + +add_setup(async function () { + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + await UrlbarTestUtils.formHistory.clear(); + + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + + // Add a mock engine so we don't hit the network. + await SearchTestUtils.installSearchExtension({}, { setAsDefault: true }); + + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + remoteSettingsResults: [ + { + type: "data", + attachment: REMOTE_SETTINGS_RESULTS, + }, + ], + }); +}); + +// Tests telemetry recorded when toggling the +// `suggest.quicksuggest.nonsponsored` pref: +// * contextservices.quicksuggest enable_toggled event telemetry +// * TelemetryEnvironment +add_task(async function enableToggled() { + Services.telemetry.clearEvents(); + + // Toggle the suggest.quicksuggest.nonsponsored pref twice. We should get two + // events. + let enabled = UrlbarPrefs.get("suggest.quicksuggest.nonsponsored"); + for (let i = 0; i < 2; i++) { + enabled = !enabled; + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", enabled); + QuickSuggestTestUtils.assertEvents([ + { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "enable_toggled", + object: enabled ? "enabled" : "disabled", + }, + ]); + Assert.equal( + TelemetryEnvironment.currentEnvironment.settings.userPrefs[ + "browser.urlbar.suggest.quicksuggest.nonsponsored" + ], + enabled, + "suggest.quicksuggest.nonsponsored is correct in TelemetryEnvironment" + ); + } + + // Set the main quicksuggest.enabled pref to false and toggle the + // suggest.quicksuggest.nonsponsored pref again. We shouldn't get any events. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.quicksuggest.enabled", false]], + }); + enabled = !enabled; + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", enabled); + QuickSuggestTestUtils.assertEvents([]); + await SpecialPowers.popPrefEnv(); + + // Set the pref back to what it was at the start of the task. + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", !enabled); +}); + +// Tests telemetry recorded when toggling the `suggest.quicksuggest.sponsored` +// pref: +// * contextservices.quicksuggest enable_toggled event telemetry +// * TelemetryEnvironment +add_task(async function sponsoredToggled() { + Services.telemetry.clearEvents(); + + // Toggle the suggest.quicksuggest.sponsored pref twice. We should get two + // events. + let enabled = UrlbarPrefs.get("suggest.quicksuggest.sponsored"); + for (let i = 0; i < 2; i++) { + enabled = !enabled; + UrlbarPrefs.set("suggest.quicksuggest.sponsored", enabled); + QuickSuggestTestUtils.assertEvents([ + { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "sponsored_toggled", + object: enabled ? "enabled" : "disabled", + }, + ]); + Assert.equal( + TelemetryEnvironment.currentEnvironment.settings.userPrefs[ + "browser.urlbar.suggest.quicksuggest.sponsored" + ], + enabled, + "suggest.quicksuggest.sponsored is correct in TelemetryEnvironment" + ); + } + + // Set the main quicksuggest.enabled pref to false and toggle the + // suggest.quicksuggest.sponsored pref again. We shouldn't get any events. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.quicksuggest.enabled", false]], + }); + enabled = !enabled; + UrlbarPrefs.set("suggest.quicksuggest.sponsored", enabled); + QuickSuggestTestUtils.assertEvents([]); + await SpecialPowers.popPrefEnv(); + + // Set the pref back to what it was at the start of the task. + UrlbarPrefs.set("suggest.quicksuggest.sponsored", !enabled); +}); + +// Tests telemetry recorded when toggling the +// `quicksuggest.dataCollection.enabled` pref: +// * contextservices.quicksuggest data_collect_toggled event telemetry +// * TelemetryEnvironment +add_task(async function dataCollectionToggled() { + Services.telemetry.clearEvents(); + + // Toggle the quicksuggest.dataCollection.enabled pref twice. We should get + // two events. + let enabled = UrlbarPrefs.get("quicksuggest.dataCollection.enabled"); + for (let i = 0; i < 2; i++) { + enabled = !enabled; + UrlbarPrefs.set("quicksuggest.dataCollection.enabled", enabled); + QuickSuggestTestUtils.assertEvents([ + { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "data_collect_toggled", + object: enabled ? "enabled" : "disabled", + }, + ]); + Assert.equal( + TelemetryEnvironment.currentEnvironment.settings.userPrefs[ + "browser.urlbar.quicksuggest.dataCollection.enabled" + ], + enabled, + "quicksuggest.dataCollection.enabled is correct in TelemetryEnvironment" + ); + } + + // Set the main quicksuggest.enabled pref to false and toggle the data + // collection pref again. We shouldn't get any events. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.quicksuggest.enabled", false]], + }); + enabled = !enabled; + UrlbarPrefs.set("quicksuggest.dataCollection.enabled", enabled); + QuickSuggestTestUtils.assertEvents([]); + await SpecialPowers.popPrefEnv(); + + // Set the pref back to what it was at the start of the task. + UrlbarPrefs.set("quicksuggest.dataCollection.enabled", !enabled); +}); + +// Tests telemetry recorded when clicking the checkbox for best match in +// preferences UI. The telemetry will be stored as following keyed scalar. +// scalar: browser.ui.interaction.preferences_panePrivacy +// key: firefoxSuggestBestMatch +add_task(async function bestmatchCheckbox() { + // Set the initial enabled status. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.bestMatch.enabled", true]], + }); + + // Open preferences page for best match. + await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:preferences#privacy", + true + ); + + for (let i = 0; i < 2; i++) { + Services.telemetry.clearScalars(); + + // Click on the checkbox. + const doc = gBrowser.selectedBrowser.contentDocument; + const checkboxId = "firefoxSuggestBestMatch"; + const checkbox = doc.getElementById(checkboxId); + checkbox.scrollIntoView(); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#" + checkboxId, + {}, + gBrowser.selectedBrowser + ); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "browser.ui.interaction.preferences_panePrivacy", + checkboxId, + 1 + ); + } + + // Clean up. + gBrowser.removeCurrentTab(); + await SpecialPowers.popPrefEnv(); +}); + +// Tests telemetry recorded when opening the learn more link for best match in +// the preferences UI. The telemetry will be stored as following keyed scalar. +// scalar: browser.ui.interaction.preferences_panePrivacy +// key: firefoxSuggestBestMatchLearnMore +add_task(async function bestmatchLearnMore() { + // Set the initial enabled status. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.bestMatch.enabled", true]], + }); + + // Open preferences page for best match. + await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:preferences#privacy", + true + ); + + // Click on the learn more link. + Services.telemetry.clearScalars(); + const learnMoreLinkId = "firefoxSuggestBestMatchLearnMore"; + const doc = gBrowser.selectedBrowser.contentDocument; + const link = doc.getElementById(learnMoreLinkId); + link.scrollIntoView(); + const onLearnMoreOpenedByClick = BrowserTestUtils.waitForNewTab( + gBrowser, + QuickSuggest.HELP_URL + ); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#" + learnMoreLinkId, + {}, + gBrowser.selectedBrowser + ); + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "browser.ui.interaction.preferences_panePrivacy", + "firefoxSuggestBestMatchLearnMore", + 1 + ); + await onLearnMoreOpenedByClick; + gBrowser.removeCurrentTab(); + + // Type enter key on the learm more link. + Services.telemetry.clearScalars(); + link.focus(); + const onLearnMoreOpenedByKey = BrowserTestUtils.waitForNewTab( + gBrowser, + QuickSuggest.HELP_URL + ); + await BrowserTestUtils.synthesizeKey( + "KEY_Enter", + {}, + gBrowser.selectedBrowser + ); + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "browser.ui.interaction.preferences_panePrivacy", + "firefoxSuggestBestMatchLearnMore", + 1 + ); + await onLearnMoreOpenedByKey; + gBrowser.removeCurrentTab(); + + // Clean up. + gBrowser.removeCurrentTab(); + await SpecialPowers.popPrefEnv(); +}); + +// Simulates the race on startup between telemetry environment initialization +// and the initial update of the Suggest scenario. After startup is done, +// telemetry environment should record the correct values for startup prefs. +add_task(async function telemetryEnvironmentOnStartup() { + await QuickSuggestTestUtils.setScenario(null); + + // Restart telemetry environment so we know it's watching its default set of + // prefs. + await TelemetryEnvironment.testCleanRestart().onInitialized(); + + // Get the prefs that UrlbarPrefs sets when the Suggest scenario is updated on + // startup. They're the union of the prefs exposed in the UI and the prefs + // that are set on the default branch per scenario. + let prefs = [ + ...new Set([ + ...Object.values(UrlbarPrefs.FIREFOX_SUGGEST_UI_PREFS_BY_VARIABLE), + ...Object.values(UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS) + .map(valuesByPrefName => Object.keys(valuesByPrefName)) + .flat(), + ]), + ]; + + // Not all of the prefs are recorded in telemetry environment. Filter in the + // ones that are. + prefs = prefs.filter( + p => + `browser.urlbar.${p}` in + TelemetryEnvironment.currentEnvironment.settings.userPrefs + ); + + info("Got startup prefs: " + JSON.stringify(prefs)); + + // Sanity check the expected prefs. This isn't strictly necessary since we + // programmatically get the prefs above, but it's an extra layer of defense, + // for example in case we accidentally filtered out some expected prefs above. + // If this fails, you might have added a startup pref but didn't update this + // array here. + Assert.deepEqual( + prefs.sort(), + [ + "quicksuggest.dataCollection.enabled", + "suggest.quicksuggest.nonsponsored", + "suggest.quicksuggest.sponsored", + ], + "Expected startup prefs" + ); + + // Make sure the prefs don't have user values that would mask the default + // values. + for (let p of prefs) { + UrlbarPrefs.clear(p); + } + + // Build a map of default values. + let defaultValues = Object.fromEntries( + prefs.map(p => [p, UrlbarPrefs.get(p)]) + ); + + // Now simulate startup. Restart telemetry environment but don't wait for it + // to finish before calling `updateFirefoxSuggestScenario()`. This simulates + // startup where telemetry environment's initialization races the intial + // update of the Suggest scenario. + let environmentInitPromise = + TelemetryEnvironment.testCleanRestart().onInitialized(); + + // Update the scenario and force the startup prefs to take on values that are + // the inverse of what they are now. + await UrlbarPrefs.updateFirefoxSuggestScenario({ + isStartup: true, + scenario: "online", + defaultPrefs: { + online: Object.fromEntries( + Object.entries(defaultValues).map(([p, value]) => [p, !value]) + ), + }, + }); + + // At this point telemetry environment should be done initializing since + // `updateFirefoxSuggestScenario()` waits for it, but await our promise now. + await environmentInitPromise; + + // TelemetryEnvironment should have cached the new values. + for (let [p, value] of Object.entries(defaultValues)) { + let expected = !value; + Assert.strictEqual( + TelemetryEnvironment.currentEnvironment.settings.userPrefs[ + `browser.urlbar.${p}` + ], + expected, + `Check 1: ${p} is ${expected} in TelemetryEnvironment` + ); + } + + // Simulate another startup and set all prefs back to their original default + // values. + environmentInitPromise = + TelemetryEnvironment.testCleanRestart().onInitialized(); + + await UrlbarPrefs.updateFirefoxSuggestScenario({ + isStartup: true, + scenario: "online", + defaultPrefs: { + online: defaultValues, + }, + }); + + await environmentInitPromise; + + // TelemetryEnvironment should have cached the new (original) values. + for (let [p, value] of Object.entries(defaultValues)) { + let expected = value; + Assert.strictEqual( + TelemetryEnvironment.currentEnvironment.settings.userPrefs[ + `browser.urlbar.${p}` + ], + expected, + `Check 2: ${p} is ${expected} in TelemetryEnvironment` + ); + } + + await TelemetryEnvironment.testCleanRestart().onInitialized(); +}); diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_sponsored.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_sponsored.js new file mode 100644 index 0000000000..498b942a12 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_sponsored.js @@ -0,0 +1,367 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This file tests primary telemetry for sponsored suggestions. + */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + CONTEXTUAL_SERVICES_PING_TYPES: + "resource:///modules/PartnerLinkAttribution.sys.mjs", +}); + +const { TELEMETRY_SCALARS } = UrlbarProviderQuickSuggest; + +const REMOTE_SETTINGS_RESULT = { + id: 1, + url: "https://example.com/sponsored", + title: "Sponsored suggestion", + keywords: ["sponsored"], + click_url: "https://example.com/click", + impression_url: "https://example.com/impression", + advertiser: "testadvertiser", +}; + +const suggestion_type = "sponsored"; +const index = 1; +const position = index + 1; + +add_setup(async function () { + await setUpTelemetryTest({ + remoteSettingsResults: [ + { + type: "data", + attachment: [REMOTE_SETTINGS_RESULT], + }, + ], + }); +}); + +// sponsored +add_task(async function sponsored() { + let match_type = "firefox-suggest"; + + // Make sure `improve_suggest_experience_checked` is recorded correctly + // depending on the value of the related pref. + for (let improve_suggest_experience_checked of [false, true]) { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "browser.urlbar.quicksuggest.dataCollection.enabled", + improve_suggest_experience_checked, + ], + ], + }); + await doTelemetryTest({ + index, + suggestion: REMOTE_SETTINGS_RESULT, + // impression-only + impressionOnly: { + scalars: { + [TELEMETRY_SCALARS.IMPRESSION_SPONSORED]: position, + }, + event: { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "engagement", + object: "impression_only", + extra: { + suggestion_type, + match_type, + position: position.toString(), + }, + }, + ping: { + type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION, + payload: { + match_type, + position, + improve_suggest_experience_checked, + is_clicked: false, + block_id: REMOTE_SETTINGS_RESULT.id, + advertiser: REMOTE_SETTINGS_RESULT.advertiser, + }, + }, + }, + selectables: { + // click + "urlbarView-row-inner": { + scalars: { + [TELEMETRY_SCALARS.IMPRESSION_SPONSORED]: position, + [TELEMETRY_SCALARS.CLICK_SPONSORED]: position, + }, + event: { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "engagement", + object: "click", + extra: { + suggestion_type, + match_type, + position: position.toString(), + }, + }, + pings: [ + { + type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION, + payload: { + match_type, + position, + improve_suggest_experience_checked, + is_clicked: true, + block_id: REMOTE_SETTINGS_RESULT.id, + advertiser: REMOTE_SETTINGS_RESULT.advertiser, + }, + }, + { + type: CONTEXTUAL_SERVICES_PING_TYPES.QS_SELECTION, + payload: { + match_type, + position, + improve_suggest_experience_checked, + block_id: REMOTE_SETTINGS_RESULT.id, + advertiser: REMOTE_SETTINGS_RESULT.advertiser, + }, + }, + ], + }, + // block + "urlbarView-button-block": { + scalars: { + [TELEMETRY_SCALARS.IMPRESSION_SPONSORED]: position, + [TELEMETRY_SCALARS.BLOCK_SPONSORED]: position, + }, + event: { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "engagement", + object: "block", + extra: { + suggestion_type, + match_type, + position: position.toString(), + }, + }, + pings: [ + { + type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION, + payload: { + match_type, + position, + improve_suggest_experience_checked, + is_clicked: false, + block_id: REMOTE_SETTINGS_RESULT.id, + advertiser: REMOTE_SETTINGS_RESULT.advertiser, + }, + }, + { + type: CONTEXTUAL_SERVICES_PING_TYPES.QS_BLOCK, + payload: { + match_type, + position, + improve_suggest_experience_checked, + block_id: REMOTE_SETTINGS_RESULT.id, + advertiser: REMOTE_SETTINGS_RESULT.advertiser, + iab_category: REMOTE_SETTINGS_RESULT.iab_category, + }, + }, + ], + }, + // help + "urlbarView-button-help": { + scalars: { + [TELEMETRY_SCALARS.IMPRESSION_SPONSORED]: position, + [TELEMETRY_SCALARS.HELP_SPONSORED]: position, + }, + event: { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "engagement", + object: "help", + extra: { + suggestion_type, + match_type, + position: position.toString(), + }, + }, + pings: [ + { + type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION, + payload: { + match_type, + position, + improve_suggest_experience_checked, + is_clicked: false, + block_id: REMOTE_SETTINGS_RESULT.id, + advertiser: REMOTE_SETTINGS_RESULT.advertiser, + }, + }, + ], + }, + }, + }); + await SpecialPowers.popPrefEnv(); + } +}); + +// sponsored best match +add_task(async function sponsoredBestMatch() { + let match_type = "best-match"; + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.bestMatch.enabled", true]], + }); + await QuickSuggestTestUtils.setConfig( + QuickSuggestTestUtils.BEST_MATCH_CONFIG + ); + await doTelemetryTest({ + index, + suggestion: REMOTE_SETTINGS_RESULT, + // impression-only + impressionOnly: { + scalars: { + [TELEMETRY_SCALARS.IMPRESSION_SPONSORED]: position, + [TELEMETRY_SCALARS.IMPRESSION_SPONSORED_BEST_MATCH]: position, + }, + event: { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "engagement", + object: "impression_only", + extra: { + suggestion_type, + match_type, + position: position.toString(), + }, + }, + ping: { + type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION, + payload: { + match_type, + position, + is_clicked: false, + improve_suggest_experience_checked: false, + block_id: REMOTE_SETTINGS_RESULT.id, + advertiser: REMOTE_SETTINGS_RESULT.advertiser, + }, + }, + }, + selectables: { + // click + "urlbarView-row-inner": { + scalars: { + [TELEMETRY_SCALARS.IMPRESSION_SPONSORED]: position, + [TELEMETRY_SCALARS.IMPRESSION_SPONSORED_BEST_MATCH]: position, + [TELEMETRY_SCALARS.CLICK_SPONSORED]: position, + [TELEMETRY_SCALARS.CLICK_SPONSORED_BEST_MATCH]: position, + }, + event: { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "engagement", + object: "click", + extra: { + suggestion_type, + match_type, + position: position.toString(), + }, + }, + pings: [ + { + type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION, + payload: { + match_type, + position, + is_clicked: true, + improve_suggest_experience_checked: false, + block_id: REMOTE_SETTINGS_RESULT.id, + advertiser: REMOTE_SETTINGS_RESULT.advertiser, + }, + }, + { + type: CONTEXTUAL_SERVICES_PING_TYPES.QS_SELECTION, + payload: { + match_type, + position, + improve_suggest_experience_checked: false, + block_id: REMOTE_SETTINGS_RESULT.id, + advertiser: REMOTE_SETTINGS_RESULT.advertiser, + }, + }, + ], + }, + // block + "urlbarView-button-block": { + scalars: { + [TELEMETRY_SCALARS.IMPRESSION_SPONSORED]: position, + [TELEMETRY_SCALARS.IMPRESSION_SPONSORED_BEST_MATCH]: position, + [TELEMETRY_SCALARS.BLOCK_SPONSORED]: position, + [TELEMETRY_SCALARS.BLOCK_SPONSORED_BEST_MATCH]: position, + }, + event: { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "engagement", + object: "block", + extra: { + suggestion_type, + match_type, + position: position.toString(), + }, + }, + pings: [ + { + type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION, + payload: { + match_type, + position, + is_clicked: false, + improve_suggest_experience_checked: false, + block_id: REMOTE_SETTINGS_RESULT.id, + advertiser: REMOTE_SETTINGS_RESULT.advertiser, + }, + }, + { + type: CONTEXTUAL_SERVICES_PING_TYPES.QS_BLOCK, + payload: { + match_type, + position, + improve_suggest_experience_checked: false, + block_id: REMOTE_SETTINGS_RESULT.id, + advertiser: REMOTE_SETTINGS_RESULT.advertiser, + iab_category: REMOTE_SETTINGS_RESULT.iab_category, + }, + }, + ], + }, + // help + "urlbarView-button-help": { + scalars: { + [TELEMETRY_SCALARS.IMPRESSION_SPONSORED]: position, + [TELEMETRY_SCALARS.IMPRESSION_SPONSORED_BEST_MATCH]: position, + [TELEMETRY_SCALARS.HELP_SPONSORED]: position, + [TELEMETRY_SCALARS.HELP_SPONSORED_BEST_MATCH]: position, + }, + event: { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "engagement", + object: "help", + extra: { + suggestion_type, + match_type, + position: position.toString(), + }, + }, + pings: [ + { + type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION, + payload: { + match_type, + position, + is_clicked: false, + improve_suggest_experience_checked: false, + block_id: REMOTE_SETTINGS_RESULT.id, + advertiser: REMOTE_SETTINGS_RESULT.advertiser, + }, + }, + ], + }, + }, + }); + await QuickSuggestTestUtils.setConfig(QuickSuggestTestUtils.DEFAULT_CONFIG); + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_weather.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_weather.js new file mode 100644 index 0000000000..b60fa9fe85 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_weather.js @@ -0,0 +1,152 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This file tests primary telemetry for weather suggestions. + */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarProviderWeather: "resource:///modules/UrlbarProviderWeather.sys.mjs", +}); + +const suggestion_type = "weather"; +const match_type = "firefox-suggest"; +const index = 1; +const position = index + 1; + +const { TELEMETRY_SCALARS: WEATHER_SCALARS } = UrlbarProviderWeather; +const { WEATHER_SUGGESTION: suggestion, WEATHER_RS_DATA } = MerinoTestUtils; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + // Make sure quick actions are disabled because showing them in the top + // sites view interferes with this test. + ["browser.urlbar.suggest.quickactions", false], + ], + }); + + await setUpTelemetryTest({ + suggestions: [], + remoteSettingsResults: [ + { + type: "weather", + weather: WEATHER_RS_DATA, + }, + ], + }); + await MerinoTestUtils.initWeather(); + await updateTopSitesAndAwaitChanged(); +}); + +add_task(async function () { + await doTelemetryTest({ + index, + suggestion, + providerName: UrlbarProviderWeather.name, + showSuggestion: async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: MerinoTestUtils.WEATHER_KEYWORD, + }); + }, + teardown: async () => { + // Picking the block button sets this pref to false and disables weather + // suggestions. We need to flip it back to true and wait for the + // suggestion to be fetched again before continuing to the next selectable + // test. The view also also stay open, so close it afterward. + if (!UrlbarPrefs.get("suggest.weather")) { + await UrlbarTestUtils.promisePopupClose(window); + gURLBar.handleRevert(); + let fetchPromise = QuickSuggest.weather.waitForFetches(); + UrlbarPrefs.clear("suggest.weather"); + await fetchPromise; + } + }, + // impression-only + impressionOnly: { + scalars: { + [WEATHER_SCALARS.IMPRESSION]: position, + }, + event: { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "engagement", + object: "impression_only", + extra: { + suggestion_type, + match_type, + position: position.toString(), + }, + }, + }, + selectables: { + // click + "urlbarView-row-inner": { + scalars: { + [WEATHER_SCALARS.IMPRESSION]: position, + [WEATHER_SCALARS.CLICK]: position, + }, + event: { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "engagement", + object: "click", + extra: { + suggestion_type, + match_type, + position: position.toString(), + }, + }, + }, + // block + "urlbarView-button-block": { + scalars: { + [WEATHER_SCALARS.IMPRESSION]: position, + [WEATHER_SCALARS.BLOCK]: position, + }, + event: { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "engagement", + object: "block", + extra: { + suggestion_type, + match_type, + position: position.toString(), + }, + }, + }, + // help + "urlbarView-button-help": { + scalars: { + [WEATHER_SCALARS.IMPRESSION]: position, + [WEATHER_SCALARS.HELP]: position, + }, + event: { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "engagement", + object: "help", + extra: { + suggestion_type, + match_type, + position: position.toString(), + }, + }, + }, + }, + }); +}); + +async function updateTopSitesAndAwaitChanged() { + let url = "http://mochi.test:8888/topsite"; + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits(url); + } + + info("Updating top sites and awaiting newtab-top-sites-changed"); + let changedPromise = TestUtils.topicObserved("newtab-top-sites-changed").then( + () => info("Observed newtab-top-sites-changed") + ); + await updateTopSites(sites => sites?.length); + await changedPromise; +} diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_weather.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_weather.js new file mode 100644 index 0000000000..049b25dd09 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_weather.js @@ -0,0 +1,379 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Browser test for the weather suggestion. + */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarProviderWeather: "resource:///modules/UrlbarProviderWeather.sys.mjs", +}); + +add_setup(async function () { + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + remoteSettingsResults: [ + { + type: "weather", + weather: MerinoTestUtils.WEATHER_RS_DATA, + }, + ], + }); + await MerinoTestUtils.initWeather(); +}); + +// This test ensures the browser navigates to the weather webpage after +// the weather result is selected. +add_task(async function test_weather_result_selection() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: MerinoTestUtils.WEATHER_KEYWORD, + }); + + info(`Select the weather result`); + EventUtils.synthesizeKey("KEY_ArrowDown"); + + info(`Navigate to the weather url`); + EventUtils.synthesizeKey("KEY_Enter"); + + await browserLoadedPromise; + + Assert.equal( + gBrowser.currentURI.spec, + "https://example.com/weather", + "Assert the page navigated to the weather webpage after selecting the weather result." + ); + + BrowserTestUtils.removeTab(tab); + await PlacesUtils.history.clear(); +}); + +// Does a search, clicks the "Show less frequently" result menu command, and +// repeats both steps until the min keyword length cap is reached. +add_task(async function showLessFrequentlyCapReached_manySearches() { + // Set up a min keyword length and cap. + await QuickSuggestTestUtils.setRemoteSettingsResults([ + { + type: "weather", + weather: { + keywords: ["weather"], + min_keyword_length: 3, + min_keyword_length_cap: 4, + }, + }, + ]); + + // Trigger the suggestion. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "wea", + }); + + let resultIndex = 1; + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex); + Assert.equal( + details.result.providerName, + UrlbarProviderWeather.name, + "Weather suggestion should be present at expected index after 'wea' search" + ); + + // Click the command. + let command = "show_less_frequently"; + await UrlbarTestUtils.openResultMenuAndClickItem(window, command, { + resultIndex, + }); + + Assert.ok( + gURLBar.view.isOpen, + "The view should remain open clicking the command" + ); + Assert.ok( + details.element.row.hasAttribute("feedback-acknowledgment"), + "Row should have feedback acknowledgment after clicking command" + ); + Assert.equal( + UrlbarPrefs.get("weather.minKeywordLength"), + 4, + "weather.minKeywordLength should be incremented once" + ); + + // Do the same search again. The suggestion should not appear. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "wea", + }); + + for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) { + details = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + Assert.notEqual( + details.result.providerName, + UrlbarProviderWeather.name, + `Weather suggestion should be absent (checking index ${i})` + ); + } + + // Do a search using one more character. The suggestion should appear. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "weat", + }); + + details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex); + Assert.equal( + details.result.providerName, + UrlbarProviderWeather.name, + "Weather suggestion should be present at expected index after 'weat' search" + ); + Assert.ok( + !details.element.row.hasAttribute("feedback-acknowledgment"), + "Row should not have feedback acknowledgment after 'weat' search" + ); + + // Since the cap has been reached, the command should no longer appear in the + // result menu. + await UrlbarTestUtils.openResultMenu(window, { resultIndex }); + let menuitem = gURLBar.view.resultMenu.querySelector( + `menuitem[data-command=${command}]` + ); + Assert.ok(!menuitem, "Menuitem should be absent"); + gURLBar.view.resultMenu.hidePopup(true); + + await UrlbarTestUtils.promisePopupClose(window); + await QuickSuggestTestUtils.setRemoteSettingsResults([ + { + type: "weather", + weather: MerinoTestUtils.WEATHER_RS_DATA, + }, + ]); + UrlbarPrefs.clear("weather.minKeywordLength"); +}); + +// Repeatedly clicks the "Show less frequently" result menu command after doing +// a single search until the min keyword length cap is reached. +add_task(async function showLessFrequentlyCapReached_oneSearch() { + // Set up a min keyword length and cap. + await QuickSuggestTestUtils.setRemoteSettingsResults([ + { + type: "weather", + weather: { + keywords: ["weather"], + min_keyword_length: 3, + min_keyword_length_cap: 6, + }, + }, + ]); + + // Trigger the suggestion. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "wea", + }); + + let resultIndex = 1; + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex); + Assert.equal( + details.result.providerName, + UrlbarProviderWeather.name, + "Weather suggestion should be present at expected index after 'wea' search" + ); + + let command = "show_less_frequently"; + + for (let i = 0; i < 3; i++) { + await UrlbarTestUtils.openResultMenuAndClickItem(window, command, { + resultIndex, + openByMouse: true, + }); + + Assert.ok( + gURLBar.view.isOpen, + "The view should remain open clicking the command" + ); + Assert.ok( + details.element.row.hasAttribute("feedback-acknowledgment"), + "Row should have feedback acknowledgment after clicking command" + ); + Assert.equal( + UrlbarPrefs.get("weather.minKeywordLength"), + 4 + i, + "weather.minKeywordLength should be incremented once" + ); + } + + let menuitem = await UrlbarTestUtils.openResultMenuAndGetItem({ + window, + command, + resultIndex, + }); + Assert.ok( + !menuitem, + "The menuitem should not exist after the cap is reached" + ); + + gURLBar.view.resultMenu.hidePopup(true); + await UrlbarTestUtils.promisePopupClose(window); + await QuickSuggestTestUtils.setRemoteSettingsResults([ + { + type: "weather", + weather: MerinoTestUtils.WEATHER_RS_DATA, + }, + ]); + UrlbarPrefs.clear("weather.minKeywordLength"); +}); + +// Tests the "Not interested" result menu dismissal command. +add_task(async function notInterested() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: MerinoTestUtils.WEATHER_KEYWORD, + }); + await doDismissTest("not_interested"); +}); + +// Tests the "Not relevant" result menu dismissal command. +add_task(async function notRelevant() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: MerinoTestUtils.WEATHER_KEYWORD, + }); + await doDismissTest("not_relevant"); +}); + +async function doDismissTest(command) { + let resultCount = UrlbarTestUtils.getResultCount(window); + + let resultIndex = 1; + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex); + Assert.equal( + details.result.providerName, + UrlbarProviderWeather.name, + "Weather suggestion should be present" + ); + + // Click the command. + await UrlbarTestUtils.openResultMenuAndClickItem( + window, + ["[data-l10n-id=firefox-suggest-command-dont-show-this]", command], + { resultIndex, openByMouse: true } + ); + + Assert.ok( + !UrlbarPrefs.get("suggest.weather"), + "suggest.weather pref should be set to false after dismissal" + ); + + // The row should be a tip now. + Assert.ok(gURLBar.view.isOpen, "The view should remain open after dismissal"); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + resultCount, + "The result count should not haved changed after dismissal" + ); + details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex); + Assert.equal( + details.type, + UrlbarUtils.RESULT_TYPE.TIP, + "Row should be a tip after dismissal" + ); + Assert.equal( + details.result.payload.type, + "dismissalAcknowledgment", + "Tip type should be dismissalAcknowledgment" + ); + Assert.ok( + !details.element.row.hasAttribute("feedback-acknowledgment"), + "Row should not have feedback acknowledgment after dismissal" + ); + + // Get the dismissal acknowledgment's "Got it" button and click it. + let gotItButton = UrlbarTestUtils.getButtonForResultIndex( + window, + "0", + resultIndex + ); + Assert.ok(gotItButton, "Row should have a 'Got it' button"); + EventUtils.synthesizeMouseAtCenter(gotItButton, {}, window); + + // The view should remain open and the tip row should be gone. + Assert.ok( + gURLBar.view.isOpen, + "The view should remain open clicking the 'Got it' button" + ); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + resultCount - 1, + "The result count should be one less after clicking 'Got it' button" + ); + for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) { + details = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + Assert.ok( + details.type != UrlbarUtils.RESULT_TYPE.TIP && + details.result.providerName != UrlbarProviderWeather.name, + "Tip result and weather result should not be present" + ); + } + + await UrlbarTestUtils.promisePopupClose(window); + + // Enable the weather suggestion again and wait for it to be fetched. + let fetchPromise = QuickSuggest.weather.waitForFetches(); + UrlbarPrefs.clear("suggest.weather"); + info("Waiting for weather fetch after re-enabling the suggestion"); + await fetchPromise; + info("Got weather fetch"); +} + +// Tests the "Report inaccurate location" result menu command immediately +// followed by a dismissal command to make sure other commands still work +// properly while the urlbar session remains ongoing. +add_task(async function inaccurateLocationAndDismissal() { + await doSessionOngoingCommandTest("inaccurate_location"); +}); + +// Tests the "Show less frequently" result menu command immediately followed by +// a dismissal command to make sure other commands still work properly while the +// urlbar session remains ongoing. +add_task(async function showLessFrequentlyAndDismissal() { + await doSessionOngoingCommandTest("show_less_frequently"); + UrlbarPrefs.clear("weather.minKeywordLength"); +}); + +async function doSessionOngoingCommandTest(command) { + // Trigger the suggestion. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: MerinoTestUtils.WEATHER_KEYWORD, + }); + + let resultIndex = 1; + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex); + Assert.equal( + details.result.providerName, + UrlbarProviderWeather.name, + "Weather suggestion should be present at expected index after search" + ); + + // Click the command. + await UrlbarTestUtils.openResultMenuAndClickItem(window, command, { + resultIndex, + }); + + Assert.ok( + gURLBar.view.isOpen, + "The view should remain open clicking the command" + ); + Assert.ok( + details.element.row.hasAttribute("feedback-acknowledgment"), + "Row should have feedback acknowledgment after clicking command" + ); + + info("Doing dismissal"); + await doDismissTest("not_interested"); +} diff --git a/browser/components/urlbar/tests/quicksuggest/browser/head.js b/browser/components/urlbar/tests/quicksuggest/browser/head.js new file mode 100644 index 0000000000..07979080fd --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/head.js @@ -0,0 +1,569 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let sandbox; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/components/urlbar/tests/browser/head-common.js", + this +); + +ChromeUtils.defineESModuleGetters(this, { + CONTEXTUAL_SERVICES_PING_TYPES: + "resource:///modules/PartnerLinkAttribution.jsm", + QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs", + TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs", + UrlbarProviderQuickSuggest: + "resource:///modules/UrlbarProviderQuickSuggest.sys.mjs", +}); + +XPCOMUtils.defineLazyGetter(this, "QuickSuggestTestUtils", () => { + const { QuickSuggestTestUtils: module } = ChromeUtils.importESModule( + "resource://testing-common/QuickSuggestTestUtils.sys.mjs" + ); + module.init(this); + return module; +}); + +XPCOMUtils.defineLazyGetter(this, "MerinoTestUtils", () => { + const { MerinoTestUtils: module } = ChromeUtils.importESModule( + "resource://testing-common/MerinoTestUtils.sys.mjs" + ); + module.init(this); + return module; +}); + +registerCleanupFunction(async () => { + // Ensure the popup is always closed at the end of each test to avoid + // interfering with the next test. + await UrlbarTestUtils.promisePopupClose(window); +}); + +/** + * 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.discoverystream.endpointSpocsClear", + "", + ], + ["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"); +} + +/** + * Call this in your setup task if you use `doTelemetryTest()`. + * + * @param {object} options + * Options + * @param {Array} options.remoteSettingsResults + * Array of remote settings result objects. If not given, no suggestions + * will be present in remote settings. + * @param {Array} options.merinoSuggestions + * Array of Merino suggestion objects. If given, this function will start + * the mock Merino server and set `quicksuggest.dataCollection.enabled` to + * true so that `UrlbarProviderQuickSuggest` will fetch suggestions from it. + * Otherwise Merino will not serve suggestions, but you can still set up + * Merino without using this function by using `MerinoTestUtils` directly. + * @param {Array} options.config + * Quick suggest will be initialized with this config. Leave undefined to use + * the default config. See `QuickSuggestTestUtils` for details. + */ +async function setUpTelemetryTest({ + remoteSettingsResults, + merinoSuggestions = null, + config = QuickSuggestTestUtils.DEFAULT_CONFIG, +}) { + if (UrlbarPrefs.get("resultMenu")) { + todo( + false, + "telemetry for the result menu to be implemented in bug 1790020" + ); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.resultMenu", false]], + }); + } + await SpecialPowers.pushPrefEnv({ + set: [ + // Enable blocking on primary sponsored and nonsponsored suggestions so we + // can test the block button. + ["browser.urlbar.quicksuggest.blockingEnabled", true], + ["browser.urlbar.bestMatch.blockingEnabled", true], + // Switch-to-tab results can sometimes appear after the test clicks a help + // button and closes the new tab, which interferes with the expected + // indexes of quick suggest results, so disable them. + ["browser.urlbar.suggest.openpage", false], + // Disable the persisted-search-terms search tip because it can interfere. + ["browser.urlbar.tipShownCount.searchTip_persist", 999], + ], + }); + + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + await UrlbarTestUtils.formHistory.clear(); + + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + + // Add a mock engine so we don't hit the network. + await SearchTestUtils.installSearchExtension({}, { setAsDefault: true }); + + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + remoteSettingsResults, + merinoSuggestions, + config, + }); +} + +/** + * Main entry point for testing primary telemetry for quick suggest suggestions: + * impressions, clicks, helps, and blocks. This can be used to declaratively + * test all primary telemetry for any suggestion type. + * + * @param {object} options + * Options + * @param {number} options.index + * The expected index of the suggestion in the results list. + * @param {object} options.suggestion + * The suggestion being tested. + * @param {object} options.impressionOnly + * An object describing the expected impression-only telemetry, i.e., + * telemetry recorded when an impression occurs but not a click. It must have + * the following properties: + * {object} scalars + * An object that maps expected scalar names to values. + * {object} event + * The expected recorded event. + * {object} ping + * The expected recorded custom telemetry ping. If no ping is expected, + * leave this undefined or pass null. + * @param {object} options.selectables + * An object describing the telemetry that's expected to be recorded when each + * selectable element in the suggestion's row is picked. This object maps HTML + * class names to objects. Each property's name must be an HTML class name + * that uniquely identifies a selectable element within the row. The value + * must be an object that describes the telemetry that's expected to be + * recorded when that element is picked, and this inner object must have the + * following properties: + * {object} scalars + * An object that maps expected scalar names to values. + * {object} event + * The expected recorded event. + * {Array} pings + * A list of expected recorded custom telemetry pings. If no pings are + * expected, pass an empty array. + * @param {string} options.providerName + * The name of the provider that is expected to create the UrlbarResult for + * the suggestion. + * @param {Function} options.teardown + * If given, this function will be called after each selectable test. If + * picking an element causes side effects that need to be cleaned up before + * starting the next selectable test, they can be cleaned up here. + * @param {Function} options.showSuggestion + * This function should open the view and show the suggestion. + */ +async function doTelemetryTest({ + index, + suggestion, + impressionOnly, + selectables, + providerName = UrlbarProviderQuickSuggest.name, + teardown = null, + showSuggestion = () => + UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + // If the suggestion object is a remote settings result, it will have a + // `keywords` property. Otherwise the suggestion object must be a Merino + // suggestion, and the search string doesn't matter in that case because + // the mock Merino server will be set up to return suggestions regardless. + value: suggestion.keywords?.[0] || "test", + fireInputEvent: true, + }), +}) { + // Do the impression-only test. It will return the `classList` values of all + // the selectable elements in the row so we can use them below. + let selectableClassLists = await doImpressionOnlyTest({ + index, + suggestion, + providerName, + showSuggestion, + expected: impressionOnly, + }); + if (!selectableClassLists) { + Assert.ok( + false, + "Impression test didn't complete successfully, stopping telemetry test" + ); + return; + } + + info( + "Got classLists of actual selectable elements in the row: " + + JSON.stringify(selectableClassLists) + ); + + let allMatchedExpectedClasses = new Set(); + + // For each actual selectable element in the row, do a selectable test by + // picking the element and checking telemetry. + for (let classList of selectableClassLists) { + info( + "Setting up selectable test for actual element with classList " + + JSON.stringify(classList) + ); + + // Each of the actual selectable elements should match exactly one of the + // test's expected selectable classes. + // + // * If an element doesn't match any expected class, then the test does not + // account for that element, which is an error in the test. + // * If an element matches more than one expected class, then the expected + // class is not specific enough, which is also an error in the test. + + // Collect all the expected classes that match the actual element. + let matchingExpectedClasses = Object.keys(selectables).filter(className => + classList.includes(className) + ); + + if (!matchingExpectedClasses.length) { + Assert.ok( + false, + "Actual selectable element doesn't match any expected classes. The element's classList is " + + JSON.stringify(classList) + ); + continue; + } + if (matchingExpectedClasses.length > 1) { + Assert.ok( + false, + "Actual selectable element matches multiple expected classes. The element's classList is " + + JSON.stringify(classList) + ); + continue; + } + + let className = matchingExpectedClasses[0]; + allMatchedExpectedClasses.add(className); + + await doSelectableTest({ + suggestion, + providerName, + showSuggestion, + index, + className, + expected: selectables[className], + }); + + if (teardown) { + info("Calling teardown"); + await teardown(); + info("Finished teardown"); + } + } + + // Finally, if an expected class doesn't match any actual element, then the + // test expects an element to be picked that either isn't present or isn't + // selectable, which is an error in the test. + Assert.deepEqual( + Object.keys(selectables).filter( + className => !allMatchedExpectedClasses.has(className) + ), + [], + "There should be no expected classes that didn't match actual selectable elements" + ); +} + +/** + * Helper for `doTelemetryTest()` that does an impression-only test. + * + * @param {object} options + * Options + * @param {number} options.index + * The expected index of the suggestion in the results list. + * @param {object} options.suggestion + * The suggestion being tested. + * @param {string} options.providerName + * The name of the provider that is expected to create the UrlbarResult for + * the suggestion. + * @param {object} options.expected + * An object describing the expected impression-only telemetry. It must have + * the following properties: + * {object} scalars + * An object that maps expected scalar names to values. + * {object} event + * The expected recorded event. + * {object} ping + * The expected recorded custom telemetry ping. If no ping is expected, + * leave this undefined or pass null. + * @param {Function} options.showSuggestion + * This function should open the view and show the suggestion. + * @returns {Array} + * The `classList` values of all the selectable elements in the suggestion's + * row. Each item in this array is a selectable element's `classList` that has + * been converted to an array of strings. + */ +async function doImpressionOnlyTest({ + index, + suggestion, + providerName, + expected, + showSuggestion, +}) { + info("Starting impression-only test"); + + Services.telemetry.clearEvents(); + let { spy, spyCleanup } = QuickSuggestTestUtils.createTelemetryPingSpy(); + + info("Showing suggestion"); + await showSuggestion(); + + // Get the suggestion row. + let row = await validateSuggestionRow(index, suggestion, providerName); + if (!row) { + Assert.ok( + false, + "Couldn't get suggestion row, stopping impression-only test" + ); + await spyCleanup(); + return null; + } + + // We need to get a different selectable row so we can pick it to trigger + // impression-only telemetry. For simplicity we'll look for a row that will + // load a URL when picked. We'll also verify no other rows are from the + // expected provider. + let otherRow; + let rowCount = UrlbarTestUtils.getResultCount(window); + for (let i = 0; i < rowCount; i++) { + if (i != index) { + let r = await UrlbarTestUtils.waitForAutocompleteResultAt(window, i); + Assert.notEqual( + r.result.providerName, + providerName, + "No other row should be from expected provider: index = " + i + ); + if ( + !otherRow && + (r.result.payload.url || + (r.result.type == UrlbarUtils.RESULT_TYPE.SEARCH && + (r.result.payload.query || r.result.payload.suggestion))) && + r.hasAttribute("row-selectable") + ) { + otherRow = r; + } + } + } + if (!otherRow) { + Assert.ok( + false, + "Couldn't get a different selectable row with a URL, stopping impression-only test" + ); + await spyCleanup(); + return null; + } + + // Collect the `classList` values for all selectable elements in the row. + let selectableClassLists = []; + let selectables = row.querySelectorAll(":is([selectable], [role=button])"); + for (let element of selectables) { + selectableClassLists.push([...element.classList]); + } + + // Pick the different row. Assumptions: + // * The middle of the row is selectable + // * Picking the row will load a page + info("Clicking different row and waiting for view to close"); + let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeMouseAtCenter(otherRow, {}) + ); + + info("Waiting for page to load after clicking different row"); + await loadPromise; + + // Check telemetry. + info("Checking scalars. Expected: " + JSON.stringify(expected.scalars)); + QuickSuggestTestUtils.assertScalars(expected.scalars); + + info("Checking events. Expected: " + JSON.stringify([expected.event])); + QuickSuggestTestUtils.assertEvents([expected.event]); + + let expectedPings = expected.ping ? [expected.ping] : []; + info("Checking pings. Expected: " + JSON.stringify(expectedPings)); + QuickSuggestTestUtils.assertPings(spy, expectedPings); + + // Clean up. + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(); + await spyCleanup(); + + info("Finished impression-only test"); + + return selectableClassLists; +} + +/** + * Helper for `doTelemetryTest()` that picks a selectable element in a + * suggestion's row and checks telemetry. + * + * @param {object} options + * Options + * @param {number} options.index + * The expected index of the suggestion in the results list. + * @param {object} options.suggestion + * The suggestion being tested. + * @param {string} options.providerName + * The name of the provider that is expected to create the UrlbarResult for + * the suggestion. + * @param {string} options.className + * An HTML class name that should uniquely identify the selectable element + * within its row. + * @param {object} options.expected + * An object describing the telemetry that's expected to be recorded when the + * selectable element is picked. It must have the following properties: + * {object} scalars + * An object that maps expected scalar names to values. + * {object} event + * The expected recorded event. + * {Array} pings + * A list of expected recorded custom telemetry pings. If no pings are + * expected, leave this undefined or pass an empty array. + * @param {Function} options.showSuggestion + * This function should open the view and show the suggestion. + */ +async function doSelectableTest({ + index, + suggestion, + providerName, + className, + expected, + showSuggestion, +}) { + info("Starting selectable test: " + JSON.stringify({ className })); + + Services.telemetry.clearEvents(); + let { spy, spyCleanup } = QuickSuggestTestUtils.createTelemetryPingSpy(); + + info("Showing suggestion"); + await showSuggestion(); + + let row = await validateSuggestionRow(index, suggestion, providerName); + if (!row) { + Assert.ok(false, "Couldn't get suggestion row, stopping selectable test"); + await spyCleanup(); + return; + } + + let element = row.querySelector("." + className); + Assert.ok(element, "Sanity check: Target selectable element should exist"); + + let loadPromise; + if (className == "urlbarView-row-inner") { + // We assume clicking the row-inner will cause a page to load in the current + // browser. + loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + } else if (className == "urlbarView-button-help") { + loadPromise = BrowserTestUtils.waitForNewTab(gBrowser); + } + + info("Clicking element: " + className); + EventUtils.synthesizeMouseAtCenter(element, {}); + + if (loadPromise) { + info("Waiting for load"); + await loadPromise; + await TestUtils.waitForTick(); + if (className == "urlbarView-button-help") { + info("Closing help tab"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + } + } + + info("Checking scalars. Expected: " + JSON.stringify(expected.scalars)); + QuickSuggestTestUtils.assertScalars(expected.scalars); + + info("Checking events. Expected: " + JSON.stringify([expected.event])); + QuickSuggestTestUtils.assertEvents([expected.event]); + + let expectedPings = expected.pings ?? []; + info("Checking pings. Expected: " + JSON.stringify(expectedPings)); + QuickSuggestTestUtils.assertPings(spy, expectedPings); + + if (className == "urlbarView-button-block") { + await QuickSuggest.blockedSuggestions.clear(); + } + await PlacesUtils.history.clear(); + await spyCleanup(); + + info("Finished selectable test: " + JSON.stringify({ className })); +} + +/** + * Gets a row in the view, which is assumed to be open, and asserts that it's a + * particular quick suggest row. If it is, the row is returned. If it's not, + * null is returned. + * + * @param {number} index + * The expected index of the quick suggest row. + * @param {object} suggestion + * The expected suggestion. + * @param {string} providerName + * The name of the provider that is expected to create the UrlbarResult for + * the suggestion. + * @returns {Element} + * If the row is the expected suggestion, the row element is returned. + * Otherwise null is returned. + */ +async function validateSuggestionRow(index, suggestion, providerName) { + let rowCount = UrlbarTestUtils.getResultCount(window); + Assert.less( + index, + rowCount, + "Expected suggestion row index should be < row count" + ); + if (rowCount <= index) { + return null; + } + + let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, index); + Assert.equal( + row.result.providerName, + providerName, + "Expected suggestion row should be from expected provider" + ); + Assert.equal( + row.result.payload.url, + suggestion.url, + "The suggestion row should represent the expected suggestion" + ); + if ( + row.result.providerName != providerName || + row.result.payload.url != suggestion.url + ) { + return null; + } + + return row; +} diff --git a/browser/components/urlbar/tests/quicksuggest/browser/searchSuggestionEngine.sjs b/browser/components/urlbar/tests/quicksuggest/browser/searchSuggestionEngine.sjs new file mode 100644 index 0000000000..145392fcf2 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/searchSuggestionEngine.sjs @@ -0,0 +1,57 @@ +/* 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/quicksuggest/browser/searchSuggestionEngine.xml b/browser/components/urlbar/tests/quicksuggest/browser/searchSuggestionEngine.xml new file mode 100644 index 0000000000..142c91849c --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/searchSuggestionEngine.xml @@ -0,0 +1,11 @@ + + + + +browser_searchSuggestionEngine searchSuggestionEngine.xml + + + + + diff --git a/browser/components/urlbar/tests/quicksuggest/browser/subdialog.xhtml b/browser/components/urlbar/tests/quicksuggest/browser/subdialog.xhtml new file mode 100644 index 0000000000..67303f19ac --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/subdialog.xhtml @@ -0,0 +1,14 @@ + + + + + + + + + A sample sub-dialog for testing + + diff --git a/browser/components/urlbar/tests/quicksuggest/unit/head.js b/browser/components/urlbar/tests/quicksuggest/unit/head.js new file mode 100644 index 0000000000..39f920fce5 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/head.js @@ -0,0 +1,227 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* import-globals-from ../../unit/head.js */ + +ChromeUtils.defineESModuleGetters(this, { + QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs", + QuickSuggestRemoteSettings: + "resource:///modules/urlbar/private/QuickSuggestRemoteSettings.sys.mjs", + TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs", + UrlbarProviderAutofill: "resource:///modules/UrlbarProviderAutofill.sys.mjs", + UrlbarProviderQuickSuggest: + "resource:///modules/UrlbarProviderQuickSuggest.sys.mjs", +}); + +add_setup(async function setUpQuickSuggestXpcshellTest() { + // Initializing TelemetryEnvironment in an xpcshell environment requires + // jumping through a bunch of hoops. Suggest's use of TelemetryEnvironment is + // tested in browser tests, and there's no other necessary reason to wait for + // TelemetryEnvironment initialization in xpcshell tests, so just skip it. + UrlbarPrefs._testSkipTelemetryEnvironmentInit = true; +}); + +/** + * Tests quick suggest prefs migrations. + * + * @param {object} options + * The options object. + * @param {object} options.testOverrides + * An object that modifies how migration is performed. It has the following + * properties, and all are optional: + * + * {number} migrationVersion + * Migration will stop at this version, so for example you can test + * migration only up to version 1 even when the current actual version is + * larger than 1. + * {object} defaultPrefs + * An object that maps pref names (relative to `browser.urlbar`) to + * default-branch values. These should be the default prefs for the given + * `migrationVersion` and will be set as defaults before migration occurs. + * + * @param {string} options.scenario + * The scenario to set at the time migration occurs. + * @param {object} options.expectedPrefs + * The expected prefs after migration: `{ defaultBranch, userBranch }` + * Pref names should be relative to `browser.urlbar`. + * @param {object} [options.initialUserBranch] + * Prefs to set on the user branch before migration ocurs. Use these to + * simulate user actions like disabling prefs or opting in or out of the + * online modal. Pref names should be relative to `browser.urlbar`. + */ +async function doMigrateTest({ + testOverrides, + scenario, + expectedPrefs, + initialUserBranch = {}, +}) { + info( + "Testing migration: " + + JSON.stringify({ + testOverrides, + initialUserBranch, + scenario, + expectedPrefs, + }) + ); + + function setPref(branch, name, value) { + switch (typeof value) { + case "boolean": + branch.setBoolPref(name, value); + break; + case "number": + branch.setIntPref(name, value); + break; + case "string": + branch.setCharPref(name, value); + break; + default: + Assert.ok( + false, + `Pref type not handled for setPref: ${name} = ${value}` + ); + break; + } + } + + function getPref(branch, name) { + let type = typeof UrlbarPrefs.get(name); + switch (type) { + case "boolean": + return branch.getBoolPref(name); + case "number": + return branch.getIntPref(name); + case "string": + return branch.getCharPref(name); + default: + Assert.ok(false, `Pref type not handled for getPref: ${name} ${type}`); + break; + } + return null; + } + + let defaultBranch = Services.prefs.getDefaultBranch("browser.urlbar."); + let userBranch = Services.prefs.getBranch("browser.urlbar."); + + // Set initial prefs. `initialDefaultBranch` are firefox.js values, i.e., + // defaults immediately after startup and before any scenario update and + // migration happens. + UrlbarPrefs._updatingFirefoxSuggestScenario = true; + UrlbarPrefs.clear("quicksuggest.migrationVersion"); + let initialDefaultBranch = { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + "quicksuggest.dataCollection.enabled": false, + }; + for (let name of Object.keys(initialDefaultBranch)) { + userBranch.clearUserPref(name); + } + for (let [branch, prefs] of [ + [defaultBranch, initialDefaultBranch], + [userBranch, initialUserBranch], + ]) { + for (let [name, value] of Object.entries(prefs)) { + if (value !== undefined) { + setPref(branch, name, value); + } + } + } + UrlbarPrefs._updatingFirefoxSuggestScenario = false; + + // Update the scenario and check prefs twice. The first time the migration + // should happen, and the second time the migration should not happen and + // all the prefs should stay the same. + for (let i = 0; i < 2; i++) { + info(`Calling updateFirefoxSuggestScenario, i=${i}`); + + // Do the scenario update and set `isStartup` to simulate startup. + await UrlbarPrefs.updateFirefoxSuggestScenario({ + ...testOverrides, + scenario, + isStartup: true, + }); + + // Check expected pref values. Store expected effective values as we go so + // we can check them afterward. For a given pref, the expected effective + // value is the user value, or if there's not a user value, the default + // value. + let expectedEffectivePrefs = {}; + let { + defaultBranch: expectedDefaultBranch, + userBranch: expectedUserBranch, + } = expectedPrefs; + expectedDefaultBranch = expectedDefaultBranch || {}; + expectedUserBranch = expectedUserBranch || {}; + for (let [branch, prefs, branchType] of [ + [defaultBranch, expectedDefaultBranch, "default"], + [userBranch, expectedUserBranch, "user"], + ]) { + let entries = Object.entries(prefs); + if (!entries.length) { + continue; + } + + info( + `Checking expected prefs on ${branchType} branch after updating scenario` + ); + for (let [name, value] of entries) { + expectedEffectivePrefs[name] = value; + if (branch == userBranch) { + Assert.ok( + userBranch.prefHasUserValue(name), + `Pref ${name} is on user branch` + ); + } + Assert.equal( + getPref(branch, name), + value, + `Pref ${name} value on ${branchType} branch` + ); + } + } + + info( + `Making sure prefs on the default branch without expected user-branch values are not on the user branch` + ); + for (let name of Object.keys(initialDefaultBranch)) { + if (!expectedUserBranch.hasOwnProperty(name)) { + Assert.ok( + !userBranch.prefHasUserValue(name), + `Pref ${name} is not on user branch` + ); + } + } + + info(`Checking expected effective prefs`); + for (let [name, value] of Object.entries(expectedEffectivePrefs)) { + Assert.equal( + UrlbarPrefs.get(name), + value, + `Pref ${name} effective value` + ); + } + + let currentVersion = + testOverrides?.migrationVersion === undefined + ? UrlbarPrefs.FIREFOX_SUGGEST_MIGRATION_VERSION + : testOverrides.migrationVersion; + Assert.equal( + UrlbarPrefs.get("quicksuggest.migrationVersion"), + currentVersion, + "quicksuggest.migrationVersion is correct after migration" + ); + } + + // Clean up. + UrlbarPrefs._updatingFirefoxSuggestScenario = true; + UrlbarPrefs.clear("quicksuggest.migrationVersion"); + let userBranchNames = [ + ...Object.keys(initialUserBranch), + ...Object.keys(expectedPrefs.userBranch || {}), + ]; + for (let name of userBranchNames) { + userBranch.clearUserPref(name); + } + UrlbarPrefs._updatingFirefoxSuggestScenario = false; +} diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_merinoClient.js b/browser/components/urlbar/tests/quicksuggest/unit/test_merinoClient.js new file mode 100644 index 0000000000..cd45cb11a7 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_merinoClient.js @@ -0,0 +1,647 @@ +/* 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 MerinoClient. + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + ExperimentFakes: "resource://testing-common/NimbusTestUtils.sys.mjs", + MerinoClient: "resource:///modules/MerinoClient.sys.mjs", + NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", +}); + +// Set the `merino.timeoutMs` pref to a large value so that the client will not +// inadvertently time out during fetches. This is especially important on CI and +// when running this test in verify mode. Tasks that specifically test timeouts +// may need to set a more reasonable value for their duration. +const TEST_TIMEOUT_MS = 30000; + +// The expected suggestion objects returned from `MerinoClient.fetch()`. +const EXPECTED_MERINO_SUGGESTIONS = []; + +const { SEARCH_PARAMS } = MerinoClient; + +let gClient; + +add_setup(async function init() { + UrlbarPrefs.set("merino.timeoutMs", TEST_TIMEOUT_MS); + registerCleanupFunction(() => { + UrlbarPrefs.clear("merino.timeoutMs"); + }); + + gClient = new MerinoClient(); + await MerinoTestUtils.server.start(); + + for (let suggestion of MerinoTestUtils.server.response.body.suggestions) { + EXPECTED_MERINO_SUGGESTIONS.push({ + ...suggestion, + request_id: MerinoTestUtils.server.response.body.request_id, + source: "merino", + }); + } +}); + +// Checks client names. +add_task(async function name() { + Assert.equal( + gClient.name, + "anonymous", + "gClient name is 'anonymous' since it wasn't given a name" + ); + + let client = new MerinoClient("New client"); + Assert.equal(client.name, "New client", "newClient name is correct"); +}); + +// Does a successful fetch. +add_task(async function success() { + let histograms = MerinoTestUtils.getAndClearHistograms(); + + await fetchAndCheckSuggestions({ + expected: EXPECTED_MERINO_SUGGESTIONS, + }); + + Assert.equal( + gClient.lastFetchStatus, + "success", + "The request successfully finished" + ); + MerinoTestUtils.checkAndClearHistograms({ + histograms, + response: "success", + latencyRecorded: true, + client: gClient, + }); +}); + +// Does a successful fetch that doesn't return any suggestions. +add_task(async function noSuggestions() { + let { suggestions } = MerinoTestUtils.server.response.body; + MerinoTestUtils.server.response.body.suggestions = []; + + let histograms = MerinoTestUtils.getAndClearHistograms(); + + await fetchAndCheckSuggestions({ + expected: [], + }); + + Assert.equal( + gClient.lastFetchStatus, + "no_suggestion", + "The request successfully finished without suggestions" + ); + MerinoTestUtils.checkAndClearHistograms({ + histograms, + response: "no_suggestion", + latencyRecorded: true, + client: gClient, + }); + + MerinoTestUtils.server.response.body.suggestions = suggestions; +}); + +// Checks a response that's valid but also has some unexpected properties. +add_task(async function unexpectedResponseProperties() { + let histograms = MerinoTestUtils.getAndClearHistograms(); + + MerinoTestUtils.server.response.body.unexpectedString = "some value"; + MerinoTestUtils.server.response.body.unexpectedArray = ["a", "b", "c"]; + MerinoTestUtils.server.response.body.unexpectedObject = { foo: "bar" }; + + await fetchAndCheckSuggestions({ + expected: EXPECTED_MERINO_SUGGESTIONS, + }); + + Assert.equal( + gClient.lastFetchStatus, + "success", + "The request successfully finished" + ); + MerinoTestUtils.checkAndClearHistograms({ + histograms, + response: "success", + latencyRecorded: true, + client: gClient, + }); +}); + +// Checks some responses with unexpected response bodies. +add_task(async function unexpectedResponseBody() { + let histograms = MerinoTestUtils.getAndClearHistograms(); + + let responses = [ + { body: {} }, + { body: { bogus: [] } }, + { body: { suggestions: {} } }, + { body: { suggestions: [] } }, + { body: "" }, + { body: "bogus", contentType: "text/html" }, + ]; + + for (let r of responses) { + info("Testing response: " + JSON.stringify(r)); + + MerinoTestUtils.server.response = r; + await fetchAndCheckSuggestions({ expected: [] }); + + Assert.equal( + gClient.lastFetchStatus, + "no_suggestion", + "The request successfully finished without suggestions" + ); + MerinoTestUtils.checkAndClearHistograms({ + histograms, + response: "no_suggestion", + latencyRecorded: true, + client: gClient, + }); + } + + MerinoTestUtils.server.reset(); +}); + +// Tests with a network error. +add_task(async function networkError() { + let histograms = MerinoTestUtils.getAndClearHistograms(); + + // This promise will be resolved when the client processes the network error. + let responsePromise = gClient.waitForNextResponse(); + + await MerinoTestUtils.server.withNetworkError(async () => { + await fetchAndCheckSuggestions({ expected: [] }); + }); + + // The client should have nulled out the timeout timer before `fetch()` + // returned. + Assert.strictEqual( + gClient._test_timeoutTimer, + null, + "timeoutTimer does not exist after fetch finished" + ); + + // Wait for the client to process the network error. + await responsePromise; + + Assert.equal( + gClient.lastFetchStatus, + "network_error", + "The request failed with a network error" + ); + MerinoTestUtils.checkAndClearHistograms({ + histograms, + response: "network_error", + latencyRecorded: false, + client: gClient, + }); +}); + +// Tests with an HTTP error. +add_task(async function httpError() { + let histograms = MerinoTestUtils.getAndClearHistograms(); + + MerinoTestUtils.server.response = { status: 500 }; + await fetchAndCheckSuggestions({ expected: [] }); + + Assert.equal( + gClient.lastFetchStatus, + "http_error", + "The request failed with an HTTP error" + ); + MerinoTestUtils.checkAndClearHistograms({ + histograms, + response: "http_error", + latencyRecorded: true, + client: gClient, + }); + + MerinoTestUtils.server.reset(); +}); + +// Tests a client timeout. +add_task(async function clientTimeout() { + await doClientTimeoutTest({ + prefTimeoutMs: 200, + responseDelayMs: 400, + }); +}); + +// Tests a client timeout followed by an HTTP error. Only the timeout should be +// recorded. +add_task(async function clientTimeoutFollowedByHTTPError() { + MerinoTestUtils.server.response = { status: 500 }; + await doClientTimeoutTest({ + prefTimeoutMs: 200, + responseDelayMs: 400, + expectedResponseStatus: 500, + }); +}); + +// Tests a client timeout when a timeout value is passed to `fetch()`, which +// should override the value in the `merino.timeoutMs` pref. +add_task(async function timeoutPassedToFetch() { + // Set up a timeline like this: + // + // 1ms: The timeout passed to `fetch()` elapses + // 400ms: Merino returns a response + // 30000ms: The timeout in the pref elapses + // + // The expected behavior is that the 1ms timeout is hit, the request fails + // with a timeout, and Merino later returns a response. If the 1ms timeout is + // not hit, then Merino will return a response before the 30000ms timeout + // elapses and the request will complete successfully. + + await doClientTimeoutTest({ + prefTimeoutMs: 30000, + responseDelayMs: 400, + fetchArgs: { query: "search", timeoutMs: 1 }, + }); +}); + +async function doClientTimeoutTest({ + prefTimeoutMs, + responseDelayMs, + fetchArgs = { query: "search" }, + expectedResponseStatus = 200, +} = {}) { + let histograms = MerinoTestUtils.getAndClearHistograms(); + + let originalPrefTimeoutMs = UrlbarPrefs.get("merino.timeoutMs"); + UrlbarPrefs.set("merino.timeoutMs", prefTimeoutMs); + + // Make the server return a delayed response so the client times out waiting + // for it. + MerinoTestUtils.server.response.delay = responseDelayMs; + + let responsePromise = gClient.waitForNextResponse(); + await fetchAndCheckSuggestions({ args: fetchArgs, expected: [] }); + + Assert.equal(gClient.lastFetchStatus, "timeout", "The request timed out"); + + // The client should have nulled out the timeout timer. + Assert.strictEqual( + gClient._test_timeoutTimer, + null, + "timeoutTimer does not exist after fetch finished" + ); + + // The fetch controller should still exist because the fetch should remain + // ongoing. + Assert.ok( + gClient._test_fetchController, + "fetchController still exists after fetch finished" + ); + Assert.ok( + !gClient._test_fetchController.signal.aborted, + "fetchController is not aborted" + ); + + // The latency histogram should not be updated since the response has not been + // received. + MerinoTestUtils.checkAndClearHistograms({ + histograms, + response: "timeout", + latencyRecorded: false, + latencyStopwatchRunning: true, + client: gClient, + }); + + // Wait for the client to receive the response. + let httpResponse = await responsePromise; + Assert.ok(httpResponse, "Response was received"); + Assert.equal(httpResponse.status, expectedResponseStatus, "Response status"); + + // The client should have nulled out the fetch controller. + Assert.ok(!gClient._test_fetchController, "fetchController no longer exists"); + + // The `checkAndClearHistograms()` call above cleared the histograms. After + // that, nothing else should have been recorded for the response. + MerinoTestUtils.checkAndClearHistograms({ + histograms, + response: null, + latencyRecorded: true, + client: gClient, + }); + + MerinoTestUtils.server.reset(); + UrlbarPrefs.set("merino.timeoutMs", originalPrefTimeoutMs); +} + +// By design, when a fetch times out, the client allows it to finish so we can +// record its latency. But when a second fetch starts before the first finishes, +// the client should abort the first so that there is at most one fetch at a +// time. +add_task(async function newFetchAbortsPrevious() { + let histograms = MerinoTestUtils.getAndClearHistograms(); + + // Make the server return a very delayed response so that it would time out + // and we can start a second fetch that will abort the first fetch. + MerinoTestUtils.server.response.delay = + 100 * UrlbarPrefs.get("merino.timeoutMs"); + + // Do the first fetch. + await fetchAndCheckSuggestions({ expected: [] }); + + // At this point, the timeout timer has fired, causing our `fetch()` call to + // return. However, the client's internal fetch should still be ongoing. + + Assert.equal(gClient.lastFetchStatus, "timeout", "The request timed out"); + + // The client should have nulled out the timeout timer. + Assert.strictEqual( + gClient._test_timeoutTimer, + null, + "timeoutTimer does not exist after first fetch finished" + ); + + // The fetch controller should still exist because the fetch should remain + // ongoing. + Assert.ok( + gClient._test_fetchController, + "fetchController still exists after first fetch finished" + ); + Assert.ok( + !gClient._test_fetchController.signal.aborted, + "fetchController is not aborted" + ); + + // The latency histogram should not be updated since the fetch is still + // ongoing. + MerinoTestUtils.checkAndClearHistograms({ + histograms, + response: "timeout", + latencyRecorded: false, + latencyStopwatchRunning: true, + client: gClient, + }); + + // Do the second fetch. This time don't delay the response. + delete MerinoTestUtils.server.response.delay; + await fetchAndCheckSuggestions({ + expected: EXPECTED_MERINO_SUGGESTIONS, + }); + + Assert.equal( + gClient.lastFetchStatus, + "success", + "The request finished successfully" + ); + + // The fetch was successful, so the client should have nulled out both + // properties. + Assert.ok( + !gClient._test_fetchController, + "fetchController does not exist after second fetch finished" + ); + Assert.strictEqual( + gClient._test_timeoutTimer, + null, + "timeoutTimer does not exist after second fetch finished" + ); + + MerinoTestUtils.checkAndClearHistograms({ + histograms, + response: "success", + latencyRecorded: true, + client: gClient, + }); + + MerinoTestUtils.server.reset(); +}); + +// The client should not include the `clientVariants` and `providers` search +// params when they are not set. +add_task(async function clientVariants_providers_notSet() { + UrlbarPrefs.set("merino.clientVariants", ""); + UrlbarPrefs.set("merino.providers", ""); + + await fetchAndCheckSuggestions({ + expected: EXPECTED_MERINO_SUGGESTIONS, + }); + + MerinoTestUtils.server.checkAndClearRequests([ + { + params: { + [SEARCH_PARAMS.QUERY]: "search", + [SEARCH_PARAMS.SEQUENCE_NUMBER]: 0, + }, + }, + ]); + + UrlbarPrefs.clear("merino.clientVariants"); + UrlbarPrefs.clear("merino.providers"); +}); + +// The client should include the `clientVariants` and `providers` search params +// when they are set using preferences. +add_task(async function clientVariants_providers_preferences() { + UrlbarPrefs.set("merino.clientVariants", "green"); + UrlbarPrefs.set("merino.providers", "pink"); + + await fetchAndCheckSuggestions({ + expected: EXPECTED_MERINO_SUGGESTIONS, + }); + + MerinoTestUtils.server.checkAndClearRequests([ + { + params: { + [SEARCH_PARAMS.QUERY]: "search", + [SEARCH_PARAMS.SEQUENCE_NUMBER]: 0, + [SEARCH_PARAMS.CLIENT_VARIANTS]: "green", + [SEARCH_PARAMS.PROVIDERS]: "pink", + }, + }, + ]); + + UrlbarPrefs.clear("merino.clientVariants"); + UrlbarPrefs.clear("merino.providers"); +}); + +// The client should include the `providers` search param when it's set by +// passing in the `providers` argument to `fetch()`. The argument should +// override the pref. This tests a single provider. +add_task(async function providers_arg_single() { + UrlbarPrefs.set("merino.providers", "prefShouldNotBeUsed"); + + await fetchAndCheckSuggestions({ + args: { query: "search", providers: ["argShouldBeUsed"] }, + expected: EXPECTED_MERINO_SUGGESTIONS, + }); + + MerinoTestUtils.server.checkAndClearRequests([ + { + params: { + [SEARCH_PARAMS.QUERY]: "search", + [SEARCH_PARAMS.SEQUENCE_NUMBER]: 0, + [SEARCH_PARAMS.PROVIDERS]: "argShouldBeUsed", + }, + }, + ]); + + UrlbarPrefs.clear("merino.providers"); +}); + +// The client should include the `providers` search param when it's set by +// passing in the `providers` argument to `fetch()`. The argument should +// override the pref. This tests multiple providers. +add_task(async function providers_arg_many() { + UrlbarPrefs.set("merino.providers", "prefShouldNotBeUsed"); + + await fetchAndCheckSuggestions({ + args: { query: "search", providers: ["one", "two", "three"] }, + expected: EXPECTED_MERINO_SUGGESTIONS, + }); + + MerinoTestUtils.server.checkAndClearRequests([ + { + params: { + [SEARCH_PARAMS.QUERY]: "search", + [SEARCH_PARAMS.SEQUENCE_NUMBER]: 0, + [SEARCH_PARAMS.PROVIDERS]: "one,two,three", + }, + }, + ]); + + UrlbarPrefs.clear("merino.providers"); +}); + +// The client should include the `providers` search param when it's set by +// passing in the `providers` argument to `fetch()` even when it's an empty +// array. The argument should override the pref. +add_task(async function providers_arg_empty() { + UrlbarPrefs.set("merino.providers", "prefShouldNotBeUsed"); + + await fetchAndCheckSuggestions({ + args: { query: "search", providers: [] }, + expected: EXPECTED_MERINO_SUGGESTIONS, + }); + + MerinoTestUtils.server.checkAndClearRequests([ + { + params: { + [SEARCH_PARAMS.QUERY]: "search", + [SEARCH_PARAMS.SEQUENCE_NUMBER]: 0, + [SEARCH_PARAMS.PROVIDERS]: "", + }, + }, + ]); + + UrlbarPrefs.clear("merino.providers"); +}); + +// Passes invalid `providers` arguments to `fetch()`. +add_task(async function providers_arg_invalid() { + let providersValues = ["", "nonempty", {}]; + + for (let providers of providersValues) { + info("Calling fetch() with providers: " + JSON.stringify(providers)); + + // `Assert.throws()` doesn't seem to work with async functions... + let error; + try { + await gClient.fetch({ providers, query: "search" }); + } catch (e) { + error = e; + } + Assert.ok(error, "fetch() threw an error"); + Assert.equal( + error.message, + "providers must be an array if given", + "Expected error was thrown" + ); + } +}); + +// Tests setting the endpoint URL and query parameters via Nimbus. +add_task(async function nimbus() { + // Clear the endpoint pref so we know the URL is not being fetched from it. + let originalEndpointURL = UrlbarPrefs.get("merino.endpointURL"); + UrlbarPrefs.set("merino.endpointURL", ""); + + await UrlbarTestUtils.initNimbusFeature(); + + // First, with the endpoint pref set to an empty string, make sure no Merino + // suggestion are returned. + await fetchAndCheckSuggestions({ expected: [] }); + + // Now install an experiment that sets the endpoint and other Merino-related + // variables. Make sure a suggestion is returned and the request includes the + // correct query params. + + // `param`: The param name in the request URL + // `value`: The value to use for the param + // `variable`: The name of the Nimbus variable corresponding to the param + let expectedParams = [ + { + param: SEARCH_PARAMS.CLIENT_VARIANTS, + value: "test-client-variants", + variable: "merinoClientVariants", + }, + { + param: SEARCH_PARAMS.PROVIDERS, + value: "test-providers", + variable: "merinoProviders", + }, + ]; + + // Set up the Nimbus variable values to create the experiment with. + let experimentValues = { + merinoEndpointURL: MerinoTestUtils.server.url.toString(), + }; + for (let { variable, value } of expectedParams) { + experimentValues[variable] = value; + } + + await withExperiment(experimentValues, async () => { + await fetchAndCheckSuggestions({ expected: EXPECTED_MERINO_SUGGESTIONS }); + + let params = { + [SEARCH_PARAMS.QUERY]: "search", + [SEARCH_PARAMS.SEQUENCE_NUMBER]: 0, + }; + for (let { param, value } of expectedParams) { + params[param] = value; + } + MerinoTestUtils.server.checkAndClearRequests([{ params }]); + }); + + UrlbarPrefs.set("merino.endpointURL", originalEndpointURL); +}); + +async function fetchAndCheckSuggestions({ + expected, + args = { + query: "search", + }, +}) { + let actual = await gClient.fetch(args); + Assert.deepEqual(actual, expected, "Expected suggestions"); + gClient.resetSession(); +} + +async function withExperiment(values, callback) { + let { enrollmentPromise, doExperimentCleanup } = + ExperimentFakes.enrollmentHelper( + ExperimentFakes.recipe("mock-experiment", { + active: true, + branches: [ + { + slug: "treatment", + features: [ + { + featureId: NimbusFeatures.urlbar.featureId, + value: { + enabled: true, + ...values, + }, + }, + ], + }, + ], + }) + ); + await enrollmentPromise; + await callback(); + await doExperimentCleanup(); +} diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_merinoClient_sessions.js b/browser/components/urlbar/tests/quicksuggest/unit/test_merinoClient_sessions.js new file mode 100644 index 0000000000..70e970af8a --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_merinoClient_sessions.js @@ -0,0 +1,402 @@ +/* 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 MerinoClient sessions. + +"use strict"; + +const { MerinoClient } = ChromeUtils.importESModule( + "resource:///modules/MerinoClient.sys.mjs" +); + +const { SEARCH_PARAMS } = MerinoClient; + +let gClient; + +add_task(async function init() { + gClient = new MerinoClient(); + await MerinoTestUtils.server.start(); +}); + +// In a single session, all requests should use the same session ID and the +// sequence number should be incremented. +add_task(async function singleSession() { + for (let i = 0; i < 3; i++) { + let query = "search" + i; + await gClient.fetch({ query }); + + MerinoTestUtils.server.checkAndClearRequests([ + { + params: { + [SEARCH_PARAMS.QUERY]: query, + [SEARCH_PARAMS.SEQUENCE_NUMBER]: i, + }, + }, + ]); + } + + gClient.resetSession(); +}); + +// Different sessions should use different session IDs and the sequence number +// should be reset. +add_task(async function manySessions() { + for (let i = 0; i < 3; i++) { + let query = "search" + i; + await gClient.fetch({ query }); + + MerinoTestUtils.server.checkAndClearRequests([ + { + params: { + [SEARCH_PARAMS.QUERY]: query, + [SEARCH_PARAMS.SEQUENCE_NUMBER]: 0, + }, + }, + ]); + + gClient.resetSession(); + } +}); + +// Tests two consecutive fetches: +// +// 1. Start a fetch +// 2. Wait for the mock Merino server to receive the request +// 3. Start a second fetch before the client receives the response +// +// The first fetch will be canceled by the second but the sequence number in the +// second fetch should still be incremented. +add_task(async function twoFetches_wait() { + for (let i = 0; i < 3; i++) { + // Send the first response after a delay to make sure the client will not + // receive it before we start the second fetch. + MerinoTestUtils.server.response.delay = UrlbarPrefs.get("merino.timeoutMs"); + + // Start the first fetch but don't wait for it to finish. + let requestPromise = MerinoTestUtils.server.waitForNextRequest(); + let query1 = "search" + i; + gClient.fetch({ query: query1 }); + + // Wait until the first request is received before starting the second + // fetch, which will cancel the first. The response doesn't need to be + // delayed, so remove it to make the test run faster. + await requestPromise; + delete MerinoTestUtils.server.response.delay; + let query2 = query1 + "again"; + await gClient.fetch({ query: query2 }); + + // The sequence number should have been incremented for each fetch. + MerinoTestUtils.server.checkAndClearRequests([ + { + params: { + [SEARCH_PARAMS.QUERY]: query1, + [SEARCH_PARAMS.SEQUENCE_NUMBER]: 2 * i, + }, + }, + { + params: { + [SEARCH_PARAMS.QUERY]: query2, + [SEARCH_PARAMS.SEQUENCE_NUMBER]: 2 * i + 1, + }, + }, + ]); + } + + gClient.resetSession(); +}); + +// Tests two consecutive fetches: +// +// 1. Start a fetch +// 2. Immediately start a second fetch +// +// The first fetch will be canceled by the second but the sequence number in the +// second fetch should still be incremented. +add_task(async function twoFetches_immediate() { + for (let i = 0; i < 3; i++) { + // Send the first response after a delay to make sure the client will not + // receive it before we start the second fetch. + MerinoTestUtils.server.response.delay = + 100 * UrlbarPrefs.get("merino.timeoutMs"); + + // Start the first fetch but don't wait for it to finish. + let query1 = "search" + i; + gClient.fetch({ query: query1 }); + + // Immediately do a second fetch that cancels the first. The response + // doesn't need to be delayed, so remove it to make the test run faster. + delete MerinoTestUtils.server.response.delay; + let query2 = query1 + "again"; + await gClient.fetch({ query: query2 }); + + // The sequence number should have been incremented for each fetch, but the + // first won't have reached the server since it was immediately canceled. + MerinoTestUtils.server.checkAndClearRequests([ + { + params: { + [SEARCH_PARAMS.QUERY]: query2, + [SEARCH_PARAMS.SEQUENCE_NUMBER]: 2 * i + 1, + }, + }, + ]); + } + + gClient.resetSession(); +}); + +// When a network error occurs, the sequence number should still be incremented. +add_task(async function networkError() { + for (let i = 0; i < 3; i++) { + // Do a fetch that fails with a network error. + let query1 = "search" + i; + await MerinoTestUtils.server.withNetworkError(async () => { + await gClient.fetch({ query: query1 }); + }); + + Assert.equal( + gClient.lastFetchStatus, + "network_error", + "The request failed with a network error" + ); + + // Do another fetch that successfully finishes. + let query2 = query1 + "again"; + await gClient.fetch({ query: query2 }); + + Assert.equal( + gClient.lastFetchStatus, + "success", + "The request completed successfully" + ); + + // Only the second request should have been received but the sequence number + // should have been incremented for each. + MerinoTestUtils.server.checkAndClearRequests([ + { + params: { + [SEARCH_PARAMS.QUERY]: query2, + [SEARCH_PARAMS.SEQUENCE_NUMBER]: 2 * i + 1, + }, + }, + ]); + } + + gClient.resetSession(); +}); + +// When the server returns a response with an HTTP error, the sequence number +// should be incremented. +add_task(async function httpError() { + for (let i = 0; i < 3; i++) { + // Do a fetch that fails with an HTTP error. + MerinoTestUtils.server.response.status = 500; + let query1 = "search" + i; + await gClient.fetch({ query: query1 }); + + Assert.equal( + gClient.lastFetchStatus, + "http_error", + "The last request failed with a network error" + ); + + // Do another fetch that successfully finishes. + MerinoTestUtils.server.response.status = 200; + let query2 = query1 + "again"; + await gClient.fetch({ query: query2 }); + + Assert.equal( + gClient.lastFetchStatus, + "success", + "The last request completed successfully" + ); + + // Both requests should have been received and the sequence number should + // have been incremented for each. + MerinoTestUtils.server.checkAndClearRequests([ + { + params: { + [SEARCH_PARAMS.QUERY]: query1, + [SEARCH_PARAMS.SEQUENCE_NUMBER]: 2 * i, + }, + }, + { + params: { + [SEARCH_PARAMS.QUERY]: query2, + [SEARCH_PARAMS.SEQUENCE_NUMBER]: 2 * i + 1, + }, + }, + ]); + + MerinoTestUtils.server.reset(); + } + + gClient.resetSession(); +}); + +// When the client times out waiting for a response but later receives it and no +// other fetch happens in the meantime, the sequence number should be +// incremented. +add_task(async function clientTimeout_wait() { + for (let i = 0; i < 3; i++) { + // Do a fetch that causes the client to time out. + MerinoTestUtils.server.response.delay = + 2 * UrlbarPrefs.get("merino.timeoutMs"); + let responsePromise = gClient.waitForNextResponse(); + let query1 = "search" + i; + await gClient.fetch({ query: query1 }); + + Assert.equal( + gClient.lastFetchStatus, + "timeout", + "The last request failed with a client timeout" + ); + + // Wait for the client to receive the response. + await responsePromise; + + // Do another fetch that successfully finishes. + delete MerinoTestUtils.server.response.delay; + let query2 = query1 + "again"; + await gClient.fetch({ query: query2 }); + + Assert.equal( + gClient.lastFetchStatus, + "success", + "The last request completed successfully" + ); + + MerinoTestUtils.server.checkAndClearRequests([ + { + params: { + [SEARCH_PARAMS.QUERY]: query1, + [SEARCH_PARAMS.SEQUENCE_NUMBER]: 2 * i, + }, + }, + { + params: { + [SEARCH_PARAMS.QUERY]: query2, + [SEARCH_PARAMS.SEQUENCE_NUMBER]: 2 * i + 1, + }, + }, + ]); + } + + gClient.resetSession(); +}); + +// When the client times out waiting for a response and a second fetch starts +// before the response is received, the first fetch should be canceled but the +// sequence number should still be incremented. +add_task(async function clientTimeout_canceled() { + for (let i = 0; i < 3; i++) { + // Do a fetch that causes the client to time out. + MerinoTestUtils.server.response.delay = + 2 * UrlbarPrefs.get("merino.timeoutMs"); + let query1 = "search" + i; + await gClient.fetch({ query: query1 }); + + Assert.equal( + gClient.lastFetchStatus, + "timeout", + "The last request failed with a client timeout" + ); + + // Do another fetch that successfully finishes. + delete MerinoTestUtils.server.response.delay; + let query2 = query1 + "again"; + await gClient.fetch({ query: query2 }); + + Assert.equal( + gClient.lastFetchStatus, + "success", + "The last request completed successfully" + ); + + MerinoTestUtils.server.checkAndClearRequests([ + { + params: { + [SEARCH_PARAMS.QUERY]: query1, + [SEARCH_PARAMS.SEQUENCE_NUMBER]: 2 * i, + }, + }, + { + params: { + [SEARCH_PARAMS.QUERY]: query2, + [SEARCH_PARAMS.SEQUENCE_NUMBER]: 2 * i + 1, + }, + }, + ]); + } + + gClient.resetSession(); +}); + +// When the session times out, the next fetch should use a new session ID and +// the sequence number should be reset. +add_task(async function sessionTimeout() { + // Set the session timeout to something reasonable to test. + let originalTimeoutMs = gClient.sessionTimeoutMs; + gClient.sessionTimeoutMs = 500; + + // Do a fetch. + let query1 = "search"; + await gClient.fetch({ query: query1 }); + + // Wait for the session to time out. + await gClient.waitForNextSessionReset(); + + Assert.strictEqual( + gClient.sessionID, + null, + "sessionID is null after session timeout" + ); + Assert.strictEqual( + gClient.sequenceNumber, + 0, + "sequenceNumber is zero after session timeout" + ); + Assert.strictEqual( + gClient._test_sessionTimer, + null, + "sessionTimer is null after session timeout" + ); + + // Do another fetch. + let query2 = query1 + "again"; + await gClient.fetch({ query: query2 }); + + // The second request's sequence number should be zero due to the session + // timeout. + MerinoTestUtils.server.checkAndClearRequests([ + { + params: { + [SEARCH_PARAMS.QUERY]: query1, + [SEARCH_PARAMS.SEQUENCE_NUMBER]: 0, + }, + }, + { + params: { + [SEARCH_PARAMS.QUERY]: query2, + [SEARCH_PARAMS.SEQUENCE_NUMBER]: 0, + }, + }, + ]); + + Assert.ok( + gClient.sessionID, + "sessionID is non-null after first request in a new session" + ); + Assert.equal( + gClient.sequenceNumber, + 1, + "sequenceNumber is one after first request in a new session" + ); + Assert.ok( + gClient._test_sessionTimer, + "sessionTimer is non-null after first request in a new session" + ); + + gClient.sessionTimeoutMs = originalTimeoutMs; + gClient.resetSession(); +}); diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest.js new file mode 100644 index 0000000000..00e9820fab --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest.js @@ -0,0 +1,1341 @@ +/* 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/. */ + +// Basic tests for the quick suggest provider using the remote settings source. +// See also test_quicksuggest_merino.js. + +"use strict"; + +const TELEMETRY_REMOTE_SETTINGS_LATENCY = + "FX_URLBAR_QUICK_SUGGEST_REMOTE_SETTINGS_LATENCY_MS"; + +const SPONSORED_SEARCH_STRING = "frab"; +const NONSPONSORED_SEARCH_STRING = "nonspon"; + +const HTTP_SEARCH_STRING = "http prefix"; +const HTTPS_SEARCH_STRING = "https prefix"; +const PREFIX_SUGGESTIONS_STRIPPED_URL = "example.com/prefix-test"; + +const { TIMESTAMP_TEMPLATE, TIMESTAMP_LENGTH } = QuickSuggest; +const TIMESTAMP_SEARCH_STRING = "timestamp"; +const TIMESTAMP_SUGGESTION_URL = `http://example.com/timestamp-${TIMESTAMP_TEMPLATE}`; +const TIMESTAMP_SUGGESTION_CLICK_URL = `http://click.reporting.test.com/timestamp-${TIMESTAMP_TEMPLATE}-foo`; + +const REMOTE_SETTINGS_RESULTS = [ + { + id: 1, + url: "http://test.com/q=frabbits", + title: "frabbits", + keywords: [SPONSORED_SEARCH_STRING], + click_url: "http://click.reporting.test.com/", + impression_url: "http://impression.reporting.test.com/", + advertiser: "TestAdvertiser", + iab_category: "22 - Shopping", + }, + { + id: 2, + url: "http://test.com/?q=nonsponsored", + title: "Non-Sponsored", + keywords: [NONSPONSORED_SEARCH_STRING], + click_url: "http://click.reporting.test.com/nonsponsored", + impression_url: "http://impression.reporting.test.com/nonsponsored", + advertiser: "TestAdvertiserNonSponsored", + iab_category: "5 - Education", + }, + { + id: 3, + url: "http://" + PREFIX_SUGGESTIONS_STRIPPED_URL, + title: "http suggestion", + keywords: [HTTP_SEARCH_STRING], + click_url: "http://click.reporting.test.com/prefix", + impression_url: "http://impression.reporting.test.com/prefix", + advertiser: "TestAdvertiserPrefix", + iab_category: "22 - Shopping", + }, + { + id: 4, + url: "https://" + PREFIX_SUGGESTIONS_STRIPPED_URL, + title: "https suggestion", + keywords: [HTTPS_SEARCH_STRING], + click_url: "http://click.reporting.test.com/prefix", + impression_url: "http://impression.reporting.test.com/prefix", + advertiser: "TestAdvertiserPrefix", + iab_category: "22 - Shopping", + }, + { + id: 5, + url: TIMESTAMP_SUGGESTION_URL, + title: "Timestamp suggestion", + keywords: [TIMESTAMP_SEARCH_STRING], + click_url: TIMESTAMP_SUGGESTION_CLICK_URL, + impression_url: "http://impression.reporting.test.com/timestamp", + advertiser: "TestAdvertiserTimestamp", + iab_category: "22 - Shopping", + }, +]; + +const EXPECTED_SPONSORED_RESULT = { + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + heuristic: false, + payload: { + telemetryType: "adm_sponsored", + qsSuggestion: "frab", + title: "frabbits", + url: "http://test.com/q=frabbits", + originalUrl: "http://test.com/q=frabbits", + icon: null, + sponsoredImpressionUrl: "http://impression.reporting.test.com/", + sponsoredClickUrl: "http://click.reporting.test.com/", + sponsoredBlockId: 1, + sponsoredAdvertiser: "TestAdvertiser", + sponsoredIabCategory: "22 - Shopping", + isSponsored: true, + helpUrl: QuickSuggest.HELP_URL, + helpL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-learn-more-about-firefox-suggest" + : "firefox-suggest-urlbar-learn-more", + }, + isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"), + blockL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-dismiss-firefox-suggest" + : "firefox-suggest-urlbar-block", + }, + displayUrl: "http://test.com/q=frabbits", + source: "remote-settings", + }, +}; + +const EXPECTED_NONSPONSORED_RESULT = { + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + heuristic: false, + payload: { + telemetryType: "adm_nonsponsored", + qsSuggestion: "nonspon", + title: "Non-Sponsored", + url: "http://test.com/?q=nonsponsored", + originalUrl: "http://test.com/?q=nonsponsored", + icon: null, + sponsoredImpressionUrl: "http://impression.reporting.test.com/nonsponsored", + sponsoredClickUrl: "http://click.reporting.test.com/nonsponsored", + sponsoredBlockId: 2, + sponsoredAdvertiser: "TestAdvertiserNonSponsored", + sponsoredIabCategory: "5 - Education", + isSponsored: false, + helpUrl: QuickSuggest.HELP_URL, + helpL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-learn-more-about-firefox-suggest" + : "firefox-suggest-urlbar-learn-more", + }, + isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"), + blockL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-dismiss-firefox-suggest" + : "firefox-suggest-urlbar-block", + }, + displayUrl: "http://test.com/?q=nonsponsored", + source: "remote-settings", + }, +}; + +const EXPECTED_HTTP_RESULT = { + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + heuristic: false, + payload: { + telemetryType: "adm_sponsored", + qsSuggestion: HTTP_SEARCH_STRING, + title: "http suggestion", + url: "http://" + PREFIX_SUGGESTIONS_STRIPPED_URL, + originalUrl: "http://" + PREFIX_SUGGESTIONS_STRIPPED_URL, + icon: null, + sponsoredImpressionUrl: "http://impression.reporting.test.com/prefix", + sponsoredClickUrl: "http://click.reporting.test.com/prefix", + sponsoredBlockId: 3, + sponsoredAdvertiser: "TestAdvertiserPrefix", + sponsoredIabCategory: "22 - Shopping", + isSponsored: true, + helpUrl: QuickSuggest.HELP_URL, + helpL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-learn-more-about-firefox-suggest" + : "firefox-suggest-urlbar-learn-more", + }, + isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"), + blockL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-dismiss-firefox-suggest" + : "firefox-suggest-urlbar-block", + }, + displayUrl: "http://" + PREFIX_SUGGESTIONS_STRIPPED_URL, + source: "remote-settings", + }, +}; + +const EXPECTED_HTTPS_RESULT = { + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + heuristic: false, + payload: { + telemetryType: "adm_sponsored", + qsSuggestion: HTTPS_SEARCH_STRING, + title: "https suggestion", + url: "https://" + PREFIX_SUGGESTIONS_STRIPPED_URL, + originalUrl: "https://" + PREFIX_SUGGESTIONS_STRIPPED_URL, + icon: null, + sponsoredImpressionUrl: "http://impression.reporting.test.com/prefix", + sponsoredClickUrl: "http://click.reporting.test.com/prefix", + sponsoredBlockId: 4, + sponsoredAdvertiser: "TestAdvertiserPrefix", + sponsoredIabCategory: "22 - Shopping", + isSponsored: true, + helpUrl: QuickSuggest.HELP_URL, + helpL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-learn-more-about-firefox-suggest" + : "firefox-suggest-urlbar-learn-more", + }, + isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"), + blockL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-dismiss-firefox-suggest" + : "firefox-suggest-urlbar-block", + }, + displayUrl: PREFIX_SUGGESTIONS_STRIPPED_URL, + source: "remote-settings", + }, +}; + +add_setup(async function init() { + UrlbarPrefs.set("quicksuggest.enabled", true); + UrlbarPrefs.set("quicksuggest.shouldShowOnboardingDialog", false); + UrlbarPrefs.set("quicksuggest.remoteSettings.enabled", true); + UrlbarPrefs.set("merino.enabled", false); + + // Install a default test engine. + let engine = await addTestSuggestionsEngine(); + await Services.search.setDefault( + engine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + const testDataTypeResults = [ + Object.assign({}, REMOTE_SETTINGS_RESULTS[0], { title: "test-data-type" }), + ]; + + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + remoteSettingsResults: [ + { + type: "data", + attachment: REMOTE_SETTINGS_RESULTS, + }, + { + type: "test-data-type", + attachment: testDataTypeResults, + }, + ], + }); +}); + +// Tests with only non-sponsored suggestions enabled with a matching search +// string. +add_task(async function nonsponsoredOnly_match() { + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", false); + + let context = createContext(NONSPONSORED_SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [EXPECTED_NONSPONSORED_RESULT], + }); +}); + +// Tests with only non-sponsored suggestions enabled with a non-matching search +// string. +add_task(async function nonsponsoredOnly_noMatch() { + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", false); + + let context = createContext(SPONSORED_SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ context, matches: [] }); +}); + +// Tests with only sponsored suggestions enabled with a matching search string. +add_task(async function sponsoredOnly_sponsored() { + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + + let context = createContext(SPONSORED_SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [EXPECTED_SPONSORED_RESULT], + }); +}); + +// Tests with only sponsored suggestions enabled with a non-matching search +// string. +add_task(async function sponsoredOnly_nonsponsored() { + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + + let context = createContext(NONSPONSORED_SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ context, matches: [] }); +}); + +// Tests with both sponsored and non-sponsored suggestions enabled with a +// search string that matches the sponsored suggestion. +add_task(async function both_sponsored() { + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + + let context = createContext(SPONSORED_SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [EXPECTED_SPONSORED_RESULT], + }); +}); + +// Tests with both sponsored and non-sponsored suggestions enabled with a +// search string that matches the non-sponsored suggestion. +add_task(async function both_nonsponsored() { + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + + let context = createContext(NONSPONSORED_SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [EXPECTED_NONSPONSORED_RESULT], + }); +}); + +// Tests with both sponsored and non-sponsored suggestions enabled with a +// search string that doesn't match either suggestion. +add_task(async function both_noMatch() { + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + + let context = createContext("this doesn't match anything", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ context, matches: [] }); +}); + +// Tests with both the main and sponsored prefs disabled with a search string +// that matches the sponsored suggestion. +add_task(async function neither_sponsored() { + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", false); + + let context = createContext(SPONSORED_SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ context, matches: [] }); +}); + +// Tests with both the main and sponsored prefs disabled with a search string +// that matches the non-sponsored suggestion. +add_task(async function neither_nonsponsored() { + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", false); + + let context = createContext(NONSPONSORED_SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ context, matches: [] }); +}); + +// Search string matching should be case insensitive and ignore leading spaces. +add_task(async function caseInsensitiveAndLeadingSpaces() { + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + + let context = createContext(" " + SPONSORED_SEARCH_STRING.toUpperCase(), { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [EXPECTED_SPONSORED_RESULT], + }); +}); + +// The provider should not be active for search strings that are empty or +// contain only spaces. +add_task(async function emptySearchStringsAndSpaces() { + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + + let searchStrings = ["", " ", " ", " "]; + for (let str of searchStrings) { + let msg = JSON.stringify(str) + ` (length = ${str.length})`; + info("Testing search string: " + msg); + + let context = createContext(str, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [], + }); + Assert.ok( + !UrlbarProviderQuickSuggest.isActive(context), + "Provider should not be active for search string: " + msg + ); + } +}); + +// Results should be returned even when `browser.search.suggest.enabled` is +// false. +add_task(async function browser_search_suggest_enabled() { + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + UrlbarPrefs.set("browser.search.suggest.enabled", false); + + let context = createContext(SPONSORED_SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [EXPECTED_SPONSORED_RESULT], + }); + + UrlbarPrefs.clear("browser.search.suggest.enabled"); +}); + +// Results should be returned even when `browser.urlbar.suggest.searches` is +// false. +add_task(async function browser_search_suggest_enabled() { + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + UrlbarPrefs.set("suggest.searches", false); + + let context = createContext(SPONSORED_SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [EXPECTED_SPONSORED_RESULT], + }); + + UrlbarPrefs.clear("suggest.searches"); +}); + +// Neither sponsored nor non-sponsored results should appear in private contexts +// even when suggestions in private windows are enabled. +add_task(async function privateContext() { + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + + for (let privateSuggestionsEnabled of [true, false]) { + UrlbarPrefs.set( + "browser.search.suggest.enabled.private", + privateSuggestionsEnabled + ); + let context = createContext(SPONSORED_SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: true, + }); + await check_results({ + context, + matches: [], + }); + } + + UrlbarPrefs.clear("browser.search.suggest.enabled.private"); +}); + +// When search suggestions come before general results and the only general +// result is a quick suggest result, it should come last. +add_task(async function suggestionsBeforeGeneral_only() { + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + UrlbarPrefs.set("browser.search.suggest.enabled", true); + UrlbarPrefs.set("suggest.searches", true); + UrlbarPrefs.set("showSearchSuggestionsFirst", true); + + let context = createContext(SPONSORED_SEARCH_STRING, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + heuristic: true, + query: SPONSORED_SEARCH_STRING, + engineName: Services.search.defaultEngine.name, + }), + makeSearchResult(context, { + query: SPONSORED_SEARCH_STRING, + suggestion: SPONSORED_SEARCH_STRING + " foo", + engineName: Services.search.defaultEngine.name, + }), + makeSearchResult(context, { + query: SPONSORED_SEARCH_STRING, + suggestion: SPONSORED_SEARCH_STRING + " bar", + engineName: Services.search.defaultEngine.name, + }), + EXPECTED_SPONSORED_RESULT, + ], + }); + + UrlbarPrefs.clear("browser.search.suggest.enabled"); + UrlbarPrefs.clear("suggest.searches"); + UrlbarPrefs.clear("showSearchSuggestionsFirst"); +}); + +// When search suggestions come before general results and there are other +// general results besides quick suggest, the quick suggest result should come +// last. +add_task(async function suggestionsBeforeGeneral_others() { + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + UrlbarPrefs.set("browser.search.suggest.enabled", true); + UrlbarPrefs.set("suggest.searches", true); + UrlbarPrefs.set("showSearchSuggestionsFirst", true); + + let context = createContext(SPONSORED_SEARCH_STRING, { isPrivate: false }); + + // Add some history that will match our query below. + let maxResults = UrlbarPrefs.get("maxRichResults"); + let historyResults = []; + for (let i = 0; i < maxResults; i++) { + let url = "http://example.com/" + SPONSORED_SEARCH_STRING + i; + historyResults.push( + makeVisitResult(context, { + uri: url, + title: "test visit for " + url, + }) + ); + await PlacesTestUtils.addVisits(url); + } + historyResults = historyResults.reverse().slice(0, historyResults.length - 4); + + await check_results({ + context, + matches: [ + makeSearchResult(context, { + heuristic: true, + query: SPONSORED_SEARCH_STRING, + engineName: Services.search.defaultEngine.name, + }), + makeSearchResult(context, { + query: SPONSORED_SEARCH_STRING, + suggestion: SPONSORED_SEARCH_STRING + " foo", + engineName: Services.search.defaultEngine.name, + }), + makeSearchResult(context, { + query: SPONSORED_SEARCH_STRING, + suggestion: SPONSORED_SEARCH_STRING + " bar", + engineName: Services.search.defaultEngine.name, + }), + ...historyResults, + EXPECTED_SPONSORED_RESULT, + ], + }); + + UrlbarPrefs.clear("browser.search.suggest.enabled"); + UrlbarPrefs.clear("suggest.searches"); + UrlbarPrefs.clear("showSearchSuggestionsFirst"); + await PlacesUtils.history.clear(); +}); + +// When general results come before search suggestions and the only general +// result is a quick suggest result, it should come before suggestions. +add_task(async function generalBeforeSuggestions_only() { + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + UrlbarPrefs.set("browser.search.suggest.enabled", true); + UrlbarPrefs.set("suggest.searches", true); + UrlbarPrefs.set("showSearchSuggestionsFirst", false); + + let context = createContext(SPONSORED_SEARCH_STRING, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + heuristic: true, + query: SPONSORED_SEARCH_STRING, + engineName: Services.search.defaultEngine.name, + }), + EXPECTED_SPONSORED_RESULT, + makeSearchResult(context, { + query: SPONSORED_SEARCH_STRING, + suggestion: SPONSORED_SEARCH_STRING + " foo", + engineName: Services.search.defaultEngine.name, + }), + makeSearchResult(context, { + query: SPONSORED_SEARCH_STRING, + suggestion: SPONSORED_SEARCH_STRING + " bar", + engineName: Services.search.defaultEngine.name, + }), + ], + }); + + UrlbarPrefs.clear("browser.search.suggest.enabled"); + UrlbarPrefs.clear("suggest.searches"); + UrlbarPrefs.clear("showSearchSuggestionsFirst"); +}); + +// When general results come before search suggestions and there are other +// general results besides quick suggest, the quick suggest result should be the +// last general result. +add_task(async function generalBeforeSuggestions_others() { + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + UrlbarPrefs.set("browser.search.suggest.enabled", true); + UrlbarPrefs.set("suggest.searches", true); + UrlbarPrefs.set("showSearchSuggestionsFirst", false); + + let context = createContext(SPONSORED_SEARCH_STRING, { isPrivate: false }); + + // Add some history that will match our query below. + let maxResults = UrlbarPrefs.get("maxRichResults"); + let historyResults = []; + for (let i = 0; i < maxResults; i++) { + let url = "http://example.com/" + SPONSORED_SEARCH_STRING + i; + historyResults.push( + makeVisitResult(context, { + uri: url, + title: "test visit for " + url, + }) + ); + await PlacesTestUtils.addVisits(url); + } + historyResults = historyResults.reverse().slice(0, historyResults.length - 4); + + await check_results({ + context, + matches: [ + makeSearchResult(context, { + heuristic: true, + query: SPONSORED_SEARCH_STRING, + engineName: Services.search.defaultEngine.name, + }), + ...historyResults, + EXPECTED_SPONSORED_RESULT, + makeSearchResult(context, { + query: SPONSORED_SEARCH_STRING, + suggestion: SPONSORED_SEARCH_STRING + " foo", + engineName: Services.search.defaultEngine.name, + }), + makeSearchResult(context, { + query: SPONSORED_SEARCH_STRING, + suggestion: SPONSORED_SEARCH_STRING + " bar", + engineName: Services.search.defaultEngine.name, + }), + ], + }); + + UrlbarPrefs.clear("browser.search.suggest.enabled"); + UrlbarPrefs.clear("suggest.searches"); + UrlbarPrefs.clear("showSearchSuggestionsFirst"); + await PlacesUtils.history.clear(); +}); + +add_task(async function dedupeAgainstURL_samePrefix() { + await doDedupeAgainstURLTest({ + searchString: HTTP_SEARCH_STRING, + expectedQuickSuggestResult: EXPECTED_HTTP_RESULT, + otherPrefix: "http://", + expectOther: false, + }); +}); + +add_task(async function dedupeAgainstURL_higherPrefix() { + await doDedupeAgainstURLTest({ + searchString: HTTPS_SEARCH_STRING, + expectedQuickSuggestResult: EXPECTED_HTTPS_RESULT, + otherPrefix: "http://", + expectOther: false, + }); +}); + +add_task(async function dedupeAgainstURL_lowerPrefix() { + await doDedupeAgainstURLTest({ + searchString: HTTP_SEARCH_STRING, + expectedQuickSuggestResult: EXPECTED_HTTP_RESULT, + otherPrefix: "https://", + expectOther: true, + }); +}); + +/** + * Tests how the muxer dedupes URL results against quick suggest results. + * Depending on prefix rank, quick suggest results should be preferred over + * other URL results with the same stripped URL: Other results should be + * discarded when their prefix rank is lower than the prefix rank of the quick + * suggest. They should not be discarded when their prefix rank is higher, and + * in that case both results should be included. + * + * This function adds a visit to the URL formed by the given `otherPrefix` and + * `PREFIX_SUGGESTIONS_STRIPPED_URL`. The visit's title will be set to the given + * `searchString` so that both the visit and the quick suggest will match it. + * + * @param {object} options + * Options object. + * @param {string} options.searchString + * The search string that should trigger one of the mock prefix-test quick + * suggest results. + * @param {object} options.expectedQuickSuggestResult + * The expected quick suggest result. + * @param {string} options.otherPrefix + * The visit will be created with a URL with this prefix, e.g., "http://". + * @param {boolean} options.expectOther + * Whether the visit result should appear in the final results. + */ +async function doDedupeAgainstURLTest({ + searchString, + expectedQuickSuggestResult, + otherPrefix, + expectOther, +}) { + // Disable search suggestions. + UrlbarPrefs.set("suggest.searches", false); + + // Add a visit that will match our query below. + let otherURL = otherPrefix + PREFIX_SUGGESTIONS_STRIPPED_URL; + await PlacesTestUtils.addVisits({ uri: otherURL, title: searchString }); + + // First, do a search with quick suggest disabled to make sure the search + // string matches the visit. + info("Doing first query"); + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", false); + let context = createContext(searchString, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + heuristic: true, + query: searchString, + engineName: Services.search.defaultEngine.name, + }), + makeVisitResult(context, { + uri: otherURL, + title: searchString, + }), + ], + }); + + // Now do another search with quick suggest enabled. + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + + context = createContext(searchString, { isPrivate: false }); + + let expectedResults = [ + makeSearchResult(context, { + heuristic: true, + query: searchString, + engineName: Services.search.defaultEngine.name, + }), + ]; + if (expectOther) { + expectedResults.push( + makeVisitResult(context, { + uri: otherURL, + title: searchString, + }) + ); + } + expectedResults.push(expectedQuickSuggestResult); + + info("Doing second query"); + await check_results({ context, matches: expectedResults }); + + UrlbarPrefs.clear("suggest.quicksuggest.nonsponsored"); + UrlbarPrefs.clear("suggest.quicksuggest.sponsored"); + UrlbarPrefs.clear("suggest.searches"); + await PlacesUtils.history.clear(); +} + +// Tests the remote settings latency histogram. +add_task(async function latencyTelemetry() { + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + + let histogram = Services.telemetry.getHistogramById( + TELEMETRY_REMOTE_SETTINGS_LATENCY + ); + histogram.clear(); + + let context = createContext(SPONSORED_SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [EXPECTED_SPONSORED_RESULT], + }); + + // In the latency histogram, there should be a single value across all + // buckets. + Assert.deepEqual( + Object.values(histogram.snapshot().values).filter(v => v > 0), + [1], + "Latency histogram updated after search" + ); + Assert.ok( + !TelemetryStopwatch.running(TELEMETRY_REMOTE_SETTINGS_LATENCY, context), + "Stopwatch not running after search" + ); +}); + +// Tests setup and teardown of the remote settings client depending on whether +// quick suggest is enabled. +add_task(async function setupAndTeardown() { + // Disable the suggest prefs so the settings client starts out torn down. + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", false); + Assert.ok( + !QuickSuggestRemoteSettings.rs, + "Settings client is null after disabling suggest prefs" + ); + + // Setting one of the suggest prefs should cause the client to be set up. We + // assume all previous tasks left `quicksuggest.enabled` true (from the init + // task). + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + Assert.ok( + QuickSuggestRemoteSettings.rs, + "Settings client is non-null after enabling suggest.quicksuggest.nonsponsored" + ); + + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false); + Assert.ok( + !QuickSuggestRemoteSettings.rs, + "Settings client is null after disabling suggest.quicksuggest.nonsponsored" + ); + + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + Assert.ok( + QuickSuggestRemoteSettings.rs, + "Settings client is non-null after enabling suggest.quicksuggest.sponsored" + ); + + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + Assert.ok( + QuickSuggestRemoteSettings.rs, + "Settings client remains non-null after enabling suggest.quicksuggest.nonsponsored" + ); + + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false); + Assert.ok( + QuickSuggestRemoteSettings.rs, + "Settings client remains non-null after disabling suggest.quicksuggest.nonsponsored" + ); + + UrlbarPrefs.set("suggest.quicksuggest.sponsored", false); + Assert.ok( + !QuickSuggestRemoteSettings.rs, + "Settings client is null after disabling suggest.quicksuggest.sponsored" + ); + + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + Assert.ok( + QuickSuggestRemoteSettings.rs, + "Settings client is non-null after enabling suggest.quicksuggest.nonsponsored" + ); + + UrlbarPrefs.set("quicksuggest.enabled", false); + Assert.ok( + !QuickSuggestRemoteSettings.rs, + "Settings client is null after disabling quicksuggest.enabled" + ); + + // Leave the prefs in the same state as when the task started. + UrlbarPrefs.clear("suggest.quicksuggest.nonsponsored"); + UrlbarPrefs.clear("suggest.quicksuggest.sponsored"); + UrlbarPrefs.set("quicksuggest.enabled", true); + Assert.ok( + !QuickSuggestRemoteSettings.rs, + "Settings client remains null at end of task" + ); +}); + +// Timestamp templates in URLs should be replaced with real timestamps. +add_task(async function timestamps() { + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + + // Do a search. + let context = createContext(TIMESTAMP_SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + let controller = UrlbarTestUtils.newMockController({ + input: { + isPrivate: context.isPrivate, + onFirstResult() { + return false; + }, + getSearchSource() { + return "dummy-search-source"; + }, + window: { + location: { + href: AppConstants.BROWSER_CHROME_URL, + }, + }, + }, + }); + await controller.startQuery(context); + + // Should be one quick suggest result. + Assert.equal(context.results.length, 1, "One result returned"); + let result = context.results[0]; + + QuickSuggestTestUtils.assertTimestampsReplaced(result, { + url: TIMESTAMP_SUGGESTION_URL, + sponsoredClickUrl: TIMESTAMP_SUGGESTION_CLICK_URL, + }); +}); + +// Real quick suggest URLs include a timestamp template that +// UrlbarProviderQuickSuggest fills in when it fetches suggestions. When the +// user picks a quick suggest, its URL with its particular timestamp is added to +// history. If the user triggers the quick suggest again later, its new +// timestamp may be different from the one in the user's history. In that case, +// the two URLs should be treated as dupes and only the quick suggest should be +// shown, not the URL from history. +add_task(async function dedupeAgainstURL_timestamps() { + // Disable search suggestions. + UrlbarPrefs.set("suggest.searches", false); + + // Add a visit that will match the query below and dupe the quick suggest. + let dupeURL = TIMESTAMP_SUGGESTION_URL.replace( + TIMESTAMP_TEMPLATE, + "2013051113" + ); + + // Add other visits that will match the query and almost dupe the quick + // suggest but not quite because they have invalid timestamps. + let badTimestamps = [ + // not numeric digits + "x".repeat(TIMESTAMP_LENGTH), + // too few digits + "5".repeat(TIMESTAMP_LENGTH - 1), + // empty string, too few digits + "", + ]; + let badTimestampURLs = badTimestamps.map(str => + TIMESTAMP_SUGGESTION_URL.replace(TIMESTAMP_TEMPLATE, str) + ); + + await PlacesTestUtils.addVisits( + [dupeURL, ...badTimestampURLs].map(uri => ({ + uri, + title: TIMESTAMP_SEARCH_STRING, + })) + ); + + // First, do a search with quick suggest disabled to make sure the search + // string matches all the other URLs. + info("Doing first query"); + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", false); + let context = createContext(TIMESTAMP_SEARCH_STRING, { isPrivate: false }); + + let expectedHeuristic = makeSearchResult(context, { + heuristic: true, + query: TIMESTAMP_SEARCH_STRING, + engineName: Services.search.defaultEngine.name, + }); + let expectedDupeResult = makeVisitResult(context, { + uri: dupeURL, + title: TIMESTAMP_SEARCH_STRING, + }); + let expectedBadTimestampResults = [...badTimestampURLs].reverse().map(uri => + makeVisitResult(context, { + uri, + title: TIMESTAMP_SEARCH_STRING, + }) + ); + + await check_results({ + context, + matches: [ + expectedHeuristic, + ...expectedBadTimestampResults, + expectedDupeResult, + ], + }); + + // Now do another search with quick suggest enabled. + info("Doing second query"); + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + context = createContext(TIMESTAMP_SEARCH_STRING, { isPrivate: false }); + + // The expected quick suggest result without the timestamp-related payload + // properties. + let expectedQuickSuggest = { + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + heuristic: false, + payload: { + telemetryType: "adm_sponsored", + originalUrl: TIMESTAMP_SUGGESTION_URL, + qsSuggestion: TIMESTAMP_SEARCH_STRING, + title: "Timestamp suggestion", + icon: null, + sponsoredImpressionUrl: "http://impression.reporting.test.com/timestamp", + sponsoredBlockId: 5, + sponsoredAdvertiser: "TestAdvertiserTimestamp", + sponsoredIabCategory: "22 - Shopping", + isSponsored: true, + helpUrl: QuickSuggest.HELP_URL, + helpL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-learn-more-about-firefox-suggest" + : "firefox-suggest-urlbar-learn-more", + }, + isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"), + blockL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-dismiss-firefox-suggest" + : "firefox-suggest-urlbar-block", + }, + source: "remote-settings", + }, + }; + + let expectedResults = [ + expectedHeuristic, + ...expectedBadTimestampResults, + expectedQuickSuggest, + ]; + + let controller = UrlbarTestUtils.newMockController({ + input: { + isPrivate: false, + onFirstResult() { + return false; + }, + getSearchSource() { + return "dummy-search-source"; + }, + window: { + location: { + href: AppConstants.BROWSER_CHROME_URL, + }, + }, + }, + }); + await controller.startQuery(context); + info("Actual results: " + JSON.stringify(context.results)); + + Assert.equal( + context.results.length, + expectedResults.length, + "Found the expected number of results" + ); + + function getPayload(result, keysToIgnore = []) { + let payload = {}; + for (let [key, value] of Object.entries(result.payload)) { + if (value !== undefined && !keysToIgnore.includes(key)) { + payload[key] = value; + } + } + return payload; + } + + // Check actual vs. expected result properties. + for (let i = 0; i < expectedResults.length; i++) { + let actual = context.results[i]; + let expected = expectedResults[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}` + ); + + // Check payloads except for the last result, which should be the quick + // suggest. + if (i != expectedResults.length - 1) { + Assert.deepEqual( + getPayload(context.results[i]), + getPayload(expectedResults[i]), + "Payload at index " + i + ); + } + } + + // Check the quick suggest's payload excluding the timestamp-related + // properties. + let actualQuickSuggest = context.results[context.results.length - 1]; + let timestampKeys = [ + "displayUrl", + "sponsoredClickUrl", + "url", + "urlTimestampIndex", + ]; + Assert.deepEqual( + getPayload(actualQuickSuggest, timestampKeys), + getPayload(expectedQuickSuggest, timestampKeys), + "Quick suggest payload excluding timestamp-related keys" + ); + + // Now check the timestamps in the payload. + QuickSuggestTestUtils.assertTimestampsReplaced(actualQuickSuggest, { + url: TIMESTAMP_SUGGESTION_URL, + sponsoredClickUrl: TIMESTAMP_SUGGESTION_CLICK_URL, + }); + + // Clean up. + UrlbarPrefs.clear("suggest.quicksuggest.nonsponsored"); + UrlbarPrefs.clear("suggest.quicksuggest.sponsored"); + UrlbarPrefs.clear("suggest.searches"); + await PlacesUtils.history.clear(); +}); + +// Tests the API for blocking suggestions and the backing pref. +add_task(async function blockedSuggestionsAPI() { + // Start with no blocked suggestions. + await QuickSuggest.blockedSuggestions.clear(); + Assert.equal( + QuickSuggest.blockedSuggestions._test_digests.size, + 0, + "blockedSuggestions._test_digests is empty" + ); + Assert.equal( + UrlbarPrefs.get("quicksuggest.blockedDigests"), + "", + "quicksuggest.blockedDigests is an empty string" + ); + + // Make some URLs. + let urls = []; + for (let i = 0; i < 3; i++) { + urls.push("http://example.com/" + i); + } + + // Block each URL in turn and make sure previously blocked URLs are still + // blocked and the remaining URLs are not blocked. + for (let i = 0; i < urls.length; i++) { + await QuickSuggest.blockedSuggestions.add(urls[i]); + for (let j = 0; j < urls.length; j++) { + Assert.equal( + await QuickSuggest.blockedSuggestions.has(urls[j]), + j <= i, + `Suggestion at index ${j} is blocked or not as expected` + ); + } + } + + // Make sure all URLs are blocked for good measure. + for (let url of urls) { + Assert.ok( + await QuickSuggest.blockedSuggestions.has(url), + `Suggestion is blocked: ${url}` + ); + } + + // Check `blockedSuggestions._test_digests` and `quicksuggest.blockedDigests`. + Assert.equal( + QuickSuggest.blockedSuggestions._test_digests.size, + urls.length, + "blockedSuggestions._test_digests has correct size" + ); + let array = JSON.parse(UrlbarPrefs.get("quicksuggest.blockedDigests")); + Assert.ok(Array.isArray(array), "Parsed value of pref is an array"); + Assert.equal(array.length, urls.length, "Array has correct length"); + + // Write some junk to `quicksuggest.blockedDigests`. + // `blockedSuggestions._test_digests` should not be changed and all previously + // blocked URLs should remain blocked. + UrlbarPrefs.set("quicksuggest.blockedDigests", "not a json array"); + await QuickSuggest.blockedSuggestions._test_readyPromise; + for (let url of urls) { + Assert.ok( + await QuickSuggest.blockedSuggestions.has(url), + `Suggestion remains blocked: ${url}` + ); + } + Assert.equal( + QuickSuggest.blockedSuggestions._test_digests.size, + urls.length, + "blockedSuggestions._test_digests still has correct size" + ); + + // Block a new URL. All URLs should remain blocked and the pref should be + // updated. + let newURL = "http://example.com/new-block"; + await QuickSuggest.blockedSuggestions.add(newURL); + urls.push(newURL); + for (let url of urls) { + Assert.ok( + await QuickSuggest.blockedSuggestions.has(url), + `Suggestion is blocked: ${url}` + ); + } + Assert.equal( + QuickSuggest.blockedSuggestions._test_digests.size, + urls.length, + "blockedSuggestions._test_digests has correct size" + ); + array = JSON.parse(UrlbarPrefs.get("quicksuggest.blockedDigests")); + Assert.ok(Array.isArray(array), "Parsed value of pref is an array"); + Assert.equal(array.length, urls.length, "Array has correct length"); + + // Add a new URL digest directly to the JSON'ed array in the pref. + newURL = "http://example.com/direct-to-pref"; + urls.push(newURL); + array = JSON.parse(UrlbarPrefs.get("quicksuggest.blockedDigests")); + array.push(await QuickSuggest.blockedSuggestions._test_getDigest(newURL)); + UrlbarPrefs.set("quicksuggest.blockedDigests", JSON.stringify(array)); + await QuickSuggest.blockedSuggestions._test_readyPromise; + + // All URLs should remain blocked and the new URL should be blocked. + for (let url of urls) { + Assert.ok( + await QuickSuggest.blockedSuggestions.has(url), + `Suggestion is blocked: ${url}` + ); + } + Assert.equal( + QuickSuggest.blockedSuggestions._test_digests.size, + urls.length, + "blockedSuggestions._test_digests has correct size" + ); + + // Clear the pref. All URLs should be unblocked. + UrlbarPrefs.clear("quicksuggest.blockedDigests"); + await QuickSuggest.blockedSuggestions._test_readyPromise; + for (let url of urls) { + Assert.ok( + !(await QuickSuggest.blockedSuggestions.has(url)), + `Suggestion is no longer blocked: ${url}` + ); + } + Assert.equal( + QuickSuggest.blockedSuggestions._test_digests.size, + 0, + "blockedSuggestions._test_digests is now empty" + ); + + // Block all the URLs again and test `blockedSuggestions.clear()`. + for (let url of urls) { + await QuickSuggest.blockedSuggestions.add(url); + } + for (let url of urls) { + Assert.ok( + await QuickSuggest.blockedSuggestions.has(url), + `Suggestion is blocked: ${url}` + ); + } + await QuickSuggest.blockedSuggestions.clear(); + for (let url of urls) { + Assert.ok( + !(await QuickSuggest.blockedSuggestions.has(url)), + `Suggestion is no longer blocked: ${url}` + ); + } + Assert.equal( + QuickSuggest.blockedSuggestions._test_digests.size, + 0, + "blockedSuggestions._test_digests is now empty" + ); +}); + +// Test whether the blocking for remote settings results works. +add_task(async function block() { + for (const result of REMOTE_SETTINGS_RESULTS) { + await QuickSuggest.blockedSuggestions.add(result.url); + } + + for (const result of REMOTE_SETTINGS_RESULTS) { + const context = createContext(result.keywords[0], { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [], + }); + } + + await QuickSuggest.blockedSuggestions.clear(); +}); + +// Makes sure remote settings data is fetched using the correct `type` based on +// the value of the `quickSuggestRemoteSettingsDataType` Nimbus variable. +add_task(async function remoteSettingsDataType() { + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + + for (let dataType of [undefined, "test-data-type"]) { + // Set up a mock Nimbus rollout with the data type. + let value = {}; + if (dataType) { + value.quickSuggestRemoteSettingsDataType = dataType; + } + let cleanUpNimbus = await UrlbarTestUtils.initNimbusFeature(value); + + // Make the result for test data type. + let expected = EXPECTED_SPONSORED_RESULT; + if (dataType) { + expected = JSON.parse(JSON.stringify(expected)); + expected.payload.title = dataType; + } + + // Re-enable to trigger sync from remote settings. + UrlbarPrefs.set("quicksuggest.remoteSettings.enabled", false); + UrlbarPrefs.set("quicksuggest.remoteSettings.enabled", true); + + let context = createContext(SPONSORED_SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [expected], + }); + + await cleanUpNimbus(); + } +}); diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_addons.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_addons.js new file mode 100644 index 0000000000..d667fe35b7 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_addons.js @@ -0,0 +1,728 @@ +/* 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 addon quick suggest results. + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + AddonManager: "resource://gre/modules/AddonManager.sys.mjs", + ExtensionTestCommon: "resource://testing-common/ExtensionTestCommon.sys.mjs", +}); + +const MERINO_SUGGESTIONS = [ + { + provider: "amo", + icon: "icon", + url: "url", + title: "title", + description: "description", + is_top_pick: true, + custom_details: { + amo: { + rating: "5", + number_of_ratings: "1234567", + guid: "test@addon", + }, + }, + }, +]; + +const REMOTE_SETTINGS_RESULTS = [ + { + type: "amo-suggestions", + attachment: [ + { + url: "https://example.com/first-addon", + guid: "first@addon", + icon: "https://example.com/first-addon.svg", + title: "First Addon", + rating: "4.7", + keywords: ["first", "1st", "two words", "a b c"], + description: "Description for the First Addon", + number_of_ratings: 1256, + is_top_pick: true, + }, + { + url: "https://example.com/second-addon", + guid: "second@addon", + icon: "https://example.com/second-addon.svg", + title: "Second Addon", + rating: "1.7", + keywords: ["second", "2nd"], + description: "Description for the Second Addon", + number_of_ratings: 256, + is_top_pick: false, + }, + { + url: "https://example.com/third-addon", + guid: "third@addon", + icon: "https://example.com/third-addon.svg", + title: "Third Addon", + rating: "3.7", + keywords: ["third", "3rd"], + description: "Description for the Third Addon", + number_of_ratings: 3, + }, + ], + }, +]; + +add_setup(async function init() { + UrlbarPrefs.set("quicksuggest.enabled", true); + UrlbarPrefs.set("bestMatch.enabled", true); + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.set("addons.featureGate", true); + + // Disable search suggestions so we don't hit the network. + Services.prefs.setBoolPref("browser.search.suggest.enabled", false); + + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + remoteSettingsResults: REMOTE_SETTINGS_RESULTS, + merinoSuggestions: MERINO_SUGGESTIONS, + }); +}); + +// When non-sponsored suggestions are disabled, addon suggestions should be +// disabled. +add_task(async function nonsponsoredDisabled() { + // Disable sponsored suggestions. Addon suggestions are non-sponsored, so + // doing this should not prevent them from being enabled. + UrlbarPrefs.set("suggest.quicksuggest.sponsored", false); + + // First make sure the suggestion is added when non-sponsored suggestions are + // enabled. + await check_results({ + context: createContext("test", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [ + makeExpectedResult({ + suggestion: MERINO_SUGGESTIONS[0], + source: "merino", + isTopPick: true, + }), + ], + }); + + // Now disable them. + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false); + await check_results({ + context: createContext("test", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [], + }); + + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.clear("suggest.quicksuggest.sponsored"); +}); + +// When addon suggestions specific preference is disabled, addon suggestions +// should not be added. +add_task(async function addonSuggestionsSpecificPrefDisabled() { + const prefs = ["suggest.addons", "addons.featureGate"]; + for (const pref of prefs) { + // First make sure the suggestion is added. + await check_results({ + context: createContext("test", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [ + makeExpectedResult({ + suggestion: MERINO_SUGGESTIONS[0], + source: "merino", + isTopPick: true, + }), + ], + }); + + // Now disable the pref. + UrlbarPrefs.set(pref, false); + await check_results({ + context: createContext("test", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [], + }); + + // Revert. + UrlbarPrefs.set(pref, true); + } +}); + +// Check wheather the addon suggestions will be shown by the setup of Nimbus +// variable. +add_task(async function nimbus() { + // Disable the fature gate. + UrlbarPrefs.set("addons.featureGate", false); + await check_results({ + context: createContext("test", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [], + }); + + // Enable by Nimbus. + const cleanUpNimbusEnable = await UrlbarTestUtils.initNimbusFeature({ + addonsFeatureGate: true, + }); + await check_results({ + context: createContext("test", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [ + makeExpectedResult({ + suggestion: MERINO_SUGGESTIONS[0], + source: "merino", + isTopPick: true, + }), + ], + }); + await cleanUpNimbusEnable(); + + // Enable locally. + UrlbarPrefs.set("addons.featureGate", true); + + // Disable by Nimbus. + const cleanUpNimbusDisable = await UrlbarTestUtils.initNimbusFeature({ + addonsFeatureGate: false, + }); + await check_results({ + context: createContext("test", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [], + }); + await cleanUpNimbusDisable(); + + // Revert. + UrlbarPrefs.set("addons.featureGate", true); +}); + +add_task(async function hideIfAlreadyInstalled() { + // Show suggestion. + await check_results({ + context: createContext("test", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [ + makeExpectedResult({ + suggestion: MERINO_SUGGESTIONS[0], + source: "merino", + isTopPick: true, + }), + ], + }); + + // Install an addon for the suggestion. + const xpi = ExtensionTestCommon.generateXPI({ + manifest: { + browser_specific_settings: { + gecko: { id: "test@addon" }, + }, + }, + }); + const addon = await AddonManager.installTemporaryAddon(xpi); + + // Show suggestion for the addon installed. + await check_results({ + context: createContext("test", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [], + }); + + await addon.uninstall(); + xpi.remove(false); +}); + +add_task(async function remoteSettings() { + const testCases = [ + { + input: "f", + expected: null, + }, + { + input: "fi", + expected: null, + }, + { + input: "fir", + expected: null, + }, + { + input: "firs", + expected: null, + }, + { + input: "first", + expected: makeExpectedResult({ + suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0], + source: "remote-settings", + isTopPick: true, + }), + }, + { + input: "1st", + expected: makeExpectedResult({ + suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0], + source: "remote-settings", + isTopPick: true, + }), + }, + { + input: "t", + expected: null, + }, + { + input: "tw", + expected: null, + }, + { + input: "two", + expected: makeExpectedResult({ + suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0], + source: "remote-settings", + isTopPick: true, + }), + }, + { + input: "two ", + expected: makeExpectedResult({ + suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0], + source: "remote-settings", + isTopPick: true, + }), + }, + { + input: "two w", + expected: makeExpectedResult({ + suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0], + source: "remote-settings", + isTopPick: true, + }), + }, + { + input: "two wo", + expected: makeExpectedResult({ + suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0], + source: "remote-settings", + isTopPick: true, + }), + }, + { + input: "two wor", + expected: makeExpectedResult({ + suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0], + source: "remote-settings", + isTopPick: true, + }), + }, + { + input: "two word", + expected: makeExpectedResult({ + suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0], + source: "remote-settings", + isTopPick: true, + }), + }, + { + input: "two words", + expected: makeExpectedResult({ + suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0], + source: "remote-settings", + isTopPick: true, + }), + }, + { + input: "a", + expected: makeExpectedResult({ + suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0], + source: "remote-settings", + isTopPick: true, + }), + }, + { + input: "a ", + expected: makeExpectedResult({ + suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0], + source: "remote-settings", + isTopPick: true, + }), + }, + { + input: "a b", + expected: makeExpectedResult({ + suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0], + source: "remote-settings", + isTopPick: true, + }), + }, + { + input: "a b ", + expected: makeExpectedResult({ + suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0], + source: "remote-settings", + isTopPick: true, + }), + }, + { + input: "a b c", + expected: makeExpectedResult({ + suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0], + source: "remote-settings", + isTopPick: true, + }), + }, + { + input: "second", + expected: makeExpectedResult({ + suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[1], + source: "remote-settings", + isTopPick: false, + }), + }, + { + input: "2nd", + expected: makeExpectedResult({ + suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[1], + source: "remote-settings", + isTopPick: false, + }), + }, + { + input: "third", + expected: makeExpectedResult({ + suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[2], + source: "remote-settings", + isTopPick: true, + }), + }, + { + input: "3rd", + expected: makeExpectedResult({ + suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[2], + source: "remote-settings", + isTopPick: true, + }), + }, + ]; + + // Disable Merino so we trigger only remote settings suggestions. + UrlbarPrefs.set("quicksuggest.dataCollection.enabled", false); + + for (const { input, expected } of testCases) { + await check_results({ + context: createContext(input, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: expected ? [expected] : [], + }); + } + + UrlbarPrefs.set("quicksuggest.dataCollection.enabled", true); +}); + +add_task(async function merinoIsTopPick() { + const suggestion = JSON.parse(JSON.stringify(MERINO_SUGGESTIONS[0])); + + // is_top_pick is specified as false. + suggestion.is_top_pick = false; + MerinoTestUtils.server.response.body.suggestions = [suggestion]; + await check_results({ + context: createContext("test", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [ + makeExpectedResult({ + suggestion, + source: "merino", + isTopPick: false, + }), + ], + }); + + // is_top_pick is undefined. + delete suggestion.is_top_pick; + MerinoTestUtils.server.response.body.suggestions = [suggestion]; + await check_results({ + context: createContext("test", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [ + makeExpectedResult({ + suggestion, + source: "merino", + isTopPick: true, + }), + ], + }); +}); + +// Tests "show less frequently" with the cap set in remote settings. +add_task(async function showLessFrequently_rs() { + await doShowLessFrequentlyTest({ + rs: { + show_less_frequently_cap: 3, + }, + tests: [ + { + showLessFrequentlyCount: 0, + canShowLessFrequently: true, + searches: { + f: false, + fi: false, + fir: false, + firs: false, + first: true, + t: false, + tw: false, + two: true, + "two ": true, + "two w": true, + "two wo": true, + "two wor": true, + "two word": true, + "two words": true, + a: true, + "a ": true, + "a b": true, + "a b ": true, + "a b c": true, + }, + }, + { + showLessFrequentlyCount: 1, + canShowLessFrequently: true, + searches: { + first: false, + two: false, + a: false, + }, + }, + { + showLessFrequentlyCount: 2, + canShowLessFrequently: true, + searches: { + "two ": false, + "a ": false, + }, + }, + { + showLessFrequentlyCount: 3, + canShowLessFrequently: false, + searches: { + "two w": false, + "a b": false, + }, + }, + { + showLessFrequentlyCount: 3, + canShowLessFrequently: false, + searches: {}, + }, + ], + }); +}); + +// Tests "show less frequently" with the cap set in both Nimbus and remote +// settings. Nimbus should override remote settings. +add_task(async function showLessFrequently_nimbus() { + await doShowLessFrequentlyTest({ + nimbus: { + addonsShowLessFrequentlyCap: 3, + }, + rs: { + show_less_frequently_cap: 10, + }, + tests: [ + { + showLessFrequentlyCount: 0, + canShowLessFrequently: true, + searches: { + a: true, + "a ": true, + "a b": true, + "a b ": true, + "a b c": true, + }, + }, + { + showLessFrequentlyCount: 1, + canShowLessFrequently: true, + searches: { + a: false, + }, + }, + { + showLessFrequentlyCount: 2, + canShowLessFrequently: true, + searches: { + "a ": false, + }, + }, + { + showLessFrequentlyCount: 3, + canShowLessFrequently: false, + searches: { + "a b": false, + }, + }, + { + showLessFrequentlyCount: 3, + canShowLessFrequently: false, + searches: {}, + }, + ], + }); +}); + +/** + * Does a group of searches, increments the `showLessFrequentlyCount`, and + * repeats until all groups are done. The cap can be set by remote settings + * config and/or Nimbus. + * + * @param {object} options + * Options object. + * @param {object} options.tests + * An array where each item describes a group of searches to perform and + * expected state. Each item should look like this: + * `{ showLessFrequentlyCount, canShowLessFrequently, searches }` + * + * {number} showLessFrequentlyCount + * The expected value of `showLessFrequentlyCount` before the group of + * searches is performed. + * {boolean} canShowLessFrequently + * The expected value of `canShowLessFrequently` before the group of + * searches is performed. + * {object} searches + * An object that maps each search string to a boolean that indicates + * whether the first remote settings suggestion should be triggered by the + * search string. `searches` objects are cumulative: The intended use is to + * pass a large initial group of searches in the first search group, and + * then each following `searches` is a diff against the previous. + * @param {object} options.rs + * The remote settings config to set. + * @param {object} options.nimbus + * The Nimbus variables to set. + */ +async function doShowLessFrequentlyTest({ tests, rs = {}, nimbus = {} }) { + // Disable Merino so we trigger only remote settings suggestions. + UrlbarPrefs.set("quicksuggest.dataCollection.enabled", false); + + // We'll be testing with the first remote settings suggestion. + let suggestion = REMOTE_SETTINGS_RESULTS[0].attachment[0]; + + let addonSuggestions = QuickSuggest.getFeature("AddonSuggestions"); + + // Set Nimbus variables and RS config. + let cleanUpNimbus = await UrlbarTestUtils.initNimbusFeature(nimbus); + await QuickSuggestTestUtils.withConfig({ + config: rs, + callback: async () => { + let cumulativeSearches = {}; + + for (let { + showLessFrequentlyCount, + canShowLessFrequently, + searches, + } of tests) { + Assert.equal( + addonSuggestions.showLessFrequentlyCount, + showLessFrequentlyCount, + "showLessFrequentlyCount should be correct initially" + ); + Assert.equal( + UrlbarPrefs.get("addons.showLessFrequentlyCount"), + showLessFrequentlyCount, + "Pref should be correct initially" + ); + Assert.equal( + addonSuggestions.canShowLessFrequently, + canShowLessFrequently, + "canShowLessFrequently should be correct initially" + ); + + // Merge the current `searches` object into the cumulative object. + cumulativeSearches = { + ...cumulativeSearches, + ...searches, + }; + + for (let [searchString, isExpected] of Object.entries( + cumulativeSearches + )) { + await check_results({ + context: createContext(searchString, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: !isExpected + ? [] + : [ + makeExpectedResult({ + suggestion, + source: "remote-settings", + isTopPick: true, + }), + ], + }); + } + + addonSuggestions.incrementShowLessFrequentlyCount(); + } + }, + }); + + await cleanUpNimbus(); + UrlbarPrefs.clear("addons.showLessFrequentlyCount"); + UrlbarPrefs.set("quicksuggest.dataCollection.enabled", true); +} + +function makeExpectedResult({ suggestion, source, isTopPick }) { + let rating; + let number_of_ratings; + if (source === "remote-settings") { + rating = suggestion.rating; + number_of_ratings = suggestion.number_of_ratings; + } else { + rating = suggestion.custom_details.amo.rating; + number_of_ratings = suggestion.custom_details.amo.number_of_ratings; + } + + return { + isBestMatch: isTopPick, + suggestedIndex: isTopPick ? 1 : -1, + type: UrlbarUtils.RESULT_TYPE.DYNAMIC, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + heuristic: false, + payload: { + telemetryType: "amo", + dynamicType: "addons", + title: suggestion.title, + url: suggestion.url, + displayUrl: suggestion.url.replace(/^https:\/\//, ""), + icon: suggestion.icon, + description: suggestion.description, + rating: Number(rating), + reviews: Number(number_of_ratings), + shouldNavigate: true, + helpUrl: QuickSuggest.HELP_URL, + source, + }, + }; +} diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_bestMatch.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_bestMatch.js new file mode 100644 index 0000000000..853073a6c0 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_bestMatch.js @@ -0,0 +1,463 @@ +/* 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 best match quick suggest results. "Best match" refers to two different +// concepts: +// +// (1) The "best match" UI treatment (labeled "top pick" in the UI) that makes a +// result's row larger than usual and sets `suggestedIndex` to 1. +// (2) The quick suggest config in remote settings can contain a `best_match` +// object that tells Firefox to use the best match UI treatment if the +// user's search string is a certain length. +// +// This file tests aspects of both concepts. +// +// See also test_quicksuggest_topPicks.js. "Top picks" refer to a similar +// concept but it is not related to (2). + +"use strict"; + +const MAX_RESULT_COUNT = UrlbarPrefs.get("maxRichResults"); + +// This search string length needs to be >= 4 to trigger its suggestion as a +// best match instead of a usual quick suggest. +const BEST_MATCH_POSITION_SEARCH_STRING = "bestmatchposition"; +const BEST_MATCH_POSITION = Math.round(MAX_RESULT_COUNT / 2); + +const REMOTE_SETTINGS_RESULTS = [ + { + id: 1, + url: "http://example.com/", + title: "Fullkeyword title", + keywords: [ + "fu", + "ful", + "full", + "fullk", + "fullke", + "fullkey", + "fullkeyw", + "fullkeywo", + "fullkeywor", + "fullkeyword", + ], + click_url: "http://example.com/click", + impression_url: "http://example.com/impression", + advertiser: "TestAdvertiser", + }, + { + id: 2, + url: "http://example.com/best-match-position", + title: `${BEST_MATCH_POSITION_SEARCH_STRING} title`, + keywords: [BEST_MATCH_POSITION_SEARCH_STRING], + click_url: "http://example.com/click", + impression_url: "http://example.com/impression", + advertiser: "TestAdvertiser", + position: BEST_MATCH_POSITION, + }, +]; + +const EXPECTED_BEST_MATCH_URLBAR_RESULT = { + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + heuristic: false, + isBestMatch: true, + payload: { + telemetryType: "adm_sponsored", + url: "http://example.com/", + originalUrl: "http://example.com/", + title: "Fullkeyword title", + icon: null, + isSponsored: true, + sponsoredImpressionUrl: "http://example.com/impression", + sponsoredClickUrl: "http://example.com/click", + sponsoredBlockId: 1, + sponsoredAdvertiser: "TestAdvertiser", + helpUrl: QuickSuggest.HELP_URL, + helpL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-learn-more-about-firefox-suggest" + : "firefox-suggest-urlbar-learn-more", + }, + isBlockable: UrlbarPrefs.get("bestMatchBlockingEnabled"), + blockL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-dismiss-firefox-suggest" + : "firefox-suggest-urlbar-block", + }, + displayUrl: "http://example.com", + source: "remote-settings", + }, +}; + +const EXPECTED_NON_BEST_MATCH_URLBAR_RESULT = { + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + heuristic: false, + payload: { + telemetryType: "adm_sponsored", + url: "http://example.com/", + originalUrl: "http://example.com/", + title: "Fullkeyword title", + qsSuggestion: "fullkeyword", + icon: null, + isSponsored: true, + sponsoredImpressionUrl: "http://example.com/impression", + sponsoredClickUrl: "http://example.com/click", + sponsoredBlockId: 1, + sponsoredAdvertiser: "TestAdvertiser", + helpUrl: QuickSuggest.HELP_URL, + helpL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-learn-more-about-firefox-suggest" + : "firefox-suggest-urlbar-learn-more", + }, + isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"), + blockL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-dismiss-firefox-suggest" + : "firefox-suggest-urlbar-block", + }, + displayUrl: "http://example.com", + source: "remote-settings", + }, +}; + +const EXPECTED_BEST_MATCH_POSITION_URLBAR_RESULT = { + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + heuristic: false, + isBestMatch: true, + payload: { + telemetryType: "adm_sponsored", + url: "http://example.com/best-match-position", + originalUrl: "http://example.com/best-match-position", + title: `${BEST_MATCH_POSITION_SEARCH_STRING} title`, + icon: null, + isSponsored: true, + sponsoredImpressionUrl: "http://example.com/impression", + sponsoredClickUrl: "http://example.com/click", + sponsoredBlockId: 2, + sponsoredAdvertiser: "TestAdvertiser", + helpUrl: QuickSuggest.HELP_URL, + helpL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-learn-more-about-firefox-suggest" + : "firefox-suggest-urlbar-learn-more", + }, + isBlockable: UrlbarPrefs.get("bestMatchBlockingEnabled"), + blockL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-dismiss-firefox-suggest" + : "firefox-suggest-urlbar-block", + }, + displayUrl: "http://example.com/best-match-position", + source: "remote-settings", + }, +}; + +add_task(async function init() { + UrlbarPrefs.set("quicksuggest.enabled", true); + UrlbarPrefs.set("bestMatch.enabled", true); + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + UrlbarPrefs.set("suggest.bestmatch", true); + + // Disable search suggestions so we don't hit the network. + Services.prefs.setBoolPref("browser.search.suggest.enabled", false); + + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + remoteSettingsResults: [ + { + type: "data", + attachment: REMOTE_SETTINGS_RESULTS, + }, + ], + config: QuickSuggestTestUtils.BEST_MATCH_CONFIG, + }); +}); + +// Tests a best match result. +add_task(async function bestMatch() { + let context = createContext("fullkeyword", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [EXPECTED_BEST_MATCH_URLBAR_RESULT], + }); + + let result = context.results[0]; + + // The title should not include the full keyword and em dash, and the part of + // the title that the search string matches should be highlighted. + Assert.equal(result.title, "Fullkeyword title", "result.title"); + Assert.deepEqual( + result.titleHighlights, + [[0, "fullkeyword".length]], + "result.titleHighlights" + ); + + Assert.equal(result.suggestedIndex, 1, "result.suggestedIndex"); + Assert.equal( + !!result.isSuggestedIndexRelativeToGroup, + false, + "result.isSuggestedIndexRelativeToGroup" + ); +}); + +// Tests a usual, non-best match quick suggest result. +add_task(async function nonBestMatch() { + // Search for a substring of the full search string so we can test title + // highlights. + let context = createContext("fu", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [EXPECTED_NON_BEST_MATCH_URLBAR_RESULT], + }); + + let result = context.results[0]; + + // The title should include the full keyword and em dash, and the part of the + // title that the search string does not match should be highlighted. + Assert.equal(result.title, "fullkeyword — Fullkeyword title", "result.title"); + Assert.deepEqual( + result.titleHighlights, + [["fu".length, "fullkeyword".length - "fu".length]], + "result.titleHighlights" + ); + + Assert.equal(result.suggestedIndex, -1, "result.suggestedIndex"); + Assert.equal( + result.isSuggestedIndexRelativeToGroup, + true, + "result.isSuggestedIndexRelativeToGroup" + ); +}); + +// Tests prefix keywords leading up to a best match. +add_task(async function prefixKeywords() { + let sawNonBestMatch = false; + let sawBestMatch = false; + for (let keyword of REMOTE_SETTINGS_RESULTS[0].keywords) { + info(`Searching for "${keyword}"`); + let context = createContext(keyword, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + + let expectedResult; + if (keyword.length < 4) { + expectedResult = EXPECTED_NON_BEST_MATCH_URLBAR_RESULT; + sawNonBestMatch = true; + } else { + expectedResult = EXPECTED_BEST_MATCH_URLBAR_RESULT; + sawBestMatch = true; + } + + await check_results({ + context, + matches: [expectedResult], + }); + } + + Assert.ok(sawNonBestMatch, "Sanity check: Saw a non-best match"); + Assert.ok(sawBestMatch, "Sanity check: Saw a best match"); +}); + +// When tab-to-search is shown in the same search, both it and the best match +// will have a `suggestedIndex` value of 1. The TTS should appear first. +add_task(async function tabToSearch() { + // Disable tab-to-search onboarding results so we get a regular TTS result, + // which we can test a little more easily with `makeSearchResult()`. + UrlbarPrefs.set("tabToSearch.onboard.interactionsLeft", 0); + + // Install a test engine. The main part of its domain name needs to match the + // best match result too so we can trigger both its TTS and the best match. + let engineURL = "https://foo.fullkeyword.com/"; + let extension = await SearchTestUtils.installSearchExtension( + { + name: "Test", + search_url: engineURL, + }, + { skipUnload: true } + ); + let engine = Services.search.getEngineByName("Test"); + + // Also need to add a visit to trigger TTS. + await PlacesTestUtils.addVisits(engineURL); + + let context = createContext("fullkeyword", { + isPrivate: false, + }); + await check_results({ + context, + matches: [ + // search heuristic + makeSearchResult(context, { + engineName: Services.search.defaultEngine.name, + engineIconUri: Services.search.defaultEngine.iconURI?.spec, + heuristic: true, + }), + // tab to search + makeSearchResult(context, { + engineName: engine.name, + engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS, + uri: UrlbarUtils.stripPublicSuffixFromHost(engine.searchUrlDomain), + providesSearchMode: true, + query: "", + providerName: "TabToSearch", + satisfiesAutofillThreshold: true, + }), + // best match + EXPECTED_BEST_MATCH_URLBAR_RESULT, + // visit + makeVisitResult(context, { + uri: engineURL, + title: `test visit for ${engineURL}`, + }), + ], + }); + + await cleanupPlaces(); + await extension.unload(); + + UrlbarPrefs.clear("tabToSearch.onboard.interactionsLeft"); +}); + +// When the best match feature gate is disabled, quick suggest results should be +// shown as the usual non-best match results. +add_task(async function disabled_featureGate() { + UrlbarPrefs.set("bestMatch.enabled", false); + await doDisabledTest(); + UrlbarPrefs.set("bestMatch.enabled", true); +}); + +// When the best match suggestions are disabled, quick suggest results should be +// shown as the usual non-best match results. +add_task(async function disabled_suggestions() { + UrlbarPrefs.set("suggest.bestmatch", false); + await doDisabledTest(); + UrlbarPrefs.set("suggest.bestmatch", true); +}); + +// When best match is disabled, quick suggest results should be shown as the +// usual, non-best match results. +async function doDisabledTest() { + let context = createContext("fullkeywor", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [EXPECTED_NON_BEST_MATCH_URLBAR_RESULT], + }); + + let result = context.results[0]; + + // The title should include the full keyword and em dash, and the part of the + // title that the search string does not match should be highlighted. + Assert.equal(result.title, "fullkeyword — Fullkeyword title", "result.title"); + Assert.deepEqual( + result.titleHighlights, + [["fullkeywor".length, 1]], + "result.titleHighlights" + ); + + Assert.equal(result.suggestedIndex, -1, "result.suggestedIndex"); + Assert.equal( + result.isSuggestedIndexRelativeToGroup, + true, + "result.isSuggestedIndexRelativeToGroup" + ); +} + +// `suggestion.position` should be ignored when the suggestion is a best match. +add_task(async function position() { + Assert.greater( + BEST_MATCH_POSITION, + 1, + "Precondition: `suggestion.position` > the best match index" + ); + + UrlbarPrefs.set("quicksuggest.allowPositionInSuggestions", true); + + let context = createContext(BEST_MATCH_POSITION_SEARCH_STRING, { + isPrivate: false, + }); + + // Add some visits to fill up the view. + let maxResultCount = UrlbarPrefs.get("maxRichResults"); + let visitResults = []; + for (let i = 0; i < maxResultCount; i++) { + let url = `http://example.com/${BEST_MATCH_POSITION_SEARCH_STRING}-${i}`; + await PlacesTestUtils.addVisits(url); + visitResults.unshift( + makeVisitResult(context, { + uri: url, + title: `test visit for ${url}`, + }) + ); + } + + // Do a search. + await check_results({ + context, + matches: [ + // search heuristic + makeSearchResult(context, { + engineName: Services.search.defaultEngine.name, + engineIconUri: Services.search.defaultEngine.iconURI?.spec, + heuristic: true, + }), + // best match whose backing suggestion has a `position` + EXPECTED_BEST_MATCH_POSITION_URLBAR_RESULT, + // visits + ...visitResults.slice(0, MAX_RESULT_COUNT - 2), + ], + }); + + await cleanupPlaces(); + UrlbarPrefs.clear("quicksuggest.allowPositionInSuggestions"); +}); + +// Tests a suggestion that is blocked from being a best match. +add_task(async function blockedAsBestMatch() { + let config = QuickSuggestTestUtils.BEST_MATCH_CONFIG; + config.best_match.blocked_suggestion_ids = [1]; + await QuickSuggestTestUtils.withConfig({ + config, + callback: async () => { + let context = createContext("fullkeyword", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [EXPECTED_NON_BEST_MATCH_URLBAR_RESULT], + }); + }, + }); +}); + +// Tests without a best_match config to make sure nothing breaks. +add_task(async function noConfig() { + await QuickSuggestTestUtils.withConfig({ + config: {}, + callback: async () => { + let context = createContext("fullkeyword", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [EXPECTED_NON_BEST_MATCH_URLBAR_RESULT], + }); + }, + }); +}); diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_dynamicWikipedia.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_dynamicWikipedia.js new file mode 100644 index 0000000000..04859c7404 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_dynamicWikipedia.js @@ -0,0 +1,95 @@ +/* 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 dynamic Wikipedia quick suggest results. + +"use strict"; + +const MERINO_SUGGESTIONS = [ + { + title: "title", + url: "url", + is_sponsored: false, + score: 0.23, + description: "description", + icon: "icon", + full_keyword: "full_keyword", + advertiser: "dynamic-wikipedia", + block_id: 0, + impression_url: "impression_url", + click_url: "click_url", + provider: "wikipedia", + }, +]; + +add_setup(async function init() { + UrlbarPrefs.set("quicksuggest.enabled", true); + UrlbarPrefs.set("bestMatch.enabled", true); + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + + // Disable search suggestions so we don't hit the network. + Services.prefs.setBoolPref("browser.search.suggest.enabled", false); + + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + merinoSuggestions: MERINO_SUGGESTIONS, + }); +}); + +// When non-sponsored suggestions are disabled, dynamic Wikipedia suggestions +// should be disabled. +add_task(async function nonsponsoredDisabled() { + // Disable sponsored suggestions. Dynamic Wikipedia suggestions are + // non-sponsored, so doing this should not prevent them from being enabled. + UrlbarPrefs.set("suggest.quicksuggest.sponsored", false); + + // First make sure the suggestion is added when non-sponsored suggestions are + // enabled. + await check_results({ + context: createContext("test", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [makeExpectedResult()], + }); + + // Now disable them. + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false); + await check_results({ + context: createContext("test", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [], + }); + + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.clear("suggest.quicksuggest.sponsored"); +}); + +function makeExpectedResult() { + return { + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + heuristic: false, + suggestedIndex: -1, + payload: { + telemetryType: "wikipedia", + title: "title", + url: "url", + displayUrl: "url", + isSponsored: false, + icon: "icon", + qsSuggestion: "full_keyword", + source: "merino", + helpUrl: QuickSuggest.HELP_URL, + helpL10n: { + id: "urlbar-result-menu-learn-more-about-firefox-suggest", + }, + isBlockable: true, + blockL10n: { + id: "urlbar-result-menu-dismiss-firefox-suggest", + }, + }, + }; +} diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_impressionCaps.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_impressionCaps.js new file mode 100644 index 0000000000..41706dabd8 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_impressionCaps.js @@ -0,0 +1,3888 @@ +/* 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 impression frequency capping for quick suggest results. + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs", + setTimeout: "resource://gre/modules/Timer.sys.mjs", +}); + +const REMOTE_SETTINGS_RESULTS = [ + { + id: 1, + url: "http://example.com/sponsored", + title: "Sponsored suggestion", + keywords: ["sponsored"], + click_url: "http://example.com/click", + impression_url: "http://example.com/impression", + advertiser: "TestAdvertiser", + iab_category: "22 - Shopping", + }, + { + id: 2, + url: "http://example.com/nonsponsored", + title: "Non-sponsored suggestion", + keywords: ["nonsponsored"], + click_url: "http://example.com/click", + impression_url: "http://example.com/impression", + advertiser: "TestAdvertiser", + iab_category: "5 - Education", + }, +]; + +const EXPECTED_SPONSORED_URLBAR_RESULT = { + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + heuristic: false, + payload: { + telemetryType: "adm_sponsored", + url: "http://example.com/sponsored", + originalUrl: "http://example.com/sponsored", + displayUrl: "http://example.com/sponsored", + title: "Sponsored suggestion", + qsSuggestion: "sponsored", + icon: null, + isSponsored: true, + sponsoredImpressionUrl: "http://example.com/impression", + sponsoredClickUrl: "http://example.com/click", + sponsoredBlockId: 1, + sponsoredAdvertiser: "TestAdvertiser", + sponsoredIabCategory: "22 - Shopping", + helpUrl: QuickSuggest.HELP_URL, + helpL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-learn-more-about-firefox-suggest" + : "firefox-suggest-urlbar-learn-more", + }, + isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"), + blockL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-dismiss-firefox-suggest" + : "firefox-suggest-urlbar-block", + }, + source: "remote-settings", + }, +}; + +const EXPECTED_NONSPONSORED_URLBAR_RESULT = { + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + heuristic: false, + payload: { + telemetryType: "adm_nonsponsored", + url: "http://example.com/nonsponsored", + originalUrl: "http://example.com/nonsponsored", + displayUrl: "http://example.com/nonsponsored", + title: "Non-sponsored suggestion", + qsSuggestion: "nonsponsored", + icon: null, + isSponsored: false, + sponsoredImpressionUrl: "http://example.com/impression", + sponsoredClickUrl: "http://example.com/click", + sponsoredBlockId: 2, + sponsoredAdvertiser: "TestAdvertiser", + sponsoredIabCategory: "5 - Education", + helpUrl: QuickSuggest.HELP_URL, + helpL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-learn-more-about-firefox-suggest" + : "firefox-suggest-urlbar-learn-more", + }, + isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"), + blockL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-dismiss-firefox-suggest" + : "firefox-suggest-urlbar-block", + }, + source: "remote-settings", + }, +}; + +let gSandbox; +let gDateNowStub; +let gStartupDateMsStub; + +add_task(async function init() { + UrlbarPrefs.set("quicksuggest.enabled", true); + UrlbarPrefs.set("quicksuggest.impressionCaps.sponsoredEnabled", true); + UrlbarPrefs.set("quicksuggest.impressionCaps.nonSponsoredEnabled", true); + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + UrlbarPrefs.set("bestMatch.enabled", false); + + // Disable search suggestions so we don't hit the network. + Services.prefs.setBoolPref("browser.search.suggest.enabled", false); + + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + remoteSettingsResults: [ + { + type: "data", + attachment: REMOTE_SETTINGS_RESULTS, + }, + ], + }); + + // Set up a sinon stub for the `Date.now()` implementation inside of + // UrlbarProviderQuickSuggest. This lets us test searches performed at + // specific times. See `doTimedCallbacks()` for more info. + gSandbox = sinon.createSandbox(); + gDateNowStub = gSandbox.stub( + Cu.getGlobalForObject(UrlbarProviderQuickSuggest).Date, + "now" + ); + + // Set up a sinon stub for `UrlbarProviderQuickSuggest._getStartupDateMs()` to + // let the test override the startup date. + gStartupDateMsStub = gSandbox.stub( + QuickSuggest.impressionCaps, + "_getStartupDateMs" + ); + gStartupDateMsStub.returns(0); +}); + +// Tests a single interval. +add_task(async function oneInterval() { + await doTest({ + config: { + impression_caps: { + sponsored: { + custom: [{ interval_s: 3, max_count: 1 }], + }, + }, + }, + callback: async () => { + await doTimedSearches("sponsored", { + 0: { + results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], + telemetry: { + events: [ + { + object: "hit", + extra: { + eventDate: "0", + intervalSeconds: "3", + maxCount: "1", + startDate: "0", + impressionDate: "0", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + ], + }, + }, + 1: { + results: [[]], + }, + 2: { + results: [[]], + }, + 3: { + results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], + telemetry: { + events: [ + { + object: "reset", + extra: { + eventDate: "3000", + intervalSeconds: "3", + maxCount: "1", + startDate: "0", + impressionDate: "0", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + { + object: "hit", + extra: { + eventDate: "3000", + intervalSeconds: "3", + maxCount: "1", + startDate: "3000", + impressionDate: "3000", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + ], + }, + }, + 4: { + results: [[]], + }, + 5: { + results: [[]], + }, + }); + }, + }); +}); + +// Tests multiple intervals. +add_task(async function multipleIntervals() { + await doTest({ + config: { + impression_caps: { + sponsored: { + custom: [ + { interval_s: 1, max_count: 1 }, + { interval_s: 5, max_count: 3 }, + { interval_s: 10, max_count: 5 }, + ], + }, + }, + }, + callback: async () => { + await doTimedSearches("sponsored", { + // 0s: 1 new impression; 1 impression total + 0: { + results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], + telemetry: { + events: [ + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "0", + intervalSeconds: "1", + maxCount: "1", + startDate: "0", + impressionDate: "0", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + ], + }, + }, + // 1s: 1 new impression; 2 impressions total + 1: { + results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "1000", + intervalSeconds: "1", + maxCount: "1", + startDate: "0", + impressionDate: "0", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "1000", + intervalSeconds: "1", + maxCount: "1", + startDate: "1000", + impressionDate: "1000", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + ], + }, + }, + // 2s: 1 new impression; 3 impressions total + 2: { + results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "2000", + intervalSeconds: "1", + maxCount: "1", + startDate: "1000", + impressionDate: "1000", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "2000", + intervalSeconds: "1", + maxCount: "1", + startDate: "2000", + impressionDate: "2000", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + // hit: interval_s: 5, max_count: 3 + { + object: "hit", + extra: { + eventDate: "2000", + intervalSeconds: "5", + maxCount: "3", + startDate: "0", + impressionDate: "2000", + count: "3", + type: "sponsored", + eventCount: "1", + }, + }, + ], + }, + }, + // 3s: no new impressions; 3 impressions total + 3: { + results: [[]], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "3000", + intervalSeconds: "1", + maxCount: "1", + startDate: "2000", + impressionDate: "2000", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + ], + }, + }, + // 4s: no new impressions; 3 impressions total + 4: { + results: [[]], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "4000", + intervalSeconds: "1", + maxCount: "1", + startDate: "3000", + impressionDate: "2000", + count: "0", + type: "sponsored", + eventCount: "1", + }, + }, + ], + }, + }, + // 5s: 1 new impression; 4 impressions total + 5: { + results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "5000", + intervalSeconds: "1", + maxCount: "1", + startDate: "4000", + impressionDate: "2000", + count: "0", + type: "sponsored", + eventCount: "1", + }, + }, + // reset: interval_s: 5, max_count: 3 + { + object: "reset", + extra: { + eventDate: "5000", + intervalSeconds: "5", + maxCount: "3", + startDate: "0", + impressionDate: "2000", + count: "3", + type: "sponsored", + eventCount: "1", + }, + }, + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "5000", + intervalSeconds: "1", + maxCount: "1", + startDate: "5000", + impressionDate: "5000", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + ], + }, + }, + // 6s: 1 new impression; 5 impressions total + 6: { + results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "6000", + intervalSeconds: "1", + maxCount: "1", + startDate: "5000", + impressionDate: "5000", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "6000", + intervalSeconds: "1", + maxCount: "1", + startDate: "6000", + impressionDate: "6000", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + // hit: interval_s: 10, max_count: 5 + { + object: "hit", + extra: { + eventDate: "6000", + intervalSeconds: "10", + maxCount: "5", + startDate: "0", + impressionDate: "6000", + count: "5", + type: "sponsored", + eventCount: "1", + }, + }, + ], + }, + }, + // 7s: no new impressions; 5 impressions total + 7: { + results: [[]], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "7000", + intervalSeconds: "1", + maxCount: "1", + startDate: "6000", + impressionDate: "6000", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + ], + }, + }, + // 8s: no new impressions; 5 impressions total + 8: { + results: [[]], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "8000", + intervalSeconds: "1", + maxCount: "1", + startDate: "7000", + impressionDate: "6000", + count: "0", + type: "sponsored", + eventCount: "1", + }, + }, + ], + }, + }, + // 9s: no new impressions; 5 impressions total + 9: { + results: [[]], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "9000", + intervalSeconds: "1", + maxCount: "1", + startDate: "8000", + impressionDate: "6000", + count: "0", + type: "sponsored", + eventCount: "1", + }, + }, + ], + }, + }, + // 10s: 1 new impression; 6 impressions total + 10: { + results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "10000", + intervalSeconds: "1", + maxCount: "1", + startDate: "9000", + impressionDate: "6000", + count: "0", + type: "sponsored", + eventCount: "1", + }, + }, + // reset: interval_s: 5, max_count: 3 + { + object: "reset", + extra: { + eventDate: "10000", + intervalSeconds: "5", + maxCount: "3", + startDate: "5000", + impressionDate: "6000", + count: "2", + type: "sponsored", + eventCount: "1", + }, + }, + // reset: interval_s: 10, max_count: 5 + { + object: "reset", + extra: { + eventDate: "10000", + intervalSeconds: "10", + maxCount: "5", + startDate: "0", + impressionDate: "6000", + count: "5", + type: "sponsored", + eventCount: "1", + }, + }, + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "10000", + intervalSeconds: "1", + maxCount: "1", + startDate: "10000", + impressionDate: "10000", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + ], + }, + }, + // 11s: 1 new impression; 7 impressions total + 11: { + results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "11000", + intervalSeconds: "1", + maxCount: "1", + startDate: "10000", + impressionDate: "10000", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "11000", + intervalSeconds: "1", + maxCount: "1", + startDate: "11000", + impressionDate: "11000", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + ], + }, + }, + // 12s: 1 new impression; 8 impressions total + 12: { + results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "12000", + intervalSeconds: "1", + maxCount: "1", + startDate: "11000", + impressionDate: "11000", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "12000", + intervalSeconds: "1", + maxCount: "1", + startDate: "12000", + impressionDate: "12000", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + // hit: interval_s: 5, max_count: 3 + { + object: "hit", + extra: { + eventDate: "12000", + intervalSeconds: "5", + maxCount: "3", + startDate: "10000", + impressionDate: "12000", + count: "3", + type: "sponsored", + eventCount: "1", + }, + }, + ], + }, + }, + // 13s: no new impressions; 8 impressions total + 13: { + results: [[]], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "13000", + intervalSeconds: "1", + maxCount: "1", + startDate: "12000", + impressionDate: "12000", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + ], + }, + }, + // 14s: no new impressions; 8 impressions total + 14: { + results: [[]], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "14000", + intervalSeconds: "1", + maxCount: "1", + startDate: "13000", + impressionDate: "12000", + count: "0", + type: "sponsored", + eventCount: "1", + }, + }, + ], + }, + }, + // 15s: 1 new impression; 9 impressions total + 15: { + results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "15000", + intervalSeconds: "1", + maxCount: "1", + startDate: "14000", + impressionDate: "12000", + count: "0", + type: "sponsored", + eventCount: "1", + }, + }, + // reset: interval_s: 5, max_count: 3 + { + object: "reset", + extra: { + eventDate: "15000", + intervalSeconds: "5", + maxCount: "3", + startDate: "10000", + impressionDate: "12000", + count: "3", + type: "sponsored", + eventCount: "1", + }, + }, + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "15000", + intervalSeconds: "1", + maxCount: "1", + startDate: "15000", + impressionDate: "15000", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + ], + }, + }, + // 16s: 1 new impression; 10 impressions total + 16: { + results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "16000", + intervalSeconds: "1", + maxCount: "1", + startDate: "15000", + impressionDate: "15000", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "16000", + intervalSeconds: "1", + maxCount: "1", + startDate: "16000", + impressionDate: "16000", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + // hit: interval_s: 10, max_count: 5 + { + object: "hit", + extra: { + eventDate: "16000", + intervalSeconds: "10", + maxCount: "5", + startDate: "10000", + impressionDate: "16000", + count: "5", + type: "sponsored", + eventCount: "1", + }, + }, + ], + }, + }, + // 17s: no new impressions; 10 impressions total + 17: { + results: [[]], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "17000", + intervalSeconds: "1", + maxCount: "1", + startDate: "16000", + impressionDate: "16000", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + ], + }, + }, + // 18s: no new impressions; 10 impressions total + 18: { + results: [[]], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "18000", + intervalSeconds: "1", + maxCount: "1", + startDate: "17000", + impressionDate: "16000", + count: "0", + type: "sponsored", + eventCount: "1", + }, + }, + ], + }, + }, + // 19s: no new impressions; 10 impressions total + 19: { + results: [[]], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "19000", + intervalSeconds: "1", + maxCount: "1", + startDate: "18000", + impressionDate: "16000", + count: "0", + type: "sponsored", + eventCount: "1", + }, + }, + ], + }, + }, + // 20s: 1 new impression; 11 impressions total + 20: { + results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "20000", + intervalSeconds: "1", + maxCount: "1", + startDate: "19000", + impressionDate: "16000", + count: "0", + type: "sponsored", + eventCount: "1", + }, + }, + // reset: interval_s: 5, max_count: 3 + { + object: "reset", + extra: { + eventDate: "20000", + intervalSeconds: "5", + maxCount: "3", + startDate: "15000", + impressionDate: "16000", + count: "2", + type: "sponsored", + eventCount: "1", + }, + }, + // reset: interval_s: 10, max_count: 5 + { + object: "reset", + extra: { + eventDate: "20000", + intervalSeconds: "10", + maxCount: "5", + startDate: "10000", + impressionDate: "16000", + count: "5", + type: "sponsored", + eventCount: "1", + }, + }, + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "20000", + intervalSeconds: "1", + maxCount: "1", + startDate: "20000", + impressionDate: "20000", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + ], + }, + }, + }); + }, + }); +}); + +// Tests a lifetime cap. +add_task(async function lifetime() { + await doTest({ + config: { + impression_caps: { + sponsored: { + lifetime: 3, + }, + }, + }, + callback: async () => { + await doTimedSearches("sponsored", { + 0: { + results: [ + [EXPECTED_SPONSORED_URLBAR_RESULT], + [EXPECTED_SPONSORED_URLBAR_RESULT], + [EXPECTED_SPONSORED_URLBAR_RESULT], + [], + ], + telemetry: { + events: [ + { + object: "hit", + extra: { + eventDate: "0", + intervalSeconds: "Infinity", + maxCount: "3", + startDate: "0", + impressionDate: "0", + count: "3", + type: "sponsored", + eventCount: "1", + }, + }, + ], + }, + }, + 1: { + results: [[]], + }, + }); + }, + }); +}); + +// Tests one interval and a lifetime cap together. +add_task(async function intervalAndLifetime() { + await doTest({ + config: { + impression_caps: { + sponsored: { + lifetime: 3, + custom: [{ interval_s: 1, max_count: 1 }], + }, + }, + }, + callback: async () => { + await doTimedSearches("sponsored", { + // 0s: 1 new impression; 1 impression total + 0: { + results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], + telemetry: { + events: [ + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "0", + intervalSeconds: "1", + maxCount: "1", + startDate: "0", + impressionDate: "0", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + ], + }, + }, + // 1s: 1 new impression; 2 impressions total + 1: { + results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "1000", + intervalSeconds: "1", + maxCount: "1", + startDate: "0", + impressionDate: "0", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "1000", + intervalSeconds: "1", + maxCount: "1", + startDate: "1000", + impressionDate: "1000", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + ], + }, + }, + // 2s: 1 new impression; 3 impressions total + 2: { + results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "2000", + intervalSeconds: "1", + maxCount: "1", + startDate: "1000", + impressionDate: "1000", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "2000", + intervalSeconds: "1", + maxCount: "1", + startDate: "2000", + impressionDate: "2000", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + // hit: interval_s: Infinity, max_count: 3 + { + object: "hit", + extra: { + eventDate: "2000", + intervalSeconds: "Infinity", + maxCount: "3", + startDate: "0", + impressionDate: "2000", + count: "3", + type: "sponsored", + eventCount: "1", + }, + }, + ], + }, + }, + 3: { + results: [[]], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "3000", + intervalSeconds: "1", + maxCount: "1", + startDate: "2000", + impressionDate: "2000", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + ], + }, + }, + }); + }, + }); +}); + +// Tests multiple intervals and a lifetime cap together. +add_task(async function multipleIntervalsAndLifetime() { + await doTest({ + config: { + impression_caps: { + sponsored: { + lifetime: 4, + custom: [ + { interval_s: 1, max_count: 1 }, + { interval_s: 5, max_count: 3 }, + ], + }, + }, + }, + callback: async () => { + await doTimedSearches("sponsored", { + // 0s: 1 new impression; 1 impression total + 0: { + results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], + telemetry: { + events: [ + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "0", + intervalSeconds: "1", + maxCount: "1", + startDate: "0", + impressionDate: "0", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + ], + }, + }, + // 1s: 1 new impression; 2 impressions total + 1: { + results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "1000", + intervalSeconds: "1", + maxCount: "1", + startDate: "0", + impressionDate: "0", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "1000", + intervalSeconds: "1", + maxCount: "1", + startDate: "1000", + impressionDate: "1000", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + ], + }, + }, + // 2s: 1 new impression; 3 impressions total + 2: { + results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "2000", + intervalSeconds: "1", + maxCount: "1", + startDate: "1000", + impressionDate: "1000", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "2000", + intervalSeconds: "1", + maxCount: "1", + startDate: "2000", + impressionDate: "2000", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + // hit: interval_s: 5, max_count: 3 + { + object: "hit", + extra: { + eventDate: "2000", + intervalSeconds: "5", + maxCount: "3", + startDate: "0", + impressionDate: "2000", + count: "3", + type: "sponsored", + eventCount: "1", + }, + }, + ], + }, + }, + // 3s: no new impressions; 3 impressions total + 3: { + results: [[]], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "3000", + intervalSeconds: "1", + maxCount: "1", + startDate: "2000", + impressionDate: "2000", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + ], + }, + }, + // 4s: no new impressions; 3 impressions total + 4: { + results: [[]], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "4000", + intervalSeconds: "1", + maxCount: "1", + startDate: "3000", + impressionDate: "2000", + count: "0", + type: "sponsored", + eventCount: "1", + }, + }, + ], + }, + }, + // 5s: 1 new impression; 4 impressions total + 5: { + results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "5000", + intervalSeconds: "1", + maxCount: "1", + startDate: "4000", + impressionDate: "2000", + count: "0", + type: "sponsored", + eventCount: "1", + }, + }, + // reset: interval_s: 5, max_count: 3 + { + object: "reset", + extra: { + eventDate: "5000", + intervalSeconds: "5", + maxCount: "3", + startDate: "0", + impressionDate: "2000", + count: "3", + type: "sponsored", + eventCount: "1", + }, + }, + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "5000", + intervalSeconds: "1", + maxCount: "1", + startDate: "5000", + impressionDate: "5000", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + // hit: interval_s: Infinity, max_count: 4 + { + object: "hit", + extra: { + eventDate: "5000", + intervalSeconds: "Infinity", + maxCount: "4", + startDate: "0", + impressionDate: "5000", + count: "4", + type: "sponsored", + eventCount: "1", + }, + }, + ], + }, + }, + // 6s: no new impressions; 4 impressions total + 6: { + results: [[]], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "6000", + intervalSeconds: "1", + maxCount: "1", + startDate: "5000", + impressionDate: "5000", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + ], + }, + }, + // 7s: no new impressions; 4 impressions total + 7: { + results: [[]], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "7000", + intervalSeconds: "1", + maxCount: "1", + startDate: "6000", + impressionDate: "5000", + count: "0", + type: "sponsored", + eventCount: "1", + }, + }, + ], + }, + }, + }); + }, + }); +}); + +// Smoke test for non-sponsored caps. Most tasks use sponsored results and caps, +// but sponsored and non-sponsored should behave the same since they use the +// same code paths. +add_task(async function nonsponsored() { + await doTest({ + config: { + impression_caps: { + nonsponsored: { + lifetime: 4, + custom: [ + { interval_s: 1, max_count: 1 }, + { interval_s: 5, max_count: 3 }, + ], + }, + }, + }, + callback: async () => { + await doTimedSearches("nonsponsored", { + // 0s: 1 new impression; 1 impression total + 0: { + results: [[EXPECTED_NONSPONSORED_URLBAR_RESULT], []], + telemetry: { + events: [ + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "0", + intervalSeconds: "1", + maxCount: "1", + startDate: "0", + impressionDate: "0", + count: "1", + type: "nonsponsored", + eventCount: "1", + }, + }, + ], + }, + }, + // 1s: 1 new impression; 2 impressions total + 1: { + results: [[EXPECTED_NONSPONSORED_URLBAR_RESULT], []], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "1000", + intervalSeconds: "1", + maxCount: "1", + startDate: "0", + impressionDate: "0", + count: "1", + type: "nonsponsored", + eventCount: "1", + }, + }, + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "1000", + intervalSeconds: "1", + maxCount: "1", + startDate: "1000", + impressionDate: "1000", + count: "1", + type: "nonsponsored", + eventCount: "1", + }, + }, + ], + }, + }, + // 2s: 1 new impression; 3 impressions total + 2: { + results: [[EXPECTED_NONSPONSORED_URLBAR_RESULT], []], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "2000", + intervalSeconds: "1", + maxCount: "1", + startDate: "1000", + impressionDate: "1000", + count: "1", + type: "nonsponsored", + eventCount: "1", + }, + }, + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "2000", + intervalSeconds: "1", + maxCount: "1", + startDate: "2000", + impressionDate: "2000", + count: "1", + type: "nonsponsored", + eventCount: "1", + }, + }, + // hit: interval_s: 5, max_count: 3 + { + object: "hit", + extra: { + eventDate: "2000", + intervalSeconds: "5", + maxCount: "3", + startDate: "0", + impressionDate: "2000", + count: "3", + type: "nonsponsored", + eventCount: "1", + }, + }, + ], + }, + }, + // 3s: no new impressions; 3 impressions total + 3: { + results: [[]], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "3000", + intervalSeconds: "1", + maxCount: "1", + startDate: "2000", + impressionDate: "2000", + count: "1", + type: "nonsponsored", + eventCount: "1", + }, + }, + ], + }, + }, + // 4s: no new impressions; 3 impressions total + 4: { + results: [[]], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "4000", + intervalSeconds: "1", + maxCount: "1", + startDate: "3000", + impressionDate: "2000", + count: "0", + type: "nonsponsored", + eventCount: "1", + }, + }, + ], + }, + }, + // 5s: 1 new impression; 4 impressions total + 5: { + results: [[EXPECTED_NONSPONSORED_URLBAR_RESULT], []], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "5000", + intervalSeconds: "1", + maxCount: "1", + startDate: "4000", + impressionDate: "2000", + count: "0", + type: "nonsponsored", + eventCount: "1", + }, + }, + // reset: interval_s: 5, max_count: 3 + { + object: "reset", + extra: { + eventDate: "5000", + intervalSeconds: "5", + maxCount: "3", + startDate: "0", + impressionDate: "2000", + count: "3", + type: "nonsponsored", + eventCount: "1", + }, + }, + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "5000", + intervalSeconds: "1", + maxCount: "1", + startDate: "5000", + impressionDate: "5000", + count: "1", + type: "nonsponsored", + eventCount: "1", + }, + }, + // hit: interval_s: Infinity, max_count: 4 + { + object: "hit", + extra: { + eventDate: "5000", + intervalSeconds: "Infinity", + maxCount: "4", + startDate: "0", + impressionDate: "5000", + count: "4", + type: "nonsponsored", + eventCount: "1", + }, + }, + ], + }, + }, + // 6s: no new impressions; 4 impressions total + 6: { + results: [[]], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "6000", + intervalSeconds: "1", + maxCount: "1", + startDate: "5000", + impressionDate: "5000", + count: "1", + type: "nonsponsored", + eventCount: "1", + }, + }, + ], + }, + }, + // 7s: no new impressions; 4 impressions total + 7: { + results: [[]], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "7000", + intervalSeconds: "1", + maxCount: "1", + startDate: "6000", + impressionDate: "5000", + count: "0", + type: "nonsponsored", + eventCount: "1", + }, + }, + ], + }, + }, + }); + }, + }); +}); + +// Smoke test for sponsored and non-sponsored caps together. Most tasks use only +// sponsored results and caps, but sponsored and non-sponsored should behave the +// same since they use the same code paths. +add_task(async function sponsoredAndNonsponsored() { + await doTest({ + config: { + impression_caps: { + sponsored: { + lifetime: 2, + }, + nonsponsored: { + lifetime: 3, + }, + }, + }, + callback: async () => { + // 1st searches + await checkSearch({ + name: "sponsored 1", + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], + }); + await checkSearch({ + name: "nonsponsored 1", + searchString: "nonsponsored", + expectedResults: [EXPECTED_NONSPONSORED_URLBAR_RESULT], + }); + await checkTelemetryEvents([]); + + // 2nd searches + await checkSearch({ + name: "sponsored 2", + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], + }); + await checkSearch({ + name: "nonsponsored 2", + searchString: "nonsponsored", + expectedResults: [EXPECTED_NONSPONSORED_URLBAR_RESULT], + }); + await checkTelemetryEvents([ + { + object: "hit", + extra: { + eventDate: "0", + intervalSeconds: "Infinity", + maxCount: "2", + startDate: "0", + impressionDate: "0", + count: "2", + type: "sponsored", + eventCount: "1", + }, + }, + ]); + + // 3rd searches + await checkSearch({ + name: "sponsored 3", + searchString: "sponsored", + expectedResults: [], + }); + await checkSearch({ + name: "nonsponsored 3", + searchString: "nonsponsored", + expectedResults: [EXPECTED_NONSPONSORED_URLBAR_RESULT], + }); + await checkTelemetryEvents([ + { + object: "hit", + extra: { + eventDate: "0", + intervalSeconds: "Infinity", + maxCount: "3", + startDate: "0", + impressionDate: "0", + count: "3", + type: "nonsponsored", + eventCount: "1", + }, + }, + ]); + + // 4th searches + await checkSearch({ + name: "sponsored 4", + searchString: "sponsored", + expectedResults: [], + }); + await checkSearch({ + name: "nonsponsored 4", + searchString: "nonsponsored", + expectedResults: [], + }); + await checkTelemetryEvents([]); + }, + }); +}); + +// Tests with an empty config to make sure results are not capped. +add_task(async function emptyConfig() { + await doTest({ + config: {}, + callback: async () => { + for (let i = 0; i < 2; i++) { + await checkSearch({ + name: "sponsored " + i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], + }); + await checkSearch({ + name: "nonsponsored " + i, + searchString: "nonsponsored", + expectedResults: [EXPECTED_NONSPONSORED_URLBAR_RESULT], + }); + } + await checkTelemetryEvents([]); + }, + }); +}); + +// Tests with sponsored caps disabled. Non-sponsored should still be capped. +add_task(async function sponsoredCapsDisabled() { + UrlbarPrefs.set("quicksuggest.impressionCaps.sponsoredEnabled", false); + await doTest({ + config: { + impression_caps: { + sponsored: { + lifetime: 0, + }, + nonsponsored: { + lifetime: 3, + }, + }, + }, + callback: async () => { + for (let i = 0; i < 3; i++) { + await checkSearch({ + name: "sponsored " + i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], + }); + await checkSearch({ + name: "nonsponsored " + i, + searchString: "nonsponsored", + expectedResults: [EXPECTED_NONSPONSORED_URLBAR_RESULT], + }); + } + await checkTelemetryEvents([ + { + object: "hit", + extra: { + eventDate: "0", + intervalSeconds: "Infinity", + maxCount: "3", + startDate: "0", + impressionDate: "0", + count: "3", + type: "nonsponsored", + eventCount: "1", + }, + }, + ]); + + await checkSearch({ + name: "sponsored additional", + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], + }); + await checkSearch({ + name: "nonsponsored additional", + searchString: "nonsponsored", + expectedResults: [], + }); + await checkTelemetryEvents([]); + }, + }); + UrlbarPrefs.set("quicksuggest.impressionCaps.sponsoredEnabled", true); +}); + +// Tests with non-sponsored caps disabled. Sponsored should still be capped. +add_task(async function nonsponsoredCapsDisabled() { + UrlbarPrefs.set("quicksuggest.impressionCaps.nonSponsoredEnabled", false); + await doTest({ + config: { + impression_caps: { + sponsored: { + lifetime: 3, + }, + nonsponsored: { + lifetime: 0, + }, + }, + }, + callback: async () => { + for (let i = 0; i < 3; i++) { + await checkSearch({ + name: "sponsored " + i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], + }); + await checkSearch({ + name: "nonsponsored " + i, + searchString: "nonsponsored", + expectedResults: [EXPECTED_NONSPONSORED_URLBAR_RESULT], + }); + } + await checkTelemetryEvents([ + { + object: "hit", + extra: { + eventDate: "0", + intervalSeconds: "Infinity", + maxCount: "3", + startDate: "0", + impressionDate: "0", + count: "3", + type: "sponsored", + eventCount: "1", + }, + }, + ]); + + await checkSearch({ + name: "sponsored additional", + searchString: "sponsored", + expectedResults: [], + }); + await checkSearch({ + name: "nonsponsored additional", + searchString: "nonsponsored", + expectedResults: [EXPECTED_NONSPONSORED_URLBAR_RESULT], + }); + await checkTelemetryEvents([]); + }, + }); + UrlbarPrefs.set("quicksuggest.impressionCaps.nonSponsoredEnabled", true); +}); + +// Tests a config change: 1 interval -> same interval with lower cap, with the +// old cap already reached +add_task(async function configChange_sameIntervalLowerCap_1() { + await doTest({ + config: { + impression_caps: { + sponsored: { + custom: [{ interval_s: 3, max_count: 3 }], + }, + }, + }, + callback: async () => { + await doTimedCallbacks({ + 0: async () => { + for (let i = 0; i < 3; i++) { + await checkSearch({ + name: "0s " + i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], + }); + } + await checkSearch({ + name: "0s additional", + searchString: "sponsored", + expectedResults: [], + }); + await checkTelemetryEvents([ + { + object: "hit", + extra: { + eventDate: "0", + intervalSeconds: "3", + maxCount: "3", + startDate: "0", + impressionDate: "0", + count: "3", + type: "sponsored", + eventCount: "1", + }, + }, + ]); + await QuickSuggestTestUtils.setConfig({ + impression_caps: { + sponsored: { + custom: [{ interval_s: 3, max_count: 1 }], + }, + }, + }); + }, + 1: async () => { + await checkSearch({ + name: "1s", + searchString: "sponsored", + expectedResults: [], + }); + await checkTelemetryEvents([]); + }, + 3: async () => { + await checkSearch({ + name: "3s 0", + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], + }); + await checkSearch({ + name: "3s additional", + searchString: "sponsored", + expectedResults: [], + }); + await checkTelemetryEvents([ + { + object: "reset", + extra: { + eventDate: "3000", + intervalSeconds: "3", + maxCount: "1", + startDate: "0", + impressionDate: "0", + count: "3", + type: "sponsored", + eventCount: "1", + }, + }, + { + object: "hit", + extra: { + eventDate: "3000", + intervalSeconds: "3", + maxCount: "1", + startDate: "3000", + impressionDate: "3000", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + ]); + }, + }); + }, + }); +}); + +// Tests a config change: 1 interval -> same interval with lower cap, with the +// old cap not reached +add_task(async function configChange_sameIntervalLowerCap_2() { + await doTest({ + config: { + impression_caps: { + sponsored: { + custom: [{ interval_s: 3, max_count: 3 }], + }, + }, + }, + callback: async () => { + await doTimedCallbacks({ + 0: async () => { + for (let i = 0; i < 2; i++) { + await checkSearch({ + name: "0s " + i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], + }); + } + await checkTelemetryEvents([]); + await QuickSuggestTestUtils.setConfig({ + impression_caps: { + sponsored: { + custom: [{ interval_s: 3, max_count: 1 }], + }, + }, + }); + }, + 1: async () => { + await checkSearch({ + name: "1s", + searchString: "sponsored", + expectedResults: [], + }); + }, + 3: async () => { + await checkSearch({ + name: "3s 0", + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], + }); + await checkSearch({ + name: "3s additional", + searchString: "sponsored", + expectedResults: [], + }); + await checkTelemetryEvents([ + { + object: "reset", + extra: { + eventDate: "3000", + intervalSeconds: "3", + maxCount: "1", + startDate: "0", + impressionDate: "0", + count: "2", + type: "sponsored", + eventCount: "1", + }, + }, + { + object: "hit", + extra: { + eventDate: "3000", + intervalSeconds: "3", + maxCount: "1", + startDate: "3000", + impressionDate: "3000", + count: "1", + type: "sponsored", + eventCount: "1", + }, + }, + ]); + }, + }); + }, + }); +}); + +// Tests a config change: 1 interval -> same interval with higher cap +add_task(async function configChange_sameIntervalHigherCap() { + await doTest({ + config: { + impression_caps: { + sponsored: { + custom: [{ interval_s: 3, max_count: 3 }], + }, + }, + }, + callback: async () => { + await doTimedCallbacks({ + 0: async () => { + for (let i = 0; i < 3; i++) { + await checkSearch({ + name: "0s " + i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], + }); + } + await checkSearch({ + name: "0s additional", + searchString: "sponsored", + expectedResults: [], + }); + await checkTelemetryEvents([ + { + object: "hit", + extra: { + eventDate: "0", + intervalSeconds: "3", + maxCount: "3", + startDate: "0", + impressionDate: "0", + count: "3", + type: "sponsored", + eventCount: "1", + }, + }, + ]); + await QuickSuggestTestUtils.setConfig({ + impression_caps: { + sponsored: { + custom: [{ interval_s: 3, max_count: 5 }], + }, + }, + }); + }, + 1: async () => { + for (let i = 0; i < 2; i++) { + await checkSearch({ + name: "1s " + i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], + }); + } + await checkSearch({ + name: "1s additional", + searchString: "sponsored", + expectedResults: [], + }); + await checkTelemetryEvents([ + { + object: "hit", + extra: { + eventDate: "1000", + intervalSeconds: "3", + maxCount: "5", + startDate: "0", + impressionDate: "1000", + count: "5", + type: "sponsored", + eventCount: "1", + }, + }, + ]); + }, + 3: async () => { + for (let i = 0; i < 5; i++) { + await checkSearch({ + name: "3s " + i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], + }); + } + await checkSearch({ + name: "3s additional", + searchString: "sponsored", + expectedResults: [], + }); + await checkTelemetryEvents([ + { + object: "reset", + extra: { + eventDate: "3000", + intervalSeconds: "3", + maxCount: "5", + startDate: "0", + impressionDate: "1000", + count: "5", + type: "sponsored", + eventCount: "1", + }, + }, + { + object: "hit", + extra: { + eventDate: "3000", + intervalSeconds: "3", + maxCount: "5", + startDate: "3000", + impressionDate: "3000", + count: "5", + type: "sponsored", + eventCount: "1", + }, + }, + ]); + }, + }); + }, + }); +}); + +// Tests a config change: 1 interval -> 2 new intervals with higher timeouts. +// Impression counts for the old interval should contribute to the new +// intervals. +add_task(async function configChange_1IntervalTo2NewIntervalsHigher() { + await doTest({ + config: { + impression_caps: { + sponsored: { + custom: [{ interval_s: 3, max_count: 3 }], + }, + }, + }, + callback: async () => { + await doTimedCallbacks({ + 0: async () => { + for (let i = 0; i < 3; i++) { + await checkSearch({ + name: "0s " + i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], + }); + } + await checkTelemetryEvents([ + { + object: "hit", + extra: { + eventDate: "0", + intervalSeconds: "3", + maxCount: "3", + startDate: "0", + impressionDate: "0", + count: "3", + type: "sponsored", + eventCount: "1", + }, + }, + ]); + await QuickSuggestTestUtils.setConfig({ + impression_caps: { + sponsored: { + custom: [ + { interval_s: 5, max_count: 3 }, + { interval_s: 10, max_count: 5 }, + ], + }, + }, + }); + }, + 3: async () => { + await checkSearch({ + name: "3s", + searchString: "sponsored", + expectedResults: [], + }); + await checkTelemetryEvents([]); + }, + 4: async () => { + await checkSearch({ + name: "4s", + searchString: "sponsored", + expectedResults: [], + }); + await checkTelemetryEvents([]); + }, + 5: async () => { + for (let i = 0; i < 2; i++) { + await checkSearch({ + name: "5s " + i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], + }); + } + await checkSearch({ + name: "5s additional", + searchString: "sponsored", + expectedResults: [], + }); + await checkTelemetryEvents([ + { + object: "reset", + extra: { + eventDate: "5000", + intervalSeconds: "5", + maxCount: "3", + startDate: "0", + impressionDate: "0", + count: "3", + type: "sponsored", + eventCount: "1", + }, + }, + { + object: "hit", + extra: { + eventDate: "5000", + intervalSeconds: "10", + maxCount: "5", + startDate: "0", + impressionDate: "5000", + count: "5", + type: "sponsored", + eventCount: "1", + }, + }, + ]); + }, + }); + }, + }); +}); + +// Tests a config change: 2 intervals -> 1 new interval with higher timeout. +// Impression counts for the old intervals should contribute to the new +// interval. +add_task(async function configChange_2IntervalsTo1NewIntervalHigher() { + await doTest({ + config: { + impression_caps: { + sponsored: { + custom: [ + { interval_s: 2, max_count: 2 }, + { interval_s: 4, max_count: 4 }, + ], + }, + }, + }, + callback: async () => { + await doTimedCallbacks({ + 0: async () => { + for (let i = 0; i < 2; i++) { + await checkSearch({ + name: "0s " + i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], + }); + } + await checkTelemetryEvents([ + { + object: "hit", + extra: { + eventDate: "0", + intervalSeconds: "2", + maxCount: "2", + startDate: "0", + impressionDate: "0", + count: "2", + type: "sponsored", + eventCount: "1", + }, + }, + ]); + }, + 2: async () => { + for (let i = 0; i < 2; i++) { + await checkSearch({ + name: "2s " + i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], + }); + } + await checkTelemetryEvents([ + { + object: "reset", + extra: { + eventDate: "2000", + intervalSeconds: "2", + maxCount: "2", + startDate: "0", + impressionDate: "0", + count: "2", + type: "sponsored", + eventCount: "1", + }, + }, + { + object: "hit", + extra: { + eventDate: "2000", + intervalSeconds: "2", + maxCount: "2", + startDate: "2000", + impressionDate: "2000", + count: "2", + type: "sponsored", + eventCount: "1", + }, + }, + { + object: "hit", + extra: { + eventDate: "2000", + intervalSeconds: "4", + maxCount: "4", + startDate: "0", + impressionDate: "2000", + count: "4", + type: "sponsored", + eventCount: "1", + }, + }, + ]); + await QuickSuggestTestUtils.setConfig({ + impression_caps: { + sponsored: { + custom: [{ interval_s: 6, max_count: 5 }], + }, + }, + }); + }, + 4: async () => { + await checkSearch({ + name: "4s 0", + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], + }); + await checkSearch({ + name: "4s 1", + searchString: "sponsored", + expectedResults: [], + }); + await checkTelemetryEvents([ + { + object: "hit", + extra: { + eventDate: "4000", + intervalSeconds: "6", + maxCount: "5", + startDate: "0", + impressionDate: "4000", + count: "5", + type: "sponsored", + eventCount: "1", + }, + }, + ]); + }, + 5: async () => { + await checkSearch({ + name: "5s", + searchString: "sponsored", + expectedResults: [], + }); + await checkTelemetryEvents([]); + }, + 6: async () => { + for (let i = 0; i < 5; i++) { + await checkSearch({ + name: "6s " + i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], + }); + } + await checkSearch({ + name: "6s additional", + searchString: "sponsored", + expectedResults: [], + }); + await checkTelemetryEvents([ + { + object: "reset", + extra: { + eventDate: "6000", + intervalSeconds: "6", + maxCount: "5", + startDate: "0", + impressionDate: "4000", + count: "5", + type: "sponsored", + eventCount: "1", + }, + }, + { + object: "hit", + extra: { + eventDate: "6000", + intervalSeconds: "6", + maxCount: "5", + startDate: "6000", + impressionDate: "6000", + count: "5", + type: "sponsored", + eventCount: "1", + }, + }, + ]); + }, + }); + }, + }); +}); + +// Tests a config change: 1 interval -> 1 new interval with lower timeout. +// Impression counts for the old interval should not contribute to the new +// interval since the new interval has a lower timeout. +add_task(async function configChange_1IntervalTo1NewIntervalLower() { + await doTest({ + config: { + impression_caps: { + sponsored: { + custom: [{ interval_s: 5, max_count: 3 }], + }, + }, + }, + callback: async () => { + await doTimedCallbacks({ + 0: async () => { + for (let i = 0; i < 3; i++) { + await checkSearch({ + name: "0s " + i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], + }); + } + await checkTelemetryEvents([ + { + object: "hit", + extra: { + eventDate: "0", + intervalSeconds: "5", + maxCount: "3", + startDate: "0", + impressionDate: "0", + count: "3", + type: "sponsored", + eventCount: "1", + }, + }, + ]); + await QuickSuggestTestUtils.setConfig({ + impression_caps: { + sponsored: { + custom: [{ interval_s: 3, max_count: 3 }], + }, + }, + }); + }, + 1: async () => { + for (let i = 0; i < 3; i++) { + await checkSearch({ + name: "3s " + i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], + }); + } + await checkSearch({ + name: "3s additional", + searchString: "sponsored", + expectedResults: [], + }); + await checkTelemetryEvents([ + { + object: "hit", + extra: { + eventDate: "1000", + intervalSeconds: "3", + maxCount: "3", + startDate: "0", + impressionDate: "1000", + count: "3", + type: "sponsored", + eventCount: "1", + }, + }, + ]); + }, + }); + }, + }); +}); + +// Tests a config change: 1 interval -> lifetime. +// Impression counts for the old interval should contribute to the new lifetime +// cap. +add_task(async function configChange_1IntervalToLifetime() { + await doTest({ + config: { + impression_caps: { + sponsored: { + custom: [{ interval_s: 3, max_count: 3 }], + }, + }, + }, + callback: async () => { + await doTimedCallbacks({ + 0: async () => { + for (let i = 0; i < 3; i++) { + await checkSearch({ + name: "0s " + i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], + }); + } + await checkTelemetryEvents([ + { + object: "hit", + extra: { + eventDate: "0", + intervalSeconds: "3", + maxCount: "3", + startDate: "0", + impressionDate: "0", + count: "3", + type: "sponsored", + eventCount: "1", + }, + }, + ]); + await QuickSuggestTestUtils.setConfig({ + impression_caps: { + sponsored: { + lifetime: 3, + }, + }, + }); + }, + 3: async () => { + await checkSearch({ + name: "3s", + searchString: "sponsored", + expectedResults: [], + }); + await checkTelemetryEvents([]); + }, + }); + }, + }); +}); + +// Tests a config change: lifetime cap -> higher lifetime cap +add_task(async function configChange_lifetimeCapHigher() { + await doTest({ + config: { + impression_caps: { + sponsored: { + lifetime: 3, + }, + }, + }, + callback: async () => { + await doTimedCallbacks({ + 0: async () => { + for (let i = 0; i < 3; i++) { + await checkSearch({ + name: "0s " + i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], + }); + } + await checkSearch({ + name: "0s additional", + searchString: "sponsored", + expectedResults: [], + }); + await checkTelemetryEvents([ + { + object: "hit", + extra: { + eventDate: "0", + intervalSeconds: "Infinity", + maxCount: "3", + startDate: "0", + impressionDate: "0", + count: "3", + type: "sponsored", + eventCount: "1", + }, + }, + ]); + await QuickSuggestTestUtils.setConfig({ + impression_caps: { + sponsored: { + lifetime: 5, + }, + }, + }); + }, + 1: async () => { + for (let i = 0; i < 2; i++) { + await checkSearch({ + name: "1s " + i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], + }); + } + await checkSearch({ + name: "1s additional", + searchString: "sponsored", + expectedResults: [], + }); + await checkTelemetryEvents([ + { + object: "hit", + extra: { + eventDate: "1000", + intervalSeconds: "Infinity", + maxCount: "5", + startDate: "0", + impressionDate: "1000", + count: "5", + type: "sponsored", + eventCount: "1", + }, + }, + ]); + }, + }); + }, + }); +}); + +// Tests a config change: lifetime cap -> lower lifetime cap +add_task(async function configChange_lifetimeCapLower() { + await doTest({ + config: { + impression_caps: { + sponsored: { + lifetime: 3, + }, + }, + }, + callback: async () => { + await doTimedCallbacks({ + 0: async () => { + for (let i = 0; i < 3; i++) { + await checkSearch({ + name: "0s " + i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], + }); + } + await checkSearch({ + name: "0s additional", + searchString: "sponsored", + expectedResults: [], + }); + await checkTelemetryEvents([ + { + object: "hit", + extra: { + eventDate: "0", + intervalSeconds: "Infinity", + maxCount: "3", + startDate: "0", + impressionDate: "0", + count: "3", + type: "sponsored", + eventCount: "1", + }, + }, + ]); + await QuickSuggestTestUtils.setConfig({ + impression_caps: { + sponsored: { + lifetime: 1, + }, + }, + }); + }, + 1: async () => { + await checkSearch({ + name: "1s", + searchString: "sponsored", + expectedResults: [], + }); + await checkTelemetryEvents([]); + }, + }); + }, + }); +}); + +// Makes sure stats are serialized to and from the pref correctly. +add_task(async function prefSync() { + await doTest({ + config: { + impression_caps: { + sponsored: { + lifetime: 5, + custom: [ + { interval_s: 3, max_count: 2 }, + { interval_s: 5, max_count: 4 }, + ], + }, + }, + }, + callback: async () => { + for (let i = 0; i < 2; i++) { + await checkSearch({ + name: i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], + }); + } + + let json = UrlbarPrefs.get("quicksuggest.impressionCaps.stats"); + Assert.ok(json, "JSON is non-empty"); + Assert.deepEqual( + JSON.parse(json), + { + sponsored: [ + { + intervalSeconds: 3, + count: 2, + maxCount: 2, + startDateMs: 0, + impressionDateMs: 0, + }, + { + intervalSeconds: 5, + count: 2, + maxCount: 4, + startDateMs: 0, + impressionDateMs: 0, + }, + { + intervalSeconds: null, + count: 2, + maxCount: 5, + startDateMs: 0, + impressionDateMs: 0, + }, + ], + }, + "JSON is correct" + ); + + QuickSuggest.impressionCaps._test_reloadStats(); + Assert.deepEqual( + QuickSuggest.impressionCaps._test_stats, + { + sponsored: [ + { + intervalSeconds: 3, + count: 2, + maxCount: 2, + startDateMs: 0, + impressionDateMs: 0, + }, + { + intervalSeconds: 5, + count: 2, + maxCount: 4, + startDateMs: 0, + impressionDateMs: 0, + }, + { + intervalSeconds: Infinity, + count: 2, + maxCount: 5, + startDateMs: 0, + impressionDateMs: 0, + }, + ], + }, + "Impression stats were properly restored from the pref" + ); + }, + }); +}); + +// Tests direct changes to the stats pref. +add_task(async function prefDirectlyChanged() { + await doTest({ + config: { + impression_caps: { + sponsored: { + lifetime: 5, + custom: [{ interval_s: 3, max_count: 3 }], + }, + }, + }, + callback: async () => { + let expectedStats = { + sponsored: [ + { + intervalSeconds: 3, + count: 0, + maxCount: 3, + startDateMs: 0, + impressionDateMs: 0, + }, + { + intervalSeconds: Infinity, + count: 0, + maxCount: 5, + startDateMs: 0, + impressionDateMs: 0, + }, + ], + }; + + UrlbarPrefs.set("quicksuggest.impressionCaps.stats", "bogus"); + Assert.deepEqual( + QuickSuggest.impressionCaps._test_stats, + expectedStats, + "Expected stats for 'bogus'" + ); + + UrlbarPrefs.set("quicksuggest.impressionCaps.stats", JSON.stringify({})); + Assert.deepEqual( + QuickSuggest.impressionCaps._test_stats, + expectedStats, + "Expected stats for {}" + ); + + UrlbarPrefs.set( + "quicksuggest.impressionCaps.stats", + JSON.stringify({ sponsored: "bogus" }) + ); + Assert.deepEqual( + QuickSuggest.impressionCaps._test_stats, + expectedStats, + "Expected stats for { sponsored: 'bogus' }" + ); + + UrlbarPrefs.set( + "quicksuggest.impressionCaps.stats", + JSON.stringify({ + sponsored: [ + { + intervalSeconds: 3, + count: 0, + maxCount: 3, + startDateMs: 0, + impressionDateMs: 0, + }, + { + intervalSeconds: "bogus", + count: 0, + maxCount: 99, + startDateMs: 0, + impressionDateMs: 0, + }, + { + intervalSeconds: Infinity, + count: 0, + maxCount: 5, + startDateMs: 0, + impressionDateMs: 0, + }, + ], + }) + ); + Assert.deepEqual( + QuickSuggest.impressionCaps._test_stats, + expectedStats, + "Expected stats with intervalSeconds: 'bogus'" + ); + + UrlbarPrefs.set( + "quicksuggest.impressionCaps.stats", + JSON.stringify({ + sponsored: [ + { + intervalSeconds: 3, + count: 0, + maxCount: 123, + startDateMs: 0, + impressionDateMs: 0, + }, + { + intervalSeconds: Infinity, + count: 0, + maxCount: 456, + startDateMs: 0, + impressionDateMs: 0, + }, + ], + }) + ); + Assert.deepEqual( + QuickSuggest.impressionCaps._test_stats, + expectedStats, + "Expected stats with `maxCount` values different from caps" + ); + + let stats = { + sponsored: [ + { + intervalSeconds: 3, + count: 1, + maxCount: 3, + startDateMs: 99, + impressionDateMs: 99, + }, + { + intervalSeconds: Infinity, + count: 7, + maxCount: 5, + startDateMs: 1337, + impressionDateMs: 1337, + }, + ], + }; + UrlbarPrefs.set( + "quicksuggest.impressionCaps.stats", + JSON.stringify(stats) + ); + Assert.deepEqual( + QuickSuggest.impressionCaps._test_stats, + stats, + "Expected stats with valid JSON" + ); + }, + }); +}); + +// Tests multiple interval periods where the cap is not hit. Telemetry should be +// recorded for these periods. +add_task(async function intervalsElapsedButCapNotHit() { + await doTest({ + config: { + impression_caps: { + sponsored: { + custom: [{ interval_s: 1, max_count: 3 }], + }, + }, + }, + callback: async () => { + await doTimedCallbacks({ + // 1s + 1: async () => { + await checkSearch({ + name: "1s", + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], + }); + }, + // 10s + 10: async () => { + QuickSuggest.impressionCaps._test_resetElapsedCounters(); + let expectedEvents = [ + // 1s: reset with count = 0 + { + object: "reset", + extra: { + eventDate: "1000", + intervalSeconds: "1", + maxCount: "3", + startDate: "0", + impressionDate: "0", + count: "0", + type: "sponsored", + eventCount: "1", + }, + }, + // 2-10s: reset with count = 1, eventCount = 9 + { + object: "reset", + extra: { + eventDate: "10000", + intervalSeconds: "1", + maxCount: "3", + startDate: "1000", + impressionDate: "1000", + count: "1", + type: "sponsored", + eventCount: "9", + }, + }, + ]; + await checkTelemetryEvents(expectedEvents); + }, + }); + }, + }); +}); + +// Simulates reset events across a restart with the following: +// +// S S R +// >----|----|----|----|----|----|----|----|----|----| +// 0s 1 2 3 4 5 6 7 8 9 10 +// +// 1. Startup at 0s +// 2. Caps and stats initialized with interval_s: 1 +// 3. Startup at 4.5s +// 4. Reset triggered at 10s +// +// Expected: +// At 10s: 6 batched resets for periods starting at 4s +add_task(async function restart_1() { + await doTest({ + config: { + impression_caps: { + sponsored: { + custom: [{ interval_s: 1, max_count: 1 }], + }, + }, + }, + callback: async () => { + gStartupDateMsStub.returns(4500); + await doTimedCallbacks({ + // 10s: 6 batched resets for periods starting at 4s + 10: async () => { + QuickSuggest.impressionCaps._test_resetElapsedCounters(); + await checkTelemetryEvents([ + { + object: "reset", + extra: { + eventDate: "10000", + intervalSeconds: "1", + maxCount: "1", + startDate: "4000", + impressionDate: "0", + count: "0", + type: "sponsored", + eventCount: "6", + }, + }, + ]); + }, + }); + }, + }); + gStartupDateMsStub.returns(0); +}); + +// Simulates reset events across a restart with the following: +// +// S S R +// >----|----|----|----|----|----|----|----|----|----| +// 0s 1 2 3 4 5 6 7 8 9 10 +// +// 1. Startup at 0s +// 2. Caps and stats initialized with interval_s: 1 +// 3. Startup at 5s +// 4. Reset triggered at 10s +// +// Expected: +// At 10s: 5 batched resets for periods starting at 5s +add_task(async function restart_2() { + await doTest({ + config: { + impression_caps: { + sponsored: { + custom: [{ interval_s: 1, max_count: 1 }], + }, + }, + }, + callback: async () => { + gStartupDateMsStub.returns(5000); + await doTimedCallbacks({ + // 10s: 5 batched resets for periods starting at 5s + 10: async () => { + QuickSuggest.impressionCaps._test_resetElapsedCounters(); + await checkTelemetryEvents([ + { + object: "reset", + extra: { + eventDate: "10000", + intervalSeconds: "1", + maxCount: "1", + startDate: "5000", + impressionDate: "0", + count: "0", + type: "sponsored", + eventCount: "5", + }, + }, + ]); + }, + }); + }, + }); + gStartupDateMsStub.returns(0); +}); + +// Simulates reset events across a restart with the following: +// +// S S R +// >----|----|----|----|----|----|----|----|----|----| +// 0s 1 2 3 4 5 6 7 8 9 10 +// +// 1. Startup at 0s +// 2. Caps and stats initialized with interval_s: 1 +// 3. Startup at 5.5s +// 4. Reset triggered at 10s +// +// Expected: +// At 10s: 5 batched resets for periods starting at 5s +add_task(async function restart_3() { + await doTest({ + config: { + impression_caps: { + sponsored: { + custom: [{ interval_s: 1, max_count: 1 }], + }, + }, + }, + callback: async () => { + gStartupDateMsStub.returns(5500); + await doTimedCallbacks({ + // 10s: 5 batched resets for periods starting at 5s + 10: async () => { + QuickSuggest.impressionCaps._test_resetElapsedCounters(); + await checkTelemetryEvents([ + { + object: "reset", + extra: { + eventDate: "10000", + intervalSeconds: "1", + maxCount: "1", + startDate: "5000", + impressionDate: "0", + count: "0", + type: "sponsored", + eventCount: "5", + }, + }, + ]); + }, + }); + }, + }); + gStartupDateMsStub.returns(0); +}); + +// Simulates reset events across a restart with the following: +// +// S S RR RR +// >---------|---------| +// 0s 10 20 +// +// 1. Startup at 0s +// 2. Caps and stats initialized with interval_s: 10 +// 3. Startup at 5s +// 4. Resets triggered at 9s, 10s, 19s, 20s +// +// Expected: +// At 10s: 1 reset for period starting at 0s +// At 20s: 1 reset for period starting at 10s +add_task(async function restart_4() { + await doTest({ + config: { + impression_caps: { + sponsored: { + custom: [{ interval_s: 10, max_count: 1 }], + }, + }, + }, + callback: async () => { + gStartupDateMsStub.returns(5000); + await doTimedCallbacks({ + // 9s: no resets + 9: async () => { + QuickSuggest.impressionCaps._test_resetElapsedCounters(); + await checkTelemetryEvents([]); + }, + // 10s: 1 reset for period starting at 0s + 10: async () => { + QuickSuggest.impressionCaps._test_resetElapsedCounters(); + await checkTelemetryEvents([ + { + object: "reset", + extra: { + eventDate: "10000", + intervalSeconds: "10", + maxCount: "1", + startDate: "0", + impressionDate: "0", + count: "0", + type: "sponsored", + eventCount: "1", + }, + }, + ]); + }, + // 19s: no resets + 19: async () => { + QuickSuggest.impressionCaps._test_resetElapsedCounters(); + await checkTelemetryEvents([]); + }, + // 20s: 1 reset for period starting at 10s + 20: async () => { + QuickSuggest.impressionCaps._test_resetElapsedCounters(); + await checkTelemetryEvents([ + { + object: "reset", + extra: { + eventDate: "20000", + intervalSeconds: "10", + maxCount: "1", + startDate: "10000", + impressionDate: "0", + count: "0", + type: "sponsored", + eventCount: "1", + }, + }, + ]); + }, + }); + }, + }); + gStartupDateMsStub.returns(0); +}); + +// Simulates reset events across a restart with the following: +// +// S S R +// >---------|---------| +// 0s 10 20 +// +// 1. Startup at 0s +// 2. Caps and stats initialized with interval_s: 10 +// 3. Startup at 5s +// 4. Reset triggered at 20s +// +// Expected: +// At 20s: 2 batched resets for periods starting at 0s +add_task(async function restart_5() { + await doTest({ + config: { + impression_caps: { + sponsored: { + custom: [{ interval_s: 10, max_count: 1 }], + }, + }, + }, + callback: async () => { + gStartupDateMsStub.returns(5000); + await doTimedCallbacks({ + // 20s: 2 batches resets for periods starting at 0s + 20: async () => { + QuickSuggest.impressionCaps._test_resetElapsedCounters(); + await checkTelemetryEvents([ + { + object: "reset", + extra: { + eventDate: "20000", + intervalSeconds: "10", + maxCount: "1", + startDate: "0", + impressionDate: "0", + count: "0", + type: "sponsored", + eventCount: "2", + }, + }, + ]); + }, + }); + }, + }); + gStartupDateMsStub.returns(0); +}); + +// Simulates reset events across a restart with the following: +// +// S S RR RR +// >---------|---------|---------| +// 0s 10 20 30 +// +// 1. Startup at 0s +// 2. Caps and stats initialized with interval_s: 10 +// 3. Startup at 15s +// 4. Resets triggered at 19s, 20s, 29s, 30s +// +// Expected: +// At 20s: 1 reset for period starting at 10s +// At 30s: 1 reset for period starting at 20s +add_task(async function restart_6() { + await doTest({ + config: { + impression_caps: { + sponsored: { + custom: [{ interval_s: 10, max_count: 1 }], + }, + }, + }, + callback: async () => { + gStartupDateMsStub.returns(15000); + await doTimedCallbacks({ + // 19s: no resets + 19: async () => { + QuickSuggest.impressionCaps._test_resetElapsedCounters(); + await checkTelemetryEvents([]); + }, + // 20s: 1 reset for period starting at 10s + 20: async () => { + QuickSuggest.impressionCaps._test_resetElapsedCounters(); + await checkTelemetryEvents([ + { + object: "reset", + extra: { + eventDate: "20000", + intervalSeconds: "10", + maxCount: "1", + startDate: "10000", + impressionDate: "0", + count: "0", + type: "sponsored", + eventCount: "1", + }, + }, + ]); + }, + // 29s: no resets + 29: async () => { + QuickSuggest.impressionCaps._test_resetElapsedCounters(); + await checkTelemetryEvents([]); + }, + // 30s: 1 reset for period starting at 20s + 30: async () => { + QuickSuggest.impressionCaps._test_resetElapsedCounters(); + await checkTelemetryEvents([ + { + object: "reset", + extra: { + eventDate: "30000", + intervalSeconds: "10", + maxCount: "1", + startDate: "20000", + impressionDate: "0", + count: "0", + type: "sponsored", + eventCount: "1", + }, + }, + ]); + }, + }); + }, + }); + gStartupDateMsStub.returns(0); +}); + +// Simulates reset events across a restart with the following: +// +// S S R +// >---------|---------|---------| +// 0s 10 20 30 +// +// 1. Startup at 0s +// 2. Caps and stats initialized with interval_s: 10 +// 3. Startup at 15s +// 4. Reset triggered at 30s +// +// Expected: +// At 30s: 2 batched resets for periods starting at 10s +add_task(async function restart_7() { + await doTest({ + config: { + impression_caps: { + sponsored: { + custom: [{ interval_s: 10, max_count: 1 }], + }, + }, + }, + callback: async () => { + gStartupDateMsStub.returns(15000); + await doTimedCallbacks({ + // 30s: 2 batched resets for periods starting at 10s + 30: async () => { + QuickSuggest.impressionCaps._test_resetElapsedCounters(); + await checkTelemetryEvents([ + { + object: "reset", + extra: { + eventDate: "30000", + intervalSeconds: "10", + maxCount: "1", + startDate: "10000", + impressionDate: "0", + count: "0", + type: "sponsored", + eventCount: "2", + }, + }, + ]); + }, + }); + }, + }); + gStartupDateMsStub.returns(0); +}); + +// Tests reset telemetry recorded on shutdown. +add_task(async function shutdown() { + await doTest({ + config: { + impression_caps: { + sponsored: { + custom: [{ interval_s: 1, max_count: 1 }], + }, + }, + }, + callback: async () => { + // Make `Date.now()` return 10s. Since the cap's `interval_s` is 1s and + // before this `Date.now()` returned 0s, 10 reset events should be + // recorded on shutdown. + gDateNowStub.returns(10000); + + // Simulate shutdown. + Services.prefs.setBoolPref("toolkit.asyncshutdown.testing", true); + AsyncShutdown.profileChangeTeardown._trigger(); + + await checkTelemetryEvents([ + { + object: "reset", + extra: { + eventDate: "10000", + intervalSeconds: "1", + maxCount: "1", + startDate: "0", + impressionDate: "0", + count: "0", + type: "sponsored", + eventCount: "10", + }, + }, + ]); + + gDateNowStub.returns(0); + Services.prefs.clearUserPref("toolkit.asyncshutdown.testing"); + }, + }); +}); + +// Tests the reset interval in realtime. +add_task(async function resetInterval() { + // Remove the test stubs so we can test in realtime. + gDateNowStub.restore(); + gStartupDateMsStub.restore(); + + await doTest({ + config: { + impression_caps: { + sponsored: { + custom: [{ interval_s: 0.1, max_count: 1 }], + }, + }, + }, + callback: async () => { + // Restart the reset interval now with a 1s period. Since the cap's + // `interval_s` is 0.1s, at least 10 reset events should be recorded the + // first time the reset interval fires. The exact number depends on timing + // and the machine running the test: how many 0.1s intervals elapse + // between when the config is set to when the reset interval fires. For + // that reason, we allow some leeway when checking `eventCount` below to + // avoid intermittent failures. + QuickSuggest.impressionCaps._test_setCountersResetInterval(1000); + + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(r => setTimeout(r, 1100)); + + // Restore the reset interval to its default. + QuickSuggest.impressionCaps._test_setCountersResetInterval(); + + await checkTelemetryEvents([ + { + object: "reset", + extra: { + eventDate: /^[0-9]+$/, + intervalSeconds: "0.1", + maxCount: "1", + startDate: /^[0-9]+$/, + impressionDate: "0", + count: "0", + type: "sponsored", + // See comment above on allowing leeway for `eventCount`. + eventCount: str => { + info(`Checking 'eventCount': ${str}`); + let count = parseInt(str); + return 10 <= count && count < 20; + }, + }, + }, + ]); + }, + }); + + // Recreate the test stubs. + gDateNowStub = gSandbox.stub( + Cu.getGlobalForObject(UrlbarProviderQuickSuggest).Date, + "now" + ); + gStartupDateMsStub = gSandbox.stub( + QuickSuggest.impressionCaps, + "_getStartupDateMs" + ); + gStartupDateMsStub.returns(0); +}); + +/** + * Main test helper. Sets up state, calls your callback, and resets state. + * + * @param {object} options + * Options object. + * @param {object} options.config + * The quick suggest config to use during the test. + * @param {Function} options.callback + * The callback that will be run with the {@link config} + */ +async function doTest({ config, callback }) { + Services.telemetry.clearEvents(); + + // Make `Date.now()` return 0 to start with. It's necessary to do this before + // calling `withConfig()` because when a new config is set, the provider + // validates its impression stats, whose `startDateMs` values depend on + // `Date.now()`. + gDateNowStub.returns(0); + + info(`Clearing stats and setting config`); + UrlbarPrefs.clear("quicksuggest.impressionCaps.stats"); + QuickSuggest.impressionCaps._test_reloadStats(); + await QuickSuggestTestUtils.withConfig({ config, callback }); +} + +/** + * Does a series of timed searches and checks their results and telemetry. This + * function relies on `doTimedCallbacks()`, so it may be helpful to look at it + * too. + * + * @param {string} searchString + * The query that should be timed + * @param {object} expectedBySecond + * An object that maps from seconds to objects that describe the searches to + * perform, their expected results, and the expected telemetry. For a given + * entry `S -> E` in this object, searches are performed S seconds after this + * function is called. `E` is an object that looks like this: + * + * { results, telemetry } + * + * {array} results + * An array of arrays. A search is performed for each sub-array in + * `results`, and the contents of the sub-array are the expected results + * for that search. + * {object} telemetry + * An object like this: { events } + * {array} events + * An array of expected telemetry events after all searches are done. + * Telemetry events are cleared after checking these. If not present, + * then it will be asserted that no events were recorded. + * + * Example: + * + * { + * 0: { + * results: [[R1], []], + * telemetry: { + * events: [ + * someExpectedEvent, + * ], + * }, + * } + * 1: { + * results: [[]], + * }, + * } + * + * 0 seconds after `doTimedSearches()` is called, two searches are + * performed. The first one is expected to return a single result R1, and + * the second search is expected to return no results. After the searches + * are done, one telemetry event is expected to be recorded. + * + * 1 second after `doTimedSearches()` is called, one search is performed. + * It's expected to return no results, and no telemetry is expected to be + * recorded. + */ +async function doTimedSearches(searchString, expectedBySecond) { + await doTimedCallbacks( + Object.entries(expectedBySecond).reduce( + (memo, [second, { results, telemetry }]) => { + memo[second] = async () => { + for (let i = 0; i < results.length; i++) { + let expectedResults = results[i]; + await checkSearch({ + searchString, + expectedResults, + name: `${second}s search ${i + 1} of ${results.length}`, + }); + } + let { events } = telemetry || {}; + await checkTelemetryEvents(events || []); + }; + return memo; + }, + {} + ) + ); +} + +/** + * Takes a series a callbacks and times at which they should be called, and + * calls them accordingly. This function is specifically designed for + * UrlbarProviderQuickSuggest and its impression capping implementation because + * it works by stubbing `Date.now()` within UrlbarProviderQuickSuggest. The + * callbacks are not actually called at the given times but instead `Date.now()` + * is stubbed so that UrlbarProviderQuickSuggest will think they are being + * called at the given times. + * + * A more general implementation of this helper function that isn't tailored to + * UrlbarProviderQuickSuggest is commented out below, and unfortunately it + * doesn't work properly on macOS. + * + * @param {object} callbacksBySecond + * An object that maps from seconds to callback functions. For a given entry + * `S -> F` in this object, the callback F is called S seconds after + * `doTimedCallbacks()` is called. + */ +async function doTimedCallbacks(callbacksBySecond) { + let entries = Object.entries(callbacksBySecond).sort(([t1], [t2]) => t1 - t2); + for (let [timeoutSeconds, callback] of entries) { + gDateNowStub.returns(1000 * timeoutSeconds); + await callback(); + } +} + +/* +// This is the original implementation of `doTimedCallbacks()`, left here for +// reference or in case the macOS problem described below is fixed. Instead of +// stubbing `Date.now()` within UrlbarProviderQuickSuggest, it starts parallel +// timers so that the callbacks are actually called at appropriate times. This +// version of `doTimedCallbacks()` is therefore more generally useful, but it +// has the drawback that your test has to run in real time. e.g., if one of your +// callbacks needs to run 10s from now, the test must actually wait 10s. +// +// Unfortunately macOS seems to have some kind of limit of ~33 total 1-second +// timers during any xpcshell test -- not 33 simultaneous timers but 33 total +// timers. After that, timers fire randomly and with huge timeout periods that +// are often exactly 10s greater than the specified period, as if some 10s +// timeout internal to macOS is being hit. This problem does not seem to happen +// when running the full browser, only during xpcshell tests. In fact the +// problem can be reproduced in an xpcshell test that simply creates an interval +// timer whose period is 1s (e.g., using `setInterval()` from Timer.sys.mjs). +// After ~33 ticks, the timer's period jumps to ~10s. +async function doTimedCallbacks(callbacksBySecond) { + await Promise.all( + Object.entries(callbacksBySecond).map( + ([timeoutSeconds, callback]) => new Promise( + resolve => setTimeout( + () => callback().then(resolve), + 1000 * parseInt(timeoutSeconds) + ) + ) + ) + ); +} +*/ + +/** + * Does a search, triggers an engagement, and checks the results. + * + * @param {object} options + * Options object. + * @param {string} options.name + * This value is the name of the search and will be logged in messages to make + * debugging easier. + * @param {string} options.searchString + * The query that should be searched. + * @param {Array} options.expectedResults + * The results that are expected from the search. + */ +async function checkSearch({ name, searchString, expectedResults }) { + info(`Preparing search "${name}" with search string "${searchString}"`); + let context = createContext(searchString, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + info(`Doing search: ${name}`); + await check_results({ + context, + matches: expectedResults, + }); + info(`Finished search: ${name}`); + + // Impression stats are updated only on engagement, so force one now. + // `selIndex` doesn't really matter but since we're not trying to simulate a + // click on the suggestion, pass in -1 to ensure we don't record a click. Pass + // in true for `isPrivate` so we don't attempt to record the impression ping + // because otherwise the following PingCentre error is logged: + // "Structured Ingestion ping failure with error: undefined" + let isPrivate = true; + if (UrlbarProviderQuickSuggest._resultFromLastQuery) { + UrlbarProviderQuickSuggest._resultFromLastQuery.isVisible = true; + } + UrlbarProviderQuickSuggest.onEngagement(isPrivate, "engagement", context, { + selIndex: -1, + }); +} + +async function checkTelemetryEvents(expectedEvents) { + QuickSuggestTestUtils.assertEvents( + expectedEvents.map(event => ({ + ...event, + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "impression_cap", + })), + // Filter in only impression_cap events. + { method: "impression_cap" } + ); +} diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_merino.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_merino.js new file mode 100644 index 0000000000..4327890a0d --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_merino.js @@ -0,0 +1,681 @@ +/* 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 Merino integration with UrlbarProviderQuickSuggest. + +"use strict"; + +// relative to `browser.urlbar` +const PREF_DATA_COLLECTION_ENABLED = "quicksuggest.dataCollection.enabled"; +const PREF_MERINO_ENABLED = "merino.enabled"; +const PREF_REMOTE_SETTINGS_ENABLED = "quicksuggest.remoteSettings.enabled"; + +const SEARCH_STRING = "frab"; + +const REMOTE_SETTINGS_RESULTS = [ + { + id: 1, + url: "http://test.com/q=frabbits", + title: "frabbits", + keywords: [SEARCH_STRING], + click_url: "http://click.reporting.test.com/", + impression_url: "http://impression.reporting.test.com/", + advertiser: "TestAdvertiser", + }, +]; + +const EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT = { + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + heuristic: false, + payload: { + telemetryType: "adm_sponsored", + qsSuggestion: SEARCH_STRING, + title: "frabbits", + url: "http://test.com/q=frabbits", + originalUrl: "http://test.com/q=frabbits", + icon: null, + sponsoredImpressionUrl: "http://impression.reporting.test.com/", + sponsoredClickUrl: "http://click.reporting.test.com/", + sponsoredBlockId: 1, + sponsoredAdvertiser: "TestAdvertiser", + isSponsored: true, + helpUrl: QuickSuggest.HELP_URL, + helpL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-learn-more-about-firefox-suggest" + : "firefox-suggest-urlbar-learn-more", + }, + isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"), + blockL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-dismiss-firefox-suggest" + : "firefox-suggest-urlbar-block", + }, + displayUrl: "http://test.com/q=frabbits", + source: "remote-settings", + }, +}; + +const EXPECTED_MERINO_URLBAR_RESULT = { + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + heuristic: false, + payload: { + telemetryType: "adm_sponsored", + qsSuggestion: "full_keyword", + title: "title", + url: "url", + originalUrl: "url", + icon: null, + sponsoredImpressionUrl: "impression_url", + sponsoredClickUrl: "click_url", + sponsoredBlockId: 1, + sponsoredAdvertiser: "advertiser", + isSponsored: true, + helpUrl: QuickSuggest.HELP_URL, + helpL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-learn-more-about-firefox-suggest" + : "firefox-suggest-urlbar-learn-more", + }, + isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"), + blockL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-dismiss-firefox-suggest" + : "firefox-suggest-urlbar-block", + }, + displayUrl: "url", + requestId: "request_id", + source: "merino", + }, +}; + +// `UrlbarProviderQuickSuggest.#merino` is lazily created on the first Merino +// fetch, so it's easiest to create `gClient` lazily too. +XPCOMUtils.defineLazyGetter( + this, + "gClient", + () => UrlbarProviderQuickSuggest._test_merino +); + +add_task(async function init() { + UrlbarPrefs.set("quicksuggest.enabled", true); + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + UrlbarPrefs.set("quicksuggest.shouldShowOnboardingDialog", false); + + await MerinoTestUtils.server.start(); + + // Set up the remote settings client with the test data. + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + remoteSettingsResults: [ + { + type: "data", + attachment: REMOTE_SETTINGS_RESULTS, + }, + ], + }); + + Assert.equal( + typeof QuickSuggestRemoteSettings.DEFAULT_SUGGESTION_SCORE, + "number", + "Sanity check: DEFAULT_SUGGESTION_SCORE is defined" + ); +}); + +// Tests with Merino enabled and remote settings disabled. +add_task(async function oneEnabled_merino() { + UrlbarPrefs.set(PREF_MERINO_ENABLED, true); + UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, false); + UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true); + + let histograms = MerinoTestUtils.getAndClearHistograms(); + + // Use a score lower than the remote settings score to make sure the + // suggestion is included regardless. + MerinoTestUtils.server.response.body.suggestions[0].score = + QuickSuggestRemoteSettings.DEFAULT_SUGGESTION_SCORE / 2; + + let context = createContext(SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [EXPECTED_MERINO_URLBAR_RESULT], + }); + + MerinoTestUtils.checkAndClearHistograms({ + histograms, + response: "success", + latencyRecorded: true, + client: gClient, + }); + + MerinoTestUtils.server.reset(); + gClient.resetSession(); +}); + +// Tests with Merino disabled and remote settings enabled. +add_task(async function oneEnabled_remoteSettings() { + UrlbarPrefs.set(PREF_MERINO_ENABLED, false); + UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, true); + UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true); + + let histograms = MerinoTestUtils.getAndClearHistograms(); + + let context = createContext(SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT], + }); + + MerinoTestUtils.checkAndClearHistograms({ + histograms, + response: null, + latencyRecorded: false, + client: gClient, + }); +}); + +// Tests with Merino enabled but with data collection disabled. Results should +// not be fetched from Merino in that case. Also tests with remote settings +// enabled. +add_task(async function dataCollectionDisabled() { + UrlbarPrefs.set(PREF_MERINO_ENABLED, true); + UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, true); + UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, false); + + let context = createContext(SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT], + }); +}); + +// When the Merino suggestion has a higher score than the remote settings +// suggestion, the Merino suggestion should be used. +add_task(async function higherScore() { + UrlbarPrefs.set(PREF_MERINO_ENABLED, true); + UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, true); + UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true); + + let histograms = MerinoTestUtils.getAndClearHistograms(); + + MerinoTestUtils.server.response.body.suggestions[0].score = + 2 * QuickSuggestRemoteSettings.DEFAULT_SUGGESTION_SCORE; + + let context = createContext(SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [EXPECTED_MERINO_URLBAR_RESULT], + }); + + MerinoTestUtils.checkAndClearHistograms({ + histograms, + response: "success", + latencyRecorded: true, + client: gClient, + }); + + MerinoTestUtils.server.reset(); + gClient.resetSession(); +}); + +// When the Merino suggestion has a lower score than the remote settings +// suggestion, the remote settings suggestion should be used. +add_task(async function lowerScore() { + UrlbarPrefs.set(PREF_MERINO_ENABLED, true); + UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, true); + UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true); + + let histograms = MerinoTestUtils.getAndClearHistograms(); + + MerinoTestUtils.server.response.body.suggestions[0].score = + QuickSuggestRemoteSettings.DEFAULT_SUGGESTION_SCORE / 2; + + let context = createContext(SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT], + }); + + MerinoTestUtils.checkAndClearHistograms({ + histograms, + response: "success", + latencyRecorded: true, + client: gClient, + }); + + MerinoTestUtils.server.reset(); + gClient.resetSession(); +}); + +// When the Merino and remote settings suggestions have the same score, the +// remote settings suggestion should be used. +add_task(async function sameScore() { + UrlbarPrefs.set(PREF_MERINO_ENABLED, true); + UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, true); + UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true); + + let histograms = MerinoTestUtils.getAndClearHistograms(); + + MerinoTestUtils.server.response.body.suggestions[0].score = + QuickSuggestRemoteSettings.DEFAULT_SUGGESTION_SCORE; + + let context = createContext(SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT], + }); + + MerinoTestUtils.checkAndClearHistograms({ + histograms, + response: "success", + latencyRecorded: true, + client: gClient, + }); + + MerinoTestUtils.server.reset(); + gClient.resetSession(); +}); + +// When the Merino suggestion does not include a score, the remote settings +// suggestion should be used. +add_task(async function noMerinoScore() { + UrlbarPrefs.set(PREF_MERINO_ENABLED, true); + UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, true); + UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true); + + let histograms = MerinoTestUtils.getAndClearHistograms(); + + Assert.equal( + typeof MerinoTestUtils.server.response.body.suggestions[0].score, + "number", + "Sanity check: First suggestion has a score" + ); + delete MerinoTestUtils.server.response.body.suggestions[0].score; + + let context = createContext(SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT], + }); + + MerinoTestUtils.checkAndClearHistograms({ + histograms, + response: "success", + latencyRecorded: true, + client: gClient, + }); + + MerinoTestUtils.server.reset(); + gClient.resetSession(); +}); + +// When remote settings doesn't return a suggestion but Merino does, the Merino +// suggestion should be used. +add_task(async function noSuggestion_remoteSettings() { + UrlbarPrefs.set(PREF_MERINO_ENABLED, true); + UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, true); + UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true); + + let histograms = MerinoTestUtils.getAndClearHistograms(); + + let context = createContext("this doesn't match remote settings", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [EXPECTED_MERINO_URLBAR_RESULT], + }); + + MerinoTestUtils.checkAndClearHistograms({ + histograms, + response: "success", + latencyRecorded: true, + client: gClient, + }); + + MerinoTestUtils.server.reset(); + gClient.resetSession(); +}); + +// When Merino doesn't return a suggestion but remote settings does, the remote +// settings suggestion should be used. +add_task(async function noSuggestion_merino() { + UrlbarPrefs.set(PREF_MERINO_ENABLED, true); + UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, true); + UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true); + + let histograms = MerinoTestUtils.getAndClearHistograms(); + + MerinoTestUtils.server.response.body.suggestions = []; + + let context = createContext(SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT], + }); + + MerinoTestUtils.checkAndClearHistograms({ + histograms, + response: "no_suggestion", + latencyRecorded: true, + client: gClient, + }); + + MerinoTestUtils.server.reset(); + gClient.resetSession(); +}); + +// Tests with both Merino and remote settings disabled. +add_task(async function bothDisabled() { + UrlbarPrefs.set(PREF_MERINO_ENABLED, false); + UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, false); + UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true); + + let histograms = MerinoTestUtils.getAndClearHistograms(); + + let context = createContext(SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ context, matches: [] }); + + MerinoTestUtils.checkAndClearHistograms({ + histograms, + response: null, + latencyRecorded: false, + client: gClient, + }); +}); + +// When Merino returns multiple suggestions, the one with the largest score +// should be used. +add_task(async function multipleMerinoSuggestions() { + UrlbarPrefs.set(PREF_MERINO_ENABLED, true); + UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, false); + UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true); + + let histograms = MerinoTestUtils.getAndClearHistograms(); + + MerinoTestUtils.server.response.body.suggestions = [ + { + provider: "adm", + full_keyword: "multipleMerinoSuggestions 0 full_keyword", + title: "multipleMerinoSuggestions 0 title", + url: "multipleMerinoSuggestions 0 url", + icon: "multipleMerinoSuggestions 0 icon", + impression_url: "multipleMerinoSuggestions 0 impression_url", + click_url: "multipleMerinoSuggestions 0 click_url", + block_id: 0, + advertiser: "multipleMerinoSuggestions 0 advertiser", + is_sponsored: true, + score: 0.1, + }, + { + provider: "adm", + full_keyword: "multipleMerinoSuggestions 1 full_keyword", + title: "multipleMerinoSuggestions 1 title", + url: "multipleMerinoSuggestions 1 url", + icon: "multipleMerinoSuggestions 1 icon", + impression_url: "multipleMerinoSuggestions 1 impression_url", + click_url: "multipleMerinoSuggestions 1 click_url", + block_id: 1, + advertiser: "multipleMerinoSuggestions 1 advertiser", + is_sponsored: true, + score: 1, + }, + { + provider: "adm", + full_keyword: "multipleMerinoSuggestions 2 full_keyword", + title: "multipleMerinoSuggestions 2 title", + url: "multipleMerinoSuggestions 2 url", + icon: "multipleMerinoSuggestions 2 icon", + impression_url: "multipleMerinoSuggestions 2 impression_url", + click_url: "multipleMerinoSuggestions 2 click_url", + block_id: 2, + advertiser: "multipleMerinoSuggestions 2 advertiser", + is_sponsored: true, + score: 0.2, + }, + ]; + + let context = createContext("test", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [ + { + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + heuristic: false, + payload: { + telemetryType: "adm_sponsored", + qsSuggestion: "multipleMerinoSuggestions 1 full_keyword", + title: "multipleMerinoSuggestions 1 title", + url: "multipleMerinoSuggestions 1 url", + originalUrl: "multipleMerinoSuggestions 1 url", + icon: "multipleMerinoSuggestions 1 icon", + sponsoredImpressionUrl: "multipleMerinoSuggestions 1 impression_url", + sponsoredClickUrl: "multipleMerinoSuggestions 1 click_url", + sponsoredBlockId: 1, + sponsoredAdvertiser: "multipleMerinoSuggestions 1 advertiser", + isSponsored: true, + helpUrl: QuickSuggest.HELP_URL, + helpL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-learn-more-about-firefox-suggest" + : "firefox-suggest-urlbar-learn-more", + }, + isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"), + blockL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-dismiss-firefox-suggest" + : "firefox-suggest-urlbar-block", + }, + displayUrl: "multipleMerinoSuggestions 1 url", + requestId: "request_id", + source: "merino", + }, + }, + ], + }); + + MerinoTestUtils.checkAndClearHistograms({ + histograms, + response: "success", + latencyRecorded: true, + client: gClient, + }); + + MerinoTestUtils.server.reset(); + gClient.resetSession(); +}); + +// Timestamp templates in URLs should be replaced with real timestamps. +add_task(async function timestamps() { + UrlbarPrefs.set(PREF_MERINO_ENABLED, true); + UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, false); + UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true); + + // Set up the Merino response with template URLs. + let suggestion = MerinoTestUtils.server.response.body.suggestions[0]; + let { TIMESTAMP_TEMPLATE } = QuickSuggest; + + suggestion.url = `http://example.com/time-${TIMESTAMP_TEMPLATE}`; + suggestion.click_url = `http://example.com/time-${TIMESTAMP_TEMPLATE}-foo`; + + // Do a search. + let context = createContext("test", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + let controller = UrlbarTestUtils.newMockController({ + input: { + isPrivate: context.isPrivate, + onFirstResult() { + return false; + }, + getSearchSource() { + return "dummy-search-source"; + }, + window: { + location: { + href: AppConstants.BROWSER_CHROME_URL, + }, + }, + }, + }); + await controller.startQuery(context); + + // Should be one quick suggest result. + Assert.equal(context.results.length, 1, "One result returned"); + let result = context.results[0]; + + QuickSuggestTestUtils.assertTimestampsReplaced(result, { + url: suggestion.click_url, + sponsoredClickUrl: suggestion.click_url, + }); + + MerinoTestUtils.server.reset(); + gClient.resetSession(); +}); + +// When both suggestion types are disabled but data collection is enabled, we +// should still send requests to Merino, and the requests should include an +// empty `providers` to tell Merino not to fetch any suggestions. +add_task(async function suggestedDisabled_dataCollectionEnabled() { + UrlbarPrefs.set(PREF_MERINO_ENABLED, true); + UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, false); + UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true); + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", false); + + let histograms = MerinoTestUtils.getAndClearHistograms(); + + let context = createContext("test", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [], + }); + + // Check that the request is received and includes an empty `providers`. + MerinoTestUtils.server.checkAndClearRequests([ + { + params: { + [MerinoTestUtils.SEARCH_PARAMS.QUERY]: "test", + [MerinoTestUtils.SEARCH_PARAMS.SEQUENCE_NUMBER]: 0, + [MerinoTestUtils.SEARCH_PARAMS.PROVIDERS]: "", + }, + }, + ]); + + MerinoTestUtils.checkAndClearHistograms({ + histograms, + response: "success", + latencyRecorded: true, + client: gClient, + }); + + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + gClient.resetSession(); +}); + +// Test whether the blocking for Merino results works. +add_task(async function block() { + UrlbarPrefs.set(PREF_MERINO_ENABLED, true); + UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, true); + UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true); + + for (const suggestion of MerinoTestUtils.server.response.body.suggestions) { + await QuickSuggest.blockedSuggestions.add(suggestion.url); + } + + const context = createContext(SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + + await check_results({ + context, + matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT], + }); + + await QuickSuggest.blockedSuggestions.clear(); + MerinoTestUtils.server.reset(); + gClient.resetSession(); +}); + +// Tests a Merino suggestion that is a best match. +add_task(async function bestMatch() { + UrlbarPrefs.set(PREF_MERINO_ENABLED, true); + UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, true); + UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true); + + // Simply enabling the best match feature should make the mock suggestion a + // best match because the search string length is greater than the required + // best match length. + UrlbarPrefs.set("bestMatch.enabled", true); + UrlbarPrefs.set("suggest.bestmatch", true); + + let expectedResult = { ...EXPECTED_MERINO_URLBAR_RESULT }; + expectedResult.payload = { ...EXPECTED_MERINO_URLBAR_RESULT.payload }; + expectedResult.isBestMatch = true; + delete expectedResult.payload.qsSuggestion; + + await QuickSuggestTestUtils.withConfig({ + config: QuickSuggestTestUtils.BEST_MATCH_CONFIG, + callback: async () => { + let context = createContext(SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [expectedResult], + }); + + // This isn't necessary since `check_results()` checks `isBestMatch`, but + // check it here explicitly for good measure. + Assert.ok(context.results[0].isBestMatch, "Result is a best match"); + }, + }); + + UrlbarPrefs.clear("bestMatch.enabled"); + UrlbarPrefs.clear("suggest.bestmatch"); + + MerinoTestUtils.server.reset(); + gClient.resetSession(); +}); diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_merinoSessions.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_merinoSessions.js new file mode 100644 index 0000000000..935577c36c --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_merinoSessions.js @@ -0,0 +1,174 @@ +/* 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 Merino session integration with UrlbarProviderQuickSuggest. + +"use strict"; + +// `UrlbarProviderQuickSuggest.#merino` is lazily created on the first Merino +// fetch, so it's easiest to create `gClient` lazily too. +XPCOMUtils.defineLazyGetter( + this, + "gClient", + () => UrlbarProviderQuickSuggest._test_merino +); + +add_task(async function init() { + UrlbarPrefs.set("quicksuggest.enabled", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + UrlbarPrefs.set("merino.enabled", true); + UrlbarPrefs.set("quicksuggest.remoteSettings.enabled", false); + UrlbarPrefs.set("quicksuggest.dataCollection.enabled", true); + + await MerinoTestUtils.server.start(); + await QuickSuggestTestUtils.ensureQuickSuggestInit(); +}); + +// In a single engagement, all requests should use the same session ID and the +// sequence number should be incremented. +add_task(async function singleEngagement() { + let controller = UrlbarTestUtils.newMockController(); + + for (let i = 0; i < 3; i++) { + let searchString = "search" + i; + await controller.startQuery( + createContext(searchString, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }) + ); + + MerinoTestUtils.server.checkAndClearRequests([ + { + params: { + [MerinoTestUtils.SEARCH_PARAMS.QUERY]: searchString, + [MerinoTestUtils.SEARCH_PARAMS.SEQUENCE_NUMBER]: i, + }, + }, + ]); + } + + // End the engagement to reset the session for the next test. + endEngagement(); +}); + +// New engagements should not use the same session ID as previous engagements +// and the sequence number should be reset. This task completes each engagement +// successfully. +add_task(async function manyEngagements_engagement() { + await doManyEngagementsTest("engagement"); +}); + +// New engagements should not use the same session ID as previous engagements +// and the sequence number should be reset. This task abandons each engagement. +add_task(async function manyEngagements_abandonment() { + await doManyEngagementsTest("abandonment"); +}); + +async function doManyEngagementsTest(state) { + let controller = UrlbarTestUtils.newMockController(); + + for (let i = 0; i < 3; i++) { + let searchString = "search" + i; + let context = createContext(searchString, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await controller.startQuery(context); + + MerinoTestUtils.server.checkAndClearRequests([ + { + params: { + [MerinoTestUtils.SEARCH_PARAMS.QUERY]: searchString, + [MerinoTestUtils.SEARCH_PARAMS.SEQUENCE_NUMBER]: 0, + }, + }, + ]); + + endEngagement(context, state); + } +} + +// When a search is canceled after the request is sent and before the Merino +// response is received, the sequence number should still be incremented. +add_task(async function canceledQueries() { + let controller = UrlbarTestUtils.newMockController(); + + for (let i = 0; i < 3; i++) { + // Send the first response after a delay to make sure the client will not + // receive it before we start the second fetch. + MerinoTestUtils.server.response.delay = UrlbarPrefs.get("merino.timeoutMs"); + + // Start the first search. + let requestPromise = MerinoTestUtils.server.waitForNextRequest(); + let searchString1 = "search" + i; + controller.startQuery( + createContext(searchString1, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }) + ); + + // Wait until the first request is received before starting the second + // search. If we started the second search immediately, the first would be + // canceled before the provider is even called due to the urlbar's 50ms + // delay (see `browser.urlbar.delay`) so the sequence number would not be + // incremented for it. Here we want to test the case where the first search + // is canceled after the request is sent and the number is incremented. + await requestPromise; + delete MerinoTestUtils.server.response.delay; + + // Now do a second search that cancels the first. + let searchString2 = searchString1 + "again"; + await controller.startQuery( + createContext(searchString2, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }) + ); + + // The sequence number should have been incremented for each search. + MerinoTestUtils.server.checkAndClearRequests([ + { + params: { + [MerinoTestUtils.SEARCH_PARAMS.QUERY]: searchString1, + [MerinoTestUtils.SEARCH_PARAMS.SEQUENCE_NUMBER]: 2 * i, + }, + }, + { + params: { + [MerinoTestUtils.SEARCH_PARAMS.QUERY]: searchString2, + [MerinoTestUtils.SEARCH_PARAMS.SEQUENCE_NUMBER]: 2 * i + 1, + }, + }, + ]); + } + + // End the engagement to reset the session for the next test. + endEngagement(); +}); + +function endEngagement(context = null, state = "engagement") { + UrlbarProviderQuickSuggest.onEngagement( + false, + state, + context || + createContext("endEngagement", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + { selIndex: -1 } + ); + + Assert.strictEqual( + gClient.sessionID, + null, + "sessionID is null after engagement" + ); + Assert.strictEqual( + gClient._test_sessionTimer, + null, + "sessionTimer is null after engagement" + ); +} diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_migrate_v1.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_migrate_v1.js new file mode 100644 index 0000000000..8e80f3639a --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_migrate_v1.js @@ -0,0 +1,490 @@ +/* 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 quick suggest prefs migration from unversioned prefs to version 1. + +"use strict"; + +// Expected version 1 default-branch prefs +const DEFAULT_PREFS = { + history: { + "quicksuggest.enabled": false, + }, + offline: { + "quicksuggest.enabled": true, + "quicksuggest.dataCollection.enabled": false, + "quicksuggest.shouldShowOnboardingDialog": false, + "suggest.quicksuggest.nonsponsored": true, + "suggest.quicksuggest.sponsored": true, + }, + online: { + "quicksuggest.enabled": true, + "quicksuggest.dataCollection.enabled": false, + "quicksuggest.shouldShowOnboardingDialog": true, + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + }, +}; + +// Migration will use these values to migrate only up to version 1 instead of +// the current version. +const TEST_OVERRIDES = { + migrationVersion: 1, + defaultPrefs: DEFAULT_PREFS, +}; + +add_task(async function init() { + await UrlbarTestUtils.initNimbusFeature(); +}); + +// The following tasks test OFFLINE TO OFFLINE + +// Migrating from: +// * Offline (suggestions on by default) +// * User did not override any defaults +// +// Scenario when migration occurs: +// * Offline +// +// Expected: +// * Non-sponsored suggestions: remain on +// * Sponsored suggestions: remain on +// * Data collection: off +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + scenario: "offline", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.offline, + }, + }); +}); + +// Migrating from: +// * Offline (suggestions on by default) +// * Main suggestions pref: user left on +// * Sponsored suggestions: user turned off +// +// Scenario when migration occurs: +// * Offline +// +// Expected: +// * Non-sponsored suggestions: on +// * Sponsored suggestions: remain off +// * Data collection: off +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "suggest.quicksuggest.sponsored": false, + }, + scenario: "offline", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.offline, + userBranch: { + "suggest.quicksuggest.sponsored": false, + }, + }, + }); +}); + +// Migrating from: +// * Offline (suggestions on by default) +// * Main suggestions pref: user turned off +// * Sponsored suggestions: user left on (but ignored since main was off) +// +// Scenario when migration occurs: +// * Offline +// +// Expected: +// * Non-sponsored suggestions: off +// * Sponsored suggestions: off +// * Data collection: off +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "suggest.quicksuggest": false, + }, + scenario: "offline", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.offline, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + }, + }, + }); +}); + +// Migrating from: +// * Offline (suggestions on by default) +// * Main suggestions pref: user turned off +// * Sponsored suggestions: user turned off +// +// Scenario when migration occurs: +// * Offline +// +// Expected: +// * Non-sponsored suggestions: off +// * Sponsored suggestions: off +// * Data collection: off +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "suggest.quicksuggest": false, + "suggest.quicksuggest.sponsored": false, + }, + scenario: "offline", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.offline, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + }, + }, + }); +}); + +// The following tasks test OFFLINE TO ONLINE + +// Migrating from: +// * Offline (suggestions on by default) +// * User did not override any defaults +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: off +// * Sponsored suggestions: off +// * Data collection: off +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + scenario: "online", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.online, + }, + }); +}); + +// Migrating from: +// * Offline (suggestions on by default) +// * Main suggestions pref: user left on +// * Sponsored suggestions: user turned off +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: off +// * Sponsored suggestions: off +// * Data collection: off +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "suggest.quicksuggest.sponsored": false, + }, + scenario: "online", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.sponsored": false, + }, + }, + }); +}); + +// Migrating from: +// * Offline (suggestions on by default) +// * Main suggestions pref: user turned off +// * Sponsored suggestions: user left on (but ignored since main was off) +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: off +// * Sponsored suggestions: off +// * Data collection: off +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "suggest.quicksuggest": false, + }, + scenario: "online", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + }, + }, + }); +}); + +// Migrating from: +// * Offline (suggestions on by default) +// * Main suggestions pref: user turned off +// * Sponsored suggestions: user turned off +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: off +// * Sponsored suggestions: off +// * Data collection: off +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "suggest.quicksuggest": false, + "suggest.quicksuggest.sponsored": false, + }, + scenario: "online", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + }, + }, + }); +}); + +// The following tasks test ONLINE TO OFFLINE + +// Migrating from: +// * Online (suggestions off by default) +// * User did not override any defaults +// +// Scenario when migration occurs: +// * Offline +// +// Expected: +// * Non-sponsored suggestions: on (since main pref had default value) +// * Sponsored suggestions: on (since main & sponsored prefs had default values) +// * Data collection: off +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + scenario: "offline", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.offline, + }, + }); +}); + +// Migrating from: +// * Online (suggestions off by default) +// * Main suggestions pref: user left off +// * Sponsored suggestions: user turned on (but ignored since main was off) +// +// Scenario when migration occurs: +// * Offline +// +// Expected: +// * Non-sponsored suggestions: off +// * Sponsored suggestions: on (see below) +// * Data collection: off +// +// It's unfortunate that sponsored suggestions are ultimately on since before +// the migration no suggestions were shown to the user. There's nothing we can +// do about it, aside from forcing off suggestions in more cases than we want. +// The reason is that at the time of migration we can't tell that the previous +// scenario was online -- or more precisely that it wasn't history. If we knew +// it wasn't history, then we'd know to turn sponsored off; if we knew it *was* +// history, then we'd know to turn sponsored -- and non-sponsored -- on, since +// the scenario at the time of migration is offline, where suggestions should be +// enabled by default. +// +// This is the reason we now record `quicksuggest.scenario` on the user branch +// and not the default branch as we previously did. +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "suggest.quicksuggest.sponsored": true, + }, + scenario: "offline", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.offline, + userBranch: { + "suggest.quicksuggest.sponsored": true, + }, + }, + }); +}); + +// Migrating from: +// * Online (suggestions off by default) +// * Main suggestions pref: user turned on +// * Sponsored suggestions: user left off +// +// Scenario when migration occurs: +// * Offline +// +// Expected: +// * Non-sponsored suggestions: remain on +// * Sponsored suggestions: remain off +// * Data collection: off (since scenario is offline) +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "suggest.quicksuggest": true, + }, + scenario: "offline", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.offline, + userBranch: { + "suggest.quicksuggest.nonsponsored": true, + }, + }, + }); +}); + +// Migrating from: +// * Online (suggestions off by default) +// * Main suggestions pref: user turned on +// * Sponsored suggestions: user turned on +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: remain on +// * Sponsored suggestions: remain on +// * Data collection: off (since scenario is offline) +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "suggest.quicksuggest": true, + "suggest.quicksuggest.sponsored": true, + }, + scenario: "offline", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.offline, + userBranch: { + "suggest.quicksuggest.nonsponsored": true, + "suggest.quicksuggest.sponsored": true, + }, + }, + }); +}); + +// The following tasks test ONLINE TO ONLINE + +// Migrating from: +// * Online (suggestions off by default) +// * User did not override any defaults +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: off +// * Sponsored suggestions: off +// * Data collection: off +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + scenario: "online", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.online, + }, + }); +}); + +// Migrating from: +// * Online (suggestions off by default) +// * Main suggestions pref: user left off +// * Sponsored suggestions: user turned on (but ignored since main was off) +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: remain off +// * Sponsored suggestions: off +// * Data collection: off +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "suggest.quicksuggest.sponsored": true, + }, + scenario: "online", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.online, + }, + }); +}); + +// Migrating from: +// * Online (suggestions off by default) +// * Main suggestions pref: user turned on +// * Sponsored suggestions: user left off +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: remain on +// * Sponsored suggestions: remain off +// * Data collection: ON (since user effectively opted in by turning on +// suggestions) +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "suggest.quicksuggest": true, + }, + scenario: "online", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": true, + "quicksuggest.dataCollection.enabled": true, + }, + }, + }); +}); + +// Migrating from: +// * Online (suggestions off by default) +// * Main suggestions pref: user turned on +// * Sponsored suggestions: user turned on +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: remain on +// * Sponsored suggestions: remain on +// * Data collection: ON (since user effectively opted in by turning on +// suggestions) +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "suggest.quicksuggest": true, + "suggest.quicksuggest.sponsored": true, + }, + scenario: "online", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": true, + "suggest.quicksuggest.sponsored": true, + "quicksuggest.dataCollection.enabled": true, + }, + }, + }); +}); diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_migrate_v2.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_migrate_v2.js new file mode 100644 index 0000000000..cd4a2149e6 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_migrate_v2.js @@ -0,0 +1,1355 @@ +/* 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 quick suggest prefs migration to version 2. + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + ExperimentFakes: "resource://testing-common/NimbusTestUtils.sys.mjs", + NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", +}); + +// Expected version 2 default-branch prefs +const DEFAULT_PREFS = { + history: { + "quicksuggest.enabled": false, + }, + offline: { + "quicksuggest.enabled": true, + "quicksuggest.dataCollection.enabled": false, + "quicksuggest.shouldShowOnboardingDialog": false, + "suggest.quicksuggest.nonsponsored": true, + "suggest.quicksuggest.sponsored": true, + }, + online: { + "quicksuggest.enabled": true, + "quicksuggest.dataCollection.enabled": false, + "quicksuggest.shouldShowOnboardingDialog": true, + "suggest.quicksuggest.nonsponsored": true, + "suggest.quicksuggest.sponsored": true, + }, +}; + +// Migration will use these values to migrate only up to version 1 instead of +// the current version. +// Currently undefined because version 2 is the current migration version and we +// want migration to use its actual values, not overrides. When version 3 is +// added, set this to an object like the one in test_quicksuggest_migrate_v1.js. +const TEST_OVERRIDES = undefined; + +add_task(async function init() { + await UrlbarTestUtils.initNimbusFeature(); +}); + +// The following tasks test OFFLINE UNVERSIONED to OFFLINE + +// Migrating from: +// * Unversioned prefs +// * Offline +// * Main suggestions pref: user left on +// * Sponsored suggestions: user left on +// +// Scenario when migration occurs: +// * Offline +// +// Expected: +// * Non-sponsored suggestions: on +// * Sponsored suggestions: on +// * Data collection: off +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + scenario: "offline", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.offline, + }, + }); +}); + +// Migrating from: +// * Unversioned prefs +// * Offline +// * Main suggestions pref: user turned off +// * Sponsored suggestions: user left on (but ignored since main was off) +// +// Scenario when migration occurs: +// * Offline +// +// Expected: +// * Non-sponsored suggestions: off +// * Sponsored suggestions: off +// * Data collection: off +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "suggest.quicksuggest": false, + }, + scenario: "offline", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.offline, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + }, + }, + }); +}); + +// Migrating from: +// * Unversioned prefs +// * Offline +// * Main suggestions pref: user left on +// * Sponsored suggestions: user turned off +// +// Scenario when migration occurs: +// * Offline +// +// Expected: +// * Non-sponsored suggestions: on +// * Sponsored suggestions: off +// * Data collection: off +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "suggest.quicksuggest.sponsored": false, + }, + scenario: "offline", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.offline, + userBranch: { + "suggest.quicksuggest.sponsored": false, + }, + }, + }); +}); + +// Migrating from: +// * Unversioned prefs +// * Offline +// * Main suggestions pref: user turned off +// * Sponsored suggestions: user turned off +// +// Scenario when migration occurs: +// * Offline +// +// Expected: +// * Non-sponsored suggestions: off +// * Sponsored suggestions: off +// * Data collection: off +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "suggest.quicksuggest": false, + "suggest.quicksuggest.sponsored": false, + }, + scenario: "offline", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.offline, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + }, + }, + }); +}); + +// The following tasks test OFFLINE UNVERSIONED to ONLINE + +// Migrating from: +// * Unversioned prefs +// * Offline +// * Main suggestions pref: user left on +// * Sponsored suggestions: user left on +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: on +// * Sponsored suggestions: on +// * Data collection: off +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + scenario: "online", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.online, + }, + }); +}); + +// Migrating from: +// * Unversioned prefs +// * Offline +// * Main suggestions pref: user turned off +// * Sponsored suggestions: user left on (but ignored since main was off) +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: off +// * Sponsored suggestions: off +// * Data collection: off +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "suggest.quicksuggest": false, + }, + scenario: "online", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + }, + }, + }); +}); + +// Migrating from: +// * Unversioned prefs +// * Offline +// * Main suggestions pref: user left on +// * Sponsored suggestions: user turned off +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: on +// * Sponsored suggestions: off +// * Data collection: off +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "suggest.quicksuggest.sponsored": false, + }, + scenario: "online", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.sponsored": false, + }, + }, + }); +}); + +// Migrating from: +// * Unversioned prefs +// * Offline +// * Main suggestions pref: user turned off +// * Sponsored suggestions: user turned off +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: off +// * Sponsored suggestions: off +// * Data collection: off +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "suggest.quicksuggest": false, + "suggest.quicksuggest.sponsored": false, + }, + scenario: "online", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + }, + }, + }); +}); + +// The following tasks test ONLINE UNVERSIONED to ONLINE when the user was NOT +// SHOWN THE MODAL (e.g., because they didn't restart) + +// Migrating from: +// * Unversioned prefs +// * Online +// * Modal shown: no +// * User enrolled in online where suggestions were disabled by default, did not +// turn on either type of suggestion, was not shown the modal (e.g., because +// they didn't restart), and upgraded to v2 +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: off +// * Sponsored suggestions: off +// * Data collection: off +add_task(async function () { + await withOnlineExperiment(async () => { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + scenario: "online", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.online, + }, + }); + }); +}); + +// Migrating from: +// * Unversioned prefs +// * Online +// * Modal shown: no +// * User enrolled in online where suggestions were disabled by default, turned +// on main suggestions pref and left off sponsored suggestions, was not shown +// the modal (e.g., because they didn't restart), and upgraded to v2 +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: on +// * Sponsored suggestions: off +// * Data collection: on (since they opted in by checking the main checkbox +// while in online) +add_task(async function () { + await withOnlineExperiment(async () => { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "suggest.quicksuggest": true, + }, + scenario: "online", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": true, + }, + }, + }); + }); +}); + +// Migrating from: +// * Unversioned prefs +// * Online +// * Modal shown: no +// * User enrolled in online where suggestions were disabled by default, left +// off main suggestions pref and turned on sponsored suggestions, was not +// shown the modal (e.g., because they didn't restart), and upgraded to v2 +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: off +// * Sponsored suggestions: off +// * Data collection: off +add_task(async function () { + await withOnlineExperiment(async () => { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "suggest.quicksuggest.sponsored": true, + }, + scenario: "online", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.sponsored": true, + }, + }, + }); + }); +}); + +// Migrating from: +// * Unversioned prefs +// * Online +// * Modal shown: no +// * User enrolled in online where suggestions were disabled by default, turned +// on main suggestions pref and sponsored suggestions, was not shown the +// modal (e.g., because they didn't restart), and upgraded to v2 +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: on +// * Sponsored suggestions: on +// * Data collection: on (since they opted in by checking the main checkbox +// while in online) +add_task(async function () { + await withOnlineExperiment(async () => { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "suggest.quicksuggest": true, + "suggest.quicksuggest.sponsored": true, + }, + scenario: "online", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": true, + "suggest.quicksuggest.sponsored": true, + }, + }, + }); + }); +}); + +// The following tasks test ONLINE UNVERSIONED to ONLINE when the user WAS SHOWN +// THE MODAL + +// Migrating from: +// * Unversioned prefs +// * Online +// * Modal shown: yes +// * The following end up with same prefs and are covered by this task: +// 1. User did not opt in and left off both the main suggestions pref and +// sponsored suggestions +// 2. User opted in but then later turned off both the main suggestions pref +// and sponsored suggestions +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: off +// * Sponsored suggestions: off +// * Data collection: off +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "quicksuggest.showedOnboardingDialog": true, + }, + scenario: "online", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + "quicksuggest.dataCollection.enabled": false, + }, + }, + }); +}); + +// Migrating from: +// * Unversioned prefs +// * Online +// * Modal shown: yes +// * The following end up with same prefs and are covered by this task: +// 1. User did not opt in but then later turned on the main suggestions pref +// 2. User opted in but then later turned off sponsored suggestions +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: on +// * Sponsored suggestions: off +// * Data collection: on +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "quicksuggest.showedOnboardingDialog": true, + "suggest.quicksuggest": true, + }, + scenario: "online", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": true, + "suggest.quicksuggest.sponsored": false, + "quicksuggest.dataCollection.enabled": true, + }, + }, + }); +}); + +// Migrating from: +// * Unversioned prefs +// * Online +// * Modal shown: yes +// * The following end up with same prefs and are covered by this task: +// 1. User did not opt in but then later turned on sponsored suggestions +// 2. User opted in but then later turned off the main suggestions pref +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: off +// * Sponsored suggestions: off +// * Data collection: off +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "quicksuggest.showedOnboardingDialog": true, + "suggest.quicksuggest.sponsored": true, + }, + scenario: "online", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + "quicksuggest.dataCollection.enabled": false, + }, + }, + }); +}); + +// Migrating from: +// * Unversioned prefs +// * Online +// * Modal shown: yes +// * The following end up with same prefs and are covered by this task: +// 1. User did not opt in but then later turned on both the main suggestions +// pref and sponsored suggestions +// 2. User opted in and left on both the main suggestions pref and sponsored +// suggestions +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: on +// * Sponsored suggestions: on +// * Data collection: on +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "quicksuggest.showedOnboardingDialog": true, + "suggest.quicksuggest": true, + "suggest.quicksuggest.sponsored": true, + }, + scenario: "online", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": true, + "suggest.quicksuggest.sponsored": true, + "quicksuggest.dataCollection.enabled": true, + }, + }, + }); +}); + +// The following tasks test OFFLINE VERSION 1 to OFFLINE + +// Migrating from: +// * Version 1 prefs +// * Offline +// * Non-sponsored suggestions: user left on +// * Sponsored suggestions: user left on +// * Data collection: user left off +// +// Scenario when migration occurs: +// * Offline +// +// Expected: +// * Non-sponsored suggestions: on +// * Sponsored suggestions: on +// * Data collection: off +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "quicksuggest.migrationVersion": 1, + "quicksuggest.scenario": "offline", + }, + scenario: "offline", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.offline, + }, + }); +}); + +// Migrating from: +// * Version 1 prefs +// * Offline +// * Non-sponsored suggestions: user left on +// * Sponsored suggestions: user left on +// * Data collection: user turned on +// +// Scenario when migration occurs: +// * Offline +// +// Expected: +// * Non-sponsored suggestions: on +// * Sponsored suggestions: on +// * Data collection: on +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "quicksuggest.migrationVersion": 1, + "quicksuggest.scenario": "offline", + "quicksuggest.dataCollection.enabled": true, + }, + scenario: "offline", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.offline, + userBranch: { + "quicksuggest.dataCollection.enabled": true, + }, + }, + }); +}); + +// Migrating from: +// * Version 1 prefs +// * Offline +// * Non-sponsored suggestions: user left on +// * Sponsored suggestions: user turned off +// * Data collection: user left off +// +// Scenario when migration occurs: +// * Offline +// +// Expected: +// * Non-sponsored suggestions: on +// * Sponsored suggestions: off +// * Data collection: off +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "quicksuggest.migrationVersion": 1, + "quicksuggest.scenario": "offline", + "suggest.quicksuggest.sponsored": false, + }, + scenario: "offline", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.offline, + userBranch: { + "suggest.quicksuggest.sponsored": false, + }, + }, + }); +}); + +// Migrating from: +// * Version 1 prefs +// * Offline +// * Non-sponsored suggestions: user turned off +// * Sponsored suggestions: user turned off +// * Data collection: user left off +// +// Scenario when migration occurs: +// * Offline +// +// Expected: +// * Non-sponsored suggestions: off +// * Sponsored suggestions: off +// * Data collection: off +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "quicksuggest.migrationVersion": 1, + "quicksuggest.scenario": "offline", + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + }, + scenario: "offline", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.offline, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + }, + }, + }); +}); + +// The following tasks test OFFLINE VERSION 1 to ONLINE + +// Migrating from: +// * Version 1 prefs +// * Offline +// * Non-sponsored suggestions: user left on +// * Sponsored suggestions: user left on +// * Data collection: user left off +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: on +// * Sponsored suggestions: on +// * Data collection: off +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "quicksuggest.migrationVersion": 1, + "quicksuggest.scenario": "offline", + }, + scenario: "online", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.online, + }, + }); +}); + +// Migrating from: +// * Version 1 prefs +// * Offline +// * Non-sponsored suggestions: user left on +// * Sponsored suggestions: user turned off +// * Data collection: user left off +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: on +// * Sponsored suggestions: off +// * Data collection: off +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "quicksuggest.migrationVersion": 1, + "quicksuggest.scenario": "offline", + "suggest.quicksuggest.sponsored": false, + }, + scenario: "online", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.sponsored": false, + }, + }, + }); +}); + +// Migrating from: +// * Version 1 prefs +// * Offline +// * Non-sponsored suggestions: user turned off +// * Sponsored suggestions: user turned off +// * Data collection: user left off +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: on +// * Sponsored suggestions: off +// * Data collection: off +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "quicksuggest.migrationVersion": 1, + "quicksuggest.scenario": "offline", + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + }, + scenario: "online", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + }, + }, + }); +}); + +// The following tasks test ONLINE VERSION 1 to ONLINE when the user was NOT +// SHOWN THE MODAL (e.g., because they didn't restart) + +// Migrating from: +// * Version 1 prefs +// * Online +// * Modal shown: no +// * Non-sponsored suggestions: user left off +// * Sponsored suggestions: user left off +// * Data collection: user left off +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: off +// * Sponsored suggestions: off +// * Data collection: off +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "quicksuggest.migrationVersion": 1, + "quicksuggest.scenario": "online", + }, + scenario: "online", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + }, + }, + }); +}); + +// Migrating from: +// * Version 1 prefs +// * Online +// * Modal shown: no +// * Non-sponsored suggestions: user left off +// * Sponsored suggestions: user left off +// * Data collection: user turned on +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: off +// * Sponsored suggestions: off +// * Data collection: on +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "quicksuggest.migrationVersion": 1, + "quicksuggest.scenario": "online", + "quicksuggest.dataCollection.enabled": true, + }, + scenario: "online", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + "quicksuggest.dataCollection.enabled": true, + }, + }, + }); +}); + +// Migrating from: +// * Version 1 prefs +// * Online +// * Modal shown: no +// * Non-sponsored suggestions: user left off +// * Sponsored suggestions: user turned on +// * Data collection: user left off +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: off +// * Sponsored suggestions: on +// * Data collection: off +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "quicksuggest.migrationVersion": 1, + "quicksuggest.scenario": "online", + "suggest.quicksuggest.sponsored": true, + }, + scenario: "online", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": true, + }, + }, + }); +}); + +// Migrating from: +// * Version 1 prefs +// * Online +// * Modal shown: no +// * Non-sponsored suggestions: user left off +// * Sponsored suggestions: user turned on +// * Data collection: user turned on +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: off +// * Sponsored suggestions: on +// * Data collection: on +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "quicksuggest.migrationVersion": 1, + "quicksuggest.scenario": "online", + "suggest.quicksuggest.sponsored": true, + "quicksuggest.dataCollection.enabled": true, + }, + scenario: "online", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": true, + "quicksuggest.dataCollection.enabled": true, + }, + }, + }); +}); + +// Migrating from: +// * Version 1 prefs +// * Online +// * Modal shown: no +// * Non-sponsored suggestions: user turned on +// * Sponsored suggestions: user left off +// * Data collection: user left off +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: on +// * Sponsored suggestions: off +// * Data collection: off +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "quicksuggest.migrationVersion": 1, + "quicksuggest.scenario": "online", + "suggest.quicksuggest.nonsponsored": true, + }, + scenario: "online", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": true, + "suggest.quicksuggest.sponsored": false, + }, + }, + }); +}); + +// Migrating from: +// * Version 1 prefs +// * Online +// * Modal shown: no +// * Non-sponsored suggestions: user turned on +// * Sponsored suggestions: user left off +// * Data collection: user turned on +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: on +// * Sponsored suggestions: off +// * Data collection: on +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "quicksuggest.migrationVersion": 1, + "quicksuggest.scenario": "online", + "suggest.quicksuggest.nonsponsored": true, + "quicksuggest.dataCollection.enabled": true, + }, + scenario: "online", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": true, + "suggest.quicksuggest.sponsored": false, + "quicksuggest.dataCollection.enabled": true, + }, + }, + }); +}); + +// Migrating from: +// * Version 1 prefs +// * Online +// * Modal shown: no +// * Non-sponsored suggestions: user turned on +// * Sponsored suggestions: user turned on +// * Data collection: user left off +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: on +// * Sponsored suggestions: on +// * Data collection: off +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "quicksuggest.migrationVersion": 1, + "quicksuggest.scenario": "online", + "suggest.quicksuggest.nonsponsored": true, + "suggest.quicksuggest.sponsored": true, + }, + scenario: "online", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": true, + "suggest.quicksuggest.sponsored": true, + }, + }, + }); +}); + +// Migrating from: +// * Version 1 prefs +// * Online +// * Modal shown: no +// * Non-sponsored suggestions: user turned on +// * Sponsored suggestions: user turned on +// * Data collection: user turned on +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: on +// * Sponsored suggestions: on +// * Data collection: on +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "quicksuggest.migrationVersion": 1, + "quicksuggest.scenario": "online", + "suggest.quicksuggest.nonsponsored": true, + "suggest.quicksuggest.sponsored": true, + "quicksuggest.dataCollection.enabled": true, + }, + scenario: "online", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": true, + "suggest.quicksuggest.sponsored": true, + "quicksuggest.dataCollection.enabled": true, + }, + }, + }); +}); + +// The following tasks test ONLINE VERSION 1 to ONLINE when the user WAS SHOWN +// THE MODAL WHILE PREFS WERE UNVERSIONED + +// Migrating from: +// * Version 1 prefs +// * Online +// * Modal shown: yes, while prefs were unversioned +// * User opted in: no +// * Non-sponsored suggestions: user left off +// * Sponsored suggestions: user left off +// * Data collection: user left off +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: off +// * Sponsored suggestions: off +// * Data collection: off +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "quicksuggest.migrationVersion": 1, + "quicksuggest.scenario": "online", + "quicksuggest.showedOnboardingDialog": true, + }, + scenario: "online", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + }, + }, + }); +}); + +// Migrating from: +// * Version 1 prefs +// * Online +// * Modal shown: yes, while prefs were unversioned +// * User opted in: no +// * Non-sponsored suggestions: user turned on +// * Sponsored suggestions: user left off +// * Data collection: user left off +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: on +// * Sponsored suggestions: off +// * Data collection: off +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "quicksuggest.migrationVersion": 1, + "quicksuggest.scenario": "online", + "quicksuggest.showedOnboardingDialog": true, + "suggest.quicksuggest.nonsponsored": true, + }, + scenario: "online", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": true, + "suggest.quicksuggest.sponsored": false, + }, + }, + }); +}); + +// Migrating from: +// * Version 1 prefs +// * Online +// * Modal shown: yes, while prefs were unversioned +// * User opted in: yes +// * Non-sponsored suggestions: user left on +// * Sponsored suggestions: user left on +// * Data collection: user left on +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: on +// * Sponsored suggestions: on +// * Data collection: on +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "quicksuggest.migrationVersion": 1, + "quicksuggest.scenario": "online", + "quicksuggest.showedOnboardingDialog": true, + "suggest.quicksuggest.nonsponsored": true, + "suggest.quicksuggest.sponsored": true, + "quicksuggest.dataCollection.enabled": true, + }, + scenario: "online", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": true, + "suggest.quicksuggest.sponsored": true, + "quicksuggest.dataCollection.enabled": true, + }, + }, + }); +}); + +// Migrating from: +// * Version 1 prefs +// * Online +// * Modal shown: yes, while prefs were unversioned +// * User opted in: yes +// * Non-sponsored suggestions: user turned off +// * Sponsored suggestions: user left on +// * Data collection: user left on +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: off +// * Sponsored suggestions: on +// * Data collection: on +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "quicksuggest.migrationVersion": 1, + "quicksuggest.scenario": "online", + "quicksuggest.showedOnboardingDialog": true, + "suggest.quicksuggest.sponsored": true, + "quicksuggest.dataCollection.enabled": true, + }, + scenario: "online", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": true, + "quicksuggest.dataCollection.enabled": true, + }, + }, + }); +}); + +// Migrating from: +// * Version 1 prefs +// * Online +// * Modal shown: yes, while prefs were unversioned +// * User opted in: yes +// * Non-sponsored suggestions: user left on +// * Sponsored suggestions: user left on +// * Data collection: user turned off +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: on +// * Sponsored suggestions: on +// * Data collection: on +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "quicksuggest.migrationVersion": 1, + "quicksuggest.scenario": "online", + "quicksuggest.showedOnboardingDialog": true, + "suggest.quicksuggest.nonsponsored": true, + "suggest.quicksuggest.sponsored": true, + "quicksuggest.dataCollection.enabled": false, + }, + scenario: "online", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": true, + "suggest.quicksuggest.sponsored": true, + "quicksuggest.dataCollection.enabled": false, + }, + }, + }); +}); + +// The following tasks test ONLINE VERSION 1 to ONLINE when the user WAS SHOWN +// THE MODAL WHILE PREFS WERE VERSION 1 + +// Migrating from: +// * Version 1 prefs +// * Online +// * Modal shown: yes, while prefs were version 1 +// * User opted in: no +// * Non-sponsored suggestions: user left off +// * Sponsored suggestions: user left off +// * Data collection: user left off +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: off +// * Sponsored suggestions: off +// * Data collection: off +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "quicksuggest.migrationVersion": 1, + "quicksuggest.scenario": "online", + "quicksuggest.showedOnboardingDialog": true, + "quicksuggest.onboardingDialogChoice": "not_now_link", + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + "quicksuggest.dataCollection.enabled": false, + }, + scenario: "online", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + "quicksuggest.dataCollection.enabled": false, + }, + }, + }); +}); + +// Migrating from: +// * Version 1 prefs +// * Online +// * Modal shown: yes, while prefs were version 1 +// * User opted in: yes +// * Non-sponsored suggestions: user left on +// * Sponsored suggestions: user left on +// * Data collection: user left on +// +// Scenario when migration occurs: +// * Online +// +// Expected: +// * Non-sponsored suggestions: on +// * Sponsored suggestions: on +// * Data collection: on +add_task(async function () { + await doMigrateTest({ + testOverrides: TEST_OVERRIDES, + initialUserBranch: { + "quicksuggest.migrationVersion": 1, + "quicksuggest.scenario": "online", + "quicksuggest.showedOnboardingDialog": true, + "quicksuggest.onboardingDialogChoice": "accept", + "suggest.quicksuggest.nonsponsored": true, + "suggest.quicksuggest.sponsored": true, + "quicksuggest.dataCollection.enabled": true, + }, + scenario: "online", + expectedPrefs: { + defaultBranch: DEFAULT_PREFS.online, + userBranch: { + "suggest.quicksuggest.nonsponsored": true, + "suggest.quicksuggest.sponsored": true, + "quicksuggest.dataCollection.enabled": true, + }, + }, + }); +}); + +async function withOnlineExperiment(callback) { + let { enrollmentPromise, doExperimentCleanup } = + ExperimentFakes.enrollmentHelper( + ExperimentFakes.recipe("firefox-suggest-offline-vs-online", { + active: true, + branches: [ + { + slug: "treatment", + features: [ + { + featureId: NimbusFeatures.urlbar.featureId, + value: { + enabled: true, + }, + }, + ], + }, + ], + }) + ); + await enrollmentPromise; + await callback(); + await doExperimentCleanup(); +} diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_nonUniqueKeywords.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_nonUniqueKeywords.js new file mode 100644 index 0000000000..84d1116e89 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_nonUniqueKeywords.js @@ -0,0 +1,282 @@ +/* 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 non-unique keywords, i.e., keywords used by multiple suggestions. + +"use strict"; + +// For each of these objects, the test creates a quick suggest result (the kind +// stored in the remote settings data, not a urlbar result), the corresponding +// expected quick suggest suggestion, and the corresponding expected urlbar +// result. The test assumes results and suggestions are returned in the order +// listed here. +let SUGGESTIONS_DATA = [ + { + keywords: ["aaa"], + isSponsored: true, + }, + { + keywords: ["aaa", "bbb"], + isSponsored: false, + score: 2 * QuickSuggestRemoteSettings.DEFAULT_SUGGESTION_SCORE, + }, + { + keywords: ["bbb"], + isSponsored: true, + score: 4 * QuickSuggestRemoteSettings.DEFAULT_SUGGESTION_SCORE, + }, + { + keywords: ["bbb"], + isSponsored: false, + score: 3 * QuickSuggestRemoteSettings.DEFAULT_SUGGESTION_SCORE, + }, + { + keywords: ["ccc"], + isSponsored: true, + }, +]; + +// Test cases. In this object, keywords map to subtest cases. For each keyword, +// the test calls `query(keyword)` and checks that the indexes (relative to +// `SUGGESTIONS_DATA`) of the returned quick suggest results are the ones in +// `expectedIndexes`. Then the test does a series of urlbar searches using the +// keyword as the search string, one search per object in `searches`. Sponsored +// and non-sponsored urlbar results are enabled as defined by `sponsored` and +// `nonsponsored`. `expectedIndex` is the expected index (relative to +// `SUGGESTIONS_DATA`) of the returned urlbar result. +let TESTS = { + aaa: { + // 0: sponsored + // 1: nonsponsored, score = 2x + expectedIndexes: [0, 1], + searches: [ + { + sponsored: true, + nonsponsored: true, + expectedIndex: 1, + }, + { + sponsored: false, + nonsponsored: true, + expectedIndex: 1, + }, + { + sponsored: true, + nonsponsored: false, + expectedIndex: 0, + }, + { + sponsored: false, + nonsponsored: false, + expectedIndex: undefined, + }, + ], + }, + bbb: { + // 1: nonsponsored, score = 2x + // 2: sponsored, score = 4x, + // 3: nonsponsored, score = 3x + expectedIndexes: [1, 2, 3], + searches: [ + { + sponsored: true, + nonsponsored: true, + expectedIndex: 2, + }, + { + sponsored: false, + nonsponsored: true, + expectedIndex: 3, + }, + { + sponsored: true, + nonsponsored: false, + expectedIndex: 2, + }, + { + sponsored: false, + nonsponsored: false, + expectedIndex: undefined, + }, + ], + }, + ccc: { + // 4: sponsored + expectedIndexes: [4], + searches: [ + { + sponsored: true, + nonsponsored: true, + expectedIndex: 4, + }, + { + sponsored: false, + nonsponsored: true, + expectedIndex: undefined, + }, + { + sponsored: true, + nonsponsored: false, + expectedIndex: 4, + }, + { + sponsored: false, + nonsponsored: false, + expectedIndex: undefined, + }, + ], + }, +}; + +add_task(async function () { + UrlbarPrefs.set("quicksuggest.enabled", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + + // Create results and suggestions based on `SUGGESTIONS_DATA`. + let qsResults = []; + let qsSuggestions = []; + let urlbarResults = []; + for (let i = 0; i < SUGGESTIONS_DATA.length; i++) { + let { keywords, isSponsored, score } = SUGGESTIONS_DATA[i]; + + // quick suggest result + let qsResult = { + keywords, + score, + id: i, + url: "http://example.com/" + i, + title: "Title " + i, + click_url: "http://example.com/click", + impression_url: "http://example.com/impression", + advertiser: "TestAdvertiser", + iab_category: isSponsored ? "22 - Shopping" : "5 - Education", + }; + qsResults.push(qsResult); + + // expected quick suggest suggestion + let qsSuggestion = { + ...qsResult, + block_id: qsResult.id, + is_sponsored: isSponsored, + score: + typeof score == "number" + ? score + : QuickSuggestRemoteSettings.DEFAULT_SUGGESTION_SCORE, + source: "remote-settings", + icon: null, + position: undefined, + provider: "AdmWikipedia", + }; + delete qsSuggestion.keywords; + delete qsSuggestion.id; + qsSuggestions.push(qsSuggestion); + + // expected urlbar result + urlbarResults.push({ + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + heuristic: false, + payload: { + isSponsored, + telemetryType: isSponsored ? "adm_sponsored" : "adm_nonsponsored", + sponsoredBlockId: qsResult.id, + url: qsResult.url, + originalUrl: qsResult.url, + displayUrl: qsResult.url, + title: qsResult.title, + sponsoredClickUrl: qsResult.click_url, + sponsoredImpressionUrl: qsResult.impression_url, + sponsoredAdvertiser: qsResult.advertiser, + sponsoredIabCategory: qsResult.iab_category, + icon: null, + helpUrl: QuickSuggest.HELP_URL, + helpL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-learn-more-about-firefox-suggest" + : "firefox-suggest-urlbar-learn-more", + }, + isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"), + blockL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-dismiss-firefox-suggest" + : "firefox-suggest-urlbar-block", + }, + source: "remote-settings", + }, + }); + } + + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + remoteSettingsResults: [ + { + type: "data", + attachment: qsResults, + }, + ], + }); + + // Run a test for each keyword. + for (let [keyword, test] of Object.entries(TESTS)) { + info("Running subtest " + JSON.stringify({ keyword, test })); + + let { expectedIndexes, searches } = test; + + // Call `query()`. + Assert.deepEqual( + await QuickSuggestRemoteSettings.query(keyword), + expectedIndexes.map(i => ({ + ...qsSuggestions[i], + full_keyword: keyword, + })), + `query() for keyword ${keyword}` + ); + + // Now do a urlbar search for the keyword with all possible combinations of + // sponsored and non-sponsored suggestions enabled and disabled. + for (let sponsored of [true, false]) { + for (let nonsponsored of [true, false]) { + // Find the matching `searches` object. + let search = searches.find( + s => s.sponsored == sponsored && s.nonsponsored == nonsponsored + ); + Assert.ok( + search, + "Sanity check: Search test case specified for " + + JSON.stringify({ keyword, sponsored, nonsponsored }) + ); + + info( + "Running urlbar search subtest " + + JSON.stringify({ keyword, expectedIndexes, search }) + ); + + UrlbarPrefs.set("suggest.quicksuggest.sponsored", sponsored); + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", nonsponsored); + + // Set up the search and do it. + let context = createContext(keyword, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + + let matches = []; + if (search.expectedIndex !== undefined) { + matches.push({ + ...urlbarResults[search.expectedIndex], + payload: { + ...urlbarResults[search.expectedIndex].payload, + qsSuggestion: keyword, + }, + }); + } + + await check_results({ context, matches }); + } + } + + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + } +}); diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_offlineDefault.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_offlineDefault.js new file mode 100644 index 0000000000..7330dd4fd5 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_offlineDefault.js @@ -0,0 +1,127 @@ +/* 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 `UrlbarPrefs.updateFirefoxSuggestScenario` in isolation under the +// assumption that the offline scenario should be enabled by default for US en. + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + Region: "resource://gre/modules/Region.sys.mjs", +}); + +// All the prefs that `updateFirefoxSuggestScenario` sets along with the +// expected default-branch values when offline is enabled and when it's not +// enabled. +const PREFS = [ + { + name: "browser.urlbar.quicksuggest.enabled", + get: "getBoolPref", + set: "setBoolPref", + expectedOfflineValue: true, + expectedOtherValue: false, + }, + { + name: "browser.urlbar.quicksuggest.shouldShowOnboardingDialog", + get: "getBoolPref", + set: "setBoolPref", + expectedOfflineValue: false, + expectedOtherValue: true, + }, + { + name: "browser.urlbar.suggest.quicksuggest.nonsponsored", + get: "getBoolPref", + set: "setBoolPref", + expectedOfflineValue: true, + expectedOtherValue: false, + }, + { + name: "browser.urlbar.suggest.quicksuggest.sponsored", + get: "getBoolPref", + set: "setBoolPref", + expectedOfflineValue: true, + expectedOtherValue: false, + }, +]; + +add_task(async function init() { + await UrlbarTestUtils.initNimbusFeature(); +}); + +add_task(async function test() { + let tests = [ + { locale: "en-US", home: "US", expectedOfflineDefault: true }, + { locale: "en-US", home: "CA", expectedOfflineDefault: false }, + { locale: "en-CA", home: "US", expectedOfflineDefault: true }, + { locale: "en-CA", home: "CA", expectedOfflineDefault: false }, + { locale: "en-GB", home: "US", expectedOfflineDefault: true }, + { locale: "en-GB", home: "GB", expectedOfflineDefault: false }, + { locale: "de", home: "US", expectedOfflineDefault: false }, + { locale: "de", home: "DE", expectedOfflineDefault: false }, + ]; + for (let { locale, home, expectedOfflineDefault } of tests) { + await doTest({ locale, home, expectedOfflineDefault }); + } +}); + +/** + * Sets the app's locale and region, calls + * `UrlbarPrefs.updateFirefoxSuggestScenario`, and asserts that the pref values + * are correct. + * + * @param {object} options + * Options object. + * @param {string} options.locale + * The locale to simulate. + * @param {string} options.home + * The "home" region to simulate. + * @param {boolean} options.expectedOfflineDefault + * The expected value of whether offline should be enabled by default given + * the locale and region. + */ +async function doTest({ locale, home, expectedOfflineDefault }) { + // Setup: Clear any user values and save original default-branch values. + for (let pref of PREFS) { + Services.prefs.clearUserPref(pref.name); + pref.originalDefault = Services.prefs + .getDefaultBranch(pref.name) + [pref.get](""); + } + + // Set the region and locale, call the function, check the pref values. + Region._setHomeRegion(home, false); + await QuickSuggestTestUtils.withLocales([locale], async () => { + await UrlbarPrefs.updateFirefoxSuggestScenario(); + for (let { name, get, expectedOfflineValue, expectedOtherValue } of PREFS) { + let expectedValue = expectedOfflineDefault + ? expectedOfflineValue + : expectedOtherValue; + + // Check the default-branch value. + Assert.strictEqual( + Services.prefs.getDefaultBranch(name)[get](""), + expectedValue, + `Default pref value for ${name}, locale ${locale}, home ${home}` + ); + + // For good measure, also check the return value of `UrlbarPrefs.get` + // since we use it everywhere. The value should be the same as the + // default-branch value. + UrlbarPrefs.get( + name.replace("browser.urlbar.", ""), + expectedValue, + `UrlbarPrefs.get() value for ${name}, locale ${locale}, home ${home}` + ); + } + }); + + // Teardown: Restore original default-branch values for the next task. + for (let { name, originalDefault, set } of PREFS) { + if (originalDefault === undefined) { + Services.prefs.deleteBranch(name); + } else { + Services.prefs.getDefaultBranch(name)[set]("", originalDefault); + } + } +} diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_positionInSuggestions.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_positionInSuggestions.js new file mode 100644 index 0000000000..2d1cf728c7 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_positionInSuggestions.js @@ -0,0 +1,487 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests for quick suggest result position specified in suggestions. + */ + +ChromeUtils.defineESModuleGetters(this, { + UrlbarProviderHeuristicFallback: + "resource:///modules/UrlbarProviderHeuristicFallback.sys.mjs", + UrlbarProviderPlaces: "resource:///modules/UrlbarProviderPlaces.sys.mjs", + UrlbarProviderTabToSearch: + "resource:///modules/UrlbarProviderTabToSearch.sys.mjs", +}); + +const SPONSORED_SECOND_POSITION_RESULT = { + id: 1, + url: "http://example.com/?q=sponsored-second", + title: "sponsored second", + keywords: ["s-s"], + click_url: "http://click.reporting.test.com/", + impression_url: "http://impression.reporting.test.com/", + advertiser: "TestAdvertiser", + iab_category: "22 - Shopping", + position: 1, +}; +const SPONSORED_NORMAL_POSITION_RESULT = { + id: 2, + url: "http://example.com/?q=sponsored-normal", + title: "sponsored normal", + keywords: ["s-n"], + click_url: "http://click.reporting.test.com/", + impression_url: "http://impression.reporting.test.com/", + advertiser: "TestAdvertiser", + iab_category: "22 - Shopping", +}; +const NONSPONSORED_SECOND_POSITION_RESULT = { + id: 3, + url: "http://example.com/?q=nonsponsored-second", + title: "nonsponsored second", + keywords: ["n-s"], + click_url: "http://click.reporting.test.com/nonsponsored", + impression_url: "http://impression.reporting.test.com/nonsponsored", + advertiser: "TestAdvertiserNonSponsored", + iab_category: "5 - Education", + position: 1, +}; +const NONSPONSORED_NORMAL_POSITION_RESULT = { + id: 4, + url: "http://example.com/?q=nonsponsored-normal", + title: "nonsponsored normal", + keywords: ["n-n"], + click_url: "http://click.reporting.test.com/nonsponsored", + impression_url: "http://impression.reporting.test.com/nonsponsored", + advertiser: "TestAdvertiserNonSponsored", + iab_category: "5 - Education", +}; +const FIRST_POSITION_RESULT = { + id: 5, + url: "http://example.com/?q=first-position", + title: "first position suggest", + keywords: ["first-position"], + click_url: "http://click.reporting.test.com/first-position", + impression_url: "http://impression.reporting.test.com/first-position", + advertiser: "TestAdvertiserFirstPositionQuickSuggest", + iab_category: "22 - Shopping", + position: 0, +}; +const SECOND_POSITION_RESULT = { + id: 6, + url: "http://example.com/?q=second-position", + title: "second position suggest", + keywords: ["second-position"], + click_url: "http://click.reporting.test.com/second-position", + impression_url: "http://impression.reporting.test.com/second-position", + advertiser: "TestAdvertiserSecondPositionQuickSuggest", + iab_category: "22 - Shopping", + position: 1, +}; +const THIRD_POSITION_RESULT = { + id: 7, + url: "http://example.com/?q=third-position", + title: "third position suggest", + keywords: ["third-position"], + click_url: "http://click.reporting.test.com/third-position", + impression_url: "http://impression.reporting.test.com/third-position", + advertiser: "TestAdvertiserThirdPositionQuickSuggest", + iab_category: "22 - Shopping", + position: 2, +}; + +const TABTOSEARCH_ENGINE_DOMAIN_FOR_FIRST_POSITION_TEST = + "first-position.example.com"; +const TABTOSEARCH_ENGINE_DOMAIN_FOR_SECOND_POSITION_TEST = + "second-position.example.com"; + +const SECOND_POSITION_INTERVENTION_RESULT = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/a" } +); +SECOND_POSITION_INTERVENTION_RESULT.suggestedIndex = 1; +const SECOND_POSITION_INTERVENTION_RESULT_PROVIDER = + new UrlbarTestUtils.TestProvider({ + results: [SECOND_POSITION_INTERVENTION_RESULT], + priority: 0, + name: "second_position_intervention_provider", + }); + +const EXPECTED_GENERAL_HEURISTIC_RESULT = { + providerName: UrlbarProviderHeuristicFallback.name, + type: UrlbarUtils.RESULT_TYPE.SEARCH, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + heuristic: true, +}; + +const EXPECTED_GENERAL_PLACES_RESULT = { + providerName: UrlbarProviderPlaces.name, + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + heuristic: false, +}; + +const EXPECTED_GENERAL_TABTOSEARCH_RESULT = { + providerName: UrlbarProviderTabToSearch.name, + type: UrlbarUtils.RESULT_TYPE.DYNAMIC, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + heuristic: false, +}; + +const EXPECTED_GENERAL_INTERVENTION_RESULT = { + providerName: SECOND_POSITION_INTERVENTION_RESULT_PROVIDER.name, + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + heuristic: false, +}; + +function createExpectedQuickSuggestResult(suggest) { + let isSponsored = suggest.iab_category !== "5 - Education"; + return { + providerName: UrlbarProviderQuickSuggest.name, + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + heuristic: false, + payload: { + telemetryType: isSponsored ? "adm_sponsored" : "adm_nonsponsored", + qsSuggestion: suggest.keywords[0], + title: suggest.title, + url: suggest.url, + originalUrl: suggest.url, + icon: null, + sponsoredImpressionUrl: suggest.impression_url, + sponsoredClickUrl: suggest.click_url, + sponsoredBlockId: suggest.id, + sponsoredAdvertiser: suggest.advertiser, + sponsoredIabCategory: suggest.iab_category, + isSponsored, + helpUrl: QuickSuggest.HELP_URL, + helpL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-learn-more-about-firefox-suggest" + : "firefox-suggest-urlbar-learn-more", + }, + isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"), + blockL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-dismiss-firefox-suggest" + : "firefox-suggest-urlbar-block", + }, + displayUrl: suggest.url, + source: "remote-settings", + }, + }; +} + +const TEST_CASES = [ + { + description: "Test for second placable sponsored suggest", + input: SPONSORED_SECOND_POSITION_RESULT.keywords[0], + prefs: { + "quicksuggest.allowPositionInSuggestions": true, + }, + providers: [ + UrlbarProviderHeuristicFallback.name, + UrlbarProviderQuickSuggest.name, + UrlbarProviderPlaces.name, + ], + expected: [ + EXPECTED_GENERAL_HEURISTIC_RESULT, + createExpectedQuickSuggestResult(SPONSORED_SECOND_POSITION_RESULT), + EXPECTED_GENERAL_PLACES_RESULT, + ], + }, + { + description: "Test for normal sponsored suggest", + input: SPONSORED_NORMAL_POSITION_RESULT.keywords[0], + prefs: { + "quicksuggest.allowPositionInSuggestions": true, + }, + providers: [ + UrlbarProviderHeuristicFallback.name, + UrlbarProviderQuickSuggest.name, + UrlbarProviderPlaces.name, + ], + expected: [ + EXPECTED_GENERAL_HEURISTIC_RESULT, + EXPECTED_GENERAL_PLACES_RESULT, + createExpectedQuickSuggestResult(SPONSORED_NORMAL_POSITION_RESULT), + ], + }, + { + description: "Test for second placable nonsponsored suggest", + input: NONSPONSORED_SECOND_POSITION_RESULT.keywords[0], + prefs: { + "quicksuggest.allowPositionInSuggestions": true, + }, + providers: [ + UrlbarProviderHeuristicFallback.name, + UrlbarProviderQuickSuggest.name, + UrlbarProviderPlaces.name, + ], + expected: [ + EXPECTED_GENERAL_HEURISTIC_RESULT, + createExpectedQuickSuggestResult(NONSPONSORED_SECOND_POSITION_RESULT), + EXPECTED_GENERAL_PLACES_RESULT, + ], + }, + { + description: "Test for normal nonsponsored suggest", + input: NONSPONSORED_NORMAL_POSITION_RESULT.keywords[0], + prefs: { + "quicksuggest.allowPositionInSuggestions": true, + }, + providers: [ + UrlbarProviderHeuristicFallback.name, + UrlbarProviderQuickSuggest.name, + UrlbarProviderPlaces.name, + ], + expected: [ + EXPECTED_GENERAL_HEURISTIC_RESULT, + EXPECTED_GENERAL_PLACES_RESULT, + createExpectedQuickSuggestResult(NONSPONSORED_NORMAL_POSITION_RESULT), + ], + }, + { + description: + "Test for second placable sponsored suggest but secondPosition pref is disabled", + input: SPONSORED_SECOND_POSITION_RESULT.keywords[0], + prefs: { + "quicksuggest.allowPositionInSuggestions": false, + }, + providers: [ + UrlbarProviderHeuristicFallback.name, + UrlbarProviderQuickSuggest.name, + UrlbarProviderPlaces.name, + ], + expected: [ + EXPECTED_GENERAL_HEURISTIC_RESULT, + EXPECTED_GENERAL_PLACES_RESULT, + createExpectedQuickSuggestResult(SPONSORED_SECOND_POSITION_RESULT), + ], + }, + { + description: "Test the results with multi providers having same index", + input: SECOND_POSITION_RESULT.keywords[0], + prefs: { + "quicksuggest.allowPositionInSuggestions": true, + }, + providers: [ + UrlbarProviderQuickSuggest.name, + UrlbarProviderTabToSearch.name, + SECOND_POSITION_INTERVENTION_RESULT_PROVIDER.name, + ], + expected: [ + EXPECTED_GENERAL_TABTOSEARCH_RESULT, + createExpectedQuickSuggestResult(SECOND_POSITION_RESULT), + EXPECTED_GENERAL_INTERVENTION_RESULT, + ], + }, + { + description: "Test the results with tab-to-search", + input: SECOND_POSITION_RESULT.keywords[0], + prefs: { + "quicksuggest.allowPositionInSuggestions": true, + }, + providers: [ + UrlbarProviderTabToSearch.name, + UrlbarProviderQuickSuggest.name, + ], + expected: [ + EXPECTED_GENERAL_TABTOSEARCH_RESULT, + createExpectedQuickSuggestResult(SECOND_POSITION_RESULT), + ], + }, + { + description: "Test the results with another intervention", + input: SECOND_POSITION_RESULT.keywords[0], + prefs: { + "quicksuggest.allowPositionInSuggestions": true, + }, + providers: [ + UrlbarProviderQuickSuggest.name, + SECOND_POSITION_INTERVENTION_RESULT_PROVIDER.name, + ], + expected: [ + createExpectedQuickSuggestResult(SECOND_POSITION_RESULT), + EXPECTED_GENERAL_INTERVENTION_RESULT, + ], + }, + { + description: "Test the results with heuristic and tab-to-search", + input: SECOND_POSITION_RESULT.keywords[0], + prefs: { + "quicksuggest.allowPositionInSuggestions": true, + }, + providers: [ + UrlbarProviderHeuristicFallback.name, + UrlbarProviderTabToSearch.name, + UrlbarProviderQuickSuggest.name, + ], + expected: [ + EXPECTED_GENERAL_HEURISTIC_RESULT, + EXPECTED_GENERAL_TABTOSEARCH_RESULT, + createExpectedQuickSuggestResult(SECOND_POSITION_RESULT), + ], + }, + { + description: "Test the results with heuristic tab-to-search and places", + input: SECOND_POSITION_RESULT.keywords[0], + prefs: { + "quicksuggest.allowPositionInSuggestions": true, + }, + providers: [ + UrlbarProviderHeuristicFallback.name, + UrlbarProviderTabToSearch.name, + UrlbarProviderQuickSuggest.name, + UrlbarProviderPlaces.name, + ], + expected: [ + EXPECTED_GENERAL_HEURISTIC_RESULT, + EXPECTED_GENERAL_TABTOSEARCH_RESULT, + createExpectedQuickSuggestResult(SECOND_POSITION_RESULT), + EXPECTED_GENERAL_PLACES_RESULT, + ], + }, + { + description: "Test the results with heuristic and another intervention", + input: SECOND_POSITION_RESULT.keywords[0], + prefs: { + "quicksuggest.allowPositionInSuggestions": true, + }, + providers: [ + UrlbarProviderHeuristicFallback.name, + UrlbarProviderQuickSuggest.name, + SECOND_POSITION_INTERVENTION_RESULT_PROVIDER.name, + ], + expected: [ + EXPECTED_GENERAL_HEURISTIC_RESULT, + createExpectedQuickSuggestResult(SECOND_POSITION_RESULT), + EXPECTED_GENERAL_INTERVENTION_RESULT, + ], + }, + { + description: + "Test the results with heuristic, another intervention and places", + input: SECOND_POSITION_RESULT.keywords[0], + prefs: { + "quicksuggest.allowPositionInSuggestions": true, + }, + providers: [ + UrlbarProviderHeuristicFallback.name, + UrlbarProviderQuickSuggest.name, + SECOND_POSITION_INTERVENTION_RESULT_PROVIDER.name, + UrlbarProviderPlaces.name, + ], + expected: [ + EXPECTED_GENERAL_HEURISTIC_RESULT, + createExpectedQuickSuggestResult(SECOND_POSITION_RESULT), + EXPECTED_GENERAL_INTERVENTION_RESULT, + EXPECTED_GENERAL_PLACES_RESULT, + ], + }, + { + description: "Test for 0 indexed quick suggest", + input: FIRST_POSITION_RESULT.keywords[0], + prefs: { + "quicksuggest.allowPositionInSuggestions": true, + }, + providers: [ + UrlbarProviderTabToSearch.name, + UrlbarProviderQuickSuggest.name, + ], + expected: [ + createExpectedQuickSuggestResult(FIRST_POSITION_RESULT), + EXPECTED_GENERAL_TABTOSEARCH_RESULT, + ], + }, + { + description: "Test for 2 indexed quick suggest", + input: THIRD_POSITION_RESULT.keywords[0], + prefs: { + "quicksuggest.allowPositionInSuggestions": true, + }, + providers: [ + UrlbarProviderQuickSuggest.name, + SECOND_POSITION_INTERVENTION_RESULT_PROVIDER.name, + ], + expected: [ + EXPECTED_GENERAL_INTERVENTION_RESULT, + createExpectedQuickSuggestResult(THIRD_POSITION_RESULT), + ], + }, +]; + +add_task(async function setup() { + UrlbarPrefs.set("quicksuggest.enabled", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + + // Setup for quick suggest result. + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + remoteSettingsResults: [ + { + type: "data", + attachment: [ + SPONSORED_SECOND_POSITION_RESULT, + SPONSORED_NORMAL_POSITION_RESULT, + NONSPONSORED_SECOND_POSITION_RESULT, + NONSPONSORED_NORMAL_POSITION_RESULT, + FIRST_POSITION_RESULT, + SECOND_POSITION_RESULT, + THIRD_POSITION_RESULT, + ], + }, + ], + }); + + // Setup for places result. + await PlacesUtils.history.clear(); + await PlacesTestUtils.addVisits([ + "http://example.com/" + SPONSORED_SECOND_POSITION_RESULT.keywords[0], + "http://example.com/" + SPONSORED_NORMAL_POSITION_RESULT.keywords[0], + "http://example.com/" + NONSPONSORED_SECOND_POSITION_RESULT.keywords[0], + "http://example.com/" + NONSPONSORED_NORMAL_POSITION_RESULT.keywords[0], + "http://example.com/" + SECOND_POSITION_RESULT.keywords[0], + ]); + + // Setup for tab-to-search result. + await SearchTestUtils.installSearchExtension({ + name: "first", + search_url: `https://${TABTOSEARCH_ENGINE_DOMAIN_FOR_FIRST_POSITION_TEST}/`, + }); + await SearchTestUtils.installSearchExtension({ + name: "second", + search_url: `https://${TABTOSEARCH_ENGINE_DOMAIN_FOR_SECOND_POSITION_TEST}/`, + }); + + /// Setup for another intervention result. + UrlbarProvidersManager.registerProvider( + SECOND_POSITION_INTERVENTION_RESULT_PROVIDER + ); +}); + +add_task(async function basic() { + for (const { description, input, prefs, providers, expected } of TEST_CASES) { + info(description); + + for (let name in prefs) { + UrlbarPrefs.set(name, prefs[name]); + } + + const context = createContext(input, { + providers, + isPrivate: false, + }); + await check_results({ + context, + matches: expected, + }); + + for (let name in prefs) { + UrlbarPrefs.clear(name); + } + } +}); diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_topPicks.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_topPicks.js new file mode 100644 index 0000000000..dd8b9dc575 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_topPicks.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/. */ + +// Tests top pick quick suggest results. "Top picks" refers to two different +// concepts: +// +// (1) Any type of suggestion from Merino can have a boolean property called +// `is_top_pick`. When true, Firefox should show the suggestion using the +// "best match" UI treatment (labeled "top pick" in the UI) that makes a +// result's row larger than usual and sets `suggestedIndex` to 1. However, +// the treatment must be enabled on Firefox via the `bestMatch.enabled` +// feature gate pref (Nimbus variable `bestMatchEnabled`) and the +// `suggest.bestMatch` pref, which corresponds to a checkbox in +// about:preferences. If the UI treatment is not enabled, Firefox should +// show the suggestion as usual. +// (2) There is a Merino provider called "top_picks" that returns a specific +// type of suggestion called "navigational suggestions". These suggestions +// also have `is_top_pick` set to true. +// +// This file tests aspects of both concepts. + +"use strict"; + +const SUGGESTION_SEARCH_STRING = "example"; +const SUGGESTION_URL = "http://example.com/"; +const SUGGESTION_URL_WWW = "http://www.example.com/"; +const SUGGESTION_URL_DISPLAY = "http://example.com"; + +const MERINO_SUGGESTIONS = [ + { + is_top_pick: true, + provider: "top_picks", + url: SUGGESTION_URL, + title: "title", + icon: "icon", + is_sponsored: false, + score: 1, + }, +]; + +add_setup(async function init() { + UrlbarPrefs.set("quicksuggest.enabled", true); + UrlbarPrefs.set("bestMatch.enabled", true); + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + + // Disable search suggestions so we don't hit the network. + Services.prefs.setBoolPref("browser.search.suggest.enabled", false); + + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + merinoSuggestions: MERINO_SUGGESTIONS, + }); +}); + +// When non-sponsored suggestions are disabled, navigational suggestions should +// be disabled. +add_task(async function nonsponsoredDisabled() { + // Disable sponsored suggestions. Navigational suggestions are non-sponsored, + // so doing this should not prevent them from being enabled. + UrlbarPrefs.set("suggest.quicksuggest.sponsored", false); + + // First make sure the suggestion is added when non-sponsored suggestions are + // enabled. + await check_results({ + context: createContext(SUGGESTION_SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [ + makeExpectedResult({ + isBestMatch: true, + suggestedIndex: 1, + }), + ], + }); + + // Now disable them. + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false); + await check_results({ + context: createContext(SUGGESTION_SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [], + }); + + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.clear("suggest.quicksuggest.sponsored"); +}); + +// Test that bestMatch navigational suggestion results are not shown when there +// is a heuristic result for the same domain. +add_task(async function heuristicDeduplication() { + let expectedNavSuggestResult = makeExpectedResult({ + isBestMatch: true, + suggestedIndex: 1, + dupedHeuristic: false, + }); + + let scenarios = [ + [SUGGESTION_URL, false], + [SUGGESTION_URL_WWW, false], + ["http://exampledomain.com/", true], + ]; + + // Stub `UrlbarProviderQuickSuggest.startQuery()` so we can collect the + // results it adds for each query. + let addedResults = []; + let sandbox = sinon.createSandbox(); + let startQueryStub = sandbox.stub(UrlbarProviderQuickSuggest, "startQuery"); + startQueryStub.callsFake((queryContext, add) => { + let fakeAdd = (provider, result) => { + addedResults.push(result); + add(provider, result); + }; + return startQueryStub.wrappedMethod.call( + UrlbarProviderQuickSuggest, + queryContext, + fakeAdd + ); + }); + + for (let [url, expectBestMatch] of scenarios) { + await PlacesTestUtils.addVisits(url); + + // Do a search and check the results. + let context = createContext(SUGGESTION_SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name, UrlbarProviderAutofill.name], + isPrivate: false, + }); + const EXPECTED_AUTOFILL_RESULT = makeVisitResult(context, { + uri: url, + title: `test visit for ${url}`, + heuristic: true, + }); + await check_results({ + context, + matches: expectBestMatch + ? [EXPECTED_AUTOFILL_RESULT, expectedNavSuggestResult] + : [EXPECTED_AUTOFILL_RESULT], + }); + + // Regardless of whether it was shown, one result should have been added and + // its `payload.dupedHeuristic` should be set properly. + Assert.equal( + addedResults.length, + 1, + "The provider should have added one result" + ); + Assert.equal( + !addedResults[0].payload.dupedHeuristic, + expectBestMatch, + "dupedHeuristic should be the opposite of expectBestMatch" + ); + addedResults = []; + + await PlacesUtils.history.clear(); + } + + sandbox.restore(); +}); + +add_task(async function prefs_0() { + await doPrefsTest({ + bestMatchEnabled: false, + suggestBestMatch: false, + expected: { + isBestMatch: false, + suggestedIndex: -1, + }, + }); +}); + +add_task(async function prefs_1() { + await doPrefsTest({ + bestMatchEnabled: false, + suggestBestMatch: true, + expected: { + isBestMatch: false, + suggestedIndex: -1, + }, + }); +}); + +add_task(async function prefs_2() { + await doPrefsTest({ + bestMatchEnabled: true, + suggestBestMatch: false, + expected: { + isBestMatch: false, + suggestedIndex: -1, + }, + }); +}); + +add_task(async function prefs_3() { + await doPrefsTest({ + bestMatchEnabled: true, + suggestBestMatch: true, + expected: { + isBestMatch: true, + suggestedIndex: 1, + }, + }); +}); + +async function doPrefsTest({ + bestMatchEnabled, + suggestBestMatch, + expected: { isBestMatch, suggestedIndex }, +}) { + UrlbarPrefs.set("bestMatch.enabled", bestMatchEnabled); + UrlbarPrefs.set("suggest.bestmatch", suggestBestMatch); + + // The mock suggestion has `provider` set to "top_picks", but Firefox should + // use only `is_top_pick` to determine whether it should be shown as best + // match, regardless of the provider. To make sure, change the provider to + // something else. + let originalProviders = []; + let provider = "some_unknown_provider"; + for (let s of MerinoTestUtils.server.response.body.suggestions) { + originalProviders.push(s.provider); + s.provider = provider; + } + + await check_results({ + context: createContext("test", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [ + makeExpectedResult({ + isBestMatch, + suggestedIndex, + telemetryType: provider, + }), + ], + }); + + UrlbarPrefs.clear("bestMatch.enabled"); + UrlbarPrefs.clear("suggest.bestmatch"); + + // Restore the original provider. + for (let s of MerinoTestUtils.server.response.body.suggestions) { + s.provider = originalProviders.shift(); + } +} + +function makeExpectedResult({ + isBestMatch, + suggestedIndex, + dupedHeuristic, + telemetryType = "top_picks", +}) { + let result = { + isBestMatch, + suggestedIndex, + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + heuristic: false, + payload: { + dupedHeuristic, + telemetryType, + title: "title", + url: SUGGESTION_URL, + displayUrl: SUGGESTION_URL_DISPLAY, + icon: "icon", + isSponsored: false, + source: "merino", + helpUrl: QuickSuggest.HELP_URL, + helpL10n: { + id: "urlbar-result-menu-learn-more-about-firefox-suggest", + }, + isBlockable: true, + blockL10n: { + id: "urlbar-result-menu-dismiss-firefox-suggest", + }, + }, + }; + if (typeof dupedHeuristic == "boolean") { + result.payload.dupedHeuristic = dupedHeuristic; + } + return result; +} diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_suggestionsMap.js b/browser/components/urlbar/tests/quicksuggest/unit/test_suggestionsMap.js new file mode 100644 index 0000000000..598f0d89a5 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_suggestionsMap.js @@ -0,0 +1,217 @@ +/* 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 the chunking feature of `RemoteSettingsClient.#addResults()`. + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + SuggestionsMap: + "resource:///modules/urlbar/private/QuickSuggestRemoteSettings.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(this, { + ObjectUtils: "resource://gre/modules/ObjectUtils.jsm", +}); + +// This overrides `SuggestionsMap.chunkSize`. Testing the actual value can make +// the test run too long. This is OK because the correctness of the chunking +// behavior doesn't depend on the chunk size. +const TEST_CHUNK_SIZE = 100; + +add_task(async function init() { + // Sanity check the actual `chunkSize` value. + Assert.equal( + typeof SuggestionsMap.chunkSize, + "number", + "Sanity check: chunkSize is a number" + ); + Assert.greater(SuggestionsMap.chunkSize, 0, "Sanity check: chunkSize > 0"); + + // Set our test value. + SuggestionsMap.chunkSize = TEST_CHUNK_SIZE; +}); + +// Tests many suggestions with one keyword each. +add_task(async function chunking_singleKeyword() { + let suggestionCounts = [ + 1 * SuggestionsMap.chunkSize - 1, + 1 * SuggestionsMap.chunkSize, + 1 * SuggestionsMap.chunkSize + 1, + 2 * SuggestionsMap.chunkSize - 1, + 2 * SuggestionsMap.chunkSize, + 2 * SuggestionsMap.chunkSize + 1, + 3 * SuggestionsMap.chunkSize - 1, + 3 * SuggestionsMap.chunkSize, + 3 * SuggestionsMap.chunkSize + 1, + ]; + for (let count of suggestionCounts) { + await doChunkingTest(count, 1); + } +}); + +// Tests a small number of suggestions with many keywords each. +add_task(async function chunking_manyKeywords() { + let keywordCounts = [ + 1 * SuggestionsMap.chunkSize - 1, + 1 * SuggestionsMap.chunkSize, + 1 * SuggestionsMap.chunkSize + 1, + 2 * SuggestionsMap.chunkSize - 1, + 2 * SuggestionsMap.chunkSize, + 2 * SuggestionsMap.chunkSize + 1, + 3 * SuggestionsMap.chunkSize - 1, + 3 * SuggestionsMap.chunkSize, + 3 * SuggestionsMap.chunkSize + 1, + ]; + for (let suggestionCount = 1; suggestionCount <= 3; suggestionCount++) { + for (let keywordCount of keywordCounts) { + await doChunkingTest(suggestionCount, keywordCount); + } + } +}); + +async function doChunkingTest(suggestionCount, keywordCountPerSuggestion) { + info( + "Running chunking test: " + + JSON.stringify({ suggestionCount, keywordCountPerSuggestion }) + ); + + // Create `suggestionCount` suggestions, each with `keywordCountPerSuggestion` + // keywords. + let suggestions = []; + for (let i = 0; i < suggestionCount; i++) { + let keywords = []; + for (let k = 0; k < keywordCountPerSuggestion; k++) { + keywords.push(`keyword-${i}-${k}`); + } + suggestions.push({ + keywords, + id: i, + url: "http://example.com/" + i, + title: "Suggestion " + i, + click_url: "http://example.com/click", + impression_url: "http://example.com/impression", + advertiser: "TestAdvertiser", + iab_category: "22 - Shopping", + }); + } + + // Add the suggestions. + let map = new SuggestionsMap(); + await map.add(suggestions); + + // Make sure all keyword-suggestion pairs have been added. + for (let i = 0; i < suggestionCount; i++) { + for (let k = 0; k < keywordCountPerSuggestion; k++) { + let keyword = `keyword-${i}-${k}`; + + // Check the map. Logging all assertions takes a ton of time and makes the + // test run much longer than it otherwise would, especially if `chunkSize` + // is large, so only log failing assertions. + let actualSuggestions = map.get(keyword); + if (!ObjectUtils.deepEqual(actualSuggestions, [suggestions[i]])) { + Assert.deepEqual( + actualSuggestions, + [suggestions[i]], + `Suggestion ${i} is present for keyword ${keyword}` + ); + } + } + } +} + +add_task(async function duplicateKeywords() { + let suggestions = [ + { + title: "suggestion 0", + keywords: ["a", "a", "a", "b", "b", "c"], + }, + { + title: "suggestion 1", + keywords: ["b", "c", "d"], + }, + { + title: "suggestion 2", + keywords: ["c", "d", "e"], + }, + { + title: "suggestion 3", + keywords: ["f", "f"], + }, + ]; + + let expectedIndexesByKeyword = { + a: [0], + b: [0, 1], + c: [0, 1, 2], + d: [1, 2], + e: [2], + f: [3], + }; + + let map = new SuggestionsMap(); + await map.add(suggestions); + + for (let [keyword, indexes] of Object.entries(expectedIndexesByKeyword)) { + Assert.deepEqual( + map.get(keyword), + indexes.map(i => suggestions[i]), + "get() with keyword: " + keyword + ); + } +}); + +add_task(async function mapKeywords() { + let suggestions = [ + { + title: "suggestion 0", + keywords: ["a", "a", "a", "b", "b", "c"], + }, + { + title: "suggestion 1", + keywords: ["b", "c", "d"], + }, + { + title: "suggestion 2", + keywords: ["c", "d", "e"], + }, + { + title: "suggestion 3", + keywords: ["f", "f"], + }, + ]; + + let expectedIndexesByKeyword = { + a: [], + b: [], + c: [], + d: [], + e: [], + f: [], + ax: [0], + bx: [0, 1], + cx: [0, 1, 2], + dx: [1, 2], + ex: [2], + fx: [3], + fy: [3], + fz: [3], + }; + + let map = new SuggestionsMap(); + await map.add(suggestions, keyword => { + if (keyword == "f") { + return [keyword + "x", keyword + "y", keyword + "z"]; + } + return [keyword + "x"]; + }); + + for (let [keyword, indexes] of Object.entries(expectedIndexesByKeyword)) { + Assert.deepEqual( + map.get(keyword), + indexes.map(i => suggestions[i]), + "get() with keyword: " + keyword + ); + } +}); diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_weather.js b/browser/components/urlbar/tests/quicksuggest/unit/test_weather.js new file mode 100644 index 0000000000..04fbc0b9d8 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_weather.js @@ -0,0 +1,1394 @@ +/* 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 the quick suggest weather feature. + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + MockRegistrar: "resource://testing-common/MockRegistrar.sys.mjs", + UrlbarProviderWeather: "resource:///modules/UrlbarProviderWeather.sys.mjs", +}); + +const HISTOGRAM_LATENCY = "FX_URLBAR_MERINO_LATENCY_WEATHER_MS"; +const HISTOGRAM_RESPONSE = "FX_URLBAR_MERINO_RESPONSE_WEATHER"; + +const { WEATHER_RS_DATA, WEATHER_SUGGESTION } = MerinoTestUtils; + +add_task(async function init() { + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + remoteSettingsResults: [ + { + type: "weather", + weather: WEATHER_RS_DATA, + }, + ], + }); + UrlbarPrefs.set("quicksuggest.enabled", true); + + await MerinoTestUtils.initWeather(); + + // Give this a small value so it doesn't delay the test too long. Choose a + // value that's unlikely to be used anywhere else in the test so that when + // `lastFetchTimeMs` is expected to be `fetchDelayAfterComingOnlineMs`, we can + // be sure the value actually came from `fetchDelayAfterComingOnlineMs`. + QuickSuggest.weather._test_fetchDelayAfterComingOnlineMs = 53; +}); + +// The feature should be properly uninitialized when it's disabled and then +// re-initialized when it's re-enabled. This task disables the feature using the +// feature gate pref. +add_task(async function disableAndEnable_featureGate() { + await doBasicDisableAndEnableTest("weather.featureGate"); +}); + +// The feature should be properly uninitialized when it's disabled and then +// re-initialized when it's re-enabled. This task disables the feature using the +// suggest pref. +add_task(async function disableAndEnable_suggestPref() { + await doBasicDisableAndEnableTest("suggest.weather"); +}); + +async function doBasicDisableAndEnableTest(pref) { + // Sanity check initial state. + assertEnabled({ + message: "Sanity check initial state", + hasSuggestion: true, + pendingFetchCount: 0, + }); + + // Disable the feature. It should be immediately uninitialized. + UrlbarPrefs.set(pref, false); + assertDisabled({ + message: "After disabling", + pendingFetchCount: 0, + }); + + // No suggestion should be returned for a search. + let context = createContext(MerinoTestUtils.WEATHER_KEYWORD, { + providers: [UrlbarProviderWeather.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [], + }); + + let histograms = MerinoTestUtils.getAndClearHistograms({ + extraLatency: HISTOGRAM_LATENCY, + extraResponse: HISTOGRAM_RESPONSE, + }); + + // Re-enable the feature. It should be immediately initialized and a fetch + // should start. + info("Re-enable the feature"); + let fetchPromise = QuickSuggest.weather.waitForFetches(); + UrlbarPrefs.set(pref, true); + assertEnabled({ + message: "Immediately after re-enabling", + hasSuggestion: false, + pendingFetchCount: 1, + }); + + await fetchPromise; + assertEnabled({ + message: "After awaiting fetch", + hasSuggestion: true, + pendingFetchCount: 0, + }); + + Assert.equal( + QuickSuggest.weather._test_merino.lastFetchStatus, + "success", + "The request successfully finished" + ); + MerinoTestUtils.checkAndClearHistograms({ + histograms, + response: "success", + latencyRecorded: true, + client: QuickSuggest.weather._test_merino, + }); + + // The suggestion should be returned for a search. + context = createContext(MerinoTestUtils.WEATHER_KEYWORD, { + providers: [UrlbarProviderWeather.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [makeExpectedResult()], + }); +} + +add_task(async function keywordsNotDefined() { + // Sanity check initial state. + assertEnabled({ + message: "Sanity check initial state", + hasSuggestion: true, + pendingFetchCount: 0, + }); + + // Set RS data without any keywords. Fetching should immediately stop. + await QuickSuggestTestUtils.setRemoteSettingsResults([ + { + type: "weather", + weather: {}, + }, + ]); + assertDisabled({ + message: "After setting RS data without keywords", + pendingFetchCount: 0, + }); + + // No suggestion should be returned for a search. + let context = createContext(MerinoTestUtils.WEATHER_KEYWORD, { + providers: [UrlbarProviderWeather.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [], + }); + + // Set keywords. Fetching should immediately start. + info("Setting keywords"); + let fetchPromise = QuickSuggest.weather.waitForFetches(); + await QuickSuggestTestUtils.setRemoteSettingsResults([ + { + type: "weather", + weather: MerinoTestUtils.WEATHER_RS_DATA, + }, + ]); + assertEnabled({ + message: "Immediately after setting keywords", + hasSuggestion: false, + pendingFetchCount: 1, + }); + + await fetchPromise; + assertEnabled({ + message: "After awaiting fetch", + hasSuggestion: true, + pendingFetchCount: 0, + }); + + Assert.equal( + QuickSuggest.weather._test_merino.lastFetchStatus, + "success", + "The request successfully finished" + ); + + // The suggestion should be returned for a search. + context = createContext(MerinoTestUtils.WEATHER_KEYWORD, { + providers: [UrlbarProviderWeather.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [makeExpectedResult()], + }); +}); + +// Disables and re-enables the feature without waiting for any intermediate +// fetches to complete, using the following steps: +// +// 1. Disable +// 2. Enable +// 3. Disable again +// +// At this point, the fetch from step 2 will remain ongoing but once it finishes +// it should be discarded since the feature is disabled. +add_task(async function disableAndEnable_immediate1() { + // Sanity check initial state. + assertEnabled({ + message: "Sanity check initial state", + hasSuggestion: true, + pendingFetchCount: 0, + }); + + // Disable the feature. It should be immediately uninitialized. + UrlbarPrefs.set("weather.featureGate", false); + assertDisabled({ + message: "After disabling", + pendingFetchCount: 0, + }); + + // Re-enable the feature. It should be immediately initialized and a fetch + // should start. + let fetchPromise = QuickSuggest.weather.waitForFetches(); + UrlbarPrefs.set("weather.featureGate", true); + assertEnabled({ + message: "Immediately after re-enabling", + hasSuggestion: false, + pendingFetchCount: 1, + }); + + // Disable it again. The fetch will remain ongoing since pending fetches + // aren't stopped when the feature is disabled. + UrlbarPrefs.set("weather.featureGate", false); + assertDisabled({ + message: "After disabling again", + pendingFetchCount: 1, + }); + + // Wait for the fetch to finish. + await fetchPromise; + + // The fetched suggestion should be discarded and the feature should remain + // uninitialized. + assertDisabled({ + message: "After awaiting fetch", + pendingFetchCount: 0, + }); + + // Clean up by re-enabling the feature for the remaining tasks. + fetchPromise = QuickSuggest.weather.waitForFetches(); + UrlbarPrefs.set("weather.featureGate", true); + await fetchPromise; + assertEnabled({ + message: "On cleanup", + hasSuggestion: true, + pendingFetchCount: 0, + }); +}); + +// Disables and re-enables the feature without waiting for any intermediate +// fetches to complete, using the following steps: +// +// 1. Disable +// 2. Enable +// 3. Disable again +// 4. Enable again +// +// At this point, the fetches from steps 2 and 4 will remain ongoing. The fetch +// from step 2 should be discarded. +add_task(async function disableAndEnable_immediate2() { + // Sanity check initial state. + assertEnabled({ + message: "Sanity check initial state", + hasSuggestion: true, + pendingFetchCount: 0, + }); + + // Disable the feature. It should be immediately uninitialized. + UrlbarPrefs.set("weather.featureGate", false); + assertDisabled({ + message: "After disabling", + pendingFetchCount: 0, + }); + + // Re-enable the feature. It should be immediately initialized and a fetch + // should start. + UrlbarPrefs.set("weather.featureGate", true); + assertEnabled({ + message: "Immediately after re-enabling", + hasSuggestion: false, + pendingFetchCount: 1, + }); + + // Disable it again. The fetch will remain ongoing since pending fetches + // aren't stopped when the feature is disabled. + UrlbarPrefs.set("weather.featureGate", false); + assertDisabled({ + message: "After disabling again", + pendingFetchCount: 1, + }); + + // Re-enable it. A new fetch should start, so now there will be two pending + // fetches. + let fetchPromise = QuickSuggest.weather.waitForFetches(); + UrlbarPrefs.set("weather.featureGate", true); + assertEnabled({ + message: "Immediately after re-enabling again", + hasSuggestion: false, + pendingFetchCount: 2, + }); + + // Wait for both fetches to finish. + await fetchPromise; + assertEnabled({ + message: "Immediately after re-enabling again", + hasSuggestion: true, + pendingFetchCount: 0, + }); +}); + +// A fetch that doesn't return a suggestion should cause the last-fetched +// suggestion to be discarded. +add_task(async function noSuggestion() { + assertEnabled({ + message: "Sanity check initial state", + hasSuggestion: true, + pendingFetchCount: 0, + }); + + let histograms = MerinoTestUtils.getAndClearHistograms({ + extraLatency: HISTOGRAM_LATENCY, + extraResponse: HISTOGRAM_RESPONSE, + }); + + let { suggestions } = MerinoTestUtils.server.response.body; + MerinoTestUtils.server.response.body.suggestions = []; + + await QuickSuggest.weather._test_fetch(); + + assertEnabled({ + message: "After fetch", + hasSuggestion: false, + pendingFetchCount: 0, + }); + Assert.equal( + QuickSuggest.weather._test_merino.lastFetchStatus, + "no_suggestion", + "The request successfully finished without suggestions" + ); + MerinoTestUtils.checkAndClearHistograms({ + histograms, + response: "no_suggestion", + latencyRecorded: true, + client: QuickSuggest.weather._test_merino, + }); + + let context = createContext(MerinoTestUtils.WEATHER_KEYWORD, { + providers: [UrlbarProviderWeather.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [], + }); + + MerinoTestUtils.server.response.body.suggestions = suggestions; + + // Clean up by forcing another fetch so the suggestion is non-null for the + // remaining tasks. + await QuickSuggest.weather._test_fetch(); + assertEnabled({ + message: "On cleanup", + hasSuggestion: true, + pendingFetchCount: 0, + }); +}); + +// A network error should cause the last-fetched suggestion to be discarded. +add_task(async function networkError() { + assertEnabled({ + message: "Sanity check initial state", + hasSuggestion: true, + pendingFetchCount: 0, + }); + + let histograms = MerinoTestUtils.getAndClearHistograms({ + extraLatency: HISTOGRAM_LATENCY, + extraResponse: HISTOGRAM_RESPONSE, + }); + + // Set the weather fetch timeout high enough that the network error exception + // will happen first. See `MerinoTestUtils.withNetworkError()`. + QuickSuggest.weather._test_setTimeoutMs(10000); + + await MerinoTestUtils.server.withNetworkError(async () => { + await QuickSuggest.weather._test_fetch(); + }); + + QuickSuggest.weather._test_setTimeoutMs(-1); + + assertEnabled({ + message: "After fetch", + hasSuggestion: false, + pendingFetchCount: 0, + }); + Assert.equal( + QuickSuggest.weather._test_merino.lastFetchStatus, + "network_error", + "The request failed with a network error" + ); + MerinoTestUtils.checkAndClearHistograms({ + histograms, + response: "network_error", + latencyRecorded: false, + client: QuickSuggest.weather._test_merino, + }); + + let context = createContext(MerinoTestUtils.WEATHER_KEYWORD, { + providers: [UrlbarProviderWeather.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [], + }); + + // Clean up by forcing another fetch so the suggestion is non-null for the + // remaining tasks. + await QuickSuggest.weather._test_fetch(); + assertEnabled({ + message: "On cleanup", + hasSuggestion: true, + pendingFetchCount: 0, + }); +}); + +// An HTTP error should cause the last-fetched suggestion to be discarded. +add_task(async function httpError() { + assertEnabled({ + message: "Sanity check initial state", + hasSuggestion: true, + pendingFetchCount: 0, + }); + + let histograms = MerinoTestUtils.getAndClearHistograms({ + extraLatency: HISTOGRAM_LATENCY, + extraResponse: HISTOGRAM_RESPONSE, + }); + + MerinoTestUtils.server.response = { status: 500 }; + await QuickSuggest.weather._test_fetch(); + + assertEnabled({ + message: "After fetch", + hasSuggestion: false, + pendingFetchCount: 0, + }); + Assert.equal( + QuickSuggest.weather._test_merino.lastFetchStatus, + "http_error", + "The request failed with an HTTP error" + ); + MerinoTestUtils.checkAndClearHistograms({ + histograms, + response: "http_error", + latencyRecorded: true, + client: QuickSuggest.weather._test_merino, + }); + + let context = createContext(MerinoTestUtils.WEATHER_KEYWORD, { + providers: [UrlbarProviderWeather.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [], + }); + + // Clean up by forcing another fetch so the suggestion is non-null for the + // remaining tasks. + MerinoTestUtils.server.reset(); + MerinoTestUtils.server.response.body.suggestions = [WEATHER_SUGGESTION]; + await QuickSuggest.weather._test_fetch(); + assertEnabled({ + message: "On cleanup", + hasSuggestion: true, + pendingFetchCount: 0, + }); +}); + +// A fetch that doesn't return a suggestion due to a client timeout should cause +// the last-fetched suggestion to be discarded. +add_task(async function clientTimeout() { + assertEnabled({ + message: "Sanity check initial state", + hasSuggestion: true, + pendingFetchCount: 0, + }); + + let histograms = MerinoTestUtils.getAndClearHistograms({ + extraLatency: HISTOGRAM_LATENCY, + extraResponse: HISTOGRAM_RESPONSE, + }); + + // Make the server return a delayed response so the Merino client times out + // waiting for it. + MerinoTestUtils.server.response.delay = 400; + + // Make the client time out immediately. + QuickSuggest.weather._test_setTimeoutMs(1); + + // Set up a promise that will be resolved when the client finally receives the + // response. + let responsePromise = QuickSuggest.weather._test_merino.waitForNextResponse(); + + await QuickSuggest.weather._test_fetch(); + + assertEnabled({ + message: "After fetch", + hasSuggestion: false, + pendingFetchCount: 0, + }); + Assert.equal( + QuickSuggest.weather._test_merino.lastFetchStatus, + "timeout", + "The request timed out" + ); + MerinoTestUtils.checkAndClearHistograms({ + histograms, + response: "timeout", + latencyRecorded: false, + latencyStopwatchRunning: true, + client: QuickSuggest.weather._test_merino, + }); + + let context = createContext(MerinoTestUtils.WEATHER_KEYWORD, { + providers: [UrlbarProviderWeather.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [], + }); + + // Await the response. + await responsePromise; + + // The `checkAndClearHistograms()` call above cleared the histograms. After + // that, nothing else should have been recorded for the response. + MerinoTestUtils.checkAndClearHistograms({ + histograms, + response: null, + latencyRecorded: true, + client: QuickSuggest.weather._test_merino, + }); + + QuickSuggest.weather._test_setTimeoutMs(-1); + delete MerinoTestUtils.server.response.delay; + + // Clean up by forcing another fetch so the suggestion is non-null for the + // remaining tasks. + await QuickSuggest.weather._test_fetch(); + assertEnabled({ + message: "On cleanup", + hasSuggestion: true, + pendingFetchCount: 0, + }); +}); + +// Locale task for when this test runs on an en-US OS. +add_task(async function locale_enUS() { + await doLocaleTest({ + shouldRunTask: osLocale => osLocale == "en-US", + osUnit: "f", + unitsByLocale: { + "en-US": "f", + // When the app's locale is set to any en-* locale, F will be used because + // `regionalPrefsLocales` will prefer the en-US OS locale. + "en-CA": "f", + "en-GB": "f", + de: "c", + }, + }); +}); + +// Locale task for when this test runs on a non-US English OS. +add_task(async function locale_nonUSEnglish() { + await doLocaleTest({ + shouldRunTask: osLocale => osLocale.startsWith("en") && osLocale != "en-US", + osUnit: "c", + unitsByLocale: { + // When the app's locale is set to en-US, C will be used because + // `regionalPrefsLocales` will prefer the non-US English OS locale. + "en-US": "c", + "en-CA": "c", + "en-GB": "c", + de: "c", + }, + }); +}); + +// Locale task for when this test runs on a non-English OS. +add_task(async function locale_nonEnglish() { + await doLocaleTest({ + shouldRunTask: osLocale => !osLocale.startsWith("en"), + osUnit: "c", + unitsByLocale: { + "en-US": "f", + "en-CA": "c", + "en-GB": "c", + de: "c", + }, + }); +}); + +/** + * Testing locales is tricky due to the weather feature's use of + * `Services.locale.regionalPrefsLocales`. By default `regionalPrefsLocales` + * prefers the OS locale if its language is the same as the app locale's + * language; otherwise it prefers the app locale. For example, assuming the OS + * locale is en-CA, then if the app locale is en-US it will prefer en-CA since + * both are English, but if the app locale is de it will prefer de. If the pref + * `intl.regional_prefs.use_os_locales` is set, then the OS locale is always + * preferred. + * + * This function tests a given set of locales with and without + * `intl.regional_prefs.use_os_locales` set. + * + * @param {object} options + * Options + * @param {Function} options.shouldRunTask + * Called with the OS locale. Should return true if the function should run. + * Use this to skip tasks that don't target a desired OS locale. + * @param {string} options.osUnit + * The expected "c" or "f" unit for the OS locale. + * @param {object} options.unitsByLocale + * The expected "c" or "f" unit when the app's locale is set to particular + * locales. This should be an object that maps locales to expected units. For + * each locale in the object, the app's locale is set to that locale and the + * actual unit is expected to be the unit in the object. + */ +async function doLocaleTest({ shouldRunTask, osUnit, unitsByLocale }) { + Services.prefs.setBoolPref("intl.regional_prefs.use_os_locales", true); + let osLocale = Services.locale.regionalPrefsLocales[0]; + Services.prefs.clearUserPref("intl.regional_prefs.use_os_locales"); + + if (!shouldRunTask(osLocale)) { + info("Skipping task, should not run for this OS locale"); + return; + } + + assertEnabled({ + message: "Sanity check initial state", + hasSuggestion: true, + pendingFetchCount: 0, + }); + + // Sanity check initial locale info. + Assert.equal( + Services.locale.appLocaleAsBCP47, + "en-US", + "Initial app locale should be en-US" + ); + Assert.ok( + !Services.prefs.getBoolPref("intl.regional_prefs.use_os_locales"), + "intl.regional_prefs.use_os_locales should be false initially" + ); + + // Check locales. + for (let [locale, temperatureUnit] of Object.entries(unitsByLocale)) { + await QuickSuggestTestUtils.withLocales([locale], async () => { + info("Checking locale: " + locale); + await check_results({ + context: createContext(MerinoTestUtils.WEATHER_KEYWORD, { + providers: [UrlbarProviderWeather.name], + isPrivate: false, + }), + matches: [makeExpectedResult({ temperatureUnit })], + }); + + info( + "Checking locale with intl.regional_prefs.use_os_locales: " + locale + ); + Services.prefs.setBoolPref("intl.regional_prefs.use_os_locales", true); + await check_results({ + context: createContext(MerinoTestUtils.WEATHER_KEYWORD, { + providers: [UrlbarProviderWeather.name], + isPrivate: false, + }), + matches: [makeExpectedResult({ temperatureUnit: osUnit })], + }); + Services.prefs.clearUserPref("intl.regional_prefs.use_os_locales"); + }); + } +} + +// Blocks a result and makes sure the weather pref is disabled. +add_task(async function block() { + // Sanity check initial state. + assertEnabled({ + message: "Sanity check initial state", + hasSuggestion: true, + pendingFetchCount: 0, + }); + Assert.ok( + UrlbarPrefs.get("suggest.weather"), + "Sanity check: suggest.weather is true initially" + ); + + // Do a search so we can get an actual result. + let context = createContext(MerinoTestUtils.WEATHER_KEYWORD, { + providers: [UrlbarProviderWeather.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [makeExpectedResult()], + }); + + // Block the result. + UrlbarProviderWeather.onEngagement(false, "engagement", context, { + result: context.results[0], + selType: "dismiss", + selIndex: context.results[0].rowIndex, + }); + Assert.ok( + !UrlbarPrefs.get("suggest.weather"), + "suggest.weather is false after blocking the result" + ); + + // Do a second search. Nothing should be returned. + context = createContext(MerinoTestUtils.WEATHER_KEYWORD, { + providers: [UrlbarProviderWeather.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [], + }); + + // Re-enable the pref and clean up. + let fetchPromise = QuickSuggest.weather.waitForFetches(); + UrlbarPrefs.set("suggest.weather", true); + await fetchPromise; + assertEnabled({ + message: "On cleanup", + hasSuggestion: true, + pendingFetchCount: 0, + }); +}); + +// Simulates wake 100ms before the start of the next fetch period. A new fetch +// should not start. +add_task(async function wakeBeforeNextFetchPeriod() { + await doWakeTest({ + sleepIntervalMs: QuickSuggest.weather._test_fetchIntervalMs - 100, + shouldFetchOnWake: false, + fetchTimerMsOnWake: 100, + }); +}); + +// Simulates wake 100ms after the start of the next fetch period. A new fetch +// should start. +add_task(async function wakeAfterNextFetchPeriod() { + await doWakeTest({ + sleepIntervalMs: QuickSuggest.weather._test_fetchIntervalMs + 100, + shouldFetchOnWake: true, + }); +}); + +// Simulates wake after many fetch periods + 100ms. A new fetch should start. +add_task(async function wakeAfterManyFetchPeriods() { + await doWakeTest({ + sleepIntervalMs: 100 * QuickSuggest.weather._test_fetchIntervalMs + 100, + shouldFetchOnWake: true, + }); +}); + +async function doWakeTest({ + sleepIntervalMs, + shouldFetchOnWake, + fetchTimerMsOnWake, +}) { + // Make `Date.now()` return a value under our control, doesn't matter what it + // is. This is the time the first fetch period will start. + let nowOnStart = 100; + let sandbox = sinon.createSandbox(); + let dateNowStub = sandbox.stub( + Cu.getGlobalForObject(QuickSuggest.weather).Date, + "now" + ); + dateNowStub.returns(nowOnStart); + + // Start the first fetch period. + info("Starting first fetch period"); + await QuickSuggest.weather._test_fetch(); + Assert.equal( + QuickSuggest.weather._test_lastFetchTimeMs, + nowOnStart, + "Last fetch time should be updated after fetch" + ); + Assert.equal( + QuickSuggest.weather._test_fetchTimerMs, + QuickSuggest.weather._test_fetchIntervalMs, + "Timer period should be full fetch interval" + ); + + let timer = QuickSuggest.weather._test_fetchTimer; + + // Advance the clock and simulate wake. + info("Sending wake notification"); + let nowOnWake = nowOnStart + sleepIntervalMs; + dateNowStub.returns(nowOnWake); + QuickSuggest.weather.observe(null, "wake_notification", ""); + + Assert.equal( + QuickSuggest.weather._test_pendingFetchCount, + 0, + "After wake, next fetch should not have immediately started" + ); + Assert.equal( + QuickSuggest.weather._test_lastFetchTimeMs, + nowOnStart, + "After wake, last fetch time should be unchanged" + ); + Assert.notEqual( + QuickSuggest.weather._test_fetchTimer, + 0, + "After wake, the timer should exist (be non-zero)" + ); + Assert.notEqual( + QuickSuggest.weather._test_fetchTimer, + timer, + "After wake, a new timer should have been created" + ); + + if (shouldFetchOnWake) { + Assert.equal( + QuickSuggest.weather._test_fetchTimerMs, + QuickSuggest.weather._test_fetchDelayAfterComingOnlineMs, + "After wake, timer period should be fetchDelayAfterComingOnlineMs" + ); + } else { + Assert.equal( + QuickSuggest.weather._test_fetchTimerMs, + fetchTimerMsOnWake, + "After wake, timer period should be the remaining interval" + ); + } + + // Wait for the fetch. If the wake didn't trigger it, then the caller should + // have passed in a `sleepIntervalMs` that will make it start soon. + info("Waiting for fetch after wake"); + await QuickSuggest.weather.waitForFetches(); + + Assert.equal( + QuickSuggest.weather._test_fetchTimerMs, + QuickSuggest.weather._test_fetchIntervalMs, + "After post-wake fetch, timer period should remain full fetch interval" + ); + Assert.equal( + QuickSuggest.weather._test_pendingFetchCount, + 0, + "After post-wake fetch, no more fetches should be pending" + ); + + dateNowStub.restore(); +} + +// When network:link-status-changed is observed and the suggestion is non-null, +// a fetch should not start. +add_task(async function networkLinkStatusChanged_nonNull() { + // See nsINetworkLinkService for possible data values. + await doOnlineTestWithSuggestion({ + topic: "network:link-status-changed", + dataValues: [ + "down", + "up", + "changed", + "unknown", + "this is not a valid data value", + ], + }); +}); + +// When network:offline-status-changed is observed and the suggestion is +// non-null, a fetch should not start. +add_task(async function networkOfflineStatusChanged_nonNull() { + // See nsIIOService for possible data values. + await doOnlineTestWithSuggestion({ + topic: "network:offline-status-changed", + dataValues: ["offline", "online", "this is not a valid data value"], + }); +}); + +// When captive-portal-login-success is observed and the suggestion is non-null, +// a fetch should not start. +add_task(async function captivePortalLoginSuccess_nonNull() { + // See nsIIOService for possible data values. + await doOnlineTestWithSuggestion({ + topic: "captive-portal-login-success", + dataValues: [""], + }); +}); + +async function doOnlineTestWithSuggestion({ topic, dataValues }) { + info("Starting fetch period"); + await QuickSuggest.weather._test_fetch(); + Assert.ok( + QuickSuggest.weather.suggestion, + "Suggestion should have been fetched" + ); + + let timer = QuickSuggest.weather._test_fetchTimer; + + for (let data of dataValues) { + info("Sending notification: " + JSON.stringify({ topic, data })); + QuickSuggest.weather.observe(null, topic, data); + + Assert.equal( + QuickSuggest.weather._test_pendingFetchCount, + 0, + "Fetch should not have started" + ); + Assert.equal( + QuickSuggest.weather._test_fetchTimer, + timer, + "Timer should not have been recreated" + ); + Assert.equal( + QuickSuggest.weather._test_fetchTimerMs, + QuickSuggest.weather._test_fetchIntervalMs, + "Timer period should be the full fetch interval" + ); + } +} + +// When network:link-status-changed is observed and the suggestion is null, a +// fetch should start unless the data indicates the status is offline. +add_task(async function networkLinkStatusChanged_null() { + // See nsINetworkLinkService for possible data values. + await doOnlineTestWithNullSuggestion({ + topic: "network:link-status-changed", + offlineData: "down", + otherDataValues: [ + "up", + "changed", + "unknown", + "this is not a valid data value", + ], + }); +}); + +// When network:offline-status-changed is observed and the suggestion is null, a +// fetch should start unless the data indicates the status is offline. +add_task(async function networkOfflineStatusChanged_null() { + // See nsIIOService for possible data values. + await doOnlineTestWithNullSuggestion({ + topic: "network:offline-status-changed", + offlineData: "offline", + otherDataValues: ["online", "this is not a valid data value"], + }); +}); + +// When captive-portal-login-success is observed and the suggestion is null, a +// fetch should start. +add_task(async function captivePortalLoginSuccess_null() { + // See nsIIOService for possible data values. + await doOnlineTestWithNullSuggestion({ + topic: "captive-portal-login-success", + otherDataValues: [""], + }); +}); + +async function doOnlineTestWithNullSuggestion({ + topic, + otherDataValues, + offlineData = "", +}) { + QuickSuggest.weather._test_setSuggestionToNull(); + Assert.ok(!QuickSuggest.weather.suggestion, "Suggestion should be null"); + + let timer = QuickSuggest.weather._test_fetchTimer; + + // First, send the notification with the offline data value. Nothing should + // happen. + if (offlineData) { + info("Sending notification: " + JSON.stringify({ topic, offlineData })); + QuickSuggest.weather.observe(null, topic, offlineData); + + Assert.ok( + !QuickSuggest.weather.suggestion, + "Suggestion should remain null" + ); + Assert.equal( + QuickSuggest.weather._test_pendingFetchCount, + 0, + "Fetch should not have started" + ); + Assert.equal( + QuickSuggest.weather._test_fetchTimer, + timer, + "Timer should not have been recreated" + ); + Assert.equal( + QuickSuggest.weather._test_fetchTimerMs, + QuickSuggest.weather._test_fetchIntervalMs, + "Timer period should be the full fetch interval" + ); + } + + // Now send it with all other data values. Fetches should be triggered. + for (let data of otherDataValues) { + QuickSuggest.weather._test_setSuggestionToNull(); + Assert.ok(!QuickSuggest.weather.suggestion, "Suggestion should be null"); + + info("Sending notification: " + JSON.stringify({ topic, data })); + QuickSuggest.weather.observe(null, topic, data); + + Assert.equal( + QuickSuggest.weather._test_pendingFetchCount, + 0, + "Fetch should not have started yet" + ); + Assert.notEqual( + QuickSuggest.weather._test_fetchTimer, + 0, + "Timer should exist" + ); + Assert.notEqual( + QuickSuggest.weather._test_fetchTimer, + timer, + "A new timer should have been created" + ); + Assert.equal( + QuickSuggest.weather._test_fetchTimerMs, + QuickSuggest.weather._test_fetchDelayAfterComingOnlineMs, + "Timer ms should be fetchDelayAfterComingOnlineMs" + ); + + timer = QuickSuggest.weather._test_fetchTimer; + + info("Waiting for fetch after notification"); + await QuickSuggest.weather.waitForFetches(); + + Assert.equal( + QuickSuggest.weather._test_pendingFetchCount, + 0, + "Fetch should not be pending" + ); + Assert.notEqual( + QuickSuggest.weather._test_fetchTimer, + 0, + "Timer should exist" + ); + Assert.notEqual( + QuickSuggest.weather._test_fetchTimer, + timer, + "A new timer should have been created" + ); + Assert.equal( + QuickSuggest.weather._test_fetchTimerMs, + QuickSuggest.weather._test_fetchIntervalMs, + "Timer period should be full fetch interval" + ); + + timer = QuickSuggest.weather._test_fetchTimer; + } +} + +// When many online notifications are received at once, only one fetch should +// start. +add_task(async function manyOnlineNotifications() { + await doManyNotificationsTest([ + ["network:link-status-changed", "changed"], + ["network:link-status-changed", "up"], + ["network:offline-status-changed", "online"], + ]); +}); + +// When wake and online notifications are received at once, only one fetch +// should start. +add_task(async function wakeAndOnlineNotifications() { + await doManyNotificationsTest([ + ["wake_notification", ""], + ["network:link-status-changed", "changed"], + ["network:link-status-changed", "up"], + ["network:offline-status-changed", "online"], + ]); +}); + +async function doManyNotificationsTest(notifications) { + // Make `Date.now()` return a value under our control, doesn't matter what it + // is. This is the time the first fetch period will start. + let nowOnStart = 100; + let sandbox = sinon.createSandbox(); + let dateNowStub = sandbox.stub( + Cu.getGlobalForObject(QuickSuggest.weather).Date, + "now" + ); + dateNowStub.returns(nowOnStart); + + // Start a first fetch period so that after we send the notifications below + // the last fetch time will be in the past. + info("Starting first fetch period"); + await QuickSuggest.weather._test_fetch(); + Assert.equal( + QuickSuggest.weather._test_lastFetchTimeMs, + nowOnStart, + "Last fetch time should be updated after fetch" + ); + + // Now advance the clock by many fetch intervals. + let nowOnWake = nowOnStart + 100 * QuickSuggest.weather._test_fetchIntervalMs; + dateNowStub.returns(nowOnWake); + + // Set the suggestion to null so online notifications will trigger a fetch. + QuickSuggest.weather._test_setSuggestionToNull(); + Assert.ok(!QuickSuggest.weather.suggestion, "Suggestion should be null"); + + // Clear the server's list of received requests. + MerinoTestUtils.server.reset(); + MerinoTestUtils.server.response.body.suggestions = [ + MerinoTestUtils.WEATHER_SUGGESTION, + ]; + + // Send the notifications. + for (let [topic, data] of notifications) { + info("Sending notification: " + JSON.stringify({ topic, data })); + QuickSuggest.weather.observe(null, topic, data); + } + + info("Waiting for fetch after notifications"); + await QuickSuggest.weather.waitForFetches(); + + Assert.equal( + QuickSuggest.weather._test_pendingFetchCount, + 0, + "Fetch should not be pending" + ); + Assert.notEqual( + QuickSuggest.weather._test_fetchTimer, + 0, + "Timer should exist" + ); + Assert.equal( + QuickSuggest.weather._test_fetchTimerMs, + QuickSuggest.weather._test_fetchIntervalMs, + "Timer period should be full fetch interval" + ); + + Assert.equal( + MerinoTestUtils.server.requests.length, + 1, + "Merino should have received only one request" + ); + + dateNowStub.restore(); +} + +// Fetching when a VPN is detected should set the suggestion to null, and +// turning off the VPN should trigger a re-fetch. +add_task(async function vpn() { + // Register a mock object that implements nsINetworkLinkService. + let mockLinkService = { + isLinkUp: true, + linkStatusKnown: true, + linkType: Ci.nsINetworkLinkService.LINK_TYPE_WIFI, + networkID: "abcd", + dnsSuffixList: [], + platformDNSIndications: Ci.nsINetworkLinkService.NONE_DETECTED, + QueryInterface: ChromeUtils.generateQI(["nsINetworkLinkService"]), + }; + let networkLinkServiceCID = MockRegistrar.register( + "@mozilla.org/network/network-link-service;1", + mockLinkService + ); + QuickSuggest.weather._test_linkService = mockLinkService; + + // At this point no VPN is detected, so a fetch should complete successfully. + await QuickSuggest.weather._test_fetch(); + Assert.ok(QuickSuggest.weather.suggestion, "Suggestion should exist"); + + // Modify the mock link service to indicate a VPN is detected. + mockLinkService.platformDNSIndications = + Ci.nsINetworkLinkService.VPN_DETECTED; + + // Now a fetch should set the suggestion to null. + await QuickSuggest.weather._test_fetch(); + Assert.ok(!QuickSuggest.weather.suggestion, "Suggestion should be null"); + + // Set `weather.ignoreVPN` and fetch again. It should complete successfully. + UrlbarPrefs.set("weather.ignoreVPN", true); + await QuickSuggest.weather._test_fetch(); + Assert.ok(QuickSuggest.weather.suggestion, "Suggestion should be fetched"); + + // Clear the pref and fetch again. It should set the suggestion back to null. + UrlbarPrefs.clear("weather.ignoreVPN"); + await QuickSuggest.weather._test_fetch(); + Assert.ok(!QuickSuggest.weather.suggestion, "Suggestion should be null"); + + // Simulate the link status changing. Since the mock link service still + // indicates a VPN is detected, the suggestion should remain null. + let fetchPromise = QuickSuggest.weather.waitForFetches(); + QuickSuggest.weather.observe(null, "network:link-status-changed", "changed"); + await fetchPromise; + Assert.ok(!QuickSuggest.weather.suggestion, "Suggestion should remain null"); + + // Modify the mock link service to indicate a VPN is no longer detected. + mockLinkService.platformDNSIndications = + Ci.nsINetworkLinkService.NONE_DETECTED; + + // Simulate the link status changing again. The suggestion should be fetched. + fetchPromise = QuickSuggest.weather.waitForFetches(); + QuickSuggest.weather.observe(null, "network:link-status-changed", "changed"); + await fetchPromise; + Assert.ok(QuickSuggest.weather.suggestion, "Suggestion should be fetched"); + + MockRegistrar.unregister(networkLinkServiceCID); + delete QuickSuggest.weather._test_linkService; +}); + +// When a Nimbus experiment is installed, it should override the remote settings +// config. +add_task(async function nimbusOverride() { + // Sanity check initial state. + assertEnabled({ + message: "Sanity check initial state", + hasSuggestion: true, + pendingFetchCount: 0, + }); + + // Verify a search works as expected with the default remote settings config. + await check_results({ + context: createContext(MerinoTestUtils.WEATHER_KEYWORD, { + providers: [UrlbarProviderWeather.name], + isPrivate: false, + }), + matches: [makeExpectedResult()], + }); + + // Install an experiment with a different keyword and min length. + let nimbusCleanup = await UrlbarTestUtils.initNimbusFeature({ + weatherKeywords: ["nimbusoverride"], + weatherKeywordsMinimumLength: "nimbus".length, + }); + + // The usual default keyword shouldn't match. + await check_results({ + context: createContext(MerinoTestUtils.WEATHER_KEYWORD, { + providers: [UrlbarProviderWeather.name], + isPrivate: false, + }), + matches: [], + }); + + // The new keyword from Nimbus should match. + await check_results({ + context: createContext("nimbusoverride", { + providers: [UrlbarProviderWeather.name], + isPrivate: false, + }), + matches: [makeExpectedResult()], + }); + await check_results({ + context: createContext("nimbus", { + providers: [UrlbarProviderWeather.name], + isPrivate: false, + }), + matches: [makeExpectedResult()], + }); + + // Uninstall the experiment. + await nimbusCleanup(); + + // The usual default keyword should match again. + await check_results({ + context: createContext(MerinoTestUtils.WEATHER_KEYWORD, { + providers: [UrlbarProviderWeather.name], + isPrivate: false, + }), + matches: [makeExpectedResult()], + }); + + // The keywords from Nimbus shouldn't match anymore. + await check_results({ + context: createContext("nimbusoverride", { + providers: [UrlbarProviderWeather.name], + isPrivate: false, + }), + matches: [], + }); + await check_results({ + context: createContext("nimbus", { + providers: [UrlbarProviderWeather.name], + isPrivate: false, + }), + matches: [], + }); +}); + +function assertEnabled({ message, hasSuggestion, pendingFetchCount }) { + info("Asserting feature is enabled"); + if (message) { + info(message); + } + + Assert.equal( + !!QuickSuggest.weather.suggestion, + hasSuggestion, + "Suggestion is null or non-null as expected" + ); + Assert.notEqual( + QuickSuggest.weather._test_fetchTimer, + 0, + "Fetch timer is non-zero" + ); + Assert.ok(QuickSuggest.weather._test_merino, "Merino client is non-null"); + Assert.equal( + QuickSuggest.weather._test_pendingFetchCount, + pendingFetchCount, + "Expected pending fetch count" + ); +} + +function assertDisabled({ message, pendingFetchCount }) { + info("Asserting feature is disabled"); + if (message) { + info(message); + } + + Assert.strictEqual( + QuickSuggest.weather.suggestion, + null, + "Suggestion is null" + ); + Assert.strictEqual( + QuickSuggest.weather._test_fetchTimer, + 0, + "Fetch timer is zero" + ); + Assert.strictEqual( + QuickSuggest.weather._test_merino, + null, + "Merino client is null" + ); + Assert.equal( + QuickSuggest.weather._test_pendingFetchCount, + pendingFetchCount, + "Expected pending fetch count" + ); +} + +function makeExpectedResult({ + suggestedIndex = 1, + temperatureUnit = undefined, +} = {}) { + if (!temperatureUnit) { + temperatureUnit = + Services.locale.regionalPrefsLocales[0] == "en-US" ? "f" : "c"; + } + + return { + suggestedIndex, + type: UrlbarUtils.RESULT_TYPE.DYNAMIC, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + heuristic: false, + payload: { + temperatureUnit, + url: WEATHER_SUGGESTION.url, + iconId: "6", + helpUrl: QuickSuggest.HELP_URL, + helpL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-learn-more-about-firefox-suggest" + : "firefox-suggest-urlbar-learn-more", + }, + isBlockable: true, + blockL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-dismiss-firefox-suggest" + : "firefox-suggest-urlbar-block", + }, + requestId: MerinoTestUtils.server.response.body.request_id, + source: "merino", + merinoProvider: "accuweather", + dynamicType: "weather", + city: WEATHER_SUGGESTION.city_name, + temperature: + WEATHER_SUGGESTION.current_conditions.temperature[temperatureUnit], + currentConditions: WEATHER_SUGGESTION.current_conditions.summary, + forecast: WEATHER_SUGGESTION.forecast.summary, + high: WEATHER_SUGGESTION.forecast.high[temperatureUnit], + low: WEATHER_SUGGESTION.forecast.low[temperatureUnit], + shouldNavigate: true, + }, + }; +} diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_weather_keywords.js b/browser/components/urlbar/tests/quicksuggest/unit/test_weather_keywords.js new file mode 100644 index 0000000000..b62a243fb0 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_weather_keywords.js @@ -0,0 +1,1395 @@ +/* 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 the keywords and zero-prefix behavior of quick suggest weather. + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarProviderWeather: "resource:///modules/UrlbarProviderWeather.sys.mjs", +}); + +const { WEATHER_RS_DATA, WEATHER_SUGGESTION } = MerinoTestUtils; + +add_task(async function init() { + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + remoteSettingsResults: [ + { + type: "weather", + weather: WEATHER_RS_DATA, + }, + ], + }); + UrlbarPrefs.set("quicksuggest.enabled", true); + await MerinoTestUtils.initWeather(); +}); + +// * Settings data: none +// * Nimbus values: none +// * Min keyword length pref: none +// * Expected: no suggestion +add_task(async function () { + await doKeywordsTest({ + desc: "No data", + tests: { + "": false, + w: false, + we: false, + wea: false, + weat: false, + weath: false, + weathe: false, + weather: false, + f: false, + fo: false, + for: false, + fore: false, + forec: false, + foreca: false, + forecas: false, + forecast: false, + }, + }); +}); + +// * Settings data: empty +// * Nimbus values: none +// * Min keyword length pref: none +// * Expected: no suggestion +add_task(async function () { + await doKeywordsTest({ + desc: "Empty settings", + settingsData: {}, + tests: { + "": false, + w: false, + we: false, + wea: false, + weat: false, + weath: false, + weathe: false, + weather: false, + f: false, + fo: false, + for: false, + fore: false, + forec: false, + foreca: false, + forecas: false, + forecast: false, + }, + }); +}); + +// * Settings data: keywords only +// * Nimbus values: none +// * Min keyword length pref: none +// * Expected: full keywords only +add_task(async function () { + await doKeywordsTest({ + desc: "Settings only, keywords only", + settingsData: { + keywords: ["weather", "forecast"], + }, + tests: { + "": false, + w: false, + we: false, + wea: false, + weat: false, + weath: false, + weathe: false, + weather: true, + f: false, + fo: false, + for: false, + fore: false, + forec: false, + foreca: false, + forecas: false, + forecast: true, + }, + }); +}); + +// * Settings data: keywords and min keyword length = 0 +// * Nimbus values: none +// * Min keyword length pref: none +// * Expected: full keywords only +add_task(async function () { + await doKeywordsTest({ + desc: "Settings only, min keyword length = 0", + settingsData: { + keywords: ["weather", "forecast"], + min_keyword_length: 0, + }, + tests: { + "": false, + w: false, + we: false, + wea: false, + weat: false, + weath: false, + weathe: false, + weather: true, + f: false, + fo: false, + for: false, + fore: false, + forec: false, + foreca: false, + forecas: false, + forecast: true, + }, + }); +}); + +// * Settings data: keywords and min keyword length > 0 +// * Nimbus values: none +// * Min keyword length pref: none +// * Expected: use settings data +add_task(async function () { + await doKeywordsTest({ + desc: "Settings only, min keyword length > 0", + settingsData: { + keywords: ["weather", "forecast"], + min_keyword_length: 3, + }, + tests: { + "": false, + w: false, + we: false, + wea: true, + weat: true, + weath: true, + weathe: true, + weather: true, + f: false, + fo: false, + for: true, + fore: true, + forec: true, + foreca: true, + forecas: true, + forecast: true, + }, + }); +}); + +// * Settings data: keywords and min keyword length = 0 +// * Nimbus values: none +// * Min keyword length pref: 6 +// * Expected: use settings keywords and min keyword length pref +add_task(async function () { + await doKeywordsTest({ + desc: "Settings only, min keyword length = 0, pref exists", + settingsData: { + keywords: ["weather", "forecast"], + min_keyword_length: 0, + }, + minKeywordLength: 6, + tests: { + "": false, + w: false, + we: false, + wea: false, + weat: false, + weath: false, + weathe: true, + weather: true, + f: false, + fo: false, + for: false, + fore: false, + forec: false, + foreca: true, + forecas: true, + forecast: true, + }, + }); +}); + +// * Settings data: keywords and min keyword length > 0 +// * Nimbus values: none +// * Min keyword length pref: 6 +// * Expected: use settings keywords and min keyword length pref +add_task(async function () { + await doKeywordsTest({ + desc: "Settings only, min keyword length > 0, pref exists", + settingsData: { + keywords: ["weather", "forecast"], + min_keyword_length: 3, + }, + minKeywordLength: 6, + tests: { + "": false, + w: false, + we: false, + wea: false, + weat: false, + weath: false, + weathe: true, + weather: true, + f: false, + fo: false, + for: false, + fore: false, + forec: false, + foreca: true, + forecas: true, + forecast: true, + }, + }); +}); + +// * Settings data: empty +// * Nimbus values: empty +// * Min keyword length pref: none +// * Expected: no suggestion +add_task(async function () { + await doKeywordsTest({ + desc: "Settings: empty; Nimbus: empty", + settingsData: {}, + nimbusValues: {}, + tests: { + "": false, + w: false, + we: false, + wea: false, + weat: false, + weath: false, + weathe: false, + weather: false, + f: false, + fo: false, + for: false, + fore: false, + forec: false, + foreca: false, + forecas: false, + forecast: false, + }, + }); +}); + +// * Settings data: keywords only +// * Nimbus values: keywords only +// * Min keyword length pref: none +// * Expected: full keywords in Nimbus +add_task(async function () { + await doKeywordsTest({ + desc: "Settings: keywords; Nimbus: keywords", + settingsData: { + keywords: ["weather"], + }, + nimbusValues: { + weatherKeywords: ["forecast"], + }, + tests: { + "": false, + w: false, + we: false, + wea: false, + weat: false, + weath: false, + weathe: false, + weather: false, + f: false, + fo: false, + for: false, + fore: false, + forec: false, + foreca: false, + forecas: false, + forecast: true, + }, + }); +}); + +// * Settings data: keywords and min keyword length = 0 +// * Nimbus values: keywords only +// * Min keyword length pref: none +// * Expected: full keywords in Nimbus +add_task(async function () { + await doKeywordsTest({ + desc: "Settings: keywords, min keyword length = 0; Nimbus: keywords", + settingsData: { + keywords: ["weather"], + min_keyword_length: 0, + }, + nimbusValues: { + weatherKeywords: ["forecast"], + }, + tests: { + "": false, + w: false, + we: false, + wea: false, + weat: false, + weath: false, + weathe: false, + weather: false, + f: false, + fo: false, + for: false, + fore: false, + forec: false, + foreca: false, + forecas: false, + forecast: true, + }, + }); +}); + +// * Settings data: keywords and min keyword length > 0 +// * Nimbus values: keywords only +// * Min keyword length pref: none +// * Expected: Nimbus keywords with settings min keyword length +add_task(async function () { + await doKeywordsTest({ + desc: "Settings: keywords, min keyword length > 0; Nimbus: keywords", + settingsData: { + keywords: ["weather"], + min_keyword_length: 3, + }, + nimbusValues: { + weatherKeywords: ["forecast"], + }, + tests: { + "": false, + w: false, + we: false, + wea: false, + weat: false, + weath: false, + weathe: false, + weather: false, + f: false, + fo: false, + for: true, + fore: true, + forec: true, + foreca: true, + forecas: true, + forecast: true, + }, + }); +}); + +// * Settings data: keywords and min keyword length > 0 +// * Nimbus values: keywords and min keyword length = 0 +// * Min keyword length pref: none +// * Expected: Nimbus keywords with settings min keyword length +add_task(async function () { + await doKeywordsTest({ + desc: "Settings: keywords, min keyword length > 0; Nimbus: keywords, min keyword length = 0", + settingsData: { + keywords: ["weather"], + min_keyword_length: 3, + }, + nimbusValues: { + weatherKeywords: ["forecast"], + weatherKeywordsMinimumLength: 0, + }, + tests: { + "": false, + w: false, + we: false, + wea: false, + weat: false, + weath: false, + weathe: false, + weather: false, + f: false, + fo: false, + for: true, + fore: true, + forec: true, + foreca: true, + forecas: true, + forecast: true, + }, + }); +}); + +// * Settings data: keywords and min keyword length > 0 +// * Nimbus values: keywords and min keyword length > 0 +// * Min keyword length pref: none +// * Expected: use Nimbus values +add_task(async function () { + await doKeywordsTest({ + desc: "Settings: keywords, min keyword length > 0; Nimbus: keywords, min keyword length > 0", + settingsData: { + keywords: ["weather"], + min_keyword_length: 3, + }, + nimbusValues: { + weatherKeywords: ["forecast"], + weatherKeywordsMinimumLength: 4, + }, + tests: { + "": false, + w: false, + we: false, + wea: false, + weat: false, + weath: false, + weathe: false, + weather: false, + f: false, + fo: false, + for: false, + fore: true, + forec: true, + foreca: true, + forecas: true, + forecast: true, + }, + }); +}); + +// * Settings data: keywords and min keyword length > 0 +// * Nimbus values: keywords and min keyword length = 0 +// * Min keyword length pref: exists +// * Expected: Nimbus keywords with min keyword length pref +add_task(async function () { + await doKeywordsTest({ + desc: "Settings: keywords, min keyword length > 0; Nimbus: keywords, min keyword length = 0; pref exists", + settingsData: { + keywords: ["weather"], + min_keyword_length: 3, + }, + nimbusValues: { + weatherKeywords: ["forecast"], + weatherKeywordsMinimumLength: 0, + }, + minKeywordLength: 6, + tests: { + "": false, + w: false, + we: false, + wea: false, + weat: false, + weath: false, + weathe: false, + weather: false, + f: false, + fo: false, + for: false, + fore: false, + forec: false, + foreca: true, + forecas: true, + forecast: true, + }, + }); +}); + +// * Settings data: keywords and min keyword length > 0 +// * Nimbus values: keywords and min keyword length > 0 +// * Min keyword length pref: exists +// * Expected: Nimbus keywords with min keyword length pref +add_task(async function () { + await doKeywordsTest({ + desc: "Settings: keywords, min keyword length > 0; Nimbus: keywords, min keyword length > 0; pref exists", + settingsData: { + keywords: ["weather", "forecast"], + min_keyword_length: 3, + }, + nimbusValues: { + weatherKeywords: ["forecast"], + weatherKeywordsMinimumLength: 4, + }, + minKeywordLength: 6, + tests: { + "": false, + w: false, + we: false, + wea: false, + weat: false, + weath: false, + weathe: false, + weather: false, + f: false, + fo: false, + for: false, + fore: false, + forec: false, + foreca: true, + forecas: true, + forecast: true, + }, + }); +}); + +// * Settings data: none +// * Nimbus values: keywords only +// * Min keyword length pref: none +// * Expected: full keywords +add_task(async function () { + await doKeywordsTest({ + desc: "Settings: none; Nimbus: keywords", + nimbusValues: { + weatherKeywords: ["weather", "forecast"], + }, + tests: { + "": false, + w: false, + we: false, + wea: false, + weat: false, + weath: false, + weathe: false, + weather: true, + f: false, + fo: false, + for: false, + fore: false, + forec: false, + foreca: false, + forecas: false, + forecast: true, + }, + }); +}); + +// * Settings data: none +// * Nimbus values: keywords and min keyword length = 0 +// * Min keyword length pref: none +// * Expected: full keywords +add_task(async function () { + await doKeywordsTest({ + desc: "Settings: none; Nimbus: keywords, min keyword length = 0", + nimbusValues: { + weatherKeywords: ["weather", "forecast"], + weatherKeywordsMinimumLength: 0, + }, + tests: { + "": false, + w: false, + we: false, + wea: false, + weat: false, + weath: false, + weathe: false, + weather: true, + f: false, + fo: false, + for: false, + fore: false, + forec: false, + foreca: false, + forecas: false, + forecast: true, + }, + }); +}); + +// * Settings data: none +// * Nimbus values: keywords and min keyword length > 0 +// * Min keyword length pref: none +// * Expected: use Nimbus values +add_task(async function () { + await doKeywordsTest({ + desc: "Settings: none; Nimbus: keywords, min keyword length > 0", + nimbusValues: { + weatherKeywords: ["weather", "forecast"], + weatherKeywordsMinimumLength: 3, + }, + tests: { + "": false, + w: false, + we: false, + wea: true, + weat: true, + weath: true, + weathe: true, + weather: true, + f: false, + fo: false, + for: true, + fore: true, + forec: true, + foreca: true, + forecas: true, + forecast: true, + }, + }); +}); + +// * Settings data: none +// * Nimbus values: keywords and min keyword length > 0 +// * Min keyword length pref: exists +// * Expected: use Nimbus keywords and min keyword length pref +add_task(async function () { + await doKeywordsTest({ + desc: "Settings: none; Nimbus: keywords, min keyword length > 0; pref exists", + nimbusValues: { + weatherKeywords: ["weather", "forecast"], + weatherKeywordsMinimumLength: 3, + }, + minKeywordLength: 6, + tests: { + "": false, + w: false, + we: false, + wea: false, + weat: false, + weath: false, + weathe: true, + weather: true, + f: false, + fo: false, + for: false, + fore: false, + forec: false, + foreca: true, + forecas: true, + forecast: true, + }, + }); +}); + +// When `weatherKeywords` is non-null and `weatherKeywordsMinimumLength` is +// larger than the length of all keywords, the suggestion should not be +// triggered. +add_task(async function minLength_large() { + await doKeywordsTest({ + desc: "Large min length", + nimbusValues: { + weatherKeywords: ["weather", "forecast"], + weatherKeywordsMinimumLength: 999, + }, + tests: { + "": false, + w: false, + we: false, + wea: false, + weat: false, + weath: false, + weathe: false, + weather: false, + f: false, + fo: false, + for: false, + fore: false, + forec: false, + foreca: false, + forecas: false, + forecast: false, + }, + }); +}); + +// Leading and trailing spaces should be ignored. +add_task(async function leadingAndTrailingSpaces() { + await doKeywordsTest({ + nimbusValues: { + weatherKeywords: ["weather"], + weatherKeywordsMinimumLength: 3, + }, + tests: { + " wea": true, + " wea": true, + "wea ": true, + "wea ": true, + " wea ": true, + " weat": true, + " weat": true, + "weat ": true, + "weat ": true, + " weat ": true, + }, + }); +}); + +add_task(async function caseInsensitive() { + await doKeywordsTest({ + desc: "Case insensitive", + settingsData: { + keywords: ["weather"], + min_keyword_length: 3, + }, + tests: { + wea: true, + WEA: true, + Wea: true, + WeA: true, + WEATHER: true, + Weather: true, + WeAtHeR: true, + }, + }); +}); + +async function doKeywordsTest({ + desc, + tests, + nimbusValues = null, + settingsData = null, + minKeywordLength = undefined, +}) { + info("Doing keywords test: " + desc); + info(JSON.stringify({ nimbusValues, settingsData, minKeywordLength })); + + // If a suggestion hasn't already been fetched and the data contains keywords, + // a fetch will start. Wait for it to finish below. + let fetchPromise; + if ( + !QuickSuggest.weather.suggestion && + (nimbusValues?.weatherKeywords || settingsData?.keywords) + ) { + fetchPromise = QuickSuggest.weather.waitForFetches(); + } + + let nimbusCleanup; + if (nimbusValues) { + nimbusCleanup = await UrlbarTestUtils.initNimbusFeature(nimbusValues); + } + + await QuickSuggestTestUtils.setRemoteSettingsResults([ + { + type: "weather", + weather: settingsData, + }, + ]); + + if (minKeywordLength) { + UrlbarPrefs.set("weather.minKeywordLength", minKeywordLength); + } + + if (fetchPromise) { + info("Waiting for fetch"); + assertFetchingStarted({ pendingFetchCount: 1 }); + await fetchPromise; + info("Got fetch"); + } + + for (let [searchString, expected] of Object.entries(tests)) { + info( + "Doing search: " + + JSON.stringify({ + searchString, + expected, + }) + ); + + let suggestedIndex = searchString ? 1 : 0; + await check_results({ + context: createContext(searchString, { + providers: [UrlbarProviderWeather.name], + isPrivate: false, + }), + matches: expected ? [makeExpectedResult({ suggestedIndex })] : [], + }); + } + + await nimbusCleanup?.(); + + fetchPromise = null; + if (!QuickSuggest.weather.suggestion) { + fetchPromise = QuickSuggest.weather.waitForFetches(); + } + await QuickSuggestTestUtils.setRemoteSettingsResults([ + { + type: "weather", + weather: MerinoTestUtils.WEATHER_RS_DATA, + }, + ]); + + UrlbarPrefs.clear("weather.minKeywordLength"); + await fetchPromise; +} + +// When a sponsored quick suggest result matches the same keyword as the weather +// result, the weather result should be shown and the quick suggest result +// should not be shown. +add_task(async function matchingQuickSuggest_sponsored() { + await doMatchingQuickSuggestTest("suggest.quicksuggest.sponsored", true); +}); + +// When a non-sponsored quick suggest result matches the same keyword as the +// weather result, the weather result should be shown and the quick suggest +// result should not be shown. +add_task(async function matchingQuickSuggest_nonsponsored() { + await doMatchingQuickSuggestTest("suggest.quicksuggest.nonsponsored", false); +}); + +async function doMatchingQuickSuggestTest(pref, isSponsored) { + let keyword = "test"; + let iab_category = isSponsored ? "22 - Shopping" : "5 - Education"; + + // Add a remote settings result to quick suggest. + UrlbarPrefs.set(pref, true); + await QuickSuggestTestUtils.setRemoteSettingsResults([ + { + type: "data", + attachment: [ + { + id: 1, + url: "http://example.com/", + title: "Suggestion", + keywords: [keyword], + click_url: "http://example.com/click", + impression_url: "http://example.com/impression", + advertiser: "TestAdvertiser", + iab_category, + }, + ], + }, + { + type: "weather", + weather: MerinoTestUtils.WEATHER_RS_DATA, + }, + ]); + + // First do a search to verify the quick suggest result matches the keyword. + info("Doing first search for quick suggest result"); + await check_results({ + context: createContext(keyword, { + providers: [UrlbarProviderQuickSuggest.name, UrlbarProviderWeather.name], + isPrivate: false, + }), + matches: [ + { + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + heuristic: false, + payload: { + telemetryType: isSponsored ? "adm_sponsored" : "adm_nonsponsored", + qsSuggestion: keyword, + title: "Suggestion", + url: "http://example.com/", + displayUrl: "http://example.com", + originalUrl: "http://example.com/", + icon: null, + sponsoredImpressionUrl: "http://example.com/impression", + sponsoredClickUrl: "http://example.com/click", + sponsoredBlockId: 1, + sponsoredAdvertiser: "TestAdvertiser", + sponsoredIabCategory: iab_category, + isSponsored, + helpUrl: QuickSuggest.HELP_URL, + helpL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-learn-more-about-firefox-suggest" + : "firefox-suggest-urlbar-learn-more", + }, + isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"), + blockL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-dismiss-firefox-suggest" + : "firefox-suggest-urlbar-block", + }, + source: "remote-settings", + }, + }, + ], + }); + + // Set up the keyword for the weather suggestion and do a second search to + // verify only the weather result matches. + info("Doing second search for weather suggestion"); + let cleanup = await UrlbarTestUtils.initNimbusFeature({ + weatherKeywords: [keyword], + weatherKeywordsMinimumLength: 1, + }); + await check_results({ + context: createContext(keyword, { + providers: [UrlbarProviderQuickSuggest.name, UrlbarProviderWeather.name], + isPrivate: false, + }), + matches: [makeExpectedResult({ suggestedIndex: 1 })], + }); + await cleanup(); + + UrlbarPrefs.clear(pref); +} + +add_task(async function () { + await doIncrementTest({ + desc: "Settings only", + setup: { + settingsData: { + keywords: ["forecast", "wind"], + min_keyword_length: 3, + }, + }, + tests: [ + { + minKeywordLength: 3, + canIncrement: true, + searches: { + fo: false, + for: true, + fore: true, + forec: true, + wi: false, + win: true, + wind: true, + }, + }, + { + minKeywordLength: 4, + canIncrement: true, + searches: { + fo: false, + for: false, + fore: true, + forec: true, + wi: false, + win: false, + wind: true, + }, + }, + { + minKeywordLength: 5, + canIncrement: true, + searches: { + fo: false, + for: false, + fore: false, + forec: true, + wi: false, + win: false, + wind: false, + }, + }, + ], + }); +}); + +add_task(async function () { + await doIncrementTest({ + desc: "Settings only with cap", + setup: { + settingsData: { + keywords: ["forecast", "wind"], + min_keyword_length: 3, + min_keyword_length_cap: 6, + }, + }, + tests: [ + { + minKeywordLength: 3, + canIncrement: true, + searches: { + fo: false, + for: true, + fore: true, + forec: true, + foreca: true, + forecas: true, + wi: false, + win: true, + wind: true, + }, + }, + { + minKeywordLength: 4, + canIncrement: true, + searches: { + fo: false, + for: false, + fore: true, + forec: true, + foreca: true, + forecas: true, + wi: false, + win: false, + wind: true, + }, + }, + { + minKeywordLength: 5, + canIncrement: true, + searches: { + fo: false, + for: false, + fore: false, + forec: true, + foreca: true, + forecas: true, + wi: false, + win: false, + wind: false, + windy: false, + }, + }, + { + minKeywordLength: 6, + canIncrement: false, + searches: { + fo: false, + for: false, + fore: false, + forec: false, + foreca: true, + forecas: true, + wi: false, + win: false, + wind: false, + windy: false, + }, + }, + { + minKeywordLength: 6, + canIncrement: false, + searches: { + fo: false, + for: false, + fore: false, + forec: false, + foreca: true, + forecas: true, + wi: false, + win: false, + wind: false, + windy: false, + }, + }, + ], + }); +}); + +add_task(async function () { + await doIncrementTest({ + desc: "Settings and Nimbus", + setup: { + settingsData: { + keywords: ["weather"], + min_keyword_length: 5, + }, + nimbusValues: { + weatherKeywords: ["forecast", "wind"], + weatherKeywordsMinimumLength: 3, + }, + }, + tests: [ + { + minKeywordLength: 3, + canIncrement: true, + searches: { + we: false, + wea: false, + weat: false, + weath: false, + fo: false, + for: true, + fore: true, + forec: true, + wi: false, + win: true, + wind: true, + }, + }, + { + minKeywordLength: 4, + canIncrement: true, + searches: { + we: false, + wea: false, + weat: false, + weath: false, + fo: false, + for: false, + fore: true, + forec: true, + wi: false, + win: false, + wind: true, + }, + }, + { + minKeywordLength: 5, + canIncrement: true, + searches: { + we: false, + wea: false, + weat: false, + weath: false, + fo: false, + for: false, + fore: false, + forec: true, + wi: false, + win: false, + wind: false, + windy: false, + }, + }, + ], + }); +}); + +add_task(async function () { + await doIncrementTest({ + desc: "Settings and Nimbus with cap in Nimbus", + setup: { + settingsData: { + keywords: ["weather"], + min_keyword_length: 5, + }, + nimbusValues: { + weatherKeywords: ["forecast", "wind"], + weatherKeywordsMinimumLength: 3, + weatherKeywordsMinimumLengthCap: 6, + }, + }, + tests: [ + { + minKeywordLength: 3, + canIncrement: true, + searches: { + we: false, + wea: false, + weat: false, + weath: false, + fo: false, + for: true, + fore: true, + forec: true, + foreca: true, + forecas: true, + wi: false, + win: true, + wind: true, + }, + }, + { + minKeywordLength: 4, + canIncrement: true, + searches: { + we: false, + wea: false, + weat: false, + weath: false, + fo: false, + for: false, + fore: true, + forec: true, + foreca: true, + forecas: true, + wi: false, + win: false, + wind: true, + }, + }, + { + minKeywordLength: 5, + canIncrement: true, + searches: { + we: false, + wea: false, + weat: false, + weath: false, + fo: false, + for: false, + fore: false, + forec: true, + foreca: true, + forecas: true, + wi: false, + win: false, + wind: false, + windy: false, + }, + }, + { + minKeywordLength: 6, + canIncrement: false, + searches: { + we: false, + wea: false, + weat: false, + weath: false, + fo: false, + for: false, + fore: false, + forec: false, + foreca: true, + forecas: true, + wi: false, + win: false, + wind: false, + windy: false, + }, + }, + { + minKeywordLength: 6, + canIncrement: false, + searches: { + we: false, + wea: false, + weat: false, + weath: false, + fo: false, + for: false, + fore: false, + forec: false, + foreca: true, + forecas: true, + wi: false, + win: false, + wind: false, + windy: false, + }, + }, + ], + }); +}); + +async function doIncrementTest({ desc, setup, tests }) { + info("Doing increment test: " + desc); + info(JSON.stringify({ setup })); + + let { nimbusValues, settingsData } = setup; + + let fetchPromise; + if ( + !QuickSuggest.weather.suggestion && + (nimbusValues?.weatherKeywords || settingsData?.keywords) + ) { + fetchPromise = QuickSuggest.weather.waitForFetches(); + } + + let nimbusCleanup; + if (nimbusValues) { + nimbusCleanup = await UrlbarTestUtils.initNimbusFeature(nimbusValues); + } + + await QuickSuggestTestUtils.setRemoteSettingsResults([ + { + type: "weather", + weather: settingsData, + }, + ]); + + if (fetchPromise) { + info("Waiting for fetch"); + assertFetchingStarted({ pendingFetchCount: 1 }); + await fetchPromise; + info("Got fetch"); + } + + for (let { minKeywordLength, canIncrement, searches } of tests) { + info( + "Doing increment test case: " + + JSON.stringify({ + minKeywordLength, + canIncrement, + }) + ); + + Assert.equal( + QuickSuggest.weather.minKeywordLength, + minKeywordLength, + "minKeywordLength should be correct" + ); + Assert.equal( + QuickSuggest.weather.canIncrementMinKeywordLength, + canIncrement, + "canIncrement should be correct" + ); + + for (let [searchString, expected] of Object.entries(searches)) { + Assert.equal( + QuickSuggest.weather.keywords.has(searchString), + expected, + "Keyword should be present/absent as expected: " + searchString + ); + + await check_results({ + context: createContext(searchString, { + providers: [UrlbarProviderWeather.name], + isPrivate: false, + }), + matches: expected ? [makeExpectedResult({ suggestedIndex: 1 })] : [], + }); + } + + QuickSuggest.weather.incrementMinKeywordLength(); + info( + "Incremented min keyword length, new value is: " + + QuickSuggest.weather.minKeywordLength + ); + } + + await nimbusCleanup?.(); + + fetchPromise = null; + if (!QuickSuggest.weather.suggestion) { + fetchPromise = QuickSuggest.weather.waitForFetches(); + } + await QuickSuggestTestUtils.setRemoteSettingsResults([ + { + type: "weather", + weather: MerinoTestUtils.WEATHER_RS_DATA, + }, + ]); + UrlbarPrefs.clear("weather.minKeywordLength"); + await fetchPromise; +} + +function makeExpectedResult({ + suggestedIndex = 0, + temperatureUnit = undefined, +} = {}) { + if (!temperatureUnit) { + temperatureUnit = + Services.locale.regionalPrefsLocales[0] == "en-US" ? "f" : "c"; + } + + return { + suggestedIndex, + type: UrlbarUtils.RESULT_TYPE.DYNAMIC, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + heuristic: false, + payload: { + temperatureUnit, + url: WEATHER_SUGGESTION.url, + iconId: "6", + helpUrl: QuickSuggest.HELP_URL, + helpL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-learn-more-about-firefox-suggest" + : "firefox-suggest-urlbar-learn-more", + }, + isBlockable: true, + blockL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-dismiss-firefox-suggest" + : "firefox-suggest-urlbar-block", + }, + requestId: MerinoTestUtils.server.response.body.request_id, + source: "merino", + merinoProvider: "accuweather", + dynamicType: "weather", + city: WEATHER_SUGGESTION.city_name, + temperature: + WEATHER_SUGGESTION.current_conditions.temperature[temperatureUnit], + currentConditions: WEATHER_SUGGESTION.current_conditions.summary, + forecast: WEATHER_SUGGESTION.forecast.summary, + high: WEATHER_SUGGESTION.forecast.high[temperatureUnit], + low: WEATHER_SUGGESTION.forecast.low[temperatureUnit], + shouldNavigate: true, + }, + }; +} + +function assertFetchingStarted() { + info("Asserting fetching has started"); + + Assert.notEqual( + QuickSuggest.weather._test_fetchTimer, + 0, + "Fetch timer is non-zero" + ); + Assert.ok(QuickSuggest.weather._test_merino, "Merino client is non-null"); + Assert.equal( + QuickSuggest.weather._test_pendingFetchCount, + 1, + "Expected pending fetch count" + ); +} diff --git a/browser/components/urlbar/tests/quicksuggest/unit/xpcshell.ini b/browser/components/urlbar/tests/quicksuggest/unit/xpcshell.ini new file mode 100644 index 0000000000..45a17687ae --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/xpcshell.ini @@ -0,0 +1,23 @@ +[DEFAULT] +skip-if = toolkit == 'android' # bug 1730213 +head = ../../unit/head.js head.js +firefox-appdir = browser + +[test_merinoClient.js] +[test_merinoClient_sessions.js] +[test_quicksuggest.js] +[test_quicksuggest_addons.js] +[test_quicksuggest_bestMatch.js] +[test_quicksuggest_dynamicWikipedia.js] +[test_quicksuggest_impressionCaps.js] +[test_quicksuggest_merino.js] +[test_quicksuggest_merinoSessions.js] +[test_quicksuggest_migrate_v1.js] +[test_quicksuggest_migrate_v2.js] +[test_quicksuggest_nonUniqueKeywords.js] +[test_quicksuggest_offlineDefault.js] +[test_quicksuggest_positionInSuggestions.js] +[test_quicksuggest_topPicks.js] +[test_suggestionsMap.js] +[test_weather.js] +[test_weather_keywords.js] diff --git a/browser/components/urlbar/tests/unit/data/engine.xml b/browser/components/urlbar/tests/unit/data/engine.xml new file mode 100644 index 0000000000..61d776655f --- /dev/null +++ b/browser/components/urlbar/tests/unit/data/engine.xml @@ -0,0 +1,10 @@ + + +engine.xml +A test search engine +UTF-8 + + + +http://www.example.com/ + diff --git a/browser/components/urlbar/tests/unit/head.js b/browser/components/urlbar/tests/unit/head.js new file mode 100644 index 0000000000..8f15b0280e --- /dev/null +++ b/browser/components/urlbar/tests/unit/head.js @@ -0,0 +1,1127 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +var { UrlbarMuxer, UrlbarProvider, UrlbarQueryContext, UrlbarUtils } = + ChromeUtils.importESModule("resource:///modules/UrlbarUtils.sys.mjs"); + +ChromeUtils.defineESModuleGetters(this, { + AddonTestUtils: "resource://testing-common/AddonTestUtils.sys.mjs", + PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs", + PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", + PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs", + SearchTestUtils: "resource://testing-common/SearchTestUtils.sys.mjs", + TestUtils: "resource://testing-common/TestUtils.sys.mjs", + UrlbarController: "resource:///modules/UrlbarController.sys.mjs", + UrlbarInput: "resource:///modules/UrlbarInput.sys.mjs", + UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs", + UrlbarProviderOpenTabs: "resource:///modules/UrlbarProviderOpenTabs.sys.mjs", + UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs", + UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs", + UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.sys.mjs", + sinon: "resource://testing-common/Sinon.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(this, { + HttpServer: "resource://testing-common/httpd.js", +}); + +XPCOMUtils.defineLazyGetter(this, "QuickSuggestTestUtils", () => { + const { QuickSuggestTestUtils: module } = ChromeUtils.importESModule( + "resource://testing-common/QuickSuggestTestUtils.sys.mjs" + ); + module.init(this); + return module; +}); + +XPCOMUtils.defineLazyGetter(this, "MerinoTestUtils", () => { + const { MerinoTestUtils: module } = ChromeUtils.importESModule( + "resource://testing-common/MerinoTestUtils.sys.mjs" + ); + module.init(this); + return module; +}); + +ChromeUtils.defineLazyGetter(this, "UrlbarTestUtils", () => { + const { UrlbarTestUtils: module } = ChromeUtils.importESModule( + "resource://testing-common/UrlbarTestUtils.sys.mjs" + ); + module.init(this); + return module; +}); + +XPCOMUtils.defineLazyGetter(this, "PlacesFrecencyRecalculator", () => { + return Cc["@mozilla.org/places/frecency-recalculator;1"].getService( + Ci.nsIObserver + ).wrappedJSObject; +}); + +SearchTestUtils.init(this); +AddonTestUtils.init(this, false); +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "42", + "42" +); + +const SUGGESTIONS_ENGINE_NAME = "Suggestions"; +const TAIL_SUGGESTIONS_ENGINE_NAME = "Tail Suggestions"; + +/** + * 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). + * + * @returns 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 + ) + ); + context.view = { + get visibleResults() { + return context.results; + }, + controller: { + removeResult() {}, + }, + acknowledgeDismissal() {}, + }; + 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. + * @param {string} [name] Optional, use as the provider name. + * If none, a default name is chosen. + * @returns {UrlbarProvider} The provider + */ +function registerBasicTestProvider(results = [], onCancel, type, name) { + let provider = new TestProvider({ results, onCancel, type, name }); + UrlbarProvidersManager.registerProvider(provider); + registerCleanupFunction(() => + UrlbarProvidersManager.unregisterProvider(provider) + ); + return provider; +} + +// Creates an HTTP server for the test. +function makeTestServer(port = -1) { + let httpServer = new HttpServer(); + httpServer.start(port); + registerCleanupFunction(() => httpServer.stop(() => {})); + return httpServer; +} + +/** + * 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(); + server.registerPathHandler("/suggest", (req, resp) => { + let params = new URLSearchParams(req.queryString); + let searchStr = params.get("q"); + 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)); + }); + await SearchTestUtils.installSearchExtension({ + name: SUGGESTIONS_ENGINE_NAME, + search_url: `http://localhost:${server.identity.primaryPort}/search`, + suggest_url: `http://localhost:${server.identity.primaryPort}/suggest`, + suggest_url_get_params: "?q={searchTerms}", + // test_search_suggestions_aliases.js uses the search form. + search_form: `http://localhost:${server.identity.primaryPort}/search?q={searchTerms}`, + }); + let engine = Services.search.getEngineByName("Suggestions"); + 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(); + server.registerPathHandler("/suggest", (req, resp) => { + let params = new URLSearchParams(req.queryString); + let searchStr = params.get("q"); + 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); + }); + await SearchTestUtils.installSearchExtension({ + name: TAIL_SUGGESTIONS_ENGINE_NAME, + search_url: `http://localhost:${server.identity.primaryPort}/search`, + suggest_url: `http://localhost:${server.identity.primaryPort}/suggest`, + suggest_url_get_params: "?q={searchTerms}", + }); + let engine = Services.search.getEngineByName("Tail Suggestions"); + return engine; +} + +async function addOpenPages(uri, count = 1, userContextId = 0) { + for (let i = 0; i < count; i++) { + await UrlbarProviderOpenTabs.registerOpenTab( + uri.spec, + userContextId, + false + ); + } +} + +async function removeOpenPages(aUri, aCount = 1, aUserContextId = 0) { + for (let i = 0; i < aCount; i++) { + await UrlbarProviderOpenTabs.unregisterOpenTab( + aUri.spec, + aUserContextId, + false + ); + } +} + +/** + * 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, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + }); + + Services.search.setDefault( + engine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + 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(); +} + +/** + * Creates a UrlbarResult for a bookmark result. + * + * @param {UrlbarQueryContext} queryContext + * The context that this result will be displayed in. + * @param {object} options + * Options for the result. + * @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. + * @param {number} [options.source] + * Where the results should be sourced from. See {@link UrlbarUtils.RESULT_SOURCE}. + * @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 {object} options + * Options for the result. + * @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 {object} options + * Options for the result. + * @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 payload = { + title: [description, UrlbarUtils.HIGHLIGHT.TYPED], + content: [content, UrlbarUtils.HIGHLIGHT.TYPED], + keyword: [keyword, UrlbarUtils.HIGHLIGHT.TYPED], + icon: [UrlbarUtils.ICON.EXTENSION], + }; + if (!heuristic) { + payload.blockL10n = { id: "urlbar-result-menu-dismiss-firefox-suggest" }; + } + let result = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.OMNIBOX, + UrlbarUtils.RESULT_SOURCE.ADDON, + ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, payload) + ); + result.heuristic = heuristic; + + return result; +} + +/** + * Creates a UrlbarResult for an switch-to-tab result. + * + * @param {UrlbarQueryContext} queryContext + * The context that this result will be displayed in. + * @param {object} options + * Options for the result. + * @param {string} options.uri + * The page URI. + * @param {string} [options.title] + * The page title. + * @param {string} [options.iconUri] + * A URI for the page icon. + * @returns {UrlbarResult} + */ +function makeTabSwitchResult(queryContext, { uri, title, iconUri }) { + return new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + UrlbarUtils.RESULT_SOURCE.TABS, + ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, { + url: [uri, UrlbarUtils.HIGHLIGHT.TYPED], + title: [title, UrlbarUtils.HIGHLIGHT.TYPED], + // Check against undefined so consumers can pass in the empty string. + icon: typeof iconUri != "undefined" ? iconUri : `page-icon:${uri}`, + }) + ); +} + +/** + * Creates a UrlbarResult for a keyword search result. + * + * @param {UrlbarQueryContext} queryContext + * The context that this result will be displayed in. + * @param {object} options + * Options for the result. + * @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: postData || null, + 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 {object} options + * Options for the result. + * @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 remote tab result. + * + * @param {UrlbarQueryContext} queryContext + * The context that this result will be displayed in. + * @param {object} options + * Options for the result. + * @param {string} options.uri + * The page URI. + * @param {string} options.device + * The name of the device that the remote tab comes from. + * @param {string} [options.title] + * The page title. + * @param {number} [options.lastUsed] + * The last time the remote tab was visited, in epoch seconds. Defaults + * to 0. + * @param {string} [options.iconUri] + * A URI for the page's icon. + * @returns {UrlbarResult} + */ +function makeRemoteTabResult( + queryContext, + { uri, device, title, iconUri, lastUsed = 0 } +) { + let payload = { + url: [uri, UrlbarUtils.HIGHLIGHT.TYPED], + device: [device, UrlbarUtils.HIGHLIGHT.TYPED], + // Check against undefined so consumers can pass in the empty string. + icon: + typeof iconUri != "undefined" + ? iconUri + : `moz-anno:favicon:page-icon:${uri}`, + lastUsed: lastUsed * 1000, + }; + + // Check against undefined so consumers can pass in the empty string. + if (typeof title != "undefined") { + payload.title = [title, UrlbarUtils.HIGHLIGHT.TYPED]; + } else { + payload.title = [uri, UrlbarUtils.HIGHLIGHT.TYPED]; + } + + let result = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.REMOTE_TAB, + UrlbarUtils.RESULT_SOURCE.TABS, + ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, payload) + ); + + return result; +} + +/** + * Creates a UrlbarResult for a search result. + * + * @param {UrlbarQueryContext} queryContext + * The context that this result will be displayed in. + * @param {object} options + * Options for the result. + * @param {string} [options.suggestion] + * The suggestion offered by the search engine. + * @param {string} [options.tailPrefix] + * The characters placed at the end of a Google "tail" suggestion. See + * {@link https://firefox-source-docs.mozilla.org/browser/urlbar/nontechnical-overview.html#search-suggestions} + * @param {*} [options.tail] + * The details of the URL bar tail + * @param {number} [options.tailOffsetIndex] + * The index of the first character in the tail suggestion that should be + * @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. + * @param {boolean} [options.inPrivateWindow] + * If the window to test is a private window. + * @param {boolean} [options.isPrivateEngine] + * If the engine is a private engine. + * @param {number} [options.type] + * The type of the search result. Defaults to UrlbarUtils.RESULT_TYPE.SEARCH. + * @param {number} [options.source] + * The source of the search result. Defaults to UrlbarUtils.RESULT_SOURCE.SEARCH. + * @param {boolean} [options.satisfiesAutofillThreshold] + * If this search should appear in the autofill section of the box + * @param {boolean} [options.trending] + * If the search result is a trending result. `Defaults to false`. + * @param {boolean} [options.isRichSuggestion] + * If the search result is a rich result. `Defaults to false`. + * @returns {UrlbarResult} + */ +function makeSearchResult( + queryContext, + { + suggestion, + tailPrefix, + tail, + tailOffsetIndex, + engineName, + alias, + uri, + query, + engineIconUri, + providesSearchMode, + providerName, + inPrivateWindow, + isPrivateEngine, + heuristic = false, + trending = false, + isRichSuggestion = 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); + } + payload.isGeneralPurposeEngine = false; + } + + let result = new UrlbarResult( + type, + source, + ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, payload) + ); + + if (typeof suggestion == "string") { + result.payload.lowerCaseSuggestion = + result.payload.suggestion.toLocaleLowerCase(); + result.payload.trending = trending; + result.payload.isRichSuggestion = isRichSuggestion; + } + + 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 {object} options Options for the result. + * @param {string} options.title + * The page title. + * @param {string} [options.fallbackTitle] + * The provider has capability to use the actual page title though, + * when the provider can’t get the page title, use this value as the fallback. + * @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} 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. + * @param {number} [options.source] + * The source of the result + * @returns {UrlbarResult} + */ +function makeVisitResult( + queryContext, + { + title, + fallbackTitle, + uri, + iconUri, + providerName, + tags = [], + heuristic = false, + source = UrlbarUtils.RESULT_SOURCE.HISTORY, + } +) { + let payload = { + url: [uri, UrlbarUtils.HIGHLIGHT.TYPED], + }; + + if (title) { + payload.title = [title, UrlbarUtils.HIGHLIGHT.TYPED]; + } + + if (fallbackTitle) { + payload.fallbackTitle = [fallbackTitle, 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 {object} options Options for the check. + * @param {UrlbarQueryContext} options.context + * The context for this query. + * @param {string} [options.incompleteSearch] + * A search will be fired for this string and then be immediately canceled by + * the query in `context`. + * @param {string} [options.autofilled] + * The autofilled value in the first result. + * @param {string} [options.completed] + * The value that would be filled if the autofill result was confirmed. + * Has no effect if `autofilled` is not specified. + * @param {Array} options.matches + * An array of UrlbarResults. + */ +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(); + + const controller = UrlbarTestUtils.newMockController({ + input: { + isPrivate: context.isPrivate, + onFirstResult() { + return false; + }, + getSearchSource() { + return "dummy-search-source"; + }, + 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}` + ); + Assert.equal( + !!actual.isBestMatch, + !!expected.isBestMatch, + `result.isBestMatch at result index ${i}` + ); + if (expected.providerName) { + Assert.equal( + actual.providerName, + expected.providerName, + `result.providerName at result index ${i}` + ); + } + if (expected.hasOwnProperty("suggestedIndex")) { + Assert.equal( + actual.suggestedIndex, + expected.suggestedIndex, + `result.suggestedIndex at result index ${i}` + ); + } + + if (expected.payload) { + 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_000_frecency.js b/browser/components/urlbar/tests/unit/test_000_frecency.js new file mode 100644 index 0000000000..cef110963f --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_000_frecency.js @@ -0,0 +1,245 @@ +/* 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/. */ + +/* + +Autocomplete Frecency Tests + +- add a visit for each score permutation +- search +- test number of matches +- test each item's location in results + +*/ + +testEngine_setup(); + +try { + var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].getService( + Ci.nsINavHistoryService + ); +} catch (ex) { + do_throw("Could not get services\n"); +} + +var bucketPrefs = [ + ["firstBucketCutoff", "firstBucketWeight"], + ["secondBucketCutoff", "secondBucketWeight"], + ["thirdBucketCutoff", "thirdBucketWeight"], + ["fourthBucketCutoff", "fourthBucketWeight"], + [null, "defaultBucketWeight"], +]; + +var bonusPrefs = { + embedVisitBonus: PlacesUtils.history.TRANSITION_EMBED, + framedLinkVisitBonus: PlacesUtils.history.TRANSITION_FRAMED_LINK, + linkVisitBonus: PlacesUtils.history.TRANSITION_LINK, + typedVisitBonus: PlacesUtils.history.TRANSITION_TYPED, + bookmarkVisitBonus: PlacesUtils.history.TRANSITION_BOOKMARK, + downloadVisitBonus: PlacesUtils.history.TRANSITION_DOWNLOAD, + permRedirectVisitBonus: PlacesUtils.history.TRANSITION_REDIRECT_PERMANENT, + tempRedirectVisitBonus: PlacesUtils.history.TRANSITION_REDIRECT_TEMPORARY, + reloadVisitBonus: PlacesUtils.history.TRANSITION_RELOAD, +}; + +// create test data +var searchTerm = "frecency"; +var results = []; +var now = Date.now(); +var prefPrefix = "places.frecency."; + +async function task_initializeBucket(bucket) { + let [cutoffName, weightName] = bucket; + // get pref values + let weight = Services.prefs.getIntPref(prefPrefix + weightName, 0); + let cutoff = Services.prefs.getIntPref(prefPrefix + cutoffName, 0); + if (cutoff < 1) { + return; + } + + // generate a date within the cutoff period + let dateInPeriod = (now - (cutoff - 1) * 86400 * 1000) * 1000; + + for (let [bonusName, visitType] of Object.entries(bonusPrefs)) { + let frecency = -1; + let calculatedURI = null; + let matchTitle = ""; + let bonusValue = Services.prefs.getIntPref(prefPrefix + bonusName); + // unvisited (only for first cutoff date bucket) + if ( + bonusName == "unvisitedBookmarkBonus" || + bonusName == "unvisitedTypedBonus" + ) { + if (cutoffName == "firstBucketCutoff") { + let points = Math.ceil((bonusValue / parseFloat(100.0)) * weight); + let visitCount = 1; // bonusName == "unvisitedBookmarkBonus" ? 1 : 0; + frecency = Math.ceil(visitCount * points); + calculatedURI = Services.io.newURI( + "http://" + + searchTerm + + ".com/" + + bonusName + + ":" + + bonusValue + + "/cutoff:" + + cutoff + + "/weight:" + + weight + + "/frecency:" + + frecency + ); + if (bonusName == "unvisitedBookmarkBonus") { + matchTitle = searchTerm + "UnvisitedBookmark"; + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: calculatedURI, + title: matchTitle, + }); + } else { + matchTitle = searchTerm + "UnvisitedTyped"; + await PlacesTestUtils.addVisits({ + uri: calculatedURI, + title: matchTitle, + transition: visitType, + visitDate: now, + }); + histsvc.markPageAsTyped(calculatedURI); + } + } + } else { + // visited + // visited bookmarks get the visited bookmark bonus twice + if (visitType == Ci.nsINavHistoryService.TRANSITION_BOOKMARK) { + bonusValue = bonusValue * 2; + } + + let points = Math.ceil( + (1 * ((bonusValue / parseFloat(100.0)).toFixed(6) * weight)) / 1 + ); + if (!points) { + if ( + visitType == Ci.nsINavHistoryService.TRANSITION_EMBED || + visitType == Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK || + visitType == Ci.nsINavHistoryService.TRANSITION_DOWNLOAD || + visitType == Ci.nsINavHistoryService.TRANSITION_RELOAD || + bonusName == "defaultVisitBonus" + ) { + frecency = 0; + } else { + frecency = -1; + } + } else { + frecency = points; + } + calculatedURI = Services.io.newURI( + "http://" + + searchTerm + + ".com/" + + bonusName + + ":" + + bonusValue + + "/cutoff:" + + cutoff + + "/weight:" + + weight + + "/frecency:" + + frecency + ); + if (visitType == Ci.nsINavHistoryService.TRANSITION_BOOKMARK) { + matchTitle = searchTerm + "Bookmarked"; + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: calculatedURI, + title: matchTitle, + }); + } else { + matchTitle = calculatedURI.spec.substr( + calculatedURI.spec.lastIndexOf("/") + 1 + ); + } + await PlacesTestUtils.addVisits({ + uri: calculatedURI, + transition: visitType, + visitDate: dateInPeriod, + }); + } + + if (calculatedURI && frecency) { + results.push([calculatedURI, frecency, matchTitle]); + await PlacesTestUtils.addVisits({ + uri: calculatedURI, + title: matchTitle, + transition: visitType, + visitDate: dateInPeriod, + }); + } + } +} + +add_task(async function test_frecency() { + // Disable autoFill for this test. + Services.prefs.setBoolPref("browser.urlbar.autoFill", false); + Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false); + // always search in history + bookmarks, no matter what the default is + Services.prefs.setBoolPref("browser.urlbar.suggest.history", true); + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmarks", true); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.urlbar.suggest.history"); + Services.prefs.clearUserPref("browser.urlbar.suggest.bookmarks"); + Services.prefs.clearUserPref("browser.urlbar.autoFill"); + Services.prefs.clearUserPref("browser.urlbar.suggest.searches"); + }); + for (let bucket of bucketPrefs) { + await task_initializeBucket(bucket); + } + + // Sort results by frecency. Break ties by alphabetical URL. + results.sort((a, b) => { + let frecencyDiff = b[1] - a[1]; + if (frecencyDiff == 0) { + return a[0].spec.localeCompare(b[0].spec); + } + return frecencyDiff; + }); + + // Make sure there's enough results returned + Services.prefs.setIntPref( + "browser.urlbar.maxRichResults", + // +1 for the heuristic search result. + results.length + 1 + ); + + await PlacesTestUtils.promiseAsyncUpdates(); + let context = createContext(searchTerm, { isPrivate: false }); + let urlbarResults = []; + for (let result of results) { + let url = result[0].spec; + if (url.toLowerCase().includes("bookmark")) { + urlbarResults.push( + makeBookmarkResult(context, { + uri: url, + title: result[2], + }) + ); + } else { + urlbarResults.push( + makeVisitResult(context, { + uri: url, + title: result[2], + }) + ); + } + } + + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ...urlbarResults, + ], + }); +}); 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..d94de4f439 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_UrlbarController_integration.js @@ -0,0 +1,83 @@ +/* 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.importESModule( + "resource://gre/modules/PromiseUtils.sys.mjs" +); + +const TEST_URL = "http://example.com"; +const match = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + UrlbarUtils.RESULT_SOURCE.TABS, + { url: TEST_URL } +); +let controller; + +add_task(async function setup() { + controller = UrlbarTestUtils.newMockController(); +}); + +add_task(async function test_basic_search() { + let provider = registerBasicTestProvider([match]); + const context = createContext(TEST_URL, { providers: [provider.name] }); + + 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 provider = registerBasicTestProvider( + [match], + providerCanceledDeferred.resolve + ); + const context = createContext(TEST_URL, { providers: [provider.name] }); + + 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..bce6bb21ff --- /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..6d77e6a1ac --- /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 The query 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..c681aca387 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_UrlbarPrefs.js @@ -0,0 +1,449 @@ +/* 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 + ); +}); + +// Tests UrlbarPrefs.makeResultGroups({ showSearchSuggestionsFirst: true }). +add_task(function makeResultGroups_true() { + Assert.deepEqual( + UrlbarPrefs.makeResultGroups({ showSearchSuggestionsFirst: true }), + { + children: [ + // heuristic + { + maxResultCount: 1, + children: [ + { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }, + { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_EXTENSION }, + { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_SEARCH_TIP }, + { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_OMNIBOX }, + { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_ENGINE_ALIAS }, + { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_BOOKMARK_KEYWORD }, + { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_AUTOFILL }, + { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_PRELOADED }, + { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TOKEN_ALIAS_ENGINE }, + { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_HISTORY_URL }, + { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK }, + ], + }, + // extensions using the omnibox API + { + group: UrlbarUtils.RESULT_GROUP.OMNIBOX, + }, + // main group + { + flexChildren: true, + children: [ + // suggestions + { + flex: 2, + children: [ + { + flexChildren: true, + children: [ + { + flex: 2, + group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY, + }, + { + flex: 4, + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + }, + ], + }, + { + group: UrlbarUtils.RESULT_GROUP.TAIL_SUGGESTION, + }, + ], + }, + // general + { + group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT, + flex: 1, + children: [ + { + availableSpan: 3, + group: UrlbarUtils.RESULT_GROUP.INPUT_HISTORY, + }, + { + flexChildren: true, + children: [ + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.REMOTE_TAB, + }, + { + flex: 2, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + { + flex: 2, + group: UrlbarUtils.RESULT_GROUP.ABOUT_PAGES, + }, + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.PRELOADED, + }, + ], + }, + { + group: UrlbarUtils.RESULT_GROUP.INPUT_HISTORY, + }, + ], + }, + ], + }, + ], + } + ); +}); + +// Tests UrlbarPrefs.makeResultGroups({ showSearchSuggestionsFirst: false }). +add_task(function makeResultGroups_false() { + Assert.deepEqual( + UrlbarPrefs.makeResultGroups({ showSearchSuggestionsFirst: false }), + + { + children: [ + // heuristic + { + maxResultCount: 1, + children: [ + { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }, + { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_EXTENSION }, + { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_SEARCH_TIP }, + { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_OMNIBOX }, + { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_ENGINE_ALIAS }, + { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_BOOKMARK_KEYWORD }, + { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_AUTOFILL }, + { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_PRELOADED }, + { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TOKEN_ALIAS_ENGINE }, + { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_HISTORY_URL }, + { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK }, + ], + }, + // extensions using the omnibox API + { + group: UrlbarUtils.RESULT_GROUP.OMNIBOX, + }, + // main group + { + flexChildren: true, + children: [ + // general + { + group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT, + flex: 2, + children: [ + { + availableSpan: 3, + group: UrlbarUtils.RESULT_GROUP.INPUT_HISTORY, + }, + { + flexChildren: true, + children: [ + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.REMOTE_TAB, + }, + { + flex: 2, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + { + flex: 2, + group: UrlbarUtils.RESULT_GROUP.ABOUT_PAGES, + }, + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.PRELOADED, + }, + ], + }, + { + group: UrlbarUtils.RESULT_GROUP.INPUT_HISTORY, + }, + ], + }, + // suggestions + { + flex: 1, + children: [ + { + flexChildren: true, + children: [ + { + flex: 2, + group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY, + }, + { + flex: 4, + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + }, + ], + }, + { + group: UrlbarUtils.RESULT_GROUP.TAIL_SUGGESTION, + }, + ], + }, + ], + }, + ], + } + ); +}); + +// Tests interaction between showSearchSuggestionsFirst and resultGroups. +add_task(function showSearchSuggestionsFirst_resultGroups() { + // Check initial values. + Assert.equal( + UrlbarPrefs.get("showSearchSuggestionsFirst"), + true, + "showSearchSuggestionsFirst is true initially" + ); + Assert.deepEqual( + UrlbarPrefs.resultGroups, + UrlbarPrefs.makeResultGroups({ showSearchSuggestionsFirst: true }), + "resultGroups is the same as the groups for which howSearchSuggestionsFirst is true" + ); + + // Set showSearchSuggestionsFirst = false. + UrlbarPrefs.set("showSearchSuggestionsFirst", false); + Assert.deepEqual( + UrlbarPrefs.resultGroups, + UrlbarPrefs.makeResultGroups({ showSearchSuggestionsFirst: false }), + "resultGroups is updated after setting showSearchSuggestionsFirst = false" + ); + + // Set showSearchSuggestionsFirst = true. + UrlbarPrefs.set("showSearchSuggestionsFirst", true); + Assert.deepEqual( + UrlbarPrefs.resultGroups, + UrlbarPrefs.makeResultGroups({ showSearchSuggestionsFirst: true }), + "resultGroups is updated after setting showSearchSuggestionsFirst = true" + ); + + // Set showSearchSuggestionsFirst = false again so we can clear it next. + UrlbarPrefs.set("showSearchSuggestionsFirst", false); + Assert.deepEqual( + UrlbarPrefs.resultGroups, + UrlbarPrefs.makeResultGroups({ showSearchSuggestionsFirst: false }), + "resultGroups is updated after setting showSearchSuggestionsFirst = false" + ); + + // Clear showSearchSuggestionsFirst. + Services.prefs.clearUserPref("browser.urlbar.showSearchSuggestionsFirst"); + Assert.deepEqual( + UrlbarPrefs.resultGroups, + UrlbarPrefs.makeResultGroups({ showSearchSuggestionsFirst: true }), + "resultGroups is updated immediately after clearing showSearchSuggestionsFirst" + ); + Assert.equal( + UrlbarPrefs.get("showSearchSuggestionsFirst"), + true, + "showSearchSuggestionsFirst defaults to true after clearing it" + ); + Assert.deepEqual( + UrlbarPrefs.resultGroups, + UrlbarPrefs.makeResultGroups({ showSearchSuggestionsFirst: true }), + "resultGroups remains correct after getting showSearchSuggestionsFirst" + ); +}); + +// Tests UrlbarPrefs.initializeShowSearchSuggestionsFirstPref() and the +// interaction between matchGroups, showSearchSuggestionsFirst, and +// resultGroups. It's a little complex, but the flow is: +// +// 1. The old matchGroups pref has some value +// 2. UrlbarPrefs.initializeShowSearchSuggestionsFirstPref() is called to +// translate matchGroups into the newer showSearchSuggestionsFirst pref +// 3. The update to showSearchSuggestionsFirst causes the new resultGroups +// pref to be set +add_task(function initializeShowSearchSuggestionsFirstPref() { + // Each value in `tests`: [matchGroups, expectedShowSearchSuggestionsFirst] + let tests = [ + ["suggestion:4,general:Infinity", true], + ["suggestion:4,general:5", true], + ["suggestion:1,general:5,suggestion:Infinity", true], + ["suggestion:Infinity", true], + ["suggestion:4", true], + + ["foo:1,suggestion:4,general:Infinity", true], + ["foo:2,suggestion:4,general:5", true], + ["foo:3,suggestion:1,general:5,suggestion:Infinity", true], + ["foo:4,suggestion:Infinity", true], + ["foo:5,suggestion:4", true], + + ["general:5,suggestion:Infinity", false], + ["general:5,suggestion:4", false], + ["general:1,suggestion:4,general:Infinity", false], + ["general:Infinity", false], + ["general:5", false], + + ["foo:1,general:5,suggestion:Infinity", false], + ["foo:2,general:5,suggestion:4", false], + ["foo:3,general:1,suggestion:4,general:Infinity", false], + ["foo:4,general:Infinity", false], + ["foo:5,general:5", false], + + ["", true], + ["bogus groups", true], + ]; + + for (let [matchGroups, expectedValue] of tests) { + info("Running test: " + JSON.stringify({ matchGroups, expectedValue })); + Services.prefs.clearUserPref("browser.urlbar.showSearchSuggestionsFirst"); + + // Set matchGroups. + Services.prefs.setCharPref("browser.urlbar.matchGroups", matchGroups); + + // Call initializeShowSearchSuggestionsFirstPref. + UrlbarPrefs.initializeShowSearchSuggestionsFirstPref(); + + // Both showSearchSuggestionsFirst and resultGroups should be updated. + Assert.equal( + Services.prefs.getBoolPref("browser.urlbar.showSearchSuggestionsFirst"), + expectedValue, + "showSearchSuggestionsFirst has the expected value" + ); + Assert.deepEqual( + UrlbarPrefs.resultGroups, + UrlbarPrefs.makeResultGroups({ + showSearchSuggestionsFirst: expectedValue, + }), + "resultGroups should be updated with the appropriate default" + ); + } + + Services.prefs.clearUserPref("browser.urlbar.matchGroups"); +}); + +// Tests whether observer.onNimbusChanged works. +add_task(async function onNimbusChanged() { + Services.prefs.setBoolPref( + "browser.urlbar.autoFill.adaptiveHistory.enabled", + false + ); + + // Add an observer that throws an Error and an observer that does not define + // anything to check whether the other observers can get notifications. + UrlbarPrefs.addObserver({ + onPrefChanged(pref) { + throw new Error("From onPrefChanged"); + }, + onNimbusChanged(pref) { + throw new Error("From onNimbusChanged"); + }, + }); + UrlbarPrefs.addObserver({}); + + const observer = { + onPrefChanged(pref) { + this.prefChangedList.push(pref); + }, + onNimbusChanged(pref) { + this.nimbusChangedList.push(pref); + }, + }; + observer.prefChangedList = []; + observer.nimbusChangedList = []; + UrlbarPrefs.addObserver(observer); + + const doCleanup = await UrlbarTestUtils.initNimbusFeature({ + autoFillAdaptiveHistoryEnabled: true, + }); + Assert.equal(observer.prefChangedList.length, 0); + Assert.ok( + observer.nimbusChangedList.includes("autoFillAdaptiveHistoryEnabled") + ); + doCleanup(); +}); + +// Tests whether observer.onPrefChanged works. +add_task(async function onPrefChanged() { + const doCleanup = await UrlbarTestUtils.initNimbusFeature({ + autoFillAdaptiveHistoryEnabled: false, + }); + Services.prefs.setBoolPref( + "browser.urlbar.autoFill.adaptiveHistory.enabled", + false + ); + + // Add an observer that throws an Error and an observer that does not define + // anything to check whether the other observers can get notifications. + UrlbarPrefs.addObserver({ + onPrefChanged(pref) { + throw new Error("From onPrefChanged"); + }, + onNimbusChanged(pref) { + throw new Error("From onNimbusChanged"); + }, + }); + UrlbarPrefs.addObserver({}); + + const deferred = PromiseUtils.defer(); + const observer = { + onPrefChanged(pref) { + this.prefChangedList.push(pref); + deferred.resolve(); + }, + onNimbusChanged(pref) { + this.nimbusChangedList.push(pref); + deferred.resolve(); + }, + }; + observer.prefChangedList = []; + observer.nimbusChangedList = []; + UrlbarPrefs.addObserver(observer); + + Services.prefs.setBoolPref( + "browser.urlbar.autoFill.adaptiveHistory.enabled", + true + ); + await deferred.promise; + Assert.equal(observer.prefChangedList.length, 1); + Assert.equal(observer.prefChangedList[0], "autoFill.adaptiveHistory.enabled"); + Assert.equal(observer.nimbusChangedList.length, 0); + + Services.prefs.clearUserPref( + "browser.urlbar.autoFill.adaptiveHistory.enabled" + ); + doCleanup(); +}); 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..0aadb8aef6 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_UrlbarQueryContext_restrictSource.js @@ -0,0 +1,132 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test for restrictions set through UrlbarQueryContext.sources. + */ + +testEngine_setup(); + +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/", + 0, + false + ); + + 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 != SUGGESTIONS_ENGINE_NAME), + "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 != SUGGESTIONS_ENGINE_NAME), + "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"); + await SearchTestUtils.installSearchExtension({ + name: "Test", + keyword: "match", + }); + results = await get_results({ + sources: [UrlbarUtils.RESULT_SOURCE.SEARCH], + searchString: "match this", + }); + Assert.ok( + !results.some(r => r.payload.engine != SUGGESTIONS_ENGINE_NAME), + "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.js b/browser/components/urlbar/tests/unit/test_UrlbarSearchUtils.js new file mode 100644 index 0000000000..a39815937e --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_UrlbarSearchUtils.js @@ -0,0 +1,462 @@ +/* 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.importESModule( + "resource:///modules/UrlbarSearchUtils.sys.mjs" +); + +let baconEngineExtension; + +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.resetToAppDefaultEngine(); +}); + +add_task(async function search_engine_match() { + let engine = await Services.search.getDefault(); + let domain = engine.searchUrlDomain; + 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.searchUrlDomain; + 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].searchUrlDomain != domain + ); + engine.hidden = false; + await TestUtils.waitForCondition( + async () => (await UrlbarSearchUtils.enginesForDomainPrefix(token)).length + ); + let matchedEngine2 = ( + await UrlbarSearchUtils.enginesForDomainPrefix(token) + )[0]; + Assert.ok(matchedEngine2); + await Services.search.setDefault( + engine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); +}); + +add_task(async function onlyEnabled_option_nomatch() { + let engine = await Services.search.getDefault(); + let domain = engine.searchUrlDomain; + let token = domain.substr(0, 1); + Services.prefs.setCharPref("browser.search.hiddenOneOffs", engine.name); + let matchedEngines = await UrlbarSearchUtils.enginesForDomainPrefix(token, { + onlyEnabled: true, + }); + Assert.notEqual(matchedEngines[0].searchUrlDomain, domain); + Services.prefs.clearUserPref("browser.search.hiddenOneOffs"); + matchedEngines = await UrlbarSearchUtils.enginesForDomainPrefix(token, { + onlyEnabled: true, + }); + Assert.equal(matchedEngines[0].searchUrlDomain, domain); +}); + +add_task(async function add_search_engine_match() { + Assert.equal( + 0, + (await UrlbarSearchUtils.enginesForDomainPrefix("bacon")).length + ); + baconEngineExtension = await SearchTestUtils.installSearchExtension( + { + name: "bacon", + keyword: "pork", + search_url: "https://www.bacon.moz/", + }, + { skipUnload: true } + ); + let matchedEngine = ( + await UrlbarSearchUtils.enginesForDomainPrefix("bacon") + )[0]; + Assert.ok(matchedEngine); + Assert.equal(matchedEngine.searchForm, "https://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, "https://www.bacon.moz"); + Assert.equal(matchedEngine.name, "bacon"); + Assert.equal(matchedEngine.iconURI, null); +}); + +add_task(async function match_multiple_search_engines() { + Assert.equal( + 0, + (await UrlbarSearchUtils.enginesForDomainPrefix("baseball")).length + ); + await SearchTestUtils.installSearchExtension({ + name: "baseball", + search_url: "https://www.baseball.moz/", + }); + let matchedEngines = await UrlbarSearchUtils.enginesForDomainPrefix("ba"); + Assert.equal( + matchedEngines.length, + 2, + "enginesForDomainPrefix returned two engines." + ); + Assert.equal(matchedEngines[0].searchForm, "https://www.bacon.moz"); + Assert.equal(matchedEngines[0].name, "bacon"); + Assert.equal(matchedEngines[1].searchForm, "https://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() { + Assert.equal( + 0, + (await UrlbarSearchUtils.enginesForDomainPrefix("patch")).length + ); + await SearchTestUtils.installSearchExtension({ + name: "patch", + keyword: "PR", + search_url: "https://www.patch.moz/", + }); + // 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 promiseTopic = promiseSearchTopic("engine-removed"); + await Promise.all([baconEngineExtension.unload(), 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 extension = await SearchTestUtils.installSearchExtension( + { + name: "TestEngine2", + search_url: "https://example.com/", + }, + { skipUnload: true } + ); + let engine = Services.search.getEngineByName("TestEngine2"); + Assert.equal(UrlbarSearchUtils.getRootDomainFromEngine(engine), "example"); + await extension.unload(); + + extension = await SearchTestUtils.installSearchExtension( + { + name: "TestEngine", + search_url: "https://www.subdomain.othersubdomain.example.com", + }, + { skipUnload: true } + ); + engine = Services.search.getEngineByName("TestEngine"); + Assert.equal(UrlbarSearchUtils.getRootDomainFromEngine(engine), "example"); + await extension.unload(); + + // We let engines with URL ending in .test through even though its not a valid + // TLD. + extension = await SearchTestUtils.installSearchExtension( + { + name: "TestMalformed", + search_url: "https://mochi.test/", + search_url_get_params: "search={searchTerms}", + }, + { skipUnload: true } + ); + engine = Services.search.getEngineByName("TestMalformed"); + Assert.equal(UrlbarSearchUtils.getRootDomainFromEngine(engine), "mochi"); + await extension.unload(); + + // We return the domain for engines with a malformed URL. + extension = await SearchTestUtils.installSearchExtension( + { + name: "TestMalformed", + search_url: "https://subdomain.foobar/", + search_url_get_params: "search={searchTerms}", + }, + { skipUnload: true } + ); + engine = Services.search.getEngineByName("TestMalformed"); + Assert.equal( + UrlbarSearchUtils.getRootDomainFromEngine(engine), + "subdomain.foobar" + ); + await extension.unload(); +}); + +// Tests getSearchTermIfDefaultSerpUri() by using a variety of +// input strings and nsIURI's. +// Should not throw an error if the consumer passes an input +// that when accessed, could cause an error. +add_task(async function get_search_term_if_default_serp_uri() { + let testCases = [ + { + url: null, + skipUriTest: true, + }, + { + url: "", + skipUriTest: true, + }, + { + url: "about:blank", + }, + { + url: "about:home", + }, + { + url: "about:newtab", + }, + { + url: "not://a/supported/protocol", + }, + { + url: "view-source:http://www.example.com/", + }, + { + // Not a default engine. + url: "http://mochi.test:8888/?q=chocolate&pc=sample_code", + }, + { + // Not the correct protocol. + url: "http://example.com/?q=chocolate&pc=sample_code", + }, + { + // Not the same query param values. + url: "https://example.com/?q=chocolate&pc=sample_code2", + }, + { + // Not the same query param values. + url: "https://example.com/?q=chocolate&pc=sample_code&pc2=sample_code_2", + }, + { + url: "https://example.com/?q=chocolate&pc=sample_code", + expectedString: "chocolate", + }, + { + url: "https://example.com/?q=chocolate+cakes&pc=sample_code", + expectedString: "chocolate cakes", + }, + ]; + + // Create a specific engine so that the tests are matched + // exactly against the query params used. + let extension = await SearchTestUtils.installSearchExtension( + { + name: "TestEngine", + search_url: "https://example.com/", + search_url_get_params: "?q={searchTerms}&pc=sample_code", + }, + { skipUnload: true } + ); + let engine = Services.search.getEngineByName("TestEngine"); + let originalDefaultEngine = Services.search.defaultEngine; + Services.search.defaultEngine = engine; + + for (let testCase of testCases) { + let expectedString = testCase.expectedString ?? ""; + Assert.equal( + UrlbarSearchUtils.getSearchTermIfDefaultSerpUri(testCase.url), + expectedString, + `Should return ${ + expectedString == "" ? "an empty string" : "a matching search string" + }` + ); + // Convert the string into a nsIURI and then + // try the test case with it. + if (!testCase.skipUriTest) { + Assert.equal( + UrlbarSearchUtils.getSearchTermIfDefaultSerpUri( + Services.io.newURI(testCase.url) + ), + expectedString, + `Should return ${ + expectedString == "" ? "an empty string" : "a matching search string" + }` + ); + } + } + + Services.search.defaultEngine = originalDefaultEngine; + await extension.unload(); +}); + +add_task(async function matchAllDomainLevels() { + let baseHostname = "matchalldomainlevels"; + Assert.equal( + (await UrlbarSearchUtils.enginesForDomainPrefix(baseHostname)).length, + 0, + `Sanity check: No engines initially match ${baseHostname}` + ); + + // Install engines with the following domains. When we match engines below, + // perfectly matching domains should come before partially matching domains. + let baseDomain = `${baseHostname}.com`; + let perfectDomains = [baseDomain, `www.${baseDomain}`]; + let partialDomains = [`foo.${baseDomain}`, `foo.bar.${baseDomain}`]; + + // Install engines with partially matching domains first so that the test + // isn't incidentally passing because engines are installed in the order it + // ultimately expects them in. Wait for each engine to finish installing + // before starting the next one to avoid intermittent out-of-order failures. + let extensions = []; + for (let list of [partialDomains, perfectDomains]) { + for (let domain of list) { + let ext = await SearchTestUtils.installSearchExtension( + { + name: domain, + search_url: `https://${domain}/`, + }, + { skipUnload: true } + ); + extensions.push(ext); + } + } + + // Perfect matches come before partial matches. + let expectedDomains = [...perfectDomains, ...partialDomains]; + + // Do searches for the following strings. Each should match all the engines + // installed above. + let searchStrings = [baseHostname, baseHostname + "."]; + for (let searchString of searchStrings) { + info(`Searching for "${searchString}"`); + let engines = await UrlbarSearchUtils.enginesForDomainPrefix(searchString, { + matchAllDomainLevels: true, + }); + let engineData = engines.map(e => ({ + name: e.name, + searchForm: e.searchForm, + })); + info("Matching engines: " + JSON.stringify(engineData)); + + Assert.equal( + engines.length, + expectedDomains.length, + "Expected number of matching engines" + ); + Assert.deepEqual( + engineData.map(d => d.name), + expectedDomains, + "Expected matching engine names/domains in the expected order" + ); + } + + await Promise.all(extensions.map(e => e.unload())); +}); + +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..6eabc3bb52 --- /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.importESModule( + "resource://gre/modules/PrivateBrowsingUtils.sys.mjs" +); +const { PlacesUIUtils } = ChromeUtils.importESModule( + "resource:///modules/PlacesUIUtils.sys.mjs" +); + +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..034005b0fa --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_UrlbarUtils_getShortcutOrURIAndPostData.js @@ -0,0 +1,249 @@ +/* Any copyright is dedicated to the Public Domain. + https://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", "https://bmget/search=%s", null, "foo"), + new keywordResult("https://bmget/search=foo", null), + ], + + [ + new bmKeywordData("bmpost", "https://bmpost/", "search=%s", "foo2"), + new keywordResult("https://bmpost/", "search=foo2"), + ], + + [ + new bmKeywordData( + "bmpostget", + "https://bmpostget/search1=%s", + "search2=%s", + "foo3" + ), + new keywordResult("https://bmpostget/search1=foo3", "search2=foo3"), + ], + + [ + new bmKeywordData("bmget-nosearch", "https://bmget-nosearch/", null, ""), + new keywordResult("https://bmget-nosearch/", null), + ], + + [ + new searchKeywordData( + "searchget", + "https://searchget/?search={searchTerms}", + null, + "foo4" + ), + new keywordResult("https://searchget/?search=foo4", null, true), + ], + + [ + new searchKeywordData( + "searchpost", + "https://searchpost/", + "search={searchTerms}", + "foo5" + ), + new keywordResult("https://searchpost/", "search=foo5", true), + ], + + [ + new searchKeywordData( + "searchpostget", + "https://searchpostget/?search1={searchTerms}", + "search2={searchTerms}", + "foo6" + ), + new keywordResult( + "https://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", "https://bmget-noparam/", null, "foo7"), + new keywordResult(null, null, true), + ], + [ + new bmKeywordData( + "bmpost-noparam", + "https://bmpost-noparam/", + "not_a=param", + "foo8" + ), + new keywordResult(null, null, true), + ], + + // Test escaping (%s = escaped, %S = raw) + // UTF-8 default + [ + new bmKeywordData( + "bmget-escaping", + "https://bmget/?esc=%s&raw=%S", + null, + "fo\xE9" + ), + new keywordResult("https://bmget/?esc=fo%C3%A9&raw=fo\xE9", null), + ], + // Explicitly-defined ISO-8859-1 + [ + new bmKeywordData( + "bmget-escaping2", + "https://bmget/?esc=%s&raw=%S&mozcharset=ISO-8859-1", + null, + "fo\xE9" + ), + new keywordResult("https://bmget/?esc=fo%E9&raw=fo\xE9", null), + ], + + // Bug 359809: Test escaping +, /, and @ + // UTF-8 default + [ + new bmKeywordData( + "bmget-escaping", + "https://bmget/?esc=%s&raw=%S", + null, + "+/@" + ), + new keywordResult("https://bmget/?esc=%2B%2F%40&raw=+/@", null), + ], + // Explicitly-defined ISO-8859-1 + [ + new bmKeywordData( + "bmget-escaping2", + "https://bmget/?esc=%s&raw=%S&mozcharset=ISO-8859-1", + null, + "+/@" + ), + new keywordResult("https://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: "https://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; + +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) { + await SearchTestUtils.installSearchExtension({ + name: data.keyword, + keyword: data.keyword, + search_url: data.uri.spec, + search_url_get_params: "", + search_url_post_params: data.postData, + }); + } + } +} + +async function cleanupKeywords() { + await PlacesUtils.bookmarks.remove(folder); +} 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_UrlbarUtils_unEscapeURIForUI.js b/browser/components/urlbar/tests/unit/test_UrlbarUtils_unEscapeURIForUI.js new file mode 100644 index 0000000000..6efc6711c6 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_UrlbarUtils_unEscapeURIForUI.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test for unEscapeURIForUI function in UrlbarUtils. + */ + +"use strict"; + +const TEST_DATA = [ + { + description: "Test for characters including percent encoded chars", + input: "A%E3%81%82%F0%A0%AE%B7%21", + expected: "Aあ𠮷!", + testMessage: "Unescape given characters correctly", + }, + { + description: "Test for characters over the limit", + input: "A%E3%81%82%F0%A0%AE%B7%21".repeat( + Math.ceil(UrlbarUtils.MAX_TEXT_LENGTH / 25) + ), + expected: "A%E3%81%82%F0%A0%AE%B7%21".repeat( + Math.ceil(UrlbarUtils.MAX_TEXT_LENGTH / 25) + ), + testMessage: "Return given characters as it is because of over the limit", + }, +]; + +add_task(function () { + for (const { description, input, expected, testMessage } of TEST_DATA) { + info(description); + + const result = UrlbarUtils.unEscapeURIForUI(input); + Assert.equal(result, expected, testMessage); + } +}); diff --git a/browser/components/urlbar/tests/unit/test_about_urls.js b/browser/components/urlbar/tests/unit/test_about_urls.js new file mode 100644 index 0000000000..277ddb8ee1 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_about_urls.js @@ -0,0 +1,176 @@ +/* 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 { AboutPagesUtils } = ChromeUtils.importESModule( + "resource://gre/modules/AboutPagesUtils.sys.mjs" +); + +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", + tags: null, + providerName: "AboutPages", + }), + ], + }); +}); + +// "about:" by itself matches a list of about: pages and nothing else +add_task(async function aboutColonMatchesOnlyAboutPages() { + // We generate 9 about: page results because there are 10 results total, + // and the first result is the heuristic result. + function getFirst9AboutPages() { + const aboutPageNames = AboutPagesUtils.visibleAboutUrls.slice(0, 9); + const aboutPageResults = aboutPageNames.map(aboutPageName => { + return makeVisitResult(context, { + uri: aboutPageName, + title: aboutPageName, + tags: null, + providerName: "AboutPages", + }); + }); + return aboutPageResults; + } + + let context = createContext("about:", { isPrivate: false }); + await check_results({ + context, + search: "about:", + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + providerName: "HeuristicFallback", + heuristic: true, + }), + ...getFirst9AboutPages(), + ], + }); +}); + +// Results for about: pages do not match webpage titles from the user's history +add_task(async function aboutResultsDoNotMatchTitlesInHistory() { + await PlacesTestUtils.addVisits([ + { + uri: Services.io.newURI("http://example.com/guide/config/"), + title: "Guide to config in Firefox", + }, + ]); + + let context = createContext("about:config", { isPrivate: false }); + await check_results({ + context, + search: "about:config", + matches: [ + makeVisitResult(context, { + uri: "about:config", + title: "about:config", + heuristic: true, + providerName: "Autofill", + }), + ], + }); +}); + +// Tests that about: pages are shown after general results. +add_task(async function after_general() { + await PlacesTestUtils.addVisits([ + { + uri: Services.io.newURI("http://example.com/guide/aboutaddons/"), + title: "Guide to about:addons in Firefox", + }, + ]); + + let context = createContext("about:a", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + uri: "about:about", + title: "about:about", + heuristic: true, + providerName: "Autofill", + }), + makeVisitResult(context, { + uri: "http://example.com/guide/aboutaddons/", + title: "Guide to about:addons in Firefox", + }), + makeVisitResult(context, { + uri: "about:addons", + title: "about:addons", + tags: null, + providerName: "AboutPages", + }), + ], + }); + await cleanupPlaces(); +}); diff --git a/browser/components/urlbar/tests/unit/test_autofill_adaptiveHistory.js b/browser/components/urlbar/tests/unit/test_autofill_adaptiveHistory.js new file mode 100644 index 0000000000..94b29b913a --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_autofill_adaptiveHistory.js @@ -0,0 +1,1441 @@ +/* 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"; + +// Test for adaptive history autofill. + +testEngine_setup(); + +registerCleanupFunction(async () => { + Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions"); +}); +Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false); + +const TEST_DATA = [ + { + description: "Basic behavior for adaptive history autofill", + pref: true, + visitHistory: ["http://example.com/test"], + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + userInput: "exa", + expected: { + autofilled: "example.com/test", + completed: "http://example.com/test", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/test", + title: "test visit for http://example.com/test", + heuristic: true, + }), + ], + }, + }, + { + description: "URL that has www", + pref: true, + visitHistory: ["http://www.example.com/test"], + inputHistory: [{ uri: "http://www.example.com/test", input: "exa" }], + userInput: "exa", + expected: { + autofilled: "example.com/test", + completed: "http://www.example.com/test", + results: [ + context => + makeVisitResult(context, { + uri: "http://www.example.com/test", + title: "test visit for http://www.example.com/test", + heuristic: true, + }), + ], + }, + }, + { + description: "User's input starts with www", + pref: true, + visitHistory: ["http://www.example.com/test"], + inputHistory: [{ uri: "http://www.example.com/test", input: "www.exa" }], + userInput: "www.exa", + expected: { + autofilled: "www.example.com/test", + completed: "http://www.example.com/test", + results: [ + context => + makeVisitResult(context, { + uri: "http://www.example.com/test", + title: "test visit for http://www.example.com/test", + heuristic: true, + }), + ], + }, + }, + { + description: "Case differences for user's input are ignored", + pref: true, + visitHistory: ["http://example.com/test"], + inputHistory: [{ uri: "http://example.com/test", input: "EXA" }], + userInput: "eXA", + expected: { + autofilled: "eXAmple.com/test", + completed: "http://example.com/test", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/test", + title: "test visit for http://example.com/test", + heuristic: true, + }), + ], + }, + }, + { + description: + "Case differences for user's input that starts with www are ignored", + pref: true, + visitHistory: ["http://www.example.com/test"], + inputHistory: [{ uri: "http://www.example.com/test", input: "www.exa" }], + userInput: "WWW.exa", + expected: { + autofilled: "WWW.example.com/test", + completed: "http://www.example.com/test", + results: [ + context => + makeVisitResult(context, { + uri: "http://www.example.com/test", + title: "test visit for http://www.example.com/test", + heuristic: true, + }), + ], + }, + }, + { + description: "Mutiple case difference input history", + pref: true, + visitHistory: ["http://example.com/yes", "http://example.com/no"], + inputHistory: [ + { uri: "http://example.com/yes", input: "exa" }, + { uri: "http://example.com/yes", input: "EXA" }, + { uri: "http://example.com/yes", input: "EXa" }, + { uri: "http://example.com/yes", input: "eXa" }, + { uri: "http://example.com/yes", input: "eXA" }, + { uri: "http://example.com/no", input: "exa" }, + { uri: "http://example.com/no", input: "exa" }, + { uri: "http://example.com/no", input: "exa" }, + ], + userInput: "exa", + expected: { + autofilled: "example.com/yes", + completed: "http://example.com/yes", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/yes", + title: "test visit for http://example.com/yes", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://example.com/no", + title: "test visit for http://example.com/no", + }), + ], + }, + }, + { + description: "Multiple input history count", + pref: true, + visitHistory: ["http://example.com/few", "http://example.com/many"], + inputHistory: [ + { uri: "http://example.com/many", input: "exa" }, + { uri: "http://example.com/few", input: "exa" }, + { uri: "http://example.com/many", input: "examp" }, + ], + userInput: "exa", + expected: { + autofilled: "example.com/many", + completed: "http://example.com/many", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/many", + title: "test visit for http://example.com/many", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://example.com/few", + title: "test visit for http://example.com/few", + }), + ], + }, + }, + { + description: "Multiple input history count with same input", + pref: true, + visitHistory: ["http://example.com/few", "http://example.com/many"], + inputHistory: [ + { uri: "http://example.com/many", input: "exa" }, + { uri: "http://example.com/few", input: "exa" }, + { uri: "http://example.com/many", input: "exa" }, + ], + userInput: "exa", + expected: { + autofilled: "example.com/many", + completed: "http://example.com/many", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/many", + title: "test visit for http://example.com/many", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://example.com/few", + title: "test visit for http://example.com/few", + }), + ], + }, + }, + { + description: + "Multiple input history count with same input but different frecency", + pref: true, + visitHistory: [ + "http://example.com/few", + "http://example.com/many", + "http://example.com/many", + ], + inputHistory: [ + { uri: "http://example.com/many", input: "exa" }, + { uri: "http://example.com/few", input: "exa" }, + ], + userInput: "exa", + expected: { + autofilled: "example.com/many", + completed: "http://example.com/many", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/many", + title: "test visit for http://example.com/many", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://example.com/few", + title: "test visit for http://example.com/few", + }), + ], + }, + }, + { + description: "User input is shorter than the input history", + pref: true, + visitHistory: ["http://example.com/test"], + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + userInput: "e", + expected: { + autofilled: "example.com/", + completed: "http://example.com/", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/", + fallbackTitle: "example.com", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://example.com/test", + title: "test visit for http://example.com/test", + }), + ], + }, + }, + { + description: "User input is longer than the input history", + pref: true, + visitHistory: ["http://example.com/test"], + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + userInput: "example", + expected: { + autofilled: "example.com/test", + completed: "http://example.com/test", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/test", + title: "test visit for http://example.com/test", + heuristic: true, + }), + ], + }, + }, + { + description: + "User input starts with input history and includes path of the url", + pref: true, + visitHistory: ["http://example.com/test"], + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + userInput: "example.com/te", + expected: { + autofilled: "example.com/test", + completed: "http://example.com/test", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/test", + title: "test visit for http://example.com/test", + heuristic: true, + }), + ], + }, + }, + { + description: "User input starts with input history and but another url", + pref: true, + visitHistory: ["http://example.com/test", "http://example.org/test"], + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + userInput: "example.o", + expected: { + autofilled: "example.org/", + completed: "http://example.org/", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.org/", + fallbackTitle: "example.org", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://example.org/test", + title: "test visit for http://example.org/test", + }), + ], + }, + }, + { + description: "User input does not start with input history", + pref: true, + visitHistory: ["http://example.com/test"], + inputHistory: [{ uri: "http://example.com/test", input: "notmatch" }], + userInput: "exa", + expected: { + autofilled: "example.com/", + completed: "http://example.com/", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/", + fallbackTitle: "example.com", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://example.com/test", + title: "test visit for http://example.com/test", + }), + ], + }, + }, + { + description: + "User input does not start with input history, but it includes as part of URL", + pref: true, + visitHistory: ["http://example.com/test"], + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + userInput: "test", + expected: { + results: [ + context => + makeSearchResult(context, { + engineName: "Suggestions", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://example.com/test", + title: "test visit for http://example.com/test", + }), + ], + }, + }, + { + description: "User input does not start with visited URL", + pref: true, + visitHistory: ["http://mozilla.com/test"], + inputHistory: [{ uri: "http://mozilla.com/test", input: "exa" }], + userInput: "exa", + expected: { + results: [ + context => + makeSearchResult(context, { + engineName: "Suggestions", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://mozilla.com/test", + title: "test visit for http://mozilla.com/test", + }), + ], + }, + }, + { + description: "Visited page is bookmarked", + pref: true, + visitHistory: ["http://example.com/test"], + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + bookmarks: [{ uri: "http://example.com/test", title: "test bookmark" }], + userInput: "exa", + expected: { + autofilled: "example.com/test", + completed: "http://example.com/test", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/test", + title: "test bookmark", + heuristic: true, + }), + ], + }, + }, + { + description: "Visit history and no bookamrk with HISTORY source", + pref: true, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + visitHistory: ["http://example.com/test"], + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + userInput: "exa", + expected: { + autofilled: "example.com/test", + completed: "http://example.com/test", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/test", + title: "test visit for http://example.com/test", + heuristic: true, + }), + ], + }, + }, + { + description: "Visit history and no bookamrk with BOOKMARK source", + pref: true, + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + visitHistory: ["http://example.com/test"], + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + userInput: "exa", + expected: { + results: [ + context => + makeSearchResult(context, { + engineName: "Suggestions", + heuristic: true, + }), + ], + }, + }, + { + description: "Bookmarked visit history with HISTORY source", + pref: true, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + visitHistory: ["http://example.com/test", "http://example.com/bookmarked"], + bookmarks: [ + { uri: "http://example.com/bookmarked", title: "test bookmark" }, + ], + inputHistory: [ + { + uri: "http://example.com/test", + input: "exa", + }, + { + uri: "http://example.com/bookmarked", + input: "exa", + }, + ], + userInput: "exa", + expected: { + autofilled: "example.com/bookmarked", + completed: "http://example.com/bookmarked", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/bookmarked", + title: "test bookmark", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://example.com/test", + title: "test visit for http://example.com/test", + }), + ], + }, + }, + { + description: "Bookmarked visit history with BOOKMARK source", + pref: true, + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + visitHistory: ["http://example.com/test", "http://example.com/bookmarked"], + bookmarks: [ + { uri: "http://example.com/bookmarked", title: "test bookmark" }, + ], + inputHistory: [ + { + uri: "http://example.com/test", + input: "exa", + }, + { + uri: "http://example.com/bookmarked", + input: "exa", + }, + ], + userInput: "exa", + expected: { + autofilled: "example.com/bookmarked", + completed: "http://example.com/bookmarked", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/bookmarked", + title: "test bookmark", + heuristic: true, + }), + ], + }, + }, + { + description: "No visit history with HISTORY source", + pref: true, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + userInput: "exa", + expected: { + results: [ + context => + makeSearchResult(context, { + engineName: "Suggestions", + heuristic: true, + }), + ], + }, + }, + { + description: "No visit history with BOOKMARK source", + pref: true, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + bookmarks: [{ uri: "http://example.com/bookmarked", title: "test" }], + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + userInput: "exa", + expected: { + results: [ + context => + makeSearchResult(context, { + engineName: "Suggestions", + heuristic: true, + }), + ], + }, + }, + { + description: "Match with path expression", + pref: true, + visitHistory: [ + "http://example.com/testMany", + "http://example.com/testMany", + "http://example.com/test", + ], + inputHistory: [{ uri: "http://example.com/test", input: "example.com/te" }], + userInput: "example.com/te", + expected: { + autofilled: "example.com/test", + completed: "http://example.com/test", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/test", + title: "test visit for http://example.com/test", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://example.com/testMany", + title: "test visit for http://example.com/testMany", + }), + ], + }, + }, + { + description: + "Prefixed URL for input history and the same string for user input", + pref: true, + visitHistory: [ + "http://example.com/testMany", + "http://example.com/testMany", + "http://example.com/test", + ], + inputHistory: [ + { uri: "http://example.com/test", input: "http://example.com/test" }, + ], + userInput: "http://example.com/test", + expected: { + autofilled: "http://example.com/test", + completed: "http://example.com/test", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/test", + title: "test visit for http://example.com/test", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://example.com/testMany", + title: "test visit for http://example.com/testMany", + }), + ], + }, + }, + { + description: + "Prefixed URL for input history and URL expression for user input", + pref: true, + visitHistory: [ + "http://example.com/testMany", + "http://example.com/testMany", + "http://example.com/test", + ], + inputHistory: [ + { uri: "http://example.com/test", input: "http://example.com/te" }, + ], + userInput: "http://example.com/te", + expected: { + autofilled: "http://example.com/test", + completed: "http://example.com/test", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/test", + title: "test visit for http://example.com/test", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://example.com/testMany", + title: "test visit for http://example.com/testMany", + }), + ], + }, + }, + { + description: + "Prefixed URL for input history and path expression for user input", + pref: true, + visitHistory: [ + "http://example.com/testMany", + "http://example.com/testMany", + "http://example.com/test", + ], + inputHistory: [ + { uri: "http://example.com/test", input: "http://example.com/te" }, + ], + userInput: "example.com/te", + expected: { + autofilled: "example.com/testMany", + completed: "http://example.com/testMany", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/testMany", + title: "test visit for http://example.com/testMany", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://example.com/test", + title: "test visit for http://example.com/test", + }), + ], + }, + }, + { + description: "Prefixed URL for input history and 'http' for user input", + pref: true, + visitHistory: ["http://example.com/test"], + inputHistory: [{ uri: "http://example.com/test", input: "http" }], + userInput: "http", + expected: { + results: [ + context => + makeSearchResult(context, { + engineName: "Suggestions", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://example.com/test", + title: "test visit for http://example.com/test", + }), + ], + }, + }, + { + description: "Prefixed URL for input history and 'http:' for user input", + pref: true, + visitHistory: ["http://example.com/test"], + inputHistory: [{ uri: "http://example.com/test", input: "http:" }], + userInput: "http:", + expected: { + results: [ + context => + makeSearchResult(context, { + engineName: "Suggestions", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://example.com/test", + title: "test visit for http://example.com/test", + }), + ], + }, + }, + { + description: "Prefixed URL for input history and 'http:/' for user input", + pref: true, + visitHistory: ["http://example.com/test"], + inputHistory: [{ uri: "http://example.com/test", input: "http:/" }], + userInput: "http:/", + expected: { + results: [ + context => + makeSearchResult(context, { + engineName: "Suggestions", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://example.com/test", + title: "test visit for http://example.com/test", + }), + ], + }, + }, + { + description: "Prefixed URL for input history and 'http://' for user input", + pref: true, + visitHistory: ["http://example.com/test"], + inputHistory: [{ uri: "http://example.com/test", input: "http://" }], + userInput: "http://", + expected: { + autofilled: "http://example.com/test", + completed: "http://example.com/test", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/test", + title: "test visit for http://example.com/test", + heuristic: true, + }), + ], + }, + }, + { + description: "Prefixed URL for input history and 'http://e' for user input", + pref: true, + visitHistory: ["http://example.com/test"], + inputHistory: [{ uri: "http://example.com/test", input: "http://e" }], + userInput: "http://e", + expected: { + autofilled: "http://example.com/test", + completed: "http://example.com/test", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/test", + title: "test visit for http://example.com/test", + heuristic: true, + }), + ], + }, + }, + { + description: + "Prefixed URL with www omitted for input history and 'http://e' for user input", + pref: true, + visitHistory: ["http://www.example.com/test"], + inputHistory: [{ uri: "http://www.example.com/test", input: "http://e" }], + userInput: "http://e", + expected: { + autofilled: "http://example.com/test", + completed: "http://www.example.com/test", + results: [ + context => + makeVisitResult(context, { + uri: "http://www.example.com/test", + title: "test visit for http://www.example.com/test", + heuristic: true, + }), + ], + }, + }, + { + description: + "Those that match with fixed URL take precedence over those that match prefixed URL", + pref: true, + visitHistory: ["http://http.example.com/test", "http://example.com/test"], + inputHistory: [ + { uri: "http://http.example.com/test", input: "http" }, + { uri: "http://example.com/test", input: "http://example.com/test" }, + ], + userInput: "http", + expected: { + autofilled: "http.example.com/test", + completed: "http://http.example.com/test", + results: [ + context => + makeVisitResult(context, { + uri: "http://http.example.com/test", + title: "test visit for http://http.example.com/test", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://example.com/test", + title: "test visit for http://example.com/test", + }), + ], + }, + }, + { + description: "Input history is totally different string from the URL", + pref: true, + visitHistory: [ + "http://example.com/testMany", + "http://example.com/testMany", + "http://example.com/test", + ], + inputHistory: [ + { uri: "http://example.com/test", input: "totally-different-string" }, + ], + userInput: "totally", + expected: { + results: [ + context => + makeSearchResult(context, { + engineName: "Suggestions", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://example.com/test", + title: "test visit for http://example.com/test", + }), + ], + }, + }, + { + description: + "Input history is totally different string from the URL and there is a visit history whose URL starts with the input", + pref: true, + visitHistory: ["http://example.com/test", "http://totally.example.com"], + inputHistory: [ + { uri: "http://example.com/test", input: "totally-different-string" }, + ], + userInput: "totally", + expected: { + autofilled: "totally.example.com/", + completed: "http://totally.example.com/", + results: [ + context => + makeVisitResult(context, { + uri: "http://totally.example.com/", + title: "test visit for http://totally.example.com/", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://example.com/test", + title: "test visit for http://example.com/test", + }), + ], + }, + }, + { + description: "Use count threshold is as same as use count of input history", + pref: true, + useCountThreshold: 1 * 0.9 + 1, + visitHistory: ["http://example.com/test"], + inputHistory: [ + { uri: "http://example.com/test", input: "exa" }, + { uri: "http://example.com/test", input: "exa" }, + ], + userInput: "exa", + expected: { + autofilled: "example.com/test", + completed: "http://example.com/test", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/test", + title: "test visit for http://example.com/test", + heuristic: true, + }), + ], + }, + }, + { + description: "Use count threshold is less than use count of input history", + pref: true, + useCountThreshold: 3, + visitHistory: ["http://example.com/test"], + inputHistory: [ + { uri: "http://example.com/test", input: "exa" }, + { uri: "http://example.com/test", input: "exa" }, + { uri: "http://example.com/test", input: "exa" }, + { uri: "http://example.com/test", input: "exa" }, + ], + userInput: "exa", + expected: { + autofilled: "example.com/test", + completed: "http://example.com/test", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/test", + title: "test visit for http://example.com/test", + heuristic: true, + }), + ], + }, + }, + { + description: "Use count threshold is more than use count of input history", + pref: true, + useCountThreshold: 10, + visitHistory: ["http://example.com/test"], + inputHistory: [ + { uri: "http://example.com/test", input: "exa" }, + { uri: "http://example.com/test", input: "exa" }, + { uri: "http://example.com/test", input: "exa" }, + { uri: "http://example.com/test", input: "exa" }, + { uri: "http://example.com/test", input: "exa" }, + ], + userInput: "exa", + expected: { + autofilled: "example.com/", + completed: "http://example.com/", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/", + fallbackTitle: "example.com", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://example.com/test", + title: "test visit for http://example.com/test", + }), + ], + }, + }, + { + description: "minCharsThreshold pref equals to the user input length", + pref: true, + minCharsThreshold: 3, + visitHistory: ["http://example.com/test"], + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + userInput: "exa", + expected: { + autofilled: "example.com/test", + completed: "http://example.com/test", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/test", + title: "test visit for http://example.com/test", + heuristic: true, + }), + ], + }, + }, + { + description: "minCharsThreshold pref is smaller than the user input length", + pref: true, + minCharsThreshold: 2, + visitHistory: ["http://example.com/test"], + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + userInput: "exa", + expected: { + autofilled: "example.com/test", + completed: "http://example.com/test", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/test", + title: "test visit for http://example.com/test", + heuristic: true, + }), + ], + }, + }, + { + description: "minCharsThreshold pref is larger than the user input length", + pref: true, + minCharsThreshold: 4, + visitHistory: ["http://example.com/test"], + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + userInput: "exa", + expected: { + autofilled: "example.com/", + completed: "http://example.com/", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/", + fallbackTitle: "example.com", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://example.com/test", + title: "test visit for http://example.com/test", + }), + ], + }, + }, + { + description: + "Prioritize path component with case-sensitive and that is visited", + pref: true, + visitHistory: [ + "http://example.com/TEST", + "http://example.com/TEST", + "http://example.com/test", + ], + inputHistory: [ + { uri: "http://example.com/TEST", input: "example.com/test" }, + { uri: "http://example.com/test", input: "example.com/test" }, + ], + userInput: "example.com/test", + expected: { + autofilled: "example.com/test", + completed: "http://example.com/test", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/test", + title: "test visit for http://example.com/test", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://example.com/TEST", + title: "test visit for http://example.com/TEST", + }), + ], + }, + }, + { + description: + "Prioritize path component with case-sensitive but no visited data", + pref: true, + visitHistory: ["http://example.com/TEST"], + inputHistory: [ + { uri: "http://example.com/TEST", input: "example.com/test" }, + ], + userInput: "example.com/test", + expected: { + autofilled: "example.com/test", + completed: "http://example.com/test", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/test", + fallbackTitle: "example.com/test", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://example.com/TEST", + title: "test visit for http://example.com/TEST", + }), + ], + }, + }, + { + description: + "With history and bookmarks sources, foreign_count == 0, frecency <= 0: No adaptive history autofill", + pref: true, + visitHistory: ["http://example.com/test"], + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + frecency: 0, + userInput: "exa", + expected: { + autofilled: "example.com/", + completed: "http://example.com/", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/", + fallbackTitle: "example.com", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://example.com/test", + title: "test visit for http://example.com/test", + }), + ], + }, + }, + { + description: + "With history source, visit_count == 0, foreign_count != 0: No adaptive history autofill", + pref: true, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + bookmarks: [{ uri: "http://example.com/test", title: "test bookmark" }], + userInput: "exa", + expected: { + results: [ + context => + makeSearchResult(context, { + engineName: "Suggestions", + heuristic: true, + }), + ], + }, + }, + { + description: + "With history source, visit_count > 0, foreign_count != 0, frecency <= 20: No adaptive history autofill", + pref: true, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + visitHistory: ["http://example.com/test"], + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + bookmarks: [{ uri: "http://example.com/test", title: "test bookmark" }], + frecency: 0, + userInput: "exa", + expected: { + autofilled: "example.com/", + completed: "http://example.com/", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/", + fallbackTitle: "example.com", + heuristic: true, + }), + ], + }, + }, + { + description: + "With history source, visit_count > 0, foreign_count == 0, frecency <= 20: No adaptive history autofill", + pref: true, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + visitHistory: ["http://example.com/test"], + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + frecency: 0, + userInput: "exa", + expected: { + autofilled: "example.com/", + completed: "http://example.com/", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/", + fallbackTitle: "example.com", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://example.com/test", + title: "test visit for http://example.com/test", + }), + ], + }, + }, + { + description: "Empty input string", + pref: true, + visitHistory: ["http://example.com/test"], + inputHistory: [{ uri: "http://example.com/test", input: "" }], + userInput: "exa", + expected: { + autofilled: "example.com/", + completed: "http://example.com/", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/", + fallbackTitle: "example.com", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://example.com/test", + title: "test visit for http://example.com/test", + }), + ], + }, + }, + { + description: "Turn the pref off", + pref: false, + visitHistory: ["http://example.com/test"], + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + userInput: "exa", + expected: { + autofilled: "example.com/", + completed: "http://example.com/", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/", + fallbackTitle: "example.com", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://example.com/test", + title: "test visit for http://example.com/test", + }), + ], + }, + }, +]; + +add_task(async function inputTest() { + for (const { + description, + pref, + minCharsThreshold, + useCountThreshold, + source, + visitHistory, + inputHistory, + bookmarks, + frecency, + userInput, + expected, + } of TEST_DATA) { + info(description); + + UrlbarPrefs.set("autoFill.adaptiveHistory.enabled", pref); + + if (!isNaN(minCharsThreshold)) { + UrlbarPrefs.set( + "autoFill.adaptiveHistory.minCharsThreshold", + minCharsThreshold + ); + } + + if (!isNaN(useCountThreshold)) { + UrlbarPrefs.set( + "autoFill.adaptiveHistory.useCountThreshold", + useCountThreshold + ); + } + + if (visitHistory && visitHistory.length) { + await PlacesTestUtils.addVisits(visitHistory); + } + for (const { uri, input } of inputHistory) { + await UrlbarUtils.addToInputHistory(uri, input); + } + for (const bookmark of bookmarks || []) { + await PlacesTestUtils.addBookmarkWithDetails(bookmark); + } + + if (typeof frecency == "number") { + await PlacesUtils.withConnectionWrapper("test::setFrecency", db => + db.execute( + `UPDATE moz_places SET frecency = :frecency WHERE url = :url`, + { + frecency, + url: visitHistory[0], + } + ) + ); + } + + const sources = source + ? [source] + : [ + UrlbarUtils.RESULT_SOURCE.HISTORY, + UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + ]; + + const context = createContext(userInput, { + sources, + isPrivate: false, + }); + + await check_results({ + context, + autofilled: expected.autofilled, + completed: expected.completed, + hasAutofillTitle: expected.hasAutofillTitle, + matches: expected.results.map(f => f(context)), + }); + + await cleanupPlaces(); + UrlbarPrefs.clear("autoFill.adaptiveHistory.enabled"); + UrlbarPrefs.clear("autoFill.adaptiveHistory.minCharsThreshold"); + UrlbarPrefs.clear("autoFill.adaptiveHistory.useCountThreshold"); + } +}); + +add_task(async function urlCase() { + UrlbarPrefs.set("autoFill.adaptiveHistory.enabled", true); + + const testVisitFixed = "example.com/ABC/DEF"; + const testVisitURL = `http://${testVisitFixed}`; + const testInput = "example"; + await PlacesTestUtils.addVisits([testVisitURL]); + await UrlbarUtils.addToInputHistory(testVisitURL, testInput); + + const userInput = "example.COM/abc/def"; + for (let i = 1; i <= userInput.length; i++) { + const currentUserInput = userInput.substring(0, i); + const context = createContext(currentUserInput, { isPrivate: false }); + + if (currentUserInput.length < testInput.length) { + // Autofill with host. + await check_results({ + context, + autofilled: "example.com/", + completed: "http://example.com/", + matches: [ + makeVisitResult(context, { + uri: "http://example.com/", + fallbackTitle: "example.com", + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://example.com/ABC/DEF", + title: "test visit for http://example.com/ABC/DEF", + }), + ], + }); + } else if (currentUserInput.length !== testVisitFixed.length) { + // Autofill using input history. + const autofilled = currentUserInput + testVisitFixed.substring(i); + await check_results({ + context, + autofilled, + completed: "http://example.com/ABC/DEF", + matches: [ + makeVisitResult(context, { + uri: "http://example.com/ABC/DEF", + title: "test visit for http://example.com/ABC/DEF", + heuristic: true, + }), + ], + }); + } else { + // Autofill using user's input. + await check_results({ + context, + autofilled: "example.COM/abc/def", + completed: "http://example.com/abc/def", + matches: [ + makeVisitResult(context, { + uri: "http://example.com/abc/def", + fallbackTitle: "example.com/abc/def", + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://example.com/ABC/DEF", + title: "test visit for http://example.com/ABC/DEF", + }), + ], + }); + } + } + + await cleanupPlaces(); + UrlbarPrefs.clear("autoFill.adaptiveHistory.enabled"); +}); + +add_task(async function decayTest() { + UrlbarPrefs.set("autoFill.adaptiveHistory.enabled", true); + + await PlacesTestUtils.addVisits(["http://example.com/test"]); + await UrlbarUtils.addToInputHistory("http://example.com/test", "exa"); + + const initContext = createContext("exa", { isPrivate: false }); + await check_results({ + context: initContext, + autofilled: "example.com/test", + completed: "http://example.com/test", + matches: [ + makeVisitResult(initContext, { + uri: "http://example.com/test", + title: "test visit for http://example.com/test", + heuristic: true, + }), + ], + }); + + // The decay rate for a day is 0.975, as defined in PlacesFrecencyRecalculator + // Therefore, after 30 days, as use_count will be 0.975^30 = 0.468, we set the + // useCountThreshold 0.47 to not take the input history passed 30 days. + UrlbarPrefs.set("autoFill.adaptiveHistory.useCountThreshold", 0.47); + + // Make 29 days later. + for (let i = 0; i < 29; i++) { + await Cc["@mozilla.org/places/frecency-recalculator;1"] + .getService(Ci.nsIObserver) + .wrappedJSObject.decay(); + } + const midContext = createContext("exa", { isPrivate: false }); + await check_results({ + context: midContext, + autofilled: "example.com/test", + completed: "http://example.com/test", + matches: [ + makeVisitResult(midContext, { + uri: "http://example.com/test", + title: "test visit for http://example.com/test", + heuristic: true, + }), + ], + }); + + // Total 30 days later. + await Cc["@mozilla.org/places/frecency-recalculator;1"] + .getService(Ci.nsIObserver) + .wrappedJSObject.decay(); + const context = createContext("exa", { isPrivate: false }); + await check_results({ + context, + autofilled: "example.com/", + completed: "http://example.com/", + matches: [ + makeVisitResult(context, { + uri: "http://example.com/", + fallbackTitle: "example.com", + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://example.com/test", + title: "test visit for http://example.com/test", + }), + ], + }); + + await cleanupPlaces(); + UrlbarPrefs.clear("autoFill.adaptiveHistory.enabled"); + UrlbarPrefs.clear("autoFill.adaptiveHistory.useCountThreshold"); +}); 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..9b8b77e82c --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_autofill_bookmarked.js @@ -0,0 +1,150 @@ +/* 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.clearUserPref("browser.urlbar.suggest.quickactions"); + }); + Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false); + Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", 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: `test visit for 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}/`, + fallbackTitle: `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}/`, + fallbackTitle: `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}/`, + fallbackTitle: `www.${host}`, + heuristic: true, + }), + ], + }); +}); diff --git a/browser/components/urlbar/tests/unit/test_autofill_do_not_trim.js b/browser/components/urlbar/tests/unit/test_autofill_do_not_trim.js new file mode 100644 index 0000000000..5782bca210 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_autofill_do_not_trim.js @@ -0,0 +1,140 @@ +/* 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/. */ + +// We should not autofill when the search string contains spaces. + +testEngine_setup(); + +add_task(async function setup() { + Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false); + await PlacesTestUtils.addVisits({ + uri: Services.io.newURI("http://mozilla.org/link/"), + }); + + registerCleanupFunction(async () => { + Services.prefs.clearUserPref("browser.urlbar.suggest.searches"); + await cleanupPlaces(); + }); +}); + +add_task(async function test_not_autofill_ws_1() { + info("Do not autofill whitespaced entry 1"); + let context = createContext("mozilla.org ", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + uri: "http://mozilla.org/", + fallbackTitle: "http://mozilla.org/", + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://mozilla.org/link/", + title: "test visit for http://mozilla.org/link/", + }), + ], + }); +}); + +add_task(async function test_not_autofill_ws_2() { + info("Do not autofill whitespaced entry 2"); + let context = createContext("mozilla.org/ ", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + uri: "http://mozilla.org/", + fallbackTitle: "http://mozilla.org/", + iconUri: "page-icon:http://mozilla.org/", + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://mozilla.org/link/", + title: "test visit for http://mozilla.org/link/", + }), + ], + }); +}); + +add_task(async function test_not_autofill_ws_3() { + info("Do not autofill whitespaced entry 3"); + let context = createContext("mozilla.org/link ", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + uri: "http://mozilla.org/link", + fallbackTitle: "http://mozilla.org/link", + iconUri: "page-icon:http://mozilla.org/", + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://mozilla.org/link/", + title: "test visit for http://mozilla.org/link/", + }), + ], + }); +}); + +add_task(async function test_not_autofill_ws_4() { + info( + "Do not autofill whitespaced entry 4, but UrlbarProviderPlaces provides heuristic result" + ); + let context = createContext("mozilla.org/link/ ", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + uri: "http://mozilla.org/link/", + title: "test visit for http://mozilla.org/link/", + iconUri: "page-icon:http://mozilla.org/link/", + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + heuristic: true, + }), + ], + }); +}); + +add_task(async function test_not_autofill_ws_5() { + info("Do not autofill whitespaced entry 5"); + let context = createContext("moz illa ", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + // query is made explict so makeSearchResult doesn't trim it. + query: "moz illa ", + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://mozilla.org/link/", + title: "test visit for http://mozilla.org/link/", + }), + ], + }); +}); + +add_task(async function test_not_autofill_ws_6() { + info("Do not autofill whitespaced entry 6"); + let context = createContext(" mozilla", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + // query is made explict so makeSearchResult doesn't trim it. + query: " mozilla", + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://mozilla.org/link/", + title: "test visit for http://mozilla.org/link/", + }), + ], + }); +}); 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..632a0d580d --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_autofill_functional.js @@ -0,0 +1,115 @@ +/* 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.clearUserPref("browser.urlbar.suggest.quickactions"); + }); + + Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false); + Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", 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: "test visit for http://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: "A bookmark", + 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: "test visit for http://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: + "test visit for http://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..6913b1a3b9 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_autofill_origins.js @@ -0,0 +1,1041 @@ +/* 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 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(); + +registerCleanupFunction(async () => { + Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions"); +}); +Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false); + +// "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: `test visit for http://${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: `test visit for http://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: `test visit for http://${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: `test visit for http://${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: `test visit for http://${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: `test visit for http://${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/`, + fallbackTitle: `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/`, + fallbackTitle: `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/", + fallbackTitle: "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: "test visit for http://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: `test visit for http://${str}`, + 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: "test visit for http://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 = await PlacesTestUtils.getDatabaseValue( + "moz_places", + "frecency", + { url: "http://example.com/" } + ); + let httpsFrec = await PlacesTestUtils.getDatabaseValue( + "moz_places", + "frecency", + { url: "https://example.com/" } + ); + let otherFrec = await PlacesTestUtils.getDatabaseValue( + "moz_places", + "frecency", + { url: "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: "test visit for 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 = await PlacesTestUtils.getDatabaseValue( + "moz_places", + "frecency", + { + url: "http://example.com/", + } + ); + let httpsFrec = await PlacesTestUtils.getDatabaseValue( + "moz_places", + "frecency", + { + url: "https://example.com/", + } + ); + let otherFrec = await PlacesTestUtils.getDatabaseValue( + "moz_places", + "frecency", + { + url: "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: "test visit for 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: SUGGESTIONS_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: SUGGESTIONS_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: SUGGESTIONS_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, + fallbackTitle: "example.com", + heuristic: true, + }), + makeBookmarkResult(context, { + uri: bookmarkedURL, + title: "A bookmark", + }), + ], + }); + + await cleanup(); +}); + +// This is similar to suggestHistoryFalse_bookmark_prefix_0 in +// autofill_test_autofill_originsAndQueries.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}/`, + fallbackTitle: `${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}/`, + fallbackTitle: `${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}/`, + fallbackTitle: `${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, + fallbackTitle: "example.com", + heuristic: true, + }), + makeBookmarkResult(context, { + uri: bookmarkedURL, + title: "A bookmark", + }), + ], + }); + + await cleanup(); +}); + +// When the autofilled URL is `example.com/`, a visit for `example.com/?` should +// not be included in the results since it dupes the autofill result. +add_task(async function searchParams() { + await PlacesTestUtils.addVisits([ + "http://example.com/", + "http://example.com/?", + "http://example.com/?foo", + ]); + + // First, do a search with autofill disabled to make sure the visits were + // properly added. `example.com/?foo` has the highest frecency because it was + // added last; `example.com/?` has the next highest. `example.com/` dupes + // `example.com/?`, so it should not appear. + UrlbarPrefs.set("autoFill", false); + let context = createContext("ex", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://example.com/?foo", + title: "test visit for http://example.com/?foo", + }), + makeVisitResult(context, { + uri: "http://example.com/?", + title: "test visit for http://example.com/?", + }), + ], + }); + + // Now do a search with autofill enabled. This time `example.com/` will be + // autofilled, and since `example.com/?` dupes it, `example.com/?` should not + // appear. + UrlbarPrefs.clear("autoFill"); + context = createContext("ex", { isPrivate: false }); + await check_results({ + context, + autofilled: "example.com/", + completed: "http://example.com/", + matches: [ + makeVisitResult(context, { + uri: "http://example.com/", + title: "test visit for http://example.com/", + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://example.com/?foo", + title: "test visit for http://example.com/?foo", + }), + ], + }); + + await cleanup(); +}); + +// When the autofilled URL is `example.com/`, a visit for `example.com/?` should +// not be included in the results since it dupes the autofill result. (Same as +// the previous task but with https URLs instead of http. There shouldn't be any +// substantive difference.) +add_task(async function searchParams_https() { + await PlacesTestUtils.addVisits([ + "https://example.com/", + "https://example.com/?", + "https://example.com/?foo", + ]); + + // First, do a search with autofill disabled to make sure the visits were + // properly added. `example.com/?foo` has the highest frecency because it was + // added last; `example.com/?` has the next highest. `example.com/` dupes + // `example.com/?`, so it should not appear. + UrlbarPrefs.set("autoFill", false); + let context = createContext("ex", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + heuristic: true, + }), + makeVisitResult(context, { + uri: "https://example.com/?foo", + title: "test visit for https://example.com/?foo", + }), + makeVisitResult(context, { + uri: "https://example.com/?", + title: "test visit for https://example.com/?", + }), + ], + }); + + // Now do a search with autofill enabled. This time `example.com/` will be + // autofilled, and since `example.com/?` dupes it, `example.com/?` should not + // appear. + UrlbarPrefs.clear("autoFill"); + context = createContext("ex", { isPrivate: false }); + await check_results({ + context, + autofilled: "example.com/", + completed: "https://example.com/", + matches: [ + makeVisitResult(context, { + uri: "https://example.com/", + title: "test visit for https://example.com/", + heuristic: true, + }), + makeVisitResult(context, { + uri: "https://example.com/?foo", + title: "test visit for https://example.com/?foo", + }), + ], + }); + + await cleanup(); +}); + +// Checks an origin that looks like a prefix: a scheme with no dots + a port. +add_task(async function originLooksLikePrefix() { + let hostAndPort = "localhost:8888"; + let address = `http://${hostAndPort}/`; + await PlacesTestUtils.addVisits([{ uri: address }]); + + // addTestSuggestionsEngine adds a search engine + // with localhost as a server, so we have to disable the + // TTS result or else it will show up as a second result + // when searching l to localhost + UrlbarPrefs.set("suggest.engines", false); + + for (let search of ["lo", "localhost", "localhost:", "localhost:8888"]) { + let context = createContext(search, { isPrivate: false }); + await check_results({ + context, + autofilled: hostAndPort + "/", + completed: address, + matches: [ + makeVisitResult(context, { + uri: address, + title: `test visit for http://${hostAndPort}/`, + heuristic: true, + }), + ], + }); + } + await cleanup(); +}); + +// Checks an origin whose prefix is "about:". +add_task(async function about() { + const testData = [ + { + uri: "about:config", + input: "conf", + results: [ + context => + makeSearchResult(context, { + engineName: "Suggestions", + heuristic: true, + }), + context => + makeBookmarkResult(context, { + uri: "about:config", + title: "A bookmark", + }), + ], + }, + { + uri: "about:blank", + input: "about:blan", + results: [ + context => + makeVisitResult(context, { + uri: "about:blan", + fallbackTitle: "about:blan", + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + heuristic: true, + }), + context => + makeBookmarkResult(context, { + uri: "about:blank", + title: "A bookmark", + }), + ], + }, + ]; + + for (const { uri, input, results } of testData) { + await PlacesTestUtils.addBookmarkWithDetails({ uri }); + + const context = createContext(input, { isPrivate: false }); + await check_results({ + context, + matches: results.map(f => f(context)), + }); + await cleanup(); + } +}); + +// Checks an origin whose prefix is "place:". +add_task(async function place() { + const testData = [ + { + uri: "place:transition=7&sort=4", + input: "tran", + }, + { + uri: "place:transition=7&sort=4", + input: "place:tran", + }, + ]; + + for (const { uri, input } of testData) { + await PlacesTestUtils.addBookmarkWithDetails({ uri }); + + const context = createContext(input, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: "Suggestions", + heuristic: true, + }), + ], + }); + await cleanup(); + } +}); + +add_task(async function nullTitle() { + await doTitleTest({ + visits: [ + { + uri: "http://example.com/", + // Set title of visits data to an empty string causes + // the title to be null in the database. + title: "", + frecency: 100, + }, + { + uri: "https://www.example.com/", + title: "high frecency", + frecency: 50, + }, + { + uri: "http://www.example.com/", + title: "low frecency", + frecency: 1, + }, + ], + input: "example.com", + expected: { + autofilled: "example.com/", + completed: "http://example.com/", + matches: context => [ + makeVisitResult(context, { + uri: "http://example.com/", + title: "high frecency", + heuristic: true, + }), + makeVisitResult(context, { + uri: "https://www.example.com/", + title: "high frecency", + }), + ], + }, + }); +}); + +add_task(async function domainTitle() { + await doTitleTest({ + visits: [ + { + uri: "http://example.com/", + title: "example.com", + frecency: 100, + }, + { + uri: "https://www.example.com/", + title: "", + frecency: 50, + }, + { + uri: "http://www.example.com/", + title: "lowest frecency but has title", + frecency: 1, + }, + ], + input: "example.com", + expected: { + autofilled: "example.com/", + completed: "http://example.com/", + matches: context => [ + makeVisitResult(context, { + uri: "http://example.com/", + title: "lowest frecency but has title", + heuristic: true, + }), + makeVisitResult(context, { + uri: "https://www.example.com/", + title: "www.example.com", + }), + ], + }, + }); +}); + +add_task(async function exactMatchedTitle() { + await doTitleTest({ + visits: [ + { + uri: "http://example.com/", + title: "exact match", + frecency: 50, + }, + { + uri: "https://www.example.com/", + title: "high frecency uri", + frecency: 100, + }, + ], + input: "http://example.com/", + expected: { + autofilled: "http://example.com/", + completed: "http://example.com/", + matches: context => [ + makeVisitResult(context, { + uri: "http://example.com/", + title: "exact match", + heuristic: true, + }), + makeVisitResult(context, { + uri: "https://www.example.com/", + title: "high frecency uri", + }), + ], + }, + }); +}); + +async function doTitleTest({ visits, input, expected }) { + await PlacesTestUtils.addVisits(visits); + for (const { uri, frecency } of visits) { + // Prepare data. + await PlacesUtils.withConnectionWrapper("test::doTitleTest", async db => { + await db.execute( + `UPDATE moz_places SET frecency = :frecency, recalc_frecency=0 WHERE url = :url`, + { + frecency, + url: uri, + } + ); + await db.executeCached("DELETE FROM moz_updateoriginsupdate_temp"); + }); + } + + const context = createContext(input, { isPrivate: false }); + await check_results({ + context, + autofilled: expected.autofilled, + completed: expected.completed, + matches: expected.matches(context), + }); + + 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..9b305717c0 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_autofill_originsAndQueries.js @@ -0,0 +1,2471 @@ +/* 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 HEURISTIC_FALLBACK_PROVIDERNAME = "HeuristicFallback"; +const PLACES_PROVIDERNAME = "Places"; + +/** + * 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(); + +registerCleanupFunction(async () => { + Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions"); +}); +Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false); + +let path; +let search; +let searchCase; +let visitTitle; +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"; + visitTitle = (protocol, sub) => + `test visit for ${protocol}://${sub}example.com/foo`; + url = host + path; + await callback(); + + info(`Running subtest with origins enabled: ${callback.name}`); + origins = true; + path = "/"; + search = "ex"; + searchCase = "EX"; + visitTitle = (protocol, sub) => + `test visit for ${protocol}://${sub}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: visitTitle("http", ""), + 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: visitTitle("http", ""), + 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: visitTitle("http", "www."), + 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: visitTitle("http", "www."), + 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 + "/", + fallbackTitle: "http://www." + search + "/", + displayUrl: "http://www." + search, + heuristic: true, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + ], + }); + } else { + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "http://www." + search, + fallbackTitle: "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: visitTitle("http", ""), + 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: visitTitle("http", ""), + 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: visitTitle("http", "www."), + 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: visitTitle("http", "www."), + 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, + fallbackTitle: 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, + fallbackTitle: 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: PLACES_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: visitTitle("https", ""), + 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: visitTitle("https", "www."), + 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 + "/", + fallbackTitle: "http://www." + search + "/", + displayUrl: "http://www." + search, + heuristic: true, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + ], + }); + } else { + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "http://www." + search, + fallbackTitle: "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: visitTitle("https", ""), + 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: visitTitle("https", "www."), + 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, + fallbackTitle: 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, + fallbackTitle: 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: PLACES_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: visitTitle("https", ""), + 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: visitTitle("http", ""), + 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: visitTitle("https", ""), + 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: visitTitle("http", ""), + heuristic: true, + }), + makeVisitResult(context, { + uri: "https://" + url, + title: "test visit for https://" + url, + providerName: PLACES_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: visitTitle("https", "www."), + heuristic: true, + }), + makeVisitResult(context, { + uri: "https://" + url, + title: "test visit for https://" + url, + providerName: PLACES_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: visitTitle("http", ""), + heuristic: true, + }), + makeVisitResult(context, { + uri: "https://" + url, + title: "test visit for https://" + url, + providerName: PLACES_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: visitTitle("https", ""), + 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: visitTitle("https", ""), + heuristic: true, + }), + makeVisitResult(context, { + uri: "https://not-" + url, + title: "test visit for https://not-" + url, + providerName: PLACES_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 + // HeuristicFallback 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: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "https://not-" + url, + title: "test visit for https://not-" + url, + providerName: PLACES_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "https://" + url, + title: "test visit for https://" + url, + providerName: PLACES_PROVIDERNAME, + }), + ], + }); + } else { + await check_results({ + context, + autofilled: url, + completed: "https://" + url, + matches: [ + makeVisitResult(context, { + uri: "https://" + url, + title: visitTitle("https", ""), + heuristic: true, + }), + makeVisitResult(context, { + uri: "https://not-" + url, + title: "test visit for https://not-" + url, + providerName: PLACES_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: visitTitle("https", ""), + 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: SUGGESTIONS_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, + fallbackTitle: "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, + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + // 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.getDatabaseValue( + "moz_places", + "frecency", + { url: "http://" + url } + ); + 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: "A bookmark", + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://not-" + url, + title: "test visit for http://not-" + url, + providerName: PLACES_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, + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + // 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.getDatabaseValue( + "moz_places", + "frecency", + { url: "http://" + url } + ); + 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: "A bookmark", + heuristic: true, + }), + ], + }); + + await cleanup(); +}); + +// Bookmark a page and then clear history. +// The bookmarked origin/URL should still be autofilled. +add_autofill_task(async function zeroThreshold() { + const pageUrl = "http://" + url; + await PlacesTestUtils.addBookmarkWithDetails({ + uri: pageUrl, + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + await PlacesUtils.history.clear(); + await PlacesUtils.withConnectionWrapper("zeroThreshold", async db => { + await db.execute("UPDATE moz_places SET frecency = -1 WHERE url = :url", { + url: pageUrl, + }); + await db.executeCached("DELETE FROM moz_updateoriginsupdate_temp"); + }); + + // Make sure the place's frecency is -1. + let placeFrecency = await PlacesTestUtils.getDatabaseValue( + "moz_places", + "frecency", + { url: pageUrl } + ); + Assert.equal(placeFrecency, -1); + + // 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: "A bookmark", + 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: visitTitle("http", ""), + 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: SUGGESTIONS_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, + fallbackTitle: "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: visitTitle("http", ""), + 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: SUGGESTIONS_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, + fallbackTitle: "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, + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + // 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: "A bookmark", + 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, + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + 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: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }) + ); + } else { + matches.unshift( + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "http://" + search, + fallbackTitle: "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, + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + // 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, + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + let context = createContext("http://" + search, { isPrivate: false }); + await check_results({ + context, + autofilled: "http://" + url, + completed: "http://" + url, + matches: [ + makeVisitResult(context, { + uri: "http://" + url, + title: "A bookmark", + 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, + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + 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, + fallbackTitle: 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, + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + 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, + fallbackTitle: 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, + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + 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, + fallbackTitle: 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: visitTitle("http", ""), + 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: PLACES_PROVIDERNAME, + }), + ]; + if (origins) { + matches.unshift( + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }) + ); + } else { + matches.unshift( + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: prefixedUrl, + fallbackTitle: 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: visitTitle("http", ""), + 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, + fallbackTitle: 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: PLACES_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, + fallbackTitle: 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: PLACES_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, + fallbackTitle: 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: PLACES_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, + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + let context = createContext(search, { isPrivate: false }); + await check_results({ + context, + autofilled: url, + completed: "http://" + url, + matches: [ + makeVisitResult(context, { + uri: "http://" + url, + title: "A bookmark", + 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: SUGGESTIONS_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, + fallbackTitle: "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, + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + let context = createContext("http://" + search, { isPrivate: false }); + await check_results({ + context, + autofilled: "http://" + url, + completed: "http://" + url, + matches: [ + makeVisitResult(context, { + uri: "http://" + url, + title: "A bookmark", + 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, + fallbackTitle: 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, + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + 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, + fallbackTitle: 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, + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + 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, + fallbackTitle: 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, + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + 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, + fallbackTitle: 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, + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + 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: visitTitle("http", ""), + 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, + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + 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: visitTitle("http", ""), + 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, + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + 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, + fallbackTitle: 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, + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + 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, + fallbackTitle: 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, + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + 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, + fallbackTitle: 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: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "http://some-other-" + url, + title: "test visit for http://some-other-" + url, + providerName: PLACES_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "http://" + url, + title: "test visit for http://" + url, + providerName: PLACES_PROVIDERNAME, + }), + ], + }); + // Now bookmark it and set suggest.bookmark to false. + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "http://" + url, + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false); + context = createContext(search, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "http://some-other-" + url, + title: "test visit for http://some-other-" + url, + providerName: PLACES_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "http://" + url, + title: "A bookmark", + providerName: PLACES_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}/`, + fallbackTitle: `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: PLACES_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "http://" + url, + title: "test visit for http://" + url, + providerName: PLACES_PROVIDERNAME, + }), + ], + }); + // Now bookmark it and set suggest.bookmark to false. + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "http://" + url, + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + 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}/`, + fallbackTitle: `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: PLACES_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "http://" + url, + title: "A bookmark", + providerName: PLACES_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}/`, + fallbackTitle: `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: PLACES_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "ftp://" + url, + title: "test visit for ftp://" + url, + providerName: PLACES_PROVIDERNAME, + }), + ], + }); + // Now bookmark it and set suggest.bookmark to false. + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "ftp://" + url, + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + 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}/`, + fallbackTitle: `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: PLACES_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "ftp://" + url, + title: "A bookmark", + providerName: PLACES_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}/`, + fallbackTitle: `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: PLACES_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "http://non-matching-" + url, + title: "test visit for http://non-matching-" + url, + providerName: PLACES_PROVIDERNAME, + }), + ], + }); + // Now bookmark it and set suggest.bookmark to false. + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "http://non-matching-" + url, + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + 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}/`, + fallbackTitle: `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: PLACES_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "http://non-matching-" + url, + title: "A bookmark", + providerName: PLACES_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}/`, + fallbackTitle: `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: PLACES_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "ftp://non-matching-" + url, + title: "test visit for ftp://non-matching-" + url, + providerName: PLACES_PROVIDERNAME, + }), + ], + }); + // Now bookmark it and set suggest.bookmark to false. + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "ftp://non-matching-" + url, + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + 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}/`, + fallbackTitle: `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: PLACES_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "ftp://non-matching-" + url, + title: "A bookmark", + providerName: PLACES_PROVIDERNAME, + }), + ], + }); + await cleanup(); + } +); + +// When the heuristic is hidden, "ex" should autofill http://example.com/, and +// there should be an additional http://example.com/ non-autofill result. +add_autofill_task(async function hideHeuristic() { + UrlbarPrefs.set("experimental.hideHeuristic", true); + 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: visitTitle("http", ""), + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://" + url, + title: "test visit for http://" + url, + }), + ], + }); + await cleanup(); + UrlbarPrefs.set("experimental.hideHeuristic", false); +}); diff --git a/browser/components/urlbar/tests/unit/test_autofill_origins_alt_frecency.js b/browser/components/urlbar/tests/unit/test_autofill_origins_alt_frecency.js new file mode 100644 index 0000000000..872f891f63 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_autofill_origins_alt_frecency.js @@ -0,0 +1,243 @@ +/* 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 basic autofill test to ensure enabling the alternative frecency +// algorithm doesn't break autofill or tab-to-search. A more comprehensive +// testing of the algorithm itself is not included since it's something that +// may change frequently according to experimentation results. +// Other existing autofill tests will, of course, need to be adapted once an +// algorithm is promoted to be the default. + +XPCOMUtils.defineLazyGetter(this, "PlacesFrecencyRecalculator", () => { + return Cc["@mozilla.org/places/frecency-recalculator;1"].getService( + Ci.nsIObserver + ).wrappedJSObject; +}); + +testEngine_setup(); + +add_task( + { + pref_set: [["browser.urlbar.suggest.quickactions", false]], + }, + async function test_autofill() { + const origin = "example.com"; + let context = createContext(origin.substring(0, 2), { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: "Suggestions", + heuristic: true, + }), + ], + }); + // Add many visits. + const url = `https://${origin}/`; + await PlacesTestUtils.addVisits(new Array(10).fill(url)); + Assert.equal( + await PlacesUtils.metadata.get("origin_alt_frecency_threshold", 0), + 0, + "Check there's no threshold initially" + ); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + Assert.greater( + await PlacesUtils.metadata.get("origin_alt_frecency_threshold", 0), + 0, + "Check a threshold has been calculated" + ); + await check_results({ + context, + autofilled: `${origin}/`, + completed: url, + matches: [ + makeVisitResult(context, { + uri: url, + title: `test visit for ${url}`, + heuristic: true, + }), + ], + }); + await PlacesUtils.history.clear(); + } +); + +add_task( + { + pref_set: [["browser.urlbar.suggest.quickactions", false]], + }, + async function test_autofill_www() { + const origin = "example.com"; + // Add many visits. + const url = `https://www.${origin}/`; + await PlacesTestUtils.addVisits(new Array(10).fill(url)); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + let context = createContext(origin.substring(0, 2), { isPrivate: false }); + await check_results({ + context, + autofilled: `${origin}/`, + completed: url, + matches: [ + makeVisitResult(context, { + uri: url, + title: `test visit for ${url}`, + heuristic: true, + }), + ], + }); + await PlacesUtils.history.clear(); + } +); + +add_task( + { + pref_set: [ + ["browser.urlbar.suggest.quickactions", false], + ["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0], + ], + }, + async function test_autofill_prefix_priority() { + const origin = "localhost"; + const url = `https://${origin}/`; + await PlacesTestUtils.addVisits([url, `http://${origin}/`]); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + let engine = Services.search.defaultEngine; + let context = createContext(origin.substring(0, 2), { isPrivate: false }); + await check_results({ + context, + autofilled: `${origin}/`, + completed: url, + matches: [ + makeVisitResult(context, { + uri: url, + title: `test visit for ${url}`, + heuristic: true, + }), + makeSearchResult(context, { + engineName: engine.name, + engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS, + uri: UrlbarUtils.stripPublicSuffixFromHost(engine.searchUrlDomain), + providesSearchMode: true, + query: "", + providerName: "TabToSearch", + }), + ], + }); + await PlacesUtils.history.clear(); + } +); + +add_task( + { + pref_set: [["browser.urlbar.suggest.quickactions", false]], + }, + async function test_autofill_threshold() { + async function getOriginAltFrecency(origin) { + let db = await PlacesUtils.promiseDBConnection(); + let rows = await db.execute( + "SELECT alt_frecency FROM moz_origins WHERE host = :origin", + { origin } + ); + return rows?.[0].getResultByName("alt_frecency"); + } + async function getThreshold() { + let db = await PlacesUtils.promiseDBConnection(); + let rows = await db.execute("SELECT avg(alt_frecency) FROM moz_origins"); + return rows[0].getResultByIndex(0); + } + + await PlacesTestUtils.addVisits(new Array(10).fill("https://example.com/")); + // Add more visits to the same origins to differenciate the frecency scores. + await PlacesTestUtils.addVisits([ + "https://example.com/2", + "https://example.com/3", + ]); + await PlacesTestUtils.addVisits("https://somethingelse.org/"); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + let threshold = await PlacesUtils.metadata.get( + "origin_alt_frecency_threshold", + 0 + ); + Assert.greater( + threshold, + await getOriginAltFrecency("somethingelse.org"), + "Check mozilla.org has a lower frecency than the threshold" + ); + Assert.equal( + threshold, + await getThreshold(), + "Check the threshold has been calculared correctly" + ); + + let engine = Services.search.defaultEngine; + let context = createContext("so", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + heuristic: true, + query: "so", + engineName: engine.name, + }), + makeVisitResult(context, { + uri: "https://somethingelse.org/", + title: "test visit for https://somethingelse.org/", + }), + ], + }); + await PlacesUtils.history.clear(); + } +); + +add_task( + { + pref_set: [["browser.urlbar.suggest.quickactions", false]], + }, + async function test_autofill_cutoff() { + async function getOriginAltFrecency(origin) { + let db = await PlacesUtils.promiseDBConnection(); + let rows = await db.execute( + "SELECT alt_frecency FROM moz_origins WHERE host = :origin", + { origin } + ); + return rows?.[0].getResultByName("alt_frecency"); + } + + // Add many visits older than the default 90 days cutoff. + const visitDate = new Date(Date.now() - 120 * 86400000); + await PlacesTestUtils.addVisits( + new Array(10) + .fill("https://example.com/") + .map(url => ({ url, visitDate })) + ); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + Assert.strictEqual( + await getOriginAltFrecency("example.com"), + null, + "Check example.com has a NULL frecency" + ); + + let engine = Services.search.defaultEngine; + let context = createContext("ex", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + heuristic: true, + query: "ex", + engineName: engine.name, + }), + makeVisitResult(context, { + uri: "https://example.com/", + title: "test visit for https://example.com/", + }), + ], + }); + await PlacesUtils.history.clear(); + } +); 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..dfb5de2149 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_autofill_prefix_fallback.js @@ -0,0 +1,76 @@ +/* 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.clearUserPref("browser.urlbar.suggest.quickactions"); + }); + Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false); + Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", 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}/`, + fallbackTitle: `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}/`, + fallbackTitle: `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..018a2e1681 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_autofill_search_engine_aliases.js @@ -0,0 +1,85 @@ +/* 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 SearchTestUtils.installSearchExtension({ + name: TEST_ENGINE_NAME, + keyword: TEST_ENGINE_ALIAS, + }); +}); + +// 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..7b836a7c4d --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_autofill_search_engines.js @@ -0,0 +1,246 @@ +/* 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 ProviderAutofill from normal moz_places autofill, which is tested +// in test_autofill_origins.js and test_autofill_urls.js. + +"use strict"; + +const ENGINE_NAME = "engine.xml"; + +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" + ); + }); + + // Bug 1149672: Once we drop support for http with OpenSearch engines, + // we should be able to drop the http part of this. + for (let scheme of ["https", "http"]) { + let extension; + if (scheme == "https") { + extension = await SearchTestUtils.installSearchExtension( + { + name: ENGINE_NAME, + search_url: "https://www.example.com/", + }, + { skipUnload: true } + ); + } else { + let httpServer = makeTestServer(); + httpServer.registerDirectory("/", do_get_cwd()); + await Services.search.addOpenSearchEngine( + `http://localhost:${httpServer.identity.primaryPort}/data/engine.xml`, + null + ); + } + + 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 = scheme == "http" ? "https" : "http"; + 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/", + fallbackTitle: 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/", + fallbackTitle: 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/", + fallbackTitle: "http://example/", + iconUri: "page-icon:http://example/", + heuristic: true, + }), + ], + }); + + await extension?.unload(); + } +}); 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..c816736531 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_autofill_urls.js @@ -0,0 +1,881 @@ +/* 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 PLACES_PROVIDERNAME = "Places"; + +// "example.com/foo/" should match http://example.com/foo/. +testEngine_setup(); + +registerCleanupFunction(async () => { + Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions"); +}); +Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false); + +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: "test visit for http://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: "test visit for http://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", + fallbackTitle: "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/", + fallbackTitle: "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: PLACES_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: "test visit for http://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(true); + + Services.prefs.clearUserPref("browser.urlbar.suggest.bookmark"); + Services.prefs.clearUserPref("browser.urlbar.suggest.history"); + await cleanupPlaces(); +}); + +// should *not* autofill if the URI fragment does not match with case-sensitive. +add_task(async function uriFragmentCaseSensitive() { + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com/#TEST", + }, + ]); + const context = createContext("http://example.com/#t", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "http://example.com/#t", + fallbackTitle: "http://example.com/#t", + heuristic: true, + }), + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + uri: "http://example.com/#TEST", + title: "test visit for http://example.com/#TEST", + tags: [], + }), + ], + }); + + await cleanupPlaces(); +}); + +// should autofill if the URI fragment matches with case-sensitive. +add_task(async function uriFragmentCaseSensitive() { + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com/#TEST", + }, + ]); + const context = createContext("http://example.com/#T", { isPrivate: false }); + await check_results({ + context, + autofilled: "http://example.com/#TEST", + completed: "http://example.com/#TEST", + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + uri: "http://example.com/#TEST", + title: "test visit for http://example.com/#TEST", + heuristic: true, + }), + ], + }); + + await cleanupPlaces(); +}); + +add_task(async function uriCase() { + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com/ABC/DEF", + }, + ]); + + const testData = [ + { + input: "example.COM", + expected: { + autofilled: "example.COM/", + completed: "http://example.com/", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/", + fallbackTitle: "example.com", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://example.com/ABC/DEF", + title: "test visit for http://example.com/ABC/DEF", + }), + ], + }, + }, + { + input: "example.COM/", + expected: { + autofilled: "example.COM/", + completed: "http://example.com/", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/", + fallbackTitle: "example.com/", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://example.com/ABC/DEF", + title: "test visit for http://example.com/ABC/DEF", + }), + ], + }, + }, + { + input: "example.COM/a", + expected: { + autofilled: "example.COM/aBC/", + completed: "http://example.com/ABC/", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/ABC/", + fallbackTitle: "example.com/ABC/", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://example.com/ABC/DEF", + title: "test visit for http://example.com/ABC/DEF", + }), + ], + }, + }, + { + input: "example.com/ab", + expected: { + autofilled: "example.com/abC/", + completed: "http://example.com/ABC/", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/ABC/", + fallbackTitle: "example.com/ABC/", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://example.com/ABC/DEF", + title: "test visit for http://example.com/ABC/DEF", + }), + ], + }, + }, + { + input: "example.com/abc", + expected: { + autofilled: "example.com/abc/", + completed: "http://example.com/ABC/", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/ABC/", + fallbackTitle: "example.com/ABC/", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://example.com/ABC/DEF", + title: "test visit for http://example.com/ABC/DEF", + }), + ], + }, + }, + { + input: "example.com/abc/", + expected: { + autofilled: "example.com/abc/", + completed: "http://example.com/abc/", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/abc/", + fallbackTitle: "example.com/abc/", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://example.com/ABC/DEF", + title: "test visit for http://example.com/ABC/DEF", + }), + ], + }, + }, + { + input: "example.com/abc/d", + expected: { + autofilled: "example.com/abc/dEF", + completed: "http://example.com/ABC/DEF", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/ABC/DEF", + title: "test visit for http://example.com/ABC/DEF", + heuristic: true, + }), + ], + }, + }, + { + input: "example.com/abc/de", + expected: { + autofilled: "example.com/abc/deF", + completed: "http://example.com/ABC/DEF", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/ABC/DEF", + title: "test visit for http://example.com/ABC/DEF", + heuristic: true, + }), + ], + }, + }, + { + input: "example.com/abc/def", + expected: { + autofilled: "example.com/abc/def", + completed: "http://example.com/abc/def", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/abc/def", + fallbackTitle: "example.com/abc/def", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://example.com/ABC/DEF", + title: "test visit for http://example.com/ABC/DEF", + }), + ], + }, + }, + { + input: "http://example.com/a", + expected: { + autofilled: "http://example.com/aBC/", + completed: "http://example.com/ABC/", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/ABC/", + fallbackTitle: "example.com/ABC/", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://example.com/ABC/DEF", + title: "test visit for http://example.com/ABC/DEF", + }), + ], + }, + }, + { + input: "http://example.com/abc/", + expected: { + autofilled: "http://example.com/abc/", + completed: "http://example.com/abc/", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/abc/", + fallbackTitle: "example.com/abc/", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://example.com/ABC/DEF", + title: "test visit for http://example.com/ABC/DEF", + }), + ], + }, + }, + { + input: "http://example.com/abc/d", + expected: { + autofilled: "http://example.com/abc/dEF", + completed: "http://example.com/ABC/DEF", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/ABC/DEF", + title: "test visit for http://example.com/ABC/DEF", + heuristic: true, + }), + ], + }, + }, + { + input: "http://example.com/abc/def", + expected: { + autofilled: "http://example.com/abc/def", + completed: "http://example.com/abc/def", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/abc/def", + fallbackTitle: "example.com/abc/def", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://example.com/ABC/DEF", + title: "test visit for http://example.com/ABC/DEF", + }), + ], + }, + }, + { + input: "http://eXAMple.com/ABC/DEF", + expected: { + autofilled: "http://eXAMple.com/ABC/DEF", + completed: "http://example.com/ABC/DEF", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/ABC/DEF", + title: "test visit for http://example.com/ABC/DEF", + heuristic: true, + }), + ], + }, + }, + { + input: "http://eXAMple.com/abc/def", + expected: { + autofilled: "http://eXAMple.com/abc/def", + completed: "http://example.com/abc/def", + results: [ + context => + makeVisitResult(context, { + uri: "http://example.com/abc/def", + fallbackTitle: "example.com/abc/def", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "http://example.com/ABC/DEF", + title: "test visit for http://example.com/ABC/DEF", + }), + ], + }, + }, + ]; + + for (const { input, expected } of testData) { + const context = createContext(input, { + isPrivate: false, + }); + await check_results({ + context, + autofilled: expected.autofilled, + completed: expected.completed, + matches: expected.results.map(f => f(context)), + }); + } + + await cleanupPlaces(); +}); + +async function testCaseInsensitive(isBookmark = false) { + 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: isBookmark + ? "A bookmark" + : "test visit for http://example.com/foo", + heuristic: true, + }), + ], + }); + } + + await cleanupPlaces(); +} + +// Checks a URL with an origin that looks like a prefix: a scheme with no dots + +// a port. +add_task(async function originLooksLikePrefix1() { + await PlacesTestUtils.addVisits([ + { + uri: "http://localhost:8888/foo", + }, + ]); + const context = createContext("localhost:8888/f", { isPrivate: false }); + await check_results({ + context, + autofilled: "localhost:8888/foo", + completed: "http://localhost:8888/foo", + matches: [ + makeVisitResult(context, { + uri: "http://localhost:8888/foo", + title: "test visit for http://localhost:8888/foo", + heuristic: true, + }), + ], + }); + await cleanupPlaces(); +}); + +// Same as previous (originLooksLikePrefix1) but uses a URL whose path has two +// slashes, not one. +add_task(async function originLooksLikePrefix2() { + await PlacesTestUtils.addVisits([ + { + uri: "http://localhost:8888/foo/bar", + }, + ]); + + let context = createContext("localhost:8888/f", { isPrivate: false }); + await check_results({ + context, + autofilled: "localhost:8888/foo/", + completed: "http://localhost:8888/foo/", + matches: [ + makeVisitResult(context, { + uri: "http://localhost:8888/foo/", + fallbackTitle: "localhost:8888/foo/", + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://localhost:8888/foo/bar", + title: "test visit for http://localhost:8888/foo/bar", + providerName: PLACES_PROVIDERNAME, + tags: [], + }), + ], + }); + + context = createContext("localhost:8888/foo/b", { isPrivate: false }); + await check_results({ + context, + autofilled: "localhost:8888/foo/bar", + completed: "http://localhost:8888/foo/bar", + matches: [ + makeVisitResult(context, { + uri: "http://localhost:8888/foo/bar", + title: "test visit for http://localhost:8888/foo/bar", + heuristic: true, + }), + ], + }); + await cleanupPlaces(); +}); + +// Checks view-source pages as a prefix +// Uses bookmark because addVisits does not allow non-http uri's +add_task(async function viewSourceAsPrefix() { + let address = "view-source:https://www.example.com/"; + let title = "A view source bookmark"; + await PlacesTestUtils.addBookmarkWithDetails({ + uri: address, + title, + }); + + let testData = [ + { + input: "view-source:h", + completed: "view-source:https:/", + autofilled: "view-source:https:/", + }, + { + input: "view-source:http", + completed: "view-source:https:/", + autofilled: "view-source:https:/", + }, + { + input: "VIEW-SOURCE:http", + completed: "view-source:https:/", + autofilled: "VIEW-SOURCE:https:/", + }, + ]; + + // Only autofills from view-source:h to view-source:https:/ + for (let { input, completed, autofilled } of testData) { + let context = createContext(input, { isPrivate: false }); + await check_results({ + context, + completed, + autofilled, + matches: [ + { + heuristic: true, + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + }, + makeBookmarkResult(context, { + uri: address, + iconUri: "chrome://global/skin/icons/defaultFavicon.svg", + title, + }), + ], + }); + } + + await cleanupPlaces(); +}); + +// Checks data url prefixes +// Uses bookmark because addVisits does not allow non-http uri's +add_task(async function dataAsPrefix() { + let address = "data:text/html,%3Ch1%3EHello%2C World!%3C%2Fh1%3E"; + let title = "A data url bookmark"; + await PlacesTestUtils.addBookmarkWithDetails({ + uri: address, + title, + }); + + let testData = [ + { + input: "data:t", + completed: "data:text/", + autofilled: "data:text/", + }, + { + input: "data:text", + completed: "data:text/", + autofilled: "data:text/", + }, + { + input: "DATA:text", + completed: "data:text/", + autofilled: "DATA:text/", + }, + ]; + + for (let { input, completed, autofilled } of testData) { + let context = createContext(input, { isPrivate: false }); + await check_results({ + context, + completed, + autofilled, + matches: [ + { + heuristic: true, + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + }, + makeBookmarkResult(context, { + uri: address, + iconUri: "chrome://global/skin/icons/defaultFavicon.svg", + title, + }), + ], + }); + } + + await cleanupPlaces(); +}); + +// Checks about prefixes +add_task(async function aboutAsPrefix() { + let testData = [ + { + input: "about:abou", + completed: "about:about", + autofilled: "about:about", + }, + { + input: "ABOUT:abou", + completed: "about:about", + autofilled: "ABOUT:about", + }, + ]; + + for (let { input, completed, autofilled } of testData) { + let context = createContext(input, { isPrivate: false }); + await check_results({ + context, + completed, + autofilled, + matches: [ + { + heuristic: true, + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + }, + ], + }); + } + + await cleanupPlaces(); +}); + +// Checks a URL that has www name in history. +add_task(async function wwwHistory() { + const testData = [ + { + input: "example.com/", + visitHistory: [{ uri: "http://www.example.com/", title: "Example" }], + expected: { + autofilled: "example.com/", + completed: "http://www.example.com/", + results: [ + context => + makeVisitResult(context, { + uri: "http://www.example.com/", + title: "Example", + heuristic: true, + }), + ], + }, + }, + { + input: "https://example.com/", + visitHistory: [{ uri: "https://www.example.com/", title: "Example" }], + expected: { + autofilled: "https://example.com/", + completed: "https://www.example.com/", + results: [ + context => + makeVisitResult(context, { + uri: "https://www.example.com/", + title: "Example", + heuristic: true, + }), + ], + }, + }, + { + input: "https://example.com/abc", + visitHistory: [{ uri: "https://www.example.com/abc", title: "Example" }], + expected: { + autofilled: "https://example.com/abc", + completed: "https://www.example.com/abc", + results: [ + context => + makeVisitResult(context, { + uri: "https://www.example.com/abc", + title: "Example", + heuristic: true, + }), + ], + }, + }, + { + input: "https://example.com/ABC", + visitHistory: [{ uri: "https://www.example.com/abc", title: "Example" }], + expected: { + autofilled: "https://example.com/ABC", + completed: "https://www.example.com/ABC", + results: [ + context => + makeVisitResult(context, { + uri: "https://www.example.com/ABC", + fallbackTitle: "https://www.example.com/ABC", + heuristic: true, + }), + context => + makeVisitResult(context, { + uri: "https://www.example.com/abc", + title: "Example", + }), + ], + }, + }, + ]; + + for (const { input, visitHistory, expected } of testData) { + await PlacesTestUtils.addVisits(visitHistory); + const context = createContext(input, { isPrivate: false }); + await check_results({ + context, + completed: expected.completed, + autofilled: expected.autofilled, + matches: expected.results.map(f => f(context)), + }); + await cleanupPlaces(); + } +}); 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..5e2dba3446 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_avoid_middle_complete.js @@ -0,0 +1,270 @@ +/* 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/. */ + +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: SUGGESTIONS_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: SUGGESTIONS_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); + await SearchTestUtils.installSearchExtension({ + name: "CakeSearch", + search_url: "https://cake.search", + }); + + 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); + await SearchTestUtils.installSearchExtension({ + name: "CupcakeSearch", + search_url: "https://cupcake.search", + }); + + 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: SUGGESTIONS_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); + await SearchTestUtils.installSearchExtension({ + name: "BaconSearch", + search_url: "https://bacon.search", + }); + + 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: SUGGESTIONS_ENGINE_NAME, + query: "ba ", + heuristic: true, + }), + ], + }); + + await cleanupPlaces(); +}); + +add_task(async function test_searchEngine_www_noautofill() { + Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true); + await SearchTestUtils.installSearchExtension({ + name: "HamSearch", + search_url: "https://ham.search", + }); + + 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/", + fallbackTitle: "http://www.ham/", + displayUrl: "http://www.ham", + heuristic: true, + }), + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + query: "www.ham", + }), + ], + }); + + await cleanupPlaces(); +}); + +add_task(async function test_searchEngine_different_scheme_noautofill() { + Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true); + await SearchTestUtils.installSearchExtension({ + name: "PieSearch", + search_url: "https://pie.search", + }); + + 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/", + fallbackTitle: "http://pie/", + iconUri: "", + heuristic: true, + }), + ], + }); + + await cleanupPlaces(); +}); + +add_task(async function test_searchEngine_matching_prefix_autofill() { + Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true); + await SearchTestUtils.installSearchExtension({ + name: "BeanSearch", + search_url: "https://www.bean.search", + }); + + info("Should autoFill search engine if search string has matching prefix."); + let context = createContext("https://www.be", { isPrivate: false }); + await check_results({ + context, + autofilled: "https://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("https://be", { isPrivate: false }); + await check_results({ + context, + autofilled: "https://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/", + fallbackTitle: "mozilla.org", + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://mozilla.org/test/", + title: "test visit for http://mozilla.org/test/", + providerName: "Places", + }), + ], + }); + + 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..b4aa7e50bd --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_avoid_stripping_to_empty_tokens.js @@ -0,0 +1,119 @@ +/* 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/. */ + +testEngine_setup(); + +add_task(async function test_protocol_trimming() { + for (let prot of ["http", "https"]) { + 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/", + fallbackTitle: + 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/", + fallbackTitle: + 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()}/`, + fallbackTitle: `${input.trim()}/`, + iconUri: "", + heuristic: true, + providerName: "HeuristicFallback", + }), + makeVisitResult(context, { + uri: visit.uri.spec, + title: visit.title, + providerName: "Places", + }), + ], + }); + + 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: SUGGESTIONS_ENGINE_NAME, + query: input, + heuristic: true, + }), + makeVisitResult(context, { + uri: visit.uri.spec, + title: visit.title, + providerName: "Places", + }), + ], + }); + } + + await cleanupPlaces(); + } +}); diff --git a/browser/components/urlbar/tests/unit/test_calculator.js b/browser/components/urlbar/tests/unit/test_calculator.js new file mode 100644 index 0000000000..7fa899f320 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_calculator.js @@ -0,0 +1,46 @@ +/* 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"; + +ChromeUtils.defineESModuleGetters(this, { + Calculator: "resource:///modules/UrlbarProviderCalculator.sys.mjs", +}); + +const FORMULAS = [ + ["1+1", 2], + ["3+4*2/(1-5)", 1], + ["39+4*2/(1-5)", 37], + ["(39+4)*2/(1-5)", -21.5], + ["4+-5", -1], + ["-5*6", -30], + ["-5.5*6", -33], + ["-5.5*-6.4", 35.2], + ["-6-6-6", -18], + ["6-6-6", -6], + [".001 /2", 0.0005], + ["(0-.001)/2", -0.0005], + ["-.001/(0-2)", 0.0005], + ["1000000000000000000000000+1", 1e24], + ["1000000000000000000000000-1", 1e24], + ["1e+30+10", 1e30], + ["1e+30*10", 1e31], + ["1e+30/100", 1e28], + ["10/1000000000000000000000000", 1e-23], + ["10/-1000000000000000000000000", -1e-23], + ["1,500.5+2.5", 1503], // Ignore commas when using decimal seperators + ["1,5+2,5", 4], // Support comma seperators + ["1.500,5+2,5", 1503], // Ignore periods when using comma decimal seperators +]; + +add_task(function test() { + for (let [formula, result] of FORMULAS) { + let postfix = Calculator.infix2postfix(formula); + Assert.equal( + Calculator.evaluatePostfix(postfix), + result, + `${formula} should equal ${result}` + ); + } +}); 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..91d8207c05 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_casing.js @@ -0,0 +1,370 @@ +/* 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 AUTOFILL_PROVIDERNAME = "Autofill"; +const PLACES_PROVIDERNAME = "Places"; + +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/", + fallbackTitle: "mozilla.org", + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://mozilla.org/test/", + title: "test visit for http://mozilla.org/test/", + providerName: PLACES_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: "test visit for http://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: "test visit for http://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: "test visit for http://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: "test visit for http://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/", + fallbackTitle: "mozilla.org", + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://mozilla.org/Test/", + title: "test visit for http://mozilla.org/Test/", + providerName: PLACES_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/", + fallbackTitle: "www.mozilla.org", + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://www.mozilla.org/Test/", + title: "test visit for http://www.mozilla.org/Test/", + providerName: PLACES_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: "test visit for http://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: "test visit for http://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: "test visit for http://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: "test visit for http://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: SUGGESTIONS_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: SUGGESTIONS_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: SUGGESTIONS_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: SUGGESTIONS_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: SUGGESTIONS_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..47a673d064 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_dedupe_prefix.js @@ -0,0 +1,277 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// 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", + }, + // Note that we add https://www.example.com/foo/ twice here. + { + uri: "https://www.example.com/foo/", + title: "Example Page", + }, + { + uri: "https://www.example.com/foo/", + title: "Example Page", + }, + ]); + + // Expected results: + // + // Autofill result: + // https://www.example.com has the highest origin frecency since we added 2 + // visits to https://www.example.com/foo/ and only one visit to the other + // URLs. + // Other results: + // https://example.com/foo/ has the highest possible prefix rank, and it + // does not dupe the autofill result, so only it should be included. + 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: "Example Page", + 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", + }, + ]); + } + + // Expected results: + // + // Autofill result: + // http://www.example.com now has the highest origin frecency since we added + // 4 visits to http://www.example.com/foo/ + // Other results: + // Same as before + 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: "Example Page", + 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", + }, + ]); + } + + // Expected results: + // + // Autofill result: + // https://example.com now has the highest origin frecency since we added + // 6 visits to https://example.com/foo/ + // Other results: + // https://example.com/foo/ has the highest possible prefix rank, but it + // dupes the heuristic so it should not be included. + // https://www.example.com/foo/ has the next highest prefix rank, and it + // does not dupe the heuristic, so only it should be included. + 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: "Example Page", + heuristic: true, + }), + ], + }); + + await cleanupPlaces(); +}); + +// This is the same as the previous task but with `experimental.hideHeuristic` +// enabled. +add_task(async function hideHeuristic() { + UrlbarPrefs.set("experimental.hideHeuristic", true); + + // 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", + }, + // Note that we add https://www.example.com/foo/ twice here. + { + uri: "https://www.example.com/foo/", + title: "Example Page", + }, + { + uri: "https://www.example.com/foo/", + title: "Example Page", + }, + ]); + + // Expected results: + // + // Autofill result: + // https://www.example.com has the highest origin frecency since we added 2 + // visits to https://www.example.com/foo/ and only one visit to the other + // URLs. + // Other results: + // https://example.com/foo/ has the highest possible prefix rank, and it + // does not dupe the autofill result, so only it should be included. + 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: "Example Page", + 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", + }, + ]); + } + + // Expected results: + // + // Autofill result: + // http://www.example.com now has the highest origin frecency since we added + // 4 visits to http://www.example.com/foo/ + // Other results: + // Same as before + 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: "Example Page", + heuristic: true, + }), + makeVisitResult(context, { + uri: "https://example.com/foo/", + title: "Example Page", + }), + ], + }); + + // Add enough https:// vists for it to have the highest frecency. + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits([ + { + uri: "https://example.com/foo/", + title: "Example Page", + }, + ]); + } + + // Expected results: + // + // Autofill result: + // https://example.com now has the highest origin frecency since we added + // 6 visits to https://example.com/foo/ + // Other results: + // https://example.com/foo/ has the highest possible prefix rank. It dupes + // the heuristic so ordinarily it should not be included, but because the + // heuristic is hidden, only it should appear. + 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: "Example Page", + heuristic: true, + }), + makeVisitResult(context, { + uri: "https://example.com/foo/", + title: "Example Page", + }), + ], + }); + + await cleanupPlaces(); + UrlbarPrefs.clear("experimental.hideHeuristic"); +}); diff --git a/browser/components/urlbar/tests/unit/test_dedupe_switchTab.js b/browser/components/urlbar/tests/unit/test_dedupe_switchTab.js new file mode 100644 index 0000000000..3b49866b1e --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_dedupe_switchTab.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +testEngine_setup(); + +add_task(async function test_deduplication_for_switch_tab() { + // Set up Places to think the tab is open locally. + let uri = Services.io.newURI("http://example.com/"); + + await PlacesTestUtils.addVisits({ uri, title: "An Example" }); + await addOpenPages(uri, 1); + await UrlbarUtils.addToInputHistory("http://example.com/", "An"); + + let query = "An"; + let context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeTabSwitchResult(context, { + uri: "http://example.com/", + title: "An Example", + }), + ], + }); + + await removeOpenPages(uri, 1); + await cleanupPlaces(); +}); diff --git a/browser/components/urlbar/tests/unit/test_download_embed_bookmarks.js b/browser/components/urlbar/tests/unit/test_download_embed_bookmarks.js new file mode 100644 index 0000000000..29ce557748 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_download_embed_bookmarks.js @@ -0,0 +1,137 @@ +/* -*- 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/. */ + +/** + * Tests bug 449406 to ensure that TRANSITION_DOWNLOAD, TRANSITION_EMBED and + * TRANSITION_FRAMED_LINK bookmarked uri's show up in the location bar. + */ + +testEngine_setup(); + +const TRANSITION_EMBED = PlacesUtils.history.TRANSITION_EMBED; +const TRANSITION_FRAMED_LINK = PlacesUtils.history.TRANSITION_FRAMED_LINK; +const TRANSITION_DOWNLOAD = PlacesUtils.history.TRANSITION_DOWNLOAD; + +add_task(async function test_download_embed_bookmarks() { + Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.urlbar.suggest.searches"); + }); + + let uri1 = Services.io.newURI("http://download/bookmarked"); + let uri2 = Services.io.newURI("http://embed/bookmarked"); + let uri3 = Services.io.newURI("http://framed/bookmarked"); + let uri4 = Services.io.newURI("http://download"); + let uri5 = Services.io.newURI("http://embed"); + let uri6 = Services.io.newURI("http://framed"); + await PlacesTestUtils.addVisits([ + { uri: uri1, title: "download-bookmark", transition: TRANSITION_DOWNLOAD }, + { uri: uri2, title: "embed-bookmark", transition: TRANSITION_EMBED }, + { uri: uri3, title: "framed-bookmark", transition: TRANSITION_FRAMED_LINK }, + { uri: uri4, title: "download2", transition: TRANSITION_DOWNLOAD }, + { uri: uri5, title: "embed2", transition: TRANSITION_EMBED }, + { uri: uri6, title: "framed2", transition: TRANSITION_FRAMED_LINK }, + ]); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: uri1, + title: "download-bookmark", + }); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: uri2, + title: "embed-bookmark", + }); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: uri3, + title: "framed-bookmark", + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + info("Searching for bookmarked download uri matches"); + let context = createContext("download-bookmark", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeBookmarkResult(context, { + uri: uri1.spec, + title: "download-bookmark", + }), + ], + }); + + info("Searching for bookmarked embed uri matches"); + context = createContext("embed-bookmark", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeBookmarkResult(context, { + uri: uri2.spec, + title: "embed-bookmark", + }), + ], + }); + + info("Searching for bookmarked framed uri matches"); + context = createContext("framed-bookmark", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeBookmarkResult(context, { + uri: uri3.spec, + title: "framed-bookmark", + }), + ], + }); + + info("Searching for download uri does not match"); + context = createContext("download2", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ], + }); + + info("Searching for embed uri does not match"); + context = createContext("embed2", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ], + }); + + info("Searching for framed uri does not match"); + context = createContext("framed2", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ], + }); + + await cleanupPlaces(); +}); diff --git a/browser/components/urlbar/tests/unit/test_empty_search.js b/browser/components/urlbar/tests/unit/test_empty_search.js new file mode 100644 index 0000000000..2c6dffe8e6 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_empty_search.js @@ -0,0 +1,181 @@ +/* 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 bug 426864 that makes sure searching a space only shows typed pages + * from history. + */ + +testEngine_setup(); + +add_task(async function test_empty_search() { + Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false); + Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.urlbar.suggest.searches"); + Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions"); + }); + + let uri1 = Services.io.newURI("http://t.foo/1"); + let uri2 = Services.io.newURI("http://t.foo/2"); + let uri3 = Services.io.newURI("http://t.foo/3"); + let uri4 = Services.io.newURI("http://t.foo/4"); + let uri5 = Services.io.newURI("http://t.foo/5"); + let uri6 = Services.io.newURI("http://t.foo/6"); + let uri7 = Services.io.newURI("http://t.foo/7"); + + await PlacesTestUtils.addVisits([ + { uri: uri7, title: "title" }, + { uri: uri6, title: "title" }, + { uri: uri4, title: "title" }, + { uri: uri3, title: "title" }, + { uri: uri2, title: "title" }, + { uri: uri1, title: "title" }, + ]); + + await PlacesTestUtils.addBookmarkWithDetails({ uri: uri6, title: "title" }); + await PlacesTestUtils.addBookmarkWithDetails({ uri: uri5, title: "title" }); + await PlacesTestUtils.addBookmarkWithDetails({ uri: uri4, title: "title" }); + await PlacesTestUtils.addBookmarkWithDetails({ uri: uri2, title: "title" }); + + await addOpenPages(uri7, 1); + + // Now remove page 6 from history, so it is an unvisited bookmark. + await PlacesUtils.history.remove(uri6); + + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + // With the changes above, the sites in descending order of frecency are: + // uri2 + // uri4 + // uri5 + // uri6 + // uri1 + // uri3 + // uri7 + + info("Match everything"); + let context = createContext("foo", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeBookmarkResult(context, { + uri: uri2.spec, + title: "title", + }), + makeBookmarkResult(context, { + uri: uri4.spec, + title: "title", + }), + makeBookmarkResult(context, { + uri: uri5.spec, + title: "title", + }), + makeBookmarkResult(context, { + uri: uri6.spec, + title: "title", + }), + makeVisitResult(context, { uri: uri1.spec, title: "title" }), + makeVisitResult(context, { uri: uri3.spec, title: "title" }), + makeTabSwitchResult(context, { uri: uri7.spec, title: "title" }), + ], + }); + + info("Match only history"); + context = createContext(`foo ${UrlbarTokenizer.RESTRICT.HISTORY}`, { + isPrivate: false, + }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { uri: uri2.spec, title: "title" }), + makeVisitResult(context, { uri: uri4.spec, title: "title" }), + makeVisitResult(context, { uri: uri1.spec, title: "title" }), + makeVisitResult(context, { uri: uri3.spec, title: "title" }), + makeVisitResult(context, { uri: uri7.spec, title: "title" }), + ], + }); + + info("Drop-down empty search matches history sorted by frecency desc"); + context = createContext(" ", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + // query is made explict so makeSearchResult doesn't trim it. + query: " ", + heuristic: true, + }), + makeVisitResult(context, { uri: uri2.spec, title: "title" }), + makeVisitResult(context, { uri: uri4.spec, title: "title" }), + makeVisitResult(context, { uri: uri1.spec, title: "title" }), + makeVisitResult(context, { uri: uri3.spec, title: "title" }), + makeVisitResult(context, { uri: uri7.spec, title: "title" }), + ], + }); + + info("Empty search matches only bookmarks when history is disabled"); + Services.prefs.setBoolPref("browser.urlbar.suggest.history", false); + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true); + context = createContext(" ", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + // query is made explict so makeSearchResult doesn't trim it. + query: " ", + heuristic: true, + }), + makeBookmarkResult(context, { + uri: uri2.spec, + title: "title", + }), + makeBookmarkResult(context, { + uri: uri4.spec, + title: "title", + }), + makeBookmarkResult(context, { + uri: uri5.spec, + title: "title", + }), + makeBookmarkResult(context, { + uri: uri6.spec, + title: "title", + }), + ], + }); + + info( + "Empty search matches only open tabs when bookmarks and history are disabled" + ); + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false); + context = createContext(" ", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + // query is made explict so makeSearchResult doesn't trim it. + query: " ", + heuristic: true, + }), + makeTabSwitchResult(context, { uri: uri7.spec, title: "title" }), + ], + }); + + Services.prefs.clearUserPref("browser.urlbar.suggest.history"); + Services.prefs.clearUserPref("browser.urlbar.suggest.bookmark"); + + 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..87a6015e86 --- /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, + autofilled: url, + completed: url, + matches: [ + makeVisitResult(context, { + uri: url, + title: url, + heuristic: true, + }), + ], + }); + await cleanupPlaces(); +}); diff --git a/browser/components/urlbar/tests/unit/test_escaping_badEscapedURI.js b/browser/components/urlbar/tests/unit/test_escaping_badEscapedURI.js new file mode 100644 index 0000000000..d330625bbb --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_escaping_badEscapedURI.js @@ -0,0 +1,37 @@ +/* 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 bug 422277 to make sure bad escaped uris don't get escaped. This makes + * sure we don't hit an assertion for "not a UTF8 string". + */ + +testEngine_setup(); + +add_task(async function test() { + Services.prefs.setBoolPref("browser.urlbar.autoFill", false); + Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.urlbar.suggest.searches"); + }); + + info("Bad escaped uri stays escaped"); + let uri1 = Services.io.newURI("http://site/%EAid"); + await PlacesTestUtils.addVisits([{ uri: uri1, title: "title" }]); + let context = createContext("site", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { + uri: uri1.spec, + title: "title", + }), + ], + }); + await cleanupPlaces(); +}); diff --git a/browser/components/urlbar/tests/unit/test_escaping_escapeSelf.js b/browser/components/urlbar/tests/unit/test_escaping_escapeSelf.js new file mode 100644 index 0000000000..73fe3940c3 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_escaping_escapeSelf.js @@ -0,0 +1,62 @@ +/* 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 bug 422698 to make sure searches with urls from the location bar + * correctly match itself when it contains escaped characters. + */ + +testEngine_setup(); + +add_task(async function test_escape() { + Services.prefs.setBoolPref("browser.urlbar.autoFill", false); + Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.urlbar.suggest.searches"); + }); + + let uri1 = Services.io.newURI("http://unescapeduri/"); + let uri2 = Services.io.newURI("http://escapeduri/%40/"); + await PlacesTestUtils.addVisits([ + { uri: uri1, title: "title" }, + { uri: uri2, title: "title" }, + ]); + + info("Unescaped location matches itself"); + let context = createContext("http://unescapeduri/", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + uri: uri1.spec, + title: "title", + iconUri: `page-icon:${uri1.spec}`, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + heuristic: true, + }), + // Note that uri2 does not appear in results. + ], + }); + + info("Escaped location matches itself"); + context = createContext("http://escapeduri/%40", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + uri: "http://escapeduri/%40", + fallbackTitle: "http://escapeduri/%40", + iconUri: "page-icon:http://escapeduri/", + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + heuristic: true, + }), + makeVisitResult(context, { + uri: uri2.spec, + title: "title", + }), + ], + }); + + await cleanupPlaces(); +}); diff --git a/browser/components/urlbar/tests/unit/test_exposure.js b/browser/components/urlbar/tests/unit/test_exposure.js new file mode 100644 index 0000000000..837673a868 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_exposure.js @@ -0,0 +1,195 @@ +/* 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/. */ + +ChromeUtils.defineESModuleGetters(this, { + QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs", + UrlbarProviderQuickSuggest: + "resource:///modules/UrlbarProviderQuickSuggest.sys.mjs", +}); + +// Tests that registering an exposureResults pref and triggering a match causes +// the exposure event to be recorded on the UrlbarResults. +const REMOTE_SETTINGS_RESULTS = [ + { + id: 1, + url: "http://test.com/q=frabbits", + title: "frabbits", + keywords: ["test"], + click_url: "http://click.reporting.test.com/", + impression_url: "http://impression.reporting.test.com/", + advertiser: "TestAdvertiser", + }, + { + id: 2, + url: "http://test.com/q=frabbits", + title: "frabbits", + keywords: ["non_sponsored"], + click_url: "http://click.reporting.test.com/", + impression_url: "http://impression.reporting.test.com/", + advertiser: "wikipedia", + iab_category: "5 - Education", + }, +]; + +const EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT = { + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + heuristic: false, + payload: { + telemetryType: "adm_sponsored", + qsSuggestion: "test", + title: "frabbits", + url: "http://test.com/q=frabbits", + originalUrl: "http://test.com/q=frabbits", + icon: null, + sponsoredImpressionUrl: "http://impression.reporting.test.com/", + sponsoredClickUrl: "http://click.reporting.test.com/", + sponsoredBlockId: 1, + sponsoredAdvertiser: "TestAdvertiser", + isSponsored: true, + helpUrl: QuickSuggest.HELP_URL, + helpL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-learn-more-about-firefox-suggest" + : "firefox-suggest-urlbar-learn-more", + }, + isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"), + blockL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-dismiss-firefox-suggest" + : "firefox-suggest-urlbar-block", + }, + displayUrl: "http://test.com/q=frabbits", + source: "remote-settings", + }, +}; + +const EXPECTED_NON_SPONSORED_REMOTE_SETTINGS_RESULT = { + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + heuristic: false, + payload: { + telemetryType: "adm_nonsponsored", + qsSuggestion: "non_sponsored", + title: "frabbits", + url: "http://test.com/q=frabbits", + originalUrl: "http://test.com/q=frabbits", + icon: null, + sponsoredImpressionUrl: "http://impression.reporting.test.com/", + sponsoredClickUrl: "http://click.reporting.test.com/", + sponsoredBlockId: 2, + sponsoredAdvertiser: "wikipedia", + sponsoredIabCategory: "5 - Education", + isSponsored: false, + helpUrl: QuickSuggest.HELP_URL, + helpL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-learn-more-about-firefox-suggest" + : "firefox-suggest-urlbar-learn-more", + }, + isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"), + blockL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-dismiss-firefox-suggest" + : "firefox-suggest-urlbar-block", + }, + displayUrl: "http://test.com/q=frabbits", + source: "remote-settings", + }, +}; + +add_setup(async function test_setup() { + // FOG needs a profile directory to put its data in. + do_get_profile(); + + // FOG needs to be initialized in order for data to flow. + Services.fog.initializeFOG(); + + UrlbarPrefs.set("quicksuggest.enabled", true); + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + UrlbarPrefs.set("quicksuggest.shouldShowOnboardingDialog", false); + + await MerinoTestUtils.server.start(); + + // Set up the remote settings client with the test data. + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + remoteSettingsResults: [ + { + type: "data", + attachment: REMOTE_SETTINGS_RESULTS, + }, + ], + }); +}); + +add_task(async function testExposureCheck() { + UrlbarPrefs.set("quicksuggest.remoteSettings.enabled", true); + UrlbarPrefs.set("exposureResults", "rs_adm_sponsored"); + UrlbarPrefs.set("showExposureResults", true); + + let context = createContext("test", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + + await check_results({ + context, + matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT], + }); + + Assert.equal(context.results[0].exposureResultType, "rs_adm_sponsored"); + Assert.equal(context.results[0].exposureResultHidden, false); +}); + +add_task(async function testExposureCheckMultiple() { + UrlbarPrefs.set("quicksuggest.remoteSettings.enabled", true); + UrlbarPrefs.set("exposureResults", "rs_adm_sponsored,rs_adm_nonsponsored"); + UrlbarPrefs.set("showExposureResults", true); + + let context = createContext("test", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + + await check_results({ + context, + matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT], + }); + + Assert.equal(context.results[0].exposureResultType, "rs_adm_sponsored"); + Assert.equal(context.results[0].exposureResultHidden, false); + + context = createContext("non_sponsored", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + + await check_results({ + context, + matches: [EXPECTED_NON_SPONSORED_REMOTE_SETTINGS_RESULT], + }); + + Assert.equal(context.results[0].exposureResultType, "rs_adm_nonsponsored"); + Assert.equal(context.results[0].exposureResultHidden, false); +}); + +add_task(async function exposureDisplayFiltering() { + UrlbarPrefs.set("quicksuggest.remoteSettings.enabled", true); + UrlbarPrefs.set("exposureResults", "rs_adm_sponsored"); + UrlbarPrefs.set("showExposureResults", false); + + let context = createContext("test", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + + await check_results({ + context, + matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT], + }); + + Assert.equal(context.results[0].exposureResultType, "rs_adm_sponsored"); + Assert.equal(context.results[0].exposureResultHidden, true); +}); diff --git a/browser/components/urlbar/tests/unit/test_frecency.js b/browser/components/urlbar/tests/unit/test_frecency.js new file mode 100644 index 0000000000..0d7a007e0d --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_frecency.js @@ -0,0 +1,403 @@ +/* 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 bug 406358 to make sure frecency works for empty input/search, but + * this also tests for non-empty inputs as well. Because the interactions among + * DIFFERENT* visit counts and visit dates is not well defined, this test + * holds one of the two values constant when modifying the other. + * + * Also test bug 419068 to make sure tagged pages don't necessarily have to be + * first in the results. + * + * Also test bug 426166 to make sure that the results of autocomplete searches + * are stable. Note that failures of this test will be intermittent by nature + * since we are testing to make sure that the unstable sort algorithm used + * by SQLite is not changing the order of the results on us. + */ + +testEngine_setup(); + +async function task_setCountDate(uri, count, date) { + // We need visits so that frecency can be computed over multiple visits + let visits = []; + for (let i = 0; i < count; i++) { + visits.push({ + uri, + visitDate: date, + transition: PlacesUtils.history.TRANSITION_TYPED, + }); + } + await PlacesTestUtils.addVisits(visits); +} + +async function setBookmark(uri) { + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + url: uri, + title: "bleh", + }); +} + +async function tagURI(uri, tags) { + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: uri, + title: "bleh", + }); + PlacesUtils.tagging.tagURI(uri, tags); +} + +var uri1 = Services.io.newURI("http://site.tld/1"); +var uri2 = Services.io.newURI("http://site.tld/2"); +var uri3 = Services.io.newURI("http://aaaaaaaaaa/1"); +var uri4 = Services.io.newURI("http://aaaaaaaaaa/2"); + +// d1 is younger (should show up higher) than d2 (PRTime is in usecs not msec) +// Make sure the dates fall into different frecency groups +var d1 = new Date(Date.now() - 1000 * 60 * 60) * 1000; +var d2 = new Date(Date.now() - 1000 * 60 * 60 * 24 * 10) * 1000; +// c1 is larger (should show up higher) than c2 +var c1 = 10; +var c2 = 1; + +var tests = [ + // test things without a search term + async function () { + info("Test 0: same count, different date"); + await task_setCountDate(uri1, c1, d1); + await task_setCountDate(uri2, c1, d2); + await tagURI(uri1, ["site"]); + let context = createContext(" ", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + query: " ", + }), + // uri1 is a visit result despite being a tagged bookmark because we + // are searching for the empty string. By default, the empty string + // filters to history. uri1 will be displayed as a bookmark later in the + // test when we are searching with a non-empty string. + makeVisitResult(context, { + uri: uri1.spec, + title: "bleh", + }), + makeVisitResult(context, { + uri: uri2.spec, + title: `test visit for ${uri2.spec}`, + }), + ], + }); + }, + async function () { + info("Test 1: same count, different date"); + await task_setCountDate(uri1, c1, d2); + await task_setCountDate(uri2, c1, d1); + await tagURI(uri1, ["site"]); + let context = createContext(" ", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + query: " ", + }), + makeVisitResult(context, { + uri: uri2.spec, + title: `test visit for ${uri2.spec}`, + }), + makeVisitResult(context, { + uri: uri1.spec, + title: "bleh", + }), + ], + }); + }, + async function () { + info("Test 2: different count, same date"); + await task_setCountDate(uri1, c1, d1); + await task_setCountDate(uri2, c2, d1); + await tagURI(uri1, ["site"]); + let context = createContext(" ", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + query: " ", + }), + makeVisitResult(context, { + uri: uri1.spec, + title: "bleh", + }), + makeVisitResult(context, { + uri: uri2.spec, + title: `test visit for ${uri2.spec}`, + }), + ], + }); + }, + async function () { + info("Test 3: different count, same date"); + await task_setCountDate(uri1, c2, d1); + await task_setCountDate(uri2, c1, d1); + await tagURI(uri1, ["site"]); + let context = createContext(" ", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + query: " ", + }), + makeVisitResult(context, { + uri: uri2.spec, + title: `test visit for ${uri2.spec}`, + }), + makeVisitResult(context, { + uri: uri1.spec, + title: "bleh", + }), + ], + }); + }, + + // test things with a search term + async function () { + info("Test 4: same count, different date"); + await task_setCountDate(uri1, c1, d1); + await task_setCountDate(uri2, c1, d2); + await tagURI(uri1, ["site"]); + let context = createContext("site", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeBookmarkResult(context, { + uri: uri1.spec, + title: "bleh", + tags: ["site"], + }), + makeVisitResult(context, { + uri: uri2.spec, + title: `test visit for ${uri2.spec}`, + }), + ], + }); + }, + async function () { + info("Test 5: same count, different date"); + await task_setCountDate(uri1, c1, d2); + await task_setCountDate(uri2, c1, d1); + await tagURI(uri1, ["site"]); + let context = createContext("site", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { + uri: uri2.spec, + title: `test visit for ${uri2.spec}`, + }), + makeBookmarkResult(context, { + uri: uri1.spec, + title: "bleh", + tags: ["site"], + }), + ], + }); + }, + async function () { + info("Test 6: different count, same date"); + await task_setCountDate(uri1, c1, d1); + await task_setCountDate(uri2, c2, d1); + await tagURI(uri1, ["site"]); + let context = createContext("site", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeBookmarkResult(context, { + uri: uri1.spec, + title: "bleh", + tags: ["site"], + }), + makeVisitResult(context, { + uri: uri2.spec, + title: `test visit for ${uri2.spec}`, + }), + ], + }); + }, + async function () { + info("Test 7: different count, same date"); + await task_setCountDate(uri1, c2, d1); + await task_setCountDate(uri2, c1, d1); + await tagURI(uri1, ["site"]); + let context = createContext("site", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { + uri: uri2.spec, + title: `test visit for ${uri2.spec}`, + }), + makeBookmarkResult(context, { + uri: uri1.spec, + title: "bleh", + tags: ["site"], + }), + ], + }); + }, + // There are multiple tests for 8, hence the multiple functions + // Bug 426166 section + async function () { + info("Test 8.1a: same count, same date"); + await setBookmark(uri3); + await setBookmark(uri4); + let context = createContext("a", { isPrivate: false }); + let bookmarkResults = [ + makeBookmarkResult(context, { + uri: uri4.spec, + title: "bleh", + }), + makeBookmarkResult(context, { + uri: uri3.spec, + title: "bleh", + }), + ]; + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ...bookmarkResults, + ], + }); + + context = createContext("aa", { isPrivate: false }); + await check_results({ + context, + matches: [ + // We need to continuously redefine the heuristic search result because it + // is the only one that changes with the search string. + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ...bookmarkResults, + ], + }); + + context = createContext("aaa", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ...bookmarkResults, + ], + }); + + context = createContext("aaaa", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ...bookmarkResults, + ], + }); + + context = createContext("aaa", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ...bookmarkResults, + ], + }); + + context = createContext("aa", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ...bookmarkResults, + ], + }); + + context = createContext("a", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ...bookmarkResults, + ], + }); + }, +]; + +add_task(async function test_frecency() { + Services.prefs.setBoolPref("browser.urlbar.autoFill", false); + // always search in history + bookmarks, no matter what the default is + Services.prefs.setBoolPref("browser.urlbar.suggest.history", true); + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true); + Services.prefs.setBoolPref("browser.urlbar.suggest.openpage", false); + Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false); + Services.prefs.setBoolPref("browser.urlbar.suggest.engines", false); + Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false); + for (let test of tests) { + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); + + await test(); + } + for (let type of [ + "history", + "bookmark", + "openpage", + "searches", + "engines", + "quickactions", + ]) { + Services.prefs.clearUserPref("browser.urlbar.suggest." + type); + Services.prefs.clearUserPref("browser.urlbar.autoFill"); + } +}); diff --git a/browser/components/urlbar/tests/unit/test_frecency_alternative_nimbus.js b/browser/components/urlbar/tests/unit/test_frecency_alternative_nimbus.js new file mode 100644 index 0000000000..c2d0e4bae3 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_frecency_alternative_nimbus.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const ORIGINS_FEATUREGATE = "places.frecency.origins.alternative.featureGate"; +const ORIGINS_DAYSCUTOFF = "places.frecency.origins.alternative.daysCutOff"; + +add_task(async function () { + let reset = await UrlbarTestUtils.initNimbusFeature( + { + // Empty for sanity check. + }, + "frecency", + "config" + ); + Assert.equal(Services.prefs.getBoolPref(ORIGINS_FEATUREGATE), false); + Assert.throws( + () => Services.prefs.getIntPref(ORIGINS_DAYSCUTOFF), + /NS_ERROR_UNEXPECTED/ + ); + await reset(); + + reset = await UrlbarTestUtils.initNimbusFeature( + { + originsAlternativeEnable: true, + }, + "frecency", + "config" + ); + Assert.equal(Services.prefs.getBoolPref(ORIGINS_FEATUREGATE), true); + Assert.ok(Services.prefs.prefHasUserValue(ORIGINS_FEATUREGATE)); + Assert.throws( + () => Services.prefs.getIntPref(ORIGINS_DAYSCUTOFF), + /NS_ERROR_UNEXPECTED/ + ); + await reset(); + + reset = await UrlbarTestUtils.initNimbusFeature( + { + originsAlternativeEnable: true, + originsDaysCutOff: 60, + }, + "frecency", + "config" + ); + Assert.equal(Services.prefs.getBoolPref(ORIGINS_FEATUREGATE), true); + Assert.ok(Services.prefs.prefHasUserValue(ORIGINS_FEATUREGATE)); + Assert.equal(Services.prefs.getIntPref(ORIGINS_DAYSCUTOFF, 90), 60); + Assert.ok(Services.prefs.prefHasUserValue(ORIGINS_DAYSCUTOFF)); + await reset(); +}); 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..a53786a747 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_heuristic_cancel.js @@ -0,0 +1,238 @@ +/* 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.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + UrlbarProviderAutofill: "resource:///modules/UrlbarProviderAutofill.sys.mjs", +}); + +/** + * 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 alerts 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); +}); + +/** + * Tests that UrlbarProvidersManager._heuristicProviderTimer is cancelled when + * a query is cancelled. + */ +add_task(async function timerIsCancelled() { + 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 UrlbarProviderPlaces 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) { + 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); +}); + +/** + * Tests that old autofill results aren't displayed after a query is cancelled. + * See bug 1653436. + */ +add_task(async function autofillIsCleared() { + /** + * Steps: + * 1. Start query. + * 2. Allow UrlbarProviderAutofill to start _getAutofillResult. + * 3. Execute a new query with no autofill match, cancelling the first + * query. + * 4. Test that the old result from UrlbarProviderAutofill isn't displayed. + */ + await PlacesTestUtils.addVisits("http://example.com"); + + let firstContext = createContext("e", { + providers: ["Autofill", "HeuristicFallback"], + }); + let secondContext = createContext("em", { + providers: ["Autofill", "HeuristicFallback"], + }); + + info("Sanity check: The first query autofills and the second does not."); + await check_results({ + firstContext, + autofilled: "example.com", + completed: "http://example.com/", + matches: [ + makeVisitResult(firstContext, { + uri: "http://example.com/", + title: "example.com", + heuristic: true, + }), + ], + }); + + await check_results({ + secondContext, + matches: [ + makeSearchResult(secondContext, { + engineName: (await Services.search.getDefault()).name, + providerName: "HeuristicFallback", + heuristic: true, + }), + ], + }); + + // Refresh our queries + firstContext = createContext("e", { + providers: ["Autofill", "HeuristicFallback"], + }); + secondContext = createContext("em", { + providers: ["Autofill", "HeuristicFallback"], + }); + + // Set up controller to observe queries. + let controller = UrlbarTestUtils.newMockController(); + let queryRecieved, queryCancelled; + const controllerListener = { + onQueryResults(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( + !UrlbarProviderAutofill._autofillData, + "The first result should not have populated autofill data." + ); + Assert.ok(!queryCancelled, "No more than one query should be cancelled."); + queryCancelled = true; + }, + }; + controller.addQueryListener(controllerListener); + + // Intentionally do not await this first query. + controller.startQuery(firstContext); + await controller.startQuery(secondContext); + + Assert.ok(queryCancelled, "At least one query was cancelled."); + Assert.ok(queryRecieved, "At least one query finished."); + controller.removeQueryListener(controllerListener); + await cleanupPlaces(); +}); diff --git a/browser/components/urlbar/tests/unit/test_hideSponsoredHistory.js b/browser/components/urlbar/tests/unit/test_hideSponsoredHistory.js new file mode 100644 index 0000000000..d94a655b22 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_hideSponsoredHistory.js @@ -0,0 +1,104 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This tests the muxer functionality that hides URLs in history that were +// originally sponsored. + +"use strict"; + +add_task(async function test() { + // Disable search suggestions to avoid hitting the network. + UrlbarPrefs.set("suggest.searches", false); + + let engine = await Services.search.getDefault(); + let pref = "browser.newtabpage.activity-stream.hideTopSitesWithSearchParam"; + + // This maps URL search params to objects describing whether a URL with those + // params is expected to appear in the search results. Each inner object maps + // from a value of the pref to whether the URL is expected to appear given the + // pref value. + let tests = { + "": { + "": true, + test: true, + "test=": true, + "test=hide": true, + nomatch: true, + "nomatch=": true, + "nomatch=hide": true, + }, + test: { + "": true, + test: false, + "test=": false, + "test=hide": true, + nomatch: true, + "nomatch=": true, + "nomatch=hide": true, + }, + "test=hide": { + "": true, + test: false, + "test=": true, + "test=hide": false, + nomatch: true, + "nomatch=": true, + "nomatch=hide": true, + }, + "test=foo&test=hide": { + "": true, + test: false, + "test=": true, + "test=hide": false, + nomatch: true, + "nomatch=": true, + "nomatch=hide": true, + }, + }; + + for (let [urlParams, expected] of Object.entries(tests)) { + for (let [prefValue, shouldAppear] of Object.entries(expected)) { + info( + "Running test: " + + JSON.stringify({ urlParams, prefValue, shouldAppear }) + ); + + // Add a visit to a URL with search params `urlParams`. + let url = new URL("http://example.com/"); + url.search = urlParams; + await PlacesTestUtils.addVisits(url); + + // Set the pref to `prefValue`. + Services.prefs.setCharPref(pref, prefValue); + + // Set up the context and expected results. If `shouldAppear` is true, a + // visit result for the URL should appear. + let context = createContext("ample", { isPrivate: false }); + let expectedResults = [ + makeSearchResult(context, { + heuristic: true, + engineName: engine.name, + engineIconUri: engine.iconURI?.spec, + }), + ]; + if (shouldAppear) { + expectedResults.push( + makeVisitResult(context, { + uri: url.toString(), + title: "test visit for " + url, + }) + ); + } + + // Do a search and check the results. + await check_results({ + context, + matches: expectedResults, + }); + + await PlacesUtils.history.clear(); + } + } + + Services.prefs.clearUserPref(pref); +}); 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..de60742b81 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_keywords.js @@ -0,0 +1,212 @@ +/* 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/. */ + +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/", + fallbackTitle: "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/", + title: "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/", + fallbackTitle: "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/", + fallbackTitle: "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/", + title: "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: "test visit for http://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/", + title: "http://mozilla.com/", + keyword: "moz", + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://mozilla.org/test/", + title: "test visit for http://mozilla.org/test/", + }), + ], + }); + + // 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: "A bookmark", + 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_l10nCache.js b/browser/components/urlbar/tests/unit/test_l10nCache.js new file mode 100644 index 0000000000..e92c75fa01 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_l10nCache.js @@ -0,0 +1,685 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests L10nCache in UrlbarUtils.jsm. + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + L10nCache: "resource:///modules/UrlbarUtils.sys.mjs", +}); + +add_task(async function comprehensive() { + // Set up a mock localization. + let l10n = initL10n({ + args0a: "Zero args value", + args0b: "Another zero args value", + args1a: "One arg value is { $arg1 }", + args1b: "Another one arg value is { $arg1 }", + args2a: "Two arg values are { $arg1 } and { $arg2 }", + args2b: "More two arg values are { $arg1 } and { $arg2 }", + args3a: "Three arg values are { $arg1 }, { $arg2 }, and { $arg3 }", + args3b: "More three arg values are { $arg1 }, { $arg2 }, and { $arg3 }", + attrs1: [".label = attrs1 label has zero args"], + attrs2: [ + ".label = attrs2 label has zero args", + ".tooltiptext = attrs2 tooltiptext arg value is { $arg1 }", + ], + attrs3: [ + ".label = attrs3 label has zero args", + ".tooltiptext = attrs3 tooltiptext arg value is { $arg1 }", + ".alt = attrs3 alt arg values are { $arg1 } and { $arg2 }", + ], + }); + + let tests = [ + // different strings with the same number of args and also the same strings + // with different args + { + obj: { + id: "args0a", + }, + expected: { + value: "Zero args value", + attributes: null, + }, + }, + { + obj: { + id: "args0b", + }, + expected: { + value: "Another zero args value", + attributes: null, + }, + }, + { + obj: { + id: "args1a", + args: { arg1: "foo1" }, + }, + expected: { + value: "One arg value is foo1", + attributes: null, + }, + }, + { + obj: { + id: "args1a", + args: { arg1: "foo2" }, + }, + expected: { + value: "One arg value is foo2", + attributes: null, + }, + }, + { + obj: { + id: "args1b", + args: { arg1: "foo1" }, + }, + expected: { + value: "Another one arg value is foo1", + attributes: null, + }, + }, + { + obj: { + id: "args1b", + args: { arg1: "foo2" }, + }, + expected: { + value: "Another one arg value is foo2", + attributes: null, + }, + }, + { + obj: { + id: "args2a", + args: { arg1: "foo1", arg2: "bar1" }, + }, + expected: { + value: "Two arg values are foo1 and bar1", + attributes: null, + }, + }, + { + obj: { + id: "args2a", + args: { arg1: "foo2", arg2: "bar2" }, + }, + expected: { + value: "Two arg values are foo2 and bar2", + attributes: null, + }, + }, + { + obj: { + id: "args2b", + args: { arg1: "foo1", arg2: "bar1" }, + }, + expected: { + value: "More two arg values are foo1 and bar1", + attributes: null, + }, + }, + { + obj: { + id: "args2b", + args: { arg1: "foo2", arg2: "bar2" }, + }, + expected: { + value: "More two arg values are foo2 and bar2", + attributes: null, + }, + }, + { + obj: { + id: "args3a", + args: { arg1: "foo1", arg2: "bar1", arg3: "baz1" }, + }, + expected: { + value: "Three arg values are foo1, bar1, and baz1", + attributes: null, + }, + }, + { + obj: { + id: "args3a", + args: { arg1: "foo2", arg2: "bar2", arg3: "baz2" }, + }, + expected: { + value: "Three arg values are foo2, bar2, and baz2", + attributes: null, + }, + }, + { + obj: { + id: "args3b", + args: { arg1: "foo1", arg2: "bar1", arg3: "baz1" }, + }, + expected: { + value: "More three arg values are foo1, bar1, and baz1", + attributes: null, + }, + }, + { + obj: { + id: "args3b", + args: { arg1: "foo2", arg2: "bar2", arg3: "baz2" }, + }, + expected: { + value: "More three arg values are foo2, bar2, and baz2", + attributes: null, + }, + }, + + // two instances of the same string with their args swapped + { + obj: { + id: "args2a", + args: { arg1: "arg A", arg2: "arg B" }, + }, + expected: { + value: "Two arg values are arg A and arg B", + attributes: null, + }, + }, + { + obj: { + id: "args2a", + args: { arg1: "arg B", arg2: "arg A" }, + }, + expected: { + value: "Two arg values are arg B and arg A", + attributes: null, + }, + }, + + // strings with attributes + { + obj: { + id: "attrs1", + }, + expected: { + value: null, + attributes: { + label: "attrs1 label has zero args", + }, + }, + }, + { + obj: { + id: "attrs2", + args: { + arg1: "arg A", + }, + }, + expected: { + value: null, + attributes: { + label: "attrs2 label has zero args", + tooltiptext: "attrs2 tooltiptext arg value is arg A", + }, + }, + }, + { + obj: { + id: "attrs3", + args: { + arg1: "arg A", + arg2: "arg B", + }, + }, + expected: { + value: null, + attributes: { + label: "attrs3 label has zero args", + tooltiptext: "attrs3 tooltiptext arg value is arg A", + alt: "attrs3 alt arg values are arg A and arg B", + }, + }, + }, + ]; + + let cache = new L10nCache(l10n); + + // Get some non-cached strings. + Assert.ok(!cache.get({ id: "uncached1" }), "Uncached string 1"); + Assert.ok(!cache.get({ id: "uncached2", args: "foo" }), "Uncached string 2"); + + // Add each test string and get it back. + for (let { obj, expected } of tests) { + await cache.add(obj); + let message = cache.get(obj); + Assert.deepEqual( + message, + expected, + "Expected message for obj: " + JSON.stringify(obj) + ); + } + + // Get each string again to make sure each add didn't somehow mess up the + // previously added strings. + for (let { obj, expected } of tests) { + Assert.deepEqual( + cache.get(obj), + expected, + "Expected message for obj: " + JSON.stringify(obj) + ); + } + + // Delete some of the strings. We'll delete every other one to mix it up. + for (let i = 0; i < tests.length; i++) { + if (i % 2 == 0) { + let { obj } = tests[i]; + cache.delete(obj); + Assert.ok(!cache.get(obj), "Deleted from cache: " + JSON.stringify(obj)); + } + } + + // Get each remaining string. + for (let i = 0; i < tests.length; i++) { + if (i % 2 != 0) { + let { obj, expected } = tests[i]; + Assert.deepEqual( + cache.get(obj), + expected, + "Expected message for obj: " + JSON.stringify(obj) + ); + } + } + + // Clear the cache. + cache.clear(); + for (let { obj } of tests) { + Assert.ok(!cache.get(obj), "After cache clear: " + JSON.stringify(obj)); + } + + // `ensure` each test string and get it back. + for (let { obj, expected } of tests) { + await cache.ensure(obj); + let message = cache.get(obj); + Assert.deepEqual( + message, + expected, + "Expected message for obj: " + JSON.stringify(obj) + ); + + // Call `ensure` again. This time, `add` should not be called. + let originalAdd = cache.add; + cache.add = () => Assert.ok(false, "add erroneously called"); + await cache.ensure(obj); + cache.add = originalAdd; + } + + // Clear the cache again. + cache.clear(); + for (let { obj } of tests) { + Assert.ok(!cache.get(obj), "After cache clear: " + JSON.stringify(obj)); + } + + // `ensureAll` the test strings and get them back. + let objects = tests.map(({ obj }) => obj); + await cache.ensureAll(objects); + for (let { obj, expected } of tests) { + let message = cache.get(obj); + Assert.deepEqual( + message, + expected, + "Expected message for obj: " + JSON.stringify(obj) + ); + } + + // Ensure the cache is cleared after the app locale changes + Assert.greater(cache.size(), 0, "The cache has messages in it."); + Services.obs.notifyObservers(null, "intl:app-locales-changed"); + await l10n.ready; + Assert.equal(cache.size(), 0, "The cache is empty on app locale change"); +}); + +// Tests the `excludeArgsFromCacheKey` option. +add_task(async function excludeArgsFromCacheKey() { + // Set up a mock localization. + let l10n = initL10n({ + args0: "Zero args value", + args1: "One arg value is { $arg1 }", + attrs0: [".label = attrs0 label has zero args"], + attrs1: [ + ".label = attrs1 label has zero args", + ".tooltiptext = attrs1 tooltiptext arg value is { $arg1 }", + ], + }); + + let cache = new L10nCache(l10n); + + // Test cases. For each test case, we cache a string using one or more + // methods, `cache.add({ excludeArgsFromCacheKey: true })` and/or + // `cache.ensure({ excludeArgsFromCacheKey: true })`. After calling each + // method, we call `cache.get()` to get the cached string. + // + // Test cases are cumulative, so when `cache.add()` is called for a string and + // then `cache.ensure()` is called for the same string but with different l10n + // argument values, the string should be re-cached with the new values. + // + // Each item in the tests array is: `{ methods, obj, gets }` + // + // {array} methods + // Array of cache method names, one or more of: "add", "ensure" + // Methods are called in the order they are listed. + // {object} obj + // An l10n object that will be passed to the cache methods: + // `{ id, args, excludeArgsFromCacheKey }` + // {array} gets + // An array of objects that describes a series of calls to `cache.get()` and + // the expected return values: `{ obj, expected }` + // + // {object} obj + // An l10n object that will be passed to `cache.get():` + // `{ id, args, excludeArgsFromCacheKey }` + // {object} expected + // The expected return value from `get()`. + let tests = [ + // args0: string with no args and no attributes + { + methods: ["add", "ensure"], + obj: { + id: "args0", + excludeArgsFromCacheKey: true, + }, + gets: [ + { + obj: { id: "args0" }, + expected: { + value: "Zero args value", + attributes: null, + }, + }, + { + obj: { id: "args0", excludeArgsFromCacheKey: true }, + expected: { + value: "Zero args value", + attributes: null, + }, + }, + ], + }, + + // args1: string with one arg and no attributes + { + methods: ["add"], + obj: { + id: "args1", + args: { arg1: "ADD" }, + excludeArgsFromCacheKey: true, + }, + gets: [ + { + obj: { id: "args1" }, + expected: { + value: "One arg value is ADD", + attributes: null, + }, + }, + { + obj: { id: "args1", excludeArgsFromCacheKey: true }, + expected: { + value: "One arg value is ADD", + attributes: null, + }, + }, + { + obj: { + id: "args1", + args: { arg1: "some other value" }, + excludeArgsFromCacheKey: true, + }, + expected: { + value: "One arg value is ADD", + attributes: null, + }, + }, + { + obj: { + id: "args1", + args: { arg1: "some other value" }, + }, + expected: undefined, + }, + ], + }, + { + methods: ["ensure"], + obj: { + id: "args1", + args: { arg1: "ENSURE" }, + excludeArgsFromCacheKey: true, + }, + gets: [ + { + obj: { id: "args1" }, + expected: { + value: "One arg value is ENSURE", + attributes: null, + }, + }, + { + obj: { id: "args1", excludeArgsFromCacheKey: true }, + expected: { + value: "One arg value is ENSURE", + attributes: null, + }, + }, + { + obj: { + id: "args1", + args: { arg1: "some other value" }, + excludeArgsFromCacheKey: true, + }, + expected: { + value: "One arg value is ENSURE", + attributes: null, + }, + }, + { + obj: { + id: "args1", + args: { arg1: "some other value" }, + }, + expected: undefined, + }, + ], + }, + + // attrs0: string with no args and one attribute + { + methods: ["add", "ensure"], + obj: { + id: "attrs0", + excludeArgsFromCacheKey: true, + }, + gets: [ + { + obj: { id: "attrs0" }, + expected: { + value: null, + attributes: { + label: "attrs0 label has zero args", + }, + }, + }, + { + obj: { id: "attrs0", excludeArgsFromCacheKey: true }, + expected: { + value: null, + attributes: { + label: "attrs0 label has zero args", + }, + }, + }, + ], + }, + + // attrs1: string with one arg and two attributes + { + methods: ["add"], + obj: { + id: "attrs1", + args: { arg1: "ADD" }, + excludeArgsFromCacheKey: true, + }, + gets: [ + { + obj: { id: "attrs1" }, + expected: { + value: null, + attributes: { + label: "attrs1 label has zero args", + tooltiptext: "attrs1 tooltiptext arg value is ADD", + }, + }, + }, + { + obj: { id: "attrs1", excludeArgsFromCacheKey: true }, + expected: { + value: null, + attributes: { + label: "attrs1 label has zero args", + tooltiptext: "attrs1 tooltiptext arg value is ADD", + }, + }, + }, + { + obj: { + id: "attrs1", + args: { arg1: "some other value" }, + excludeArgsFromCacheKey: true, + }, + expected: { + value: null, + attributes: { + label: "attrs1 label has zero args", + tooltiptext: "attrs1 tooltiptext arg value is ADD", + }, + }, + }, + { + obj: { + id: "attrs1", + args: { arg1: "some other value" }, + }, + expected: undefined, + }, + ], + }, + { + methods: ["ensure"], + obj: { + id: "attrs1", + args: { arg1: "ENSURE" }, + excludeArgsFromCacheKey: true, + }, + gets: [ + { + obj: { id: "attrs1" }, + expected: { + value: null, + attributes: { + label: "attrs1 label has zero args", + tooltiptext: "attrs1 tooltiptext arg value is ENSURE", + }, + }, + }, + { + obj: { id: "attrs1", excludeArgsFromCacheKey: true }, + expected: { + value: null, + attributes: { + label: "attrs1 label has zero args", + tooltiptext: "attrs1 tooltiptext arg value is ENSURE", + }, + }, + }, + { + obj: { + id: "attrs1", + args: { arg1: "some other value" }, + excludeArgsFromCacheKey: true, + }, + expected: { + value: null, + attributes: { + label: "attrs1 label has zero args", + tooltiptext: "attrs1 tooltiptext arg value is ENSURE", + }, + }, + }, + { + obj: { + id: "attrs1", + args: { arg1: "some other value" }, + }, + expected: undefined, + }, + ], + }, + ]; + + let sandbox = sinon.createSandbox(); + let spy = sandbox.spy(cache, "add"); + + for (let { methods, obj, gets } of tests) { + for (let method of methods) { + info(`Calling method '${method}' with l10n obj: ` + JSON.stringify(obj)); + await cache[method](obj); + + // `add()` should always be called: We either just called it directly, or + // `ensure({ excludeArgsFromCacheKey: true })` called it. + Assert.ok( + spy.calledOnce, + "add() should have been called once: " + JSON.stringify(obj) + ); + spy.resetHistory(); + + for (let { obj: getObj, expected } of gets) { + Assert.deepEqual( + cache.get(getObj), + expected, + "Expected message for get: " + JSON.stringify(getObj) + ); + } + } + } + + sandbox.restore(); +}); + +/** + * Sets up a mock localization. + * + * @param {object} pairs + * Fluent strings as key-value pairs. + * @returns {Localization} + * The mock Localization object. + */ +function initL10n(pairs) { + let source = Object.entries(pairs) + .map(([key, value]) => { + if (Array.isArray(value)) { + value = value.map(s => " \n" + s).join(""); + } + return `${key} = ${value}`; + }) + .join("\n"); + let registry = new L10nRegistry(); + registry.registerSources([ + L10nFileSource.createMock( + "test", + "app", + ["en-US"], + "/localization/{locale}", + [{ source, path: "/localization/en-US/test.ftl" }] + ), + ]); + return new Localization(["/test.ftl"], true, registry, ["en-US"]); +} diff --git a/browser/components/urlbar/tests/unit/test_local_suggest_prefs.js b/browser/components/urlbar/tests/unit/test_local_suggest_prefs.js new file mode 100644 index 0000000000..2d03cc4c54 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_local_suggest_prefs.js @@ -0,0 +1,126 @@ +/* -*- 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/. */ + +// Test for following preferences related to local suggest. +// * browser.urlbar.suggest.bookmark +// * browser.urlbar.suggest.history +// * browser.urlbar.suggest.openpage + +testEngine_setup(); + +add_task(async function setup() { + Services.prefs.setBoolPref("browser.urlbar.autoFill", false); + Services.prefs.setBoolPref("browser.urlbar.suggest.engines", false); + Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false); + + const uri = Services.io.newURI("http://example.com/"); + + await PlacesTestUtils.addVisits([{ uri, title: "example" }]); + await PlacesUtils.bookmarks.insert({ + url: uri, + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + }); + await addOpenPages(uri); + + registerCleanupFunction(async () => { + Services.prefs.clearUserPref("browser.urlbar.autoFill"); + Services.prefs.clearUserPref("browser.urlbar.suggest.engines"); + Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions"); + + Services.prefs.clearUserPref("browser.urlbar.suggest.bookmark"); + Services.prefs.clearUserPref("browser.urlbar.suggest.history"); + Services.prefs.clearUserPref("browser.urlbar.suggest.openpage"); + await cleanupPlaces(); + }); +}); + +add_task(async function test_prefs() { + const testData = [ + { + bookmark: true, + history: true, + openpage: true, + }, + { + bookmark: false, + history: true, + openpage: true, + }, + { + bookmark: true, + history: false, + openpage: true, + }, + { + bookmark: true, + history: true, + openpage: false, + }, + { + bookmark: false, + history: false, + openpage: true, + }, + { + bookmark: false, + history: true, + openpage: false, + }, + { + bookmark: true, + history: false, + openpage: false, + }, + { + bookmark: false, + history: false, + openpage: false, + }, + ]; + + for (const { bookmark, history, openpage } of testData) { + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", bookmark); + Services.prefs.setBoolPref("browser.urlbar.suggest.history", history); + Services.prefs.setBoolPref("browser.urlbar.suggest.openpage", openpage); + + info(`Test bookmark:${bookmark} history:${history} openpage:${openpage}`); + + const context = createContext("e", { isPrivate: false }); + const matches = []; + + matches.push( + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }) + ); + + if (openpage) { + matches.push( + makeTabSwitchResult(context, { + uri: "http://example.com/", + title: "example", + }) + ); + } else if (bookmark) { + matches.push( + makeBookmarkResult(context, { + uri: "http://example.com/", + title: "example", + }) + ); + } else if (history) { + matches.push( + makeVisitResult(context, { + uri: "http://example.com/", + title: "example", + }) + ); + } + + await check_results({ context, matches }); + } +}); diff --git a/browser/components/urlbar/tests/unit/test_match_javascript.js b/browser/components/urlbar/tests/unit/test_match_javascript.js new file mode 100644 index 0000000000..6cf7126d41 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_match_javascript.js @@ -0,0 +1,155 @@ +/* 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 bug 417798 to make sure javascript: URIs don't show up unless the + * user searches for javascript: explicitly. + */ + +testEngine_setup(); + +add_task(async function test_javascript_match() { + Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", false); + Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false); + Services.prefs.setBoolPref("browser.urlbar.suggest.engines", false); + Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.urlbar.autoFill.searchEngines"); + Services.prefs.clearUserPref("browser.urlbar.suggest.searches"); + Services.prefs.clearUserPref("browser.urlbar.suggest.engines"); + Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions"); + }); + + let uri1 = Services.io.newURI("http://abc/def"); + let uri2 = Services.io.newURI("javascript:5"); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: uri2, + title: "Title with javascript:", + }); + await PlacesTestUtils.addVisits([ + { uri: uri1, title: "Title with javascript:" }, + ]); + + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + info("Match non-javascript: with plain search"); + let context = createContext("a", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { + uri: uri1.spec, + title: "Title with javascript:", + }), + ], + }); + + info("Match non-javascript: with 'javascript'"); + context = createContext("javascript", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { + uri: uri1.spec, + title: "Title with javascript:", + }), + ], + }); + + info("Match non-javascript with 'javascript:'"); + context = createContext("javascript:", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { + uri: uri1.spec, + title: "Title with javascript:", + }), + ], + }); + + info("Match nothing with '5 javascript:'"); + context = createContext("5 javascript:", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ], + }); + + info("Match non-javascript: with 'a javascript:'"); + context = createContext("a javascript:", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { + uri: uri1.spec, + title: "Title with javascript:", + }), + ], + }); + + info("Match non-javascript: and javascript: with 'javascript: a'"); + context = createContext("javascript: a", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + uri: "javascript: a", + fallbackTitle: "javascript: a", + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + heuristic: true, + }), + makeVisitResult(context, { + uri: uri1.spec, + title: "Title with javascript:", + }), + makeBookmarkResult(context, { + uri: uri2.spec, + iconUri: "chrome://global/skin/icons/defaultFavicon.svg", + title: "Title with javascript:", + }), + ], + }); + + info("Match javascript: with 'javascript: 5'"); + context = createContext("javascript: 5", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + uri: "javascript: 5", + fallbackTitle: "javascript: 5", + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + heuristic: true, + }), + makeBookmarkResult(context, { + uri: uri2.spec, + iconUri: "chrome://global/skin/icons/defaultFavicon.svg", + title: "Title with javascript:", + }), + ], + }); + + await cleanupPlaces(); +}); diff --git a/browser/components/urlbar/tests/unit/test_multi_word_search.js b/browser/components/urlbar/tests/unit/test_multi_word_search.js new file mode 100644 index 0000000000..7054feb8aa --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_multi_word_search.js @@ -0,0 +1,126 @@ +/* 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 bug 401869 to allow multiple words separated by spaces to match in + * the page title, page url, or bookmark title to be considered a match. All + * terms must match but not all terms need to be in the title, etc. + * + * Test bug 424216 by making sure bookmark titles are always shown if one is + * available. Also bug 425056 makes sure matches aren't found partially in the + * page title and partially in the bookmark. + */ + +testEngine_setup(); + +add_task(async function test_match_beginning() { + Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.urlbar.suggest.searches"); + }); + + let uri1 = Services.io.newURI("http://a.b.c/d-e_f/h/t/p"); + let uri2 = Services.io.newURI("http://d.e.f/g-h_i/h/t/p"); + let uri3 = Services.io.newURI("http://g.h.i/j-k_l/h/t/p"); + let uri4 = Services.io.newURI("http://j.k.l/m-n_o/h/t/p"); + await PlacesTestUtils.addVisits([ + { uri: uri4, title: "f(o)o br" }, + { uri: uri3, title: "f(o)o br" }, + { uri: uri2, title: "b(a)r bz" }, + { uri: uri1, title: "f(o)o br" }, + ]); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: uri3, + title: "f(o)o br", + }); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: uri4, + title: "b(a)r bz", + }); + + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + info("Match 2 terms all in url"); + let context = createContext("c d", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { uri: uri1.spec, title: "f(o)o br" }), + ], + }); + + info("Match 1 term in url and 1 term in title"); + context = createContext("b e", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { uri: uri1.spec, title: "f(o)o br" }), + makeVisitResult(context, { uri: uri2.spec, title: "b(a)r bz" }), + ], + }); + + info("Match 3 terms all in title; display bookmark title if matched"); + context = createContext("b a z", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeBookmarkResult(context, { uri: uri4.spec, title: "b(a)r bz" }), + makeVisitResult(context, { uri: uri2.spec, title: "b(a)r bz" }), + ], + }); + + info( + "Match 2 terms in url and 1 in title; make sure bookmark title is used for search" + ); + context = createContext("k f t", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeBookmarkResult(context, { uri: uri3.spec, title: "f(o)o br" }), + ], + }); + + info("Match 3 terms in url and 1 in title"); + context = createContext("d i g z", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { uri: uri2.spec, title: "b(a)r bz" }), + ], + }); + + info("Match nothing"); + context = createContext("m o z i", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ], + }); + + 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..dcea820835 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_muxer.js @@ -0,0 +1,731 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + sinon: "resource://testing-common/Sinon.sys.mjs", +}); + +let sandbox; + +add_task(async function setup() { + sandbox = lazy.sinon.createSandbox(); + registerCleanupFunction(() => { + sandbox.restore(); + }); +}); + +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 provider = registerBasicTestProvider(matches); + let context = createContext(undefined, { providers: [provider.name] }); + let controller = UrlbarTestUtils.newMockController(); + /** + * A test muxer. + */ + class TestMuxer extends UrlbarMuxer { + get name() { + return "TestMuxer"; + } + sort(queryContext, unsortedResults) { + queryContext.results = [...unsortedResults].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 provider = registerBasicTestProvider(matches); + let context = createContext(undefined, { + providers: [provider.name], + }); + 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 provider1 = registerBasicTestProvider(matches1); + let provider2 = registerBasicTestProvider(matches2); + + let context = createContext(undefined, { + providers: [provider1.name, provider2.name], + }); + 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 provider = registerBasicTestProvider(matches); + + let context = createContext(undefined, { + providers: [provider.name], + }); + 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"); +}); + +add_task(async function test_deduplicate_for_unitConversion() { + const searchSuggestion = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.SEARCH, + UrlbarUtils.RESULT_SOURCE.SEARCH, + { + engine: "Google", + query: "10cm to m", + suggestion: "= 0.1 meters", + } + ); + const searchProvider = registerBasicTestProvider( + [searchSuggestion], + null, + UrlbarUtils.PROVIDER_TYPE.PROFILE + ); + + const unitConversionSuggestion = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.DYNAMIC, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + dynamicType: "unitConversion", + output: "0.1 m", + input: "10cm to m", + } + ); + unitConversionSuggestion.suggestedIndex = 1; + + const unitConversion = registerBasicTestProvider( + [unitConversionSuggestion], + null, + UrlbarUtils.PROVIDER_TYPE.PROFILE, + "UnitConversion" + ); + + const context = createContext(undefined, { + providers: [searchProvider.name, unitConversion.name], + }); + const controller = UrlbarTestUtils.newMockController(); + await UrlbarProvidersManager.startQuery(context, controller); + Assert.deepEqual(context.results, [unitConversionSuggestion]); +}); + +// These results are used in the badHeuristicGroups tests below. The order of +// the results in the array isn't important because they all get added at the +// same time. It's the resultGroups in each test that is important. +const BAD_HEURISTIC_RESULTS = [ + // heuristic + Object.assign( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/heuristic-0" } + ), + { heuristic: true } + ), + // heuristic + Object.assign( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/heuristic-1" } + ), + { heuristic: true } + ), + // non-heuristic + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/non-heuristic-0" } + ), + // non-heuristic + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/non-heuristic-1" } + ), +]; + +const BAD_HEURISTIC_RESULTS_FIRST_HEURISTIC = BAD_HEURISTIC_RESULTS[0]; +const BAD_HEURISTIC_RESULTS_GENERAL = [ + BAD_HEURISTIC_RESULTS[2], + BAD_HEURISTIC_RESULTS[3], +]; + +add_task(async function test_badHeuristicGroups_multiple_0() { + await doBadHeuristicGroupsTest( + [ + // 2 heuristics with child groups + { + maxResultCount: 2, + children: [{ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }], + }, + // infinite general + { + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + ], + [BAD_HEURISTIC_RESULTS_FIRST_HEURISTIC, ...BAD_HEURISTIC_RESULTS_GENERAL] + ); +}); + +add_task(async function test_badHeuristicGroups_multiple_1() { + await doBadHeuristicGroupsTest( + [ + // infinite heuristics with child groups + { + children: [{ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }], + }, + // infinite general + { + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + ], + [BAD_HEURISTIC_RESULTS_FIRST_HEURISTIC, ...BAD_HEURISTIC_RESULTS_GENERAL] + ); +}); + +add_task(async function test_badHeuristicGroups_multiple_2() { + await doBadHeuristicGroupsTest( + [ + // 2 heuristics + { + maxResultCount: 2, + group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST, + }, + // infinite general + { + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + ], + [BAD_HEURISTIC_RESULTS_FIRST_HEURISTIC, ...BAD_HEURISTIC_RESULTS_GENERAL] + ); +}); + +add_task(async function test_badHeuristicGroups_multiple_3() { + await doBadHeuristicGroupsTest( + [ + // infinite heuristics + { + group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST, + }, + // infinite general + { + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + ], + [BAD_HEURISTIC_RESULTS_FIRST_HEURISTIC, ...BAD_HEURISTIC_RESULTS_GENERAL] + ); +}); + +add_task(async function test_badHeuristicGroups_multiple_4() { + await doBadHeuristicGroupsTest( + [ + // 1 heuristic with child groups + { + maxResultCount: 1, + children: [{ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }], + }, + // infinite general + { + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + // 1 heuristic with child groups + { + maxResultCount: 1, + children: [{ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }], + }, + ], + [BAD_HEURISTIC_RESULTS_FIRST_HEURISTIC, ...BAD_HEURISTIC_RESULTS_GENERAL] + ); +}); + +add_task(async function test_badHeuristicGroups_multiple_5() { + await doBadHeuristicGroupsTest( + [ + // infinite heuristics with child groups + { + children: [{ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }], + }, + // infinite general + { + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + // infinite heuristics with child groups + { + children: [{ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }], + }, + ], + [BAD_HEURISTIC_RESULTS_FIRST_HEURISTIC, ...BAD_HEURISTIC_RESULTS_GENERAL] + ); +}); + +add_task(async function test_badHeuristicGroups_multiple_6() { + await doBadHeuristicGroupsTest( + [ + // 1 heuristic + { + maxResultCount: 1, + group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST, + }, + // infinite general + { + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + // 1 heuristic + { + maxResultCount: 1, + group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST, + }, + ], + [BAD_HEURISTIC_RESULTS_FIRST_HEURISTIC, ...BAD_HEURISTIC_RESULTS_GENERAL] + ); +}); + +add_task(async function test_badHeuristicGroups_multiple_7() { + await doBadHeuristicGroupsTest( + [ + // infinite heuristics + { + group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST, + }, + // infinite general + { + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + // infinite heuristics + { + group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST, + }, + ], + [BAD_HEURISTIC_RESULTS_FIRST_HEURISTIC, ...BAD_HEURISTIC_RESULTS_GENERAL] + ); +}); + +add_task(async function test_badHeuristicsGroups_notFirst_0() { + await doBadHeuristicGroupsTest( + [ + // infinite general first + { + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + // 1 heuristic with child groups second + { + maxResultCount: 1, + children: [{ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }], + }, + ], + [...BAD_HEURISTIC_RESULTS_GENERAL] + ); +}); + +add_task(async function test_badHeuristicsGroups_notFirst_1() { + await doBadHeuristicGroupsTest( + [ + // infinite general first + { + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + // infinite heuristics with child groups second + { + children: [{ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }], + }, + ], + [...BAD_HEURISTIC_RESULTS_GENERAL] + ); +}); + +add_task(async function test_badHeuristicsGroups_notFirst_2() { + await doBadHeuristicGroupsTest( + [ + // infinite general first + { + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + // 1 heuristic second + { + maxResultCount: 1, + group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST, + }, + ], + [...BAD_HEURISTIC_RESULTS_GENERAL] + ); +}); + +add_task(async function test_badHeuristicsGroups_notFirst_3() { + await doBadHeuristicGroupsTest( + [ + // infinite general first + { + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + // infinite heuristics second + { + group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST, + }, + ], + [...BAD_HEURISTIC_RESULTS_GENERAL] + ); +}); + +add_task(async function test_badHeuristicsGroups_notFirst_4() { + await doBadHeuristicGroupsTest( + [ + // 1 general first + { + maxResultCount: 1, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + // infinite heuristics second + { + group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST, + }, + // infinite general third + { + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + ], + [...BAD_HEURISTIC_RESULTS_GENERAL] + ); +}); + +/** + * Sets the resultGroups pref, performs a search, and then checks the results. + * Regardless of the groups, the muxer should include at most one heuristic in + * its results and it should always be the first result. + * + * @param {Array} resultGroups + * The result groups. + * @param {Array} expectedResults + * The expected results. + */ +async function doBadHeuristicGroupsTest(resultGroups, expectedResults) { + sandbox.stub(UrlbarPrefs, "resultGroups").get(() => { + return { children: resultGroups }; + }); + + let provider = registerBasicTestProvider(BAD_HEURISTIC_RESULTS); + let context = createContext("foo", { providers: [provider.name] }); + let controller = UrlbarTestUtils.newMockController(); + await UrlbarProvidersManager.startQuery(context, controller); + Assert.deepEqual(context.results, expectedResults); + + sandbox.restore(); +} + +// When `maxRichResults` is positive and taken up by suggested-index result(s), +// both the heuristic and suggested-index results should be included because we +// (a) make room for the heuristic and (b) assume all suggested-index results +// should be included even if it means exceeding `maxRichResults`. The specified +// `maxRichResults` span will be exceeded in this case. +add_task(async function roomForHeuristic_suggestedIndex() { + let results = [ + Object.assign( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://example.com/heuristic" } + ), + { heuristic: true } + ), + Object.assign( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://example.com/suggestedIndex" } + ), + { suggestedIndex: 1 } + ), + ]; + + UrlbarPrefs.set("maxRichResults", 1); + + let provider = registerBasicTestProvider(results); + let context = createContext(undefined, { providers: [provider.name] }); + await check_results({ + context, + matches: results, + }); + + UrlbarPrefs.clear("maxRichResults"); +}); + +// When `maxRichResults` is positive but less than the heuristic's result span, +// the heuristic should be included because we make room for it even if it means +// exceeding `maxRichResults`. The specified `maxRichResults` span will be +// exceeded in this case. +add_task(async function roomForHeuristic_largeResultSpan() { + let results = [ + Object.assign( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://example.com/heuristic" } + ), + { heuristic: true, resultSpan: 2 } + ), + ]; + + UrlbarPrefs.set("maxRichResults", 1); + + let provider = registerBasicTestProvider(results); + let context = createContext(undefined, { providers: [provider.name] }); + await check_results({ + context, + matches: results, + }); + + UrlbarPrefs.clear("maxRichResults"); +}); + +// When `maxRichResults` is zero and there are no suggested-index results, the +// heuristic should not be included. +add_task(async function roomForHeuristic_maxRichResultsZero() { + let results = [ + Object.assign( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://example.com/heuristic" } + ), + { heuristic: true } + ), + ]; + + UrlbarPrefs.set("maxRichResults", 0); + + let provider = registerBasicTestProvider(results); + let context = createContext(undefined, { providers: [provider.name] }); + await check_results({ + context, + matches: [], + }); + + UrlbarPrefs.clear("maxRichResults"); +}); + +// When `maxRichResults` is zero and suggested-index results are present, +// neither the heuristic nor the suggested-index results should be included. +add_task(async function roomForHeuristic_maxRichResultsZero_suggestedIndex() { + let results = [ + Object.assign( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://example.com/heuristic" } + ), + { heuristic: true } + ), + Object.assign( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://example.com/suggestedIndex" } + ), + { suggestedIndex: 1 } + ), + ]; + + UrlbarPrefs.set("maxRichResults", 0); + + let provider = registerBasicTestProvider(results); + let context = createContext(undefined, { providers: [provider.name] }); + await check_results({ + context, + matches: [], + }); + + UrlbarPrefs.clear("maxRichResults"); +}); diff --git a/browser/components/urlbar/tests/unit/test_protocol_ignore.js b/browser/components/urlbar/tests/unit/test_protocol_ignore.js new file mode 100644 index 0000000000..2e5096cb46 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_protocol_ignore.js @@ -0,0 +1,42 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test bug 424509 to make sure searching for "h" doesn't match "http" of urls. + */ + +testEngine_setup(); + +add_task(async function test_escape() { + Services.prefs.setBoolPref("browser.urlbar.autoFill", false); + Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.urlbar.suggest.searches"); + }); + + let uri1 = Services.io.newURI("http://site/"); + let uri2 = Services.io.newURI("http://happytimes/"); + await PlacesTestUtils.addVisits([ + { uri: uri1, title: "title" }, + { uri: uri2, title: "title" }, + ]); + + info("Searching for h matches site and not http://"); + let context = createContext("h", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { + uri: uri2.spec, + title: "title", + }), + ], + }); + + await cleanupPlaces(); +}); diff --git a/browser/components/urlbar/tests/unit/test_protocol_swap.js b/browser/components/urlbar/tests/unit/test_protocol_swap.js new file mode 100644 index 0000000000..980904001c --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_protocol_swap.js @@ -0,0 +1,303 @@ +/* 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 bug 424717 to make sure searching with an existing location like + * http://site/ also matches https://site/ or ftp://site/. Same thing for + * ftp://site/ and https://site/. + * + * Test bug 461483 to make sure a search for "w" doesn't match the "www." from + * site subdomains. + */ + +testEngine_setup(); + +add_task(async function test_swap_protocol() { + let uri1 = Services.io.newURI("http://www.site/"); + let uri2 = Services.io.newURI("http://site/"); + let uri3 = Services.io.newURI("ftp://ftp.site/"); + let uri4 = Services.io.newURI("ftp://site/"); + let uri5 = Services.io.newURI("https://www.site/"); + let uri6 = Services.io.newURI("https://site/"); + let uri7 = Services.io.newURI("http://woohoo/"); + let uri8 = Services.io.newURI("http://wwwwwwacko/"); + await PlacesTestUtils.addVisits([ + { uri: uri8, title: "title" }, + { uri: uri7, title: "title" }, + { uri: uri6, title: "title" }, + { uri: uri5, title: "title" }, + { uri: uri4, title: "title" }, + { uri: uri3, title: "title" }, + { uri: uri2, title: "title" }, + { uri: uri1, title: "title" }, + ]); + + // Disable autoFill to avoid handling the first result. + Services.prefs.setBoolPref("browser.urlbar.autoFill", false); + Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", false); + Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.urlbar.suggest.searches"); + }); + + info("http://www.site matches 'www.site' pages"); + let searchString = "http://www.site"; + let context = createContext(searchString, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + uri: `${searchString}/`, + title: "title", + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + heuristic: true, + }), + makeVisitResult(context, { uri: uri5.spec, title: "title" }), + ], + }); + + info("http://site matches all sites"); + searchString = "http://site"; + context = createContext(searchString, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + uri: `${searchString}/`, + title: "title", + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + heuristic: true, + }), + makeVisitResult(context, { uri: uri3.spec, title: "title" }), + makeVisitResult(context, { uri: uri4.spec, title: "title" }), + makeVisitResult(context, { uri: uri6.spec, title: "title" }), + ], + }); + + info("ftp://ftp.site matches itself"); + searchString = "ftp://ftp.site"; + context = createContext(searchString, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + uri: `${searchString}/`, + fallbackTitle: `${searchString}/`, + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + heuristic: true, + }), + makeVisitResult(context, { uri: uri3.spec, title: "title" }), + ], + }); + + info("ftp://site matches all sites"); + searchString = "ftp://site"; + context = createContext(searchString, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + uri: `${searchString}/`, + fallbackTitle: `${searchString}/`, + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + heuristic: true, + }), + makeVisitResult(context, { uri: uri3.spec, title: "title" }), + makeVisitResult(context, { uri: uri4.spec, title: "title" }), + makeVisitResult(context, { uri: uri6.spec, title: "title" }), + ], + }); + + info("https://www.site matches all sites"); + searchString = "https://www.sit"; + context = createContext(searchString, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + uri: `${searchString}/`, + fallbackTitle: `${searchString}/`, + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + heuristic: true, + }), + makeVisitResult(context, { uri: uri5.spec, title: "title" }), + ], + }); + + info("https://site matches all sites"); + searchString = "https://sit"; + context = createContext(searchString, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + uri: `${searchString}/`, + fallbackTitle: `${searchString}/`, + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + heuristic: true, + }), + makeVisitResult(context, { uri: uri3.spec, title: "title" }), + makeVisitResult(context, { uri: uri4.spec, title: "title" }), + makeVisitResult(context, { uri: uri6.spec, title: "title" }), + ], + }); + + info("www.site matches 'www.site' pages"); + searchString = "www.site"; + context = createContext(searchString, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + uri: `http://${searchString}/`, + title: "title", + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + heuristic: true, + }), + makeVisitResult(context, { uri: uri5.spec, title: "title" }), + ], + }); + + info("w matches 'w' pages, including 'www'"); + context = createContext("w", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { uri: uri5.spec, title: "title" }), + makeVisitResult(context, { uri: uri7.spec, title: "title" }), + makeVisitResult(context, { uri: uri8.spec, title: "title" }), + ], + }); + + info("http://w matches 'w' pages, including 'www'"); + searchString = "http://w"; + context = createContext(searchString, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + uri: `${searchString}/`, + fallbackTitle: `${searchString}/`, + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + heuristic: true, + }), + makeVisitResult(context, { uri: uri5.spec, title: "title" }), + makeVisitResult(context, { uri: uri7.spec, title: "title" }), + makeVisitResult(context, { uri: uri8.spec, title: "title" }), + ], + }); + + info("http://www.w matches nothing"); + searchString = "http://www.w"; + context = createContext(searchString, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + uri: `${searchString}/`, + fallbackTitle: `${searchString}/`, + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + heuristic: true, + }), + ], + }); + + info("ww matches no 'ww' pages, including 'www'"); + context = createContext("ww", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { uri: uri5.spec, title: "title" }), + makeVisitResult(context, { uri: uri8.spec, title: "title" }), + ], + }); + + info("http://ww matches no 'ww' pages, including 'www'"); + searchString = "http://ww"; + context = createContext(searchString, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + uri: `${searchString}/`, + fallbackTitle: `${searchString}/`, + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + heuristic: true, + }), + makeVisitResult(context, { uri: uri5.spec, title: "title" }), + makeVisitResult(context, { uri: uri8.spec, title: "title" }), + ], + }); + + info("http://www.ww matches nothing"); + searchString = "http://www.ww"; + context = createContext(searchString, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + uri: `${searchString}/`, + fallbackTitle: `${searchString}/`, + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + heuristic: true, + }), + ], + }); + + info("www matches 'www' pages"); + context = createContext("www", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { uri: uri5.spec, title: "title" }), + makeVisitResult(context, { uri: uri8.spec, title: "title" }), + ], + }); + + info("http://www matches 'www' pages"); + searchString = "http://www"; + context = createContext(searchString, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + uri: `${searchString}/`, + fallbackTitle: `${searchString}/`, + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + heuristic: true, + }), + makeVisitResult(context, { uri: uri5.spec, title: "title" }), + makeVisitResult(context, { uri: uri8.spec, title: "title" }), + ], + }); + + info("http://www.www matches nothing"); + searchString = "http://www.www"; + context = createContext(searchString, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + uri: `${searchString}/`, + fallbackTitle: `${searchString}/`, + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + heuristic: true, + }), + ], + }); + + await cleanupPlaces(); +}); diff --git a/browser/components/urlbar/tests/unit/test_providerAliasEngines.js b/browser/components/urlbar/tests/unit/test_providerAliasEngines.js new file mode 100644 index 0000000000..14ba368f0d --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_providerAliasEngines.js @@ -0,0 +1,114 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests search engine aliases. See + * browser/components/urlbar/tests/browser/browser_tokenAlias.js for tests of + * the token alias list (i.e. showing all aliased engines on a "@" query). + */ + +testEngine_setup(); + +// Basic test that uses two engines, a GET engine and a POST engine, neither +// providing search suggestions. +add_task(async function basicGetAndPost() { + await SearchTestUtils.installSearchExtension({ + name: "AliasedGETMozSearch", + keyword: "get", + search_url: "https://s.example.com/search", + }); + await SearchTestUtils.installSearchExtension({ + name: "AliasedPOSTMozSearch", + keyword: "post", + search_url: "https://s.example.com/search", + search_url_post_params: "q={searchTerms}", + }); + + for (let alias of ["get", "post"]) { + let context = createContext(alias, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + providerName: "HeuristicFallback", + }), + ], + }); + + context = createContext(`${alias} `, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: `Aliased${alias.toUpperCase()}MozSearch`, + query: "", + alias, + heuristic: true, + providerName: "AliasEngines", + }), + ], + }); + + context = createContext(`${alias} fire`, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: `Aliased${alias.toUpperCase()}MozSearch`, + query: "fire", + alias, + heuristic: true, + providerName: "AliasEngines", + }), + ], + }); + + context = createContext(`${alias} mozilla`, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: `Aliased${alias.toUpperCase()}MozSearch`, + query: "mozilla", + alias, + heuristic: true, + providerName: "AliasEngines", + }), + ], + }); + + context = createContext(`${alias} MoZiLlA`, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: `Aliased${alias.toUpperCase()}MozSearch`, + query: "MoZiLlA", + alias, + heuristic: true, + providerName: "AliasEngines", + }), + ], + }); + + context = createContext(`${alias} mozzarella mozilla`, { + isPrivate: false, + }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: `Aliased${alias.toUpperCase()}MozSearch`, + query: "mozzarella mozilla", + alias, + heuristic: true, + providerName: "AliasEngines", + }), + ], + }); + } + + await cleanupPlaces(); +}); 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..101f6fb21d --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_providerHeuristicFallback.js @@ -0,0 +1,691 @@ +/* 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 QUICKACTIONS_PREF = "browser.urlbar.suggest.quickactions"; +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 "]; + +testEngine_setup(); + +add_task(async function setup() { + registerCleanupFunction(async () => { + Services.prefs.clearUserPref(QUICKACTIONS_PREF); + Services.prefs.clearUserPref(SUGGEST_ENABLED_PREF); + Services.prefs.clearUserPref(PRIVATE_SEARCH_PREF); + Services.prefs.clearUserPref("keyword.enabled"); + }); + Services.prefs.setBoolPref(QUICKACTIONS_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}/`, + fallbackTitle: `http://${query}/`, + heuristic: true, + }), + makeSearchResult(context, { + engineName: SUGGESTIONS_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}/`, + fallbackTitle: `http://${query}/`, + heuristic: true, + }), + makeSearchResult(context, { + engineName: SUGGESTIONS_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}/`, + fallbackTitle: `http://${query}/`, + heuristic: true, + }), + makeSearchResult(context, { + engineName: SUGGESTIONS_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}/`, + fallbackTitle: `${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}/`, + fallbackTitle: `${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}/`, + fallbackTitle: `${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, + fallbackTitle: 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}/`, + fallbackTitle: `${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}`, + fallbackTitle: `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: SUGGESTIONS_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: SUGGESTIONS_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}/`, + fallbackTitle: `http://${query}/`, + heuristic: true, + }), + makeSearchResult(context, { + engineName: SUGGESTIONS_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}`, + fallbackTitle: `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}`, + fallbackTitle: `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}/`, + fallbackTitle: `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}/`, + fallbackTitle: `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}/`, + fallbackTitle: `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, + fallbackTitle: 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: SUGGESTIONS_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: SUGGESTIONS_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}/`, + fallbackTitle: `${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}/`, + fallbackTitle: `${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}/`, + fallbackTitle: `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: SUGGESTIONS_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: SUGGESTIONS_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, + fallbackTitle: 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, + fallbackTitle: query, + heuristic: true, + }), + ], + }); + + info("protocol with an extra slash"); + query = "http:///"; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ], + }); + + info("change default engine"); + let originalTestEngine = Services.search.getEngineByName( + SUGGESTIONS_ENGINE_NAME + ); + await SearchTestUtils.installSearchExtension({ + name: "AliasEngine", + keyword: "alias", + }); + let engine2 = Services.search.getEngineByName("AliasEngine"); + Assert.notEqual( + Services.search.defaultEngine, + engine2, + "New engine shouldn't be the current engine yet" + ); + await Services.search.setDefault( + engine2, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + query = "toronto"; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: "AliasEngine", + heuristic: true, + }), + ], + }); + await Services.search.setDefault( + originalTestEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + 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 = SUGGESTIONS_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 = SUGGESTIONS_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: SUGGESTIONS_ENGINE_NAME, + }), + ], + }); + } + } + + info( + "Test the format inputed is user@host, and the host is in domainwhitelist" + ); + Services.prefs.setBoolPref("browser.fixup.domainwhitelist.test-host", true); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.fixup.domainwhitelist.test-host"); + }); + + query = "any@test-host"; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: `http://${query}/`, + fallbackTitle: `http://${query}/`, + heuristic: true, + }), + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + }), + ], + }); + + info( + "Test the format inputed is user@host, but the host is not in domainwhitelist" + ); + query = "any@not-host"; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + heuristic: true, + query, + engineName: SUGGESTIONS_ENGINE_NAME, + }), + ], + }); + + info( + "Test if the format of user:pass@host is handled as visit even if the host is not in domainwhitelist" + ); + query = "user:pass@not-host"; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "http://user:pass@not-host/", + fallbackTitle: "http://user:pass@not-host/", + heuristic: true, + }), + ], + }); + + info("Test if the format of user@ipaddress is handled as visit"); + query = "user@192.168.0.1"; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "http://user@192.168.0.1/", + fallbackTitle: "http://user@192.168.0.1/", + heuristic: true, + }), + makeSearchResult(context, { + heuristic: false, + query, + engineName: SUGGESTIONS_ENGINE_NAME, + }), + ], + }); +}); + +/** + * 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_providerHistoryUrlHeuristic.js b/browser/components/urlbar/tests/unit/test_providerHistoryUrlHeuristic.js new file mode 100644 index 0000000000..6f26fee7cb --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_providerHistoryUrlHeuristic.js @@ -0,0 +1,197 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for the behavior of UrlbarProviderHistoryUrlHeuristic. + +add_task(async function setup() { + Services.prefs.setBoolPref("browser.urlbar.autoFill", false); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.urlbar.autoFill"); + }); +}); + +add_task(async function test_basic() { + await PlacesTestUtils.addVisits([ + { uri: "https://example.com/", title: "Example COM" }, + ]); + + const testCases = [ + { + input: "https://example.com/", + expected: context => [ + makeVisitResult(context, { + uri: "https://example.com/", + title: "Example COM", + iconUri: "page-icon:https://example.com/", + heuristic: true, + providerName: "HistoryUrlHeuristic", + }), + ], + }, + { + input: "https://www.example.com/", + expected: context => [ + makeVisitResult(context, { + uri: "https://www.example.com/", + title: "Example COM", + iconUri: "page-icon:https://example.com/", + heuristic: true, + providerName: "HistoryUrlHeuristic", + }), + ], + }, + { + input: "http://example.com/", + expected: context => [ + makeVisitResult(context, { + uri: "http://example.com/", + title: "Example COM", + iconUri: "page-icon:https://example.com/", + heuristic: true, + providerName: "HistoryUrlHeuristic", + }), + makeVisitResult(context, { + uri: "https://example.com/", + title: "Example COM", + iconUri: "page-icon:https://example.com/", + providerName: "Places", + }), + ], + }, + { + input: "example.com", + expected: context => [ + makeVisitResult(context, { + uri: "http://example.com/", + title: "Example COM", + iconUri: "page-icon:https://example.com/", + heuristic: true, + providerName: "HistoryUrlHeuristic", + }), + makeVisitResult(context, { + uri: "https://example.com/", + title: "Example COM", + iconUri: "page-icon:https://example.com/", + providerName: "Places", + }), + ], + }, + { + input: "www.example.com", + expected: context => [ + makeVisitResult(context, { + uri: "http://www.example.com/", + title: "Example COM", + iconUri: "page-icon:https://example.com/", + heuristic: true, + providerName: "HistoryUrlHeuristic", + }), + ], + }, + { + input: "htp:example.com", + expected: context => [ + makeVisitResult(context, { + uri: "http://example.com/", + title: "Example COM", + iconUri: "page-icon:https://example.com/", + heuristic: true, + providerName: "HistoryUrlHeuristic", + }), + ], + }, + ]; + + for (const { input, expected } of testCases) { + info(`Test with "${input}"`); + const context = createContext(input, { isPrivate: false }); + await check_results({ + context, + matches: expected(context), + }); + } + + await PlacesUtils.history.clear(); +}); + +add_task(async function test_null_title() { + await PlacesTestUtils.addVisits([{ uri: "https://example.com/", title: "" }]); + + const context = createContext("https://example.com/", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "https://example.com/", + fallbackTitle: "https://example.com/", + iconUri: "page-icon:https://example.com/", + heuristic: true, + providerName: "HeuristicFallback", + }), + ], + }); + + await PlacesUtils.history.clear(); +}); + +add_task(async function test_over_max_length_text() { + let uri = "https://example.com/"; + for (; uri.length < UrlbarUtils.MAX_TEXT_LENGTH; ) { + uri += "0123456789"; + } + + await PlacesTestUtils.addVisits([{ uri, title: "Example MAX" }]); + + const context = createContext(uri, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri, + fallbackTitle: uri, + iconUri: "page-icon:https://example.com/", + heuristic: true, + providerName: "HeuristicFallback", + }), + ], + }); + + await PlacesUtils.history.clear(); +}); + +add_task(async function test_unsupported_protocol() { + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "about:robots", + title: "Robots!", + }); + + const context = createContext("about:robots", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "about:robots", + fallbackTitle: "about:robots", + heuristic: true, + providerName: "HeuristicFallback", + }), + makeBookmarkResult(context, { + uri: "about:robots", + title: "Robots!", + }), + makeVisitResult(context, { + uri: "about:robots", + title: "about:robots", + tags: null, + providerName: "AboutPages", + }), + ], + }); + + await PlacesUtils.bookmarks.eraseEverything(); +}); diff --git a/browser/components/urlbar/tests/unit/test_providerKeywords.js b/browser/components/urlbar/tests/unit/test_providerKeywords.js new file mode 100644 index 0000000000..3be5fe30c0 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_providerKeywords.js @@ -0,0 +1,360 @@ +/* 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 bug 392143 that puts keyword results into the autocomplete. Makes + * sure that multiple parameter queries get spaces converted to +, + converted + * to %2B, non-ascii become escaped, and pages in history that match the + * keyword uses the page's title. + * + * Also test for bug 249468 by making sure multiple keyword bookmarks with the + * same keyword appear in the list. + */ + +testEngine_setup(); + +add_task(async function test_keyword_search() { + let uri1 = "http://abc/?search=%s"; + let uri2 = "http://abc/?search=ThisPageIsInHistory"; + let uri3 = "http://abc/?search=%s&raw=%S"; + let uri4 = "http://abc/?search=%s&raw=%S&mozcharset=ISO-8859-1"; + let uri5 = "http://def/?search=%s"; + let uri6 = "http://ghi/?search=%s&raw=%S"; + let uri7 = "http://somedomain.example/key2"; + await PlacesTestUtils.addVisits([ + { uri: uri1 }, + { uri: uri2 }, + { uri: uri3 }, + { uri: uri6 }, + { uri: uri7 }, + ]); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: uri1, + title: "Keyword", + keyword: "key", + }); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: uri1, + title: "Post", + keyword: "post", + postData: "post_search=%s", + }); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: uri3, + title: "Encoded", + keyword: "encoded", + }); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: uri4, + title: "Charset", + keyword: "charset", + }); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: uri2, + title: "Noparam", + keyword: "noparam", + }); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: uri2, + title: "Noparam-Post", + keyword: "post_noparam", + postData: "noparam=1", + }); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: uri5, + title: "Keyword", + keyword: "key2", + }); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: uri6, + title: "Charset-history", + keyword: "charset_history", + }); + + await PlacesUtils.history.update({ + url: uri6, + annotations: new Map([[PlacesUtils.CHARSET_ANNO, "ISO-8859-1"]]), + }); + + info("Plain keyword query"); + let context = createContext("key term", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeKeywordSearchResult(context, { + uri: "http://abc/?search=term", + keyword: "key", + title: "abc: term", + iconUri: "page-icon:http://abc/?search=%s", + heuristic: true, + }), + ], + }); + + info("Plain keyword UC"); + context = createContext("key TERM", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeKeywordSearchResult(context, { + uri: "http://abc/?search=TERM", + keyword: "key", + title: "abc: TERM", + iconUri: "page-icon:http://abc/?search=%s", + heuristic: true, + }), + ], + }); + + info("Multi-word keyword query"); + context = createContext("key multi word", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeKeywordSearchResult(context, { + uri: "http://abc/?search=multi%20word", + keyword: "key", + title: "abc: multi word", + iconUri: "page-icon:http://abc/?search=%s", + heuristic: true, + }), + ], + }); + + info("Keyword query with +"); + context = createContext("key blocking+", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeKeywordSearchResult(context, { + uri: "http://abc/?search=blocking%2B", + keyword: "key", + title: "abc: blocking+", + iconUri: "page-icon:http://abc/?search=%s", + heuristic: true, + }), + ], + }); + + info("Unescaped term in query"); + // ... but note that we call encodeURIComponent() on the query string when we + // build the URL, so the expected result will have the ユニコード substring + // encoded in the URL. + context = createContext("key ユニコード", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeKeywordSearchResult(context, { + uri: "http://abc/?search=" + encodeURIComponent("ユニコード"), + keyword: "key", + title: "abc: ユニコード", + iconUri: "page-icon:http://abc/?search=%s", + heuristic: true, + }), + ], + }); + + info("Keyword that happens to match a page"); + context = createContext("key ThisPageIsInHistory", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeKeywordSearchResult(context, { + uri: "http://abc/?search=ThisPageIsInHistory", + keyword: "key", + title: "abc: ThisPageIsInHistory", + iconUri: "page-icon:http://abc/?search=%s", + heuristic: true, + }), + ], + }); + + info("Keyword with partial page match"); + context = createContext("key ThisPage", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeKeywordSearchResult(context, { + uri: "http://abc/?search=ThisPage", + keyword: "key", + title: "abc: ThisPage", + iconUri: "page-icon:http://abc/?search=%s", + heuristic: true, + }), + // Only the most recent bookmark for the URL: + makeBookmarkResult(context, { + uri: "http://abc/?search=ThisPageIsInHistory", + title: "Noparam-Post", + }), + ], + }); + + // For the keyword with no query terms (with or without space after), the + // domain is different from the other tests because otherwise all the other + // test bookmarks and history entries would be matches. + info("Keyword without query (without space)"); + context = createContext("key2", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeKeywordSearchResult(context, { + uri: "http://def/?search=", + fallbackTitle: "http://def/?search=", + keyword: "key2", + iconUri: "page-icon:http://def/?search=%s", + heuristic: true, + }), + makeBookmarkResult(context, { + uri: uri5, + title: "Keyword", + }), + ], + }); + + info("Keyword without query (with space)"); + context = createContext("key2 ", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeKeywordSearchResult(context, { + uri: "http://def/?search=", + fallbackTitle: "http://def/?search=", + keyword: "key2", + iconUri: "page-icon:http://def/?search=%s", + heuristic: true, + }), + makeBookmarkResult(context, { + uri: uri5, + title: "Keyword", + }), + ], + }); + + info("POST Keyword"); + context = createContext("post foo", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeKeywordSearchResult(context, { + uri: "http://abc/?search=foo", + keyword: "post", + title: "abc: foo", + postData: "post_search=foo", + iconUri: "page-icon:http://abc/?search=%s", + heuristic: true, + }), + ], + }); + + info("escaping with default UTF-8 charset"); + context = createContext("encoded foé", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeKeywordSearchResult(context, { + uri: "http://abc/?search=fo%C3%A9&raw=foé", + keyword: "encoded", + title: "abc: foé", + iconUri: "page-icon:http://abc/?search=%s&raw=%S", + heuristic: true, + }), + ], + }); + + info("escaping with forced ISO-8859-1 charset"); + context = createContext("charset foé", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeKeywordSearchResult(context, { + uri: "http://abc/?search=fo%E9&raw=foé", + keyword: "charset", + title: "abc: foé", + iconUri: "page-icon:http://abc/?search=%s&raw=%S&mozcharset=ISO-8859-1", + heuristic: true, + }), + ], + }); + + info("escaping with ISO-8859-1 charset annotated in history"); + context = createContext("charset_history foé", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeKeywordSearchResult(context, { + uri: "http://ghi/?search=fo%E9&raw=foé", + keyword: "charset_history", + title: "ghi: foé", + iconUri: "page-icon:http://ghi/?search=%s&raw=%S", + heuristic: true, + }), + ], + }); + + info("Bug 359809: escaping +, / and @ with default UTF-8 charset"); + context = createContext("encoded +/@", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeKeywordSearchResult(context, { + uri: "http://abc/?search=%2B%2F%40&raw=+/@", + keyword: "encoded", + title: "abc: +/@", + iconUri: "page-icon:http://abc/?search=%s&raw=%S", + heuristic: true, + }), + ], + }); + + info("Bug 359809: escaping +, / and @ with forced ISO-8859-1 charset"); + context = createContext("charset +/@", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeKeywordSearchResult(context, { + uri: "http://abc/?search=%2B%2F%40&raw=+/@", + keyword: "charset", + title: "abc: +/@", + iconUri: "page-icon:http://abc/?search=%s&raw=%S&mozcharset=ISO-8859-1", + heuristic: true, + }), + ], + }); + + info("Bug 1228111 - Keyword with a space in front"); + context = createContext(" key test", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeKeywordSearchResult(context, { + uri: "http://abc/?search=test", + keyword: "key", + title: "abc: test", + iconUri: "page-icon:http://abc/?search=%s", + heuristic: true, + }), + ], + }); + + info("Bug 1481319 - Keyword with a prefix in front"); + context = createContext("http://key2", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "http://key2/", + fallbackTitle: "http://key2/", + heuristic: true, + providerName: "HeuristicFallback", + }), + makeVisitResult(context, { + uri: uri7, + title: "test visit for http://somedomain.example/key2", + }), + ], + }); + + await cleanupPlaces(); +}); 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..134158b5b1 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_providerOmnibox.js @@ -0,0 +1,887 @@ +/* -*- 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.importESModule( + "resource://gre/modules/ExtensionSearchHandler.sys.mjs" +); + +let controller = Cc["@mozilla.org/autocomplete/controller;1"].getService( + Ci.nsIAutoCompleteController +); + +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_extension_private_browsing_allowed() { + 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: "foobar", description: "second suggestion" }, + ]); + // The API doesn't have a way to notify when addition is complete. + do_timeout(1000, () => { + controller.stopSearch(); + }); + } + }, + privateBrowsingAllowed: true, + }; + + let keyword = "foo"; + ExtensionSearchHandler.registerKeyword(keyword, mockExtension); + + let query = `${keyword} foo`; + let context = createContext(query, { isPrivate: true }); + await check_results({ + context, + matches: [ + makeOmniboxResult(context, { + heuristic: true, + keyword, + description: extensionName, + content: query, + }), + makeOmniboxResult(context, { + keyword, + content: `${keyword} foobar`, + description: "second suggestion", + }), + ], + }); + + 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" }, + { content: "k", description: "eleventh suggestion" }, + { content: "l", description: "twelfth 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", + }), + makeOmniboxResult(context, { + keyword, + content: `${keyword} f`, + description: "sixth suggestion", + }), + makeOmniboxResult(context, { + keyword, + content: `${keyword} g`, + description: "seventh suggestion", + }), + makeOmniboxResult(context, { + keyword, + content: `${keyword} h`, + description: "eigth suggestion", + }), + makeOmniboxResult(context, { + keyword, + content: `${keyword} i`, + description: "ninth 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, Ci.nsISearchService.CHANGE_REASON_UNKNOWN); + + 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: SUGGESTIONS_ENGINE_NAME, + alias: keyword, + suggestion: "unmatched", + }), + makeSearchResult(context, { + query: "unmatched", + engineName: SUGGESTIONS_ENGINE_NAME, + alias: keyword, + suggestion: "unmatched foo", + }), + makeSearchResult(context, { + query: "unmatched", + engineName: SUGGESTIONS_ENGINE_NAME, + alias: keyword, + suggestion: "unmatched bar", + }), + ], + }); + + Services.search.setDefault( + oldDefaultEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + 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..6cdf632b42 --- /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, false); + UrlbarProviderOpenTabs.registerOpenTab(url, userContextId, false); + Assert.equal( + UrlbarProviderOpenTabs._openTabs.get(userContextId).length, + 2, + "Found all the expected tabs" + ); + UrlbarProviderOpenTabs.unregisterOpenTab(url, userContextId, false); + 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_providerPlaces.js b/browser/components/urlbar/tests/unit/test_providerPlaces.js new file mode 100644 index 0000000000..c64f3345e1 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_providerPlaces.js @@ -0,0 +1,250 @@ +/* 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 Places 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_places() { + 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, false); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + 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, + false + ); +}); + +add_task(async function test_bookmarkBehaviorDisabled_tagged() { + Services.prefs.setBoolPref(SUGGEST_PREF, false); + Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, false); + + // Disable the bookmark behavior. + 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 PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + 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. + 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 PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + 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. + 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 PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + 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_providerPlaces_duplicate_entries.js b/browser/components/urlbar/tests/unit/test_providerPlaces_duplicate_entries.js new file mode 100644 index 0000000000..7533921fc6 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_providerPlaces_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_providerPlaces_nonEnglish.js b/browser/components/urlbar/tests/unit/test_providerPlaces_nonEnglish.js new file mode 100644 index 0000000000..2cb5f5797a --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_providerPlaces_nonEnglish.js @@ -0,0 +1,43 @@ +/* -*- 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/. */ + +/* + +Test autocomplete for non-English URLs + +- add a visit for a page with a non-English URL +- search +- test number of matches (should be exactly one) + +*/ + +testEngine_setup(); + +add_task(async function test_autocomplete_non_english() { + Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.urlbar.suggest.searches"); + }); + + let searchTerm = "ユニコード"; + let unescaped = "http://www.foobar.com/" + searchTerm + "/"; + let uri = Services.io.newURI(unescaped); + await PlacesTestUtils.addVisits(uri); + let context = createContext(searchTerm, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { + uri: uri.spec, + title: `test visit for ${uri.spec}`, + }), + ], + }); +}); diff --git a/browser/components/urlbar/tests/unit/test_providerPreloaded.js b/browser/components/urlbar/tests/unit/test_providerPreloaded.js new file mode 100644 index 0000000000..0785e3afba --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_providerPreloaded.js @@ -0,0 +1,578 @@ +/** + * Test for bug 1211726 - preload list of top web sites for better + * autocompletion on empty profiles. + */ + +testEngine_setup(); + +const PREF_FEATURE_ENABLED = "browser.urlbar.usepreloadedtopurls.enabled"; +const PREF_FEATURE_EXPIRE_DAYS = + "browser.urlbar.usepreloadedtopurls.expire_days"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarProviderPreloadedSites: + "resource:///modules/UrlbarProviderPreloadedSites.sys.mjs", +}); + +Cu.importGlobalProperties(["fetch"]); + +let yahoooURI = "https://yahooo.com/"; +let gooogleURI = "https://gooogle.com/"; + +UrlbarProviderPreloadedSites.populatePreloadedSiteStorage([ + [yahoooURI, "Yahooo"], + [gooogleURI, "Gooogle"], +]); + +async function assert_feature_works(condition) { + info("List Results do appear " + condition); + let context = createContext("ooo", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + heuristic: true, + engineName: SUGGESTIONS_ENGINE_NAME, + providerName: "HeuristicFallback", + }), + makeVisitResult(context, { + uri: yahoooURI, + title: "Yahooo", + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + tags: null, + iconUri: `page-icon:${yahoooURI}`, + providerName: "PreloadedSites", + }), + makeVisitResult(context, { + uri: gooogleURI, + title: "Gooogle", + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + tags: null, + iconUri: `page-icon:${gooogleURI}`, + providerName: "PreloadedSites", + }), + ], + }); + + info("Autofill does appear " + condition); + context = createContext("gooo", { isPrivate: false }); + await check_results({ + context, + autofilled: "gooogle.com/", + completed: gooogleURI, + matches: [ + makeVisitResult(context, { + uri: gooogleURI, + fallbackTitle: gooogleURI.slice(0, -1), // Trim trailing slash. + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + iconUri: `page-icon:${gooogleURI}`, + providerName: "PreloadedSites", + heuristic: true, + }), + ], + }); +} + +async function assert_feature_does_not_appear(condition) { + info("List Results don't appear " + condition); + let context = createContext("ooo", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + heuristic: true, + engineName: SUGGESTIONS_ENGINE_NAME, + providerName: "HeuristicFallback", + }), + ], + }); + + info("Autofill doesn't appear " + condition); + context = createContext("gooo", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + heuristic: true, + engineName: SUGGESTIONS_ENGINE_NAME, + providerName: "HeuristicFallback", + }), + ], + }); +} + +add_task(async function test_it_works() { + // Not expired but OFF + Services.prefs.setIntPref(PREF_FEATURE_EXPIRE_DAYS, 14); + Services.prefs.setBoolPref(PREF_FEATURE_ENABLED, false); + await assert_feature_does_not_appear("when OFF by prefs"); + + // Now turn it ON + Services.prefs.setBoolPref(PREF_FEATURE_ENABLED, true); + await assert_feature_works("when ON by prefs"); + + // And expire + Services.prefs.setIntPref(PREF_FEATURE_EXPIRE_DAYS, 0); + await assert_feature_does_not_appear("when expired"); + + await cleanupPlaces(); +}); + +add_task(async function test_sorting_against_bookmark() { + let boookmarkURI = "https://boookmark.com/"; + await PlacesTestUtils.addBookmarkWithDetails({ + uri: boookmarkURI, + title: "Boookmark", + }); + + Services.prefs.setBoolPref(PREF_FEATURE_ENABLED, true); + Services.prefs.setIntPref(PREF_FEATURE_EXPIRE_DAYS, 14); + + info("Preloaded Top Sites are placed lower than Bookmarks"); + let context = createContext("ooo", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeBookmarkResult(context, { + uri: boookmarkURI, + title: "Boookmark", + }), + makeVisitResult(context, { + uri: yahoooURI, + title: "Yahooo", + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + tags: null, + iconUri: `page-icon:${yahoooURI}`, + providerName: "PreloadedSites", + }), + makeVisitResult(context, { + uri: gooogleURI, + title: "Gooogle", + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + tags: null, + iconUri: `page-icon:${gooogleURI}`, + providerName: "PreloadedSites", + }), + ], + }); + + await cleanupPlaces(); +}); + +add_task(async function test_sorting_against_history() { + let histoooryURI = "https://histooory.com/"; + await PlacesTestUtils.addVisits({ uri: histoooryURI, title: "Histooory" }); + + Services.prefs.setBoolPref(PREF_FEATURE_ENABLED, true); + Services.prefs.setIntPref(PREF_FEATURE_EXPIRE_DAYS, 14); + + info("Preloaded Top Sites are placed lower than History entries"); + let context = createContext("ooo", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { + uri: histoooryURI, + title: "Histooory", + }), + makeVisitResult(context, { + uri: yahoooURI, + title: "Yahooo", + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + tags: null, + iconUri: `page-icon:${yahoooURI}`, + providerName: "PreloadedSites", + }), + makeVisitResult(context, { + uri: gooogleURI, + title: "Gooogle", + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + tags: null, + iconUri: `page-icon:${gooogleURI}`, + providerName: "PreloadedSites", + }), + ], + }); + + await cleanupPlaces(); +}); + +add_task(async function test_scheme_and_www() { + // Order is important to check sorting + let sites = [ + ["https://www.ooops-https-www.com/", "Ooops"], + ["https://ooops-https.com/", "Ooops"], + ["HTTP://ooops-HTTP.com/", "Ooops"], + ["HTTP://www.ooops-HTTP-www.com/", "Ooops"], + ["https://foo.com/", "Title with www"], + ["https://www.bar.com/", "Tile"], + ]; + + let titlesMap = new Map(sites); + + UrlbarProviderPreloadedSites.populatePreloadedSiteStorage(sites); + + // No matches when just typing the protocol. + let context = createContext("https://", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ], + }); + + context = createContext("www.", { isPrivate: false }); + await check_results({ + context, + autofilled: "www.ooops-https-www.com/", + completed: "https://www.ooops-https-www.com/", + matches: [ + makeVisitResult(context, { + uri: "https://www.ooops-https-www.com/", + fallbackTitle: "https://www.ooops-https-www.com", + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + iconUri: "page-icon:https://www.ooops-https-www.com/", + providerName: "PreloadedSites", + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://www.ooops-http-www.com/", + title: titlesMap.get("HTTP://www.ooops-HTTP-www.com/"), + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + tags: null, + iconUri: "page-icon:http://www.ooops-http-www.com/", + providerName: "PreloadedSites", + }), + makeVisitResult(context, { + uri: "https://www.bar.com/", + title: titlesMap.get("https://www.bar.com/"), + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + tags: null, + iconUri: "page-icon:https://www.bar.com/", + providerName: "PreloadedSites", + }), + ], + }); + + context = createContext("http://www.", { isPrivate: false }); + await check_results({ + context, + autofilled: "http://www.ooops-http-www.com/", + completed: "http://www.ooops-http-www.com/", + matches: [ + makeVisitResult(context, { + uri: "http://www.ooops-http-www.com/", + fallbackTitle: "www.ooops-http-www.com", + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + iconUri: "page-icon:http://www.ooops-http-www.com/", + providerName: "PreloadedSites", + heuristic: true, + }), + ], + }); + + context = createContext("ftp://ooops", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "ftp://ooops/", + fallbackTitle: "ftp://ooops/", + providerName: "HeuristicFallback", + heuristic: true, + }), + ], + }); + + context = createContext("ww", { isPrivate: false }); + await check_results({ + context, + autofilled: "www.ooops-https-www.com/", + completed: "https://www.ooops-https-www.com/", + matches: [ + makeVisitResult(context, { + uri: "https://www.ooops-https-www.com/", + fallbackTitle: "https://www.ooops-https-www.com", + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + iconUri: "page-icon:https://www.ooops-https-www.com/", + providerName: "PreloadedSites", + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://www.ooops-http-www.com/", + title: titlesMap.get("HTTP://www.ooops-HTTP-www.com/"), + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + tags: null, + iconUri: "page-icon:http://www.ooops-http-www.com/", + providerName: "PreloadedSites", + }), + makeVisitResult(context, { + uri: "https://foo.com/", + title: titlesMap.get("https://foo.com/"), + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + tags: null, + iconUri: "page-icon:https://foo.com/", + providerName: "PreloadedSites", + }), + makeVisitResult(context, { + uri: "https://www.bar.com/", + title: titlesMap.get("https://www.bar.com/"), + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + tags: null, + iconUri: "page-icon:https://www.bar.com/", + providerName: "PreloadedSites", + }), + ], + }); + + context = createContext("ooops", { isPrivate: false }); + await check_results({ + context, + autofilled: "ooops-https-www.com/", + completed: "https://www.ooops-https-www.com/", + matches: [ + makeVisitResult(context, { + uri: "https://www.ooops-https-www.com/", + fallbackTitle: "https://www.ooops-https-www.com", + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + iconUri: "page-icon:https://www.ooops-https-www.com/", + providerName: "PreloadedSites", + heuristic: true, + }), + makeVisitResult(context, { + uri: "https://ooops-https.com/", + title: titlesMap.get("https://ooops-https.com/"), + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + tags: null, + iconUri: "page-icon:https://ooops-https.com/", + providerName: "PreloadedSites", + }), + makeVisitResult(context, { + uri: "http://ooops-http.com/", + title: titlesMap.get("HTTP://ooops-HTTP.com/"), + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + tags: null, + iconUri: "page-icon:http://ooops-http.com/", + providerName: "PreloadedSites", + }), + makeVisitResult(context, { + uri: "http://www.ooops-http-www.com/", + title: titlesMap.get("HTTP://www.ooops-HTTP-www.com/"), + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + tags: null, + iconUri: "page-icon:http://www.ooops-http-www.com/", + providerName: "PreloadedSites", + }), + ], + }); + + context = createContext("www.ooops", { isPrivate: false }); + await check_results({ + context, + autofilled: "www.ooops-https-www.com/", + completed: "https://www.ooops-https-www.com/", + matches: [ + makeVisitResult(context, { + uri: "https://www.ooops-https-www.com/", + fallbackTitle: "https://www.ooops-https-www.com", + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + iconUri: "page-icon:https://www.ooops-https-www.com/", + providerName: "PreloadedSites", + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://www.ooops-http-www.com/", + title: titlesMap.get("HTTP://www.ooops-HTTP-www.com/"), + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + tags: null, + iconUri: "page-icon:http://www.ooops-http-www.com/", + providerName: "PreloadedSites", + }), + ], + }); + + context = createContext("ooops-https-www", { isPrivate: false }); + await check_results({ + context, + autofilled: "ooops-https-www.com/", + completed: "https://www.ooops-https-www.com/", + matches: [ + makeVisitResult(context, { + uri: "https://www.ooops-https-www.com/", + fallbackTitle: "https://www.ooops-https-www.com", + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + iconUri: "page-icon:https://www.ooops-https-www.com/", + providerName: "PreloadedSites", + heuristic: true, + }), + ], + }); + + context = createContext("www.ooops-https.", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "http://www.ooops-https./", + fallbackTitle: "http://www.ooops-https./", + providerName: "HeuristicFallback", + heuristic: true, + }), + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + providerName: "HeuristicFallback", + }), + ], + }); + + context = createContext("https://ooops", { isPrivate: false }); + await check_results({ + context, + autofilled: "https://ooops-https-www.com/", + completed: "https://www.ooops-https-www.com/", + matches: [ + makeVisitResult(context, { + uri: "https://www.ooops-https-www.com/", + fallbackTitle: "https://www.ooops-https-www.com", + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + iconUri: "page-icon:https://www.ooops-https-www.com/", + providerName: "PreloadedSites", + heuristic: true, + }), + makeVisitResult(context, { + uri: "https://ooops-https.com/", + title: titlesMap.get("https://ooops-https.com/"), + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + tags: null, + iconUri: "page-icon:https://ooops-https.com/", + providerName: "PreloadedSites", + }), + ], + }); + + context = createContext("https://www.ooops", { isPrivate: false }); + await check_results({ + context, + autofilled: "https://www.ooops-https-www.com/", + completed: "https://www.ooops-https-www.com/", + matches: [ + makeVisitResult(context, { + uri: "https://www.ooops-https-www.com/", + fallbackTitle: "https://www.ooops-https-www.com", + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + iconUri: "page-icon:https://www.ooops-https-www.com/", + providerName: "PreloadedSites", + heuristic: true, + }), + ], + }); + + context = createContext("http://www.ooops-http.", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "http://www.ooops-http./", + fallbackTitle: "http://www.ooops-http./", + providerName: "HeuristicFallback", + heuristic: true, + }), + ], + }); + + context = createContext("http://ooops-https", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "http://ooops-https/", + fallbackTitle: "http://ooops-https/", + providerName: "HeuristicFallback", + heuristic: true, + }), + ], + }); + + await cleanupPlaces(); +}); + +add_task(async function test_data_file() { + let response = await fetch( + "chrome://browser/content/urlbar/preloaded-top-urls.json" + ); + + info("Source file is supplied and fetched OK"); + Assert.ok(response.ok); + + info("The JSON is parsed"); + let sites = await response.json(); + + // Add test site so this test doesn't depend on the contents of the data file. + sites.push(["https://www.example.com/", "Example"]); + + info("Storage is populated"); + UrlbarProviderPreloadedSites.populatePreloadedSiteStorage(sites); + + let lastSite = sites.pop(); + let uri = Services.io.newURI(lastSite[0]); + + info("Storage is populated from JSON correctly"); + let context = createContext(uri.host, { isPrivate: false }); + await check_results({ + context, + autofilled: uri.host + "/", + completed: uri.spec, + matches: [ + makeVisitResult(context, { + uri: uri.spec, + fallbackTitle: uri.spec.slice(0, -1), // Trim trailing slash. + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + iconUri: `page-icon:${uri.spec}`, + providerName: "PreloadedSites", + heuristic: true, + }), + ], + }); + + await cleanupPlaces(); +}); + +add_task(async function test_partial_scheme() { + // "tt" should not result in a match of "ttps://whatever.com/". + let testUrl = "http://www.ttt.com/"; + UrlbarProviderPreloadedSites.populatePreloadedSiteStorage([ + [testUrl, "Test"], + ]); + let context = createContext("tt", { isPrivate: false }); + await check_results({ + context, + autofilled: "ttt.com/", + completed: testUrl, + matches: [ + makeVisitResult(context, { + uri: testUrl, + fallbackTitle: "www.ttt.com", + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + iconUri: `page-icon:${testUrl}`, + heuristic: true, + providerName: "PreloadedSites", + }), + ], + }); + await cleanupPlaces(); +}); 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..fe625f7bb9 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_providerTabToSearch.js @@ -0,0 +1,535 @@ +/* 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 + ); + await SearchTestUtils.installSearchExtension({ name: "Test" }); + testEngine = await Services.search.getEngineByName("Test"); + + registerCleanupFunction(async () => { + 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: "test visit for https://example.com/", + heuristic: true, + providerName: "Autofill", + }), + makeSearchResult(context, { + engineName: testEngine.name, + engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS, + uri: UrlbarUtils.stripPublicSuffixFromHost(testEngine.searchUrlDomain), + 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: "test visit for https://example.com/", + heuristic: true, + providerName: "Autofill", + }), + ], + }); + Services.prefs.clearUserPref("browser.urlbar.suggest.engines"); + + await cleanupPlaces(); +}); + +// Tests that tab-to-search results are shown when the typed string matches an +// engine domain even when 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", + }), + makeSearchResult(context, { + engineName: testEngine.name, + engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS, + uri: UrlbarUtils.stripPublicSuffixFromHost(testEngine.searchUrlDomain), + providesSearchMode: true, + query: "", + providerName: "TabToSearch", + }), + ], + }); +}); + +// 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: "test visit for 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: "test visit for https://www.example.com/", + heuristic: true, + providerName: "Autofill", + }), + makeSearchResult(context, { + engineName: testEngine.name, + engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS, + uri: UrlbarUtils.stripPublicSuffixFromHost(testEngine.searchUrlDomain), + providesSearchMode: true, + query: "", + providerName: "TabToSearch", + }), + ], + }); + await cleanupPlaces(); + + // The engine has www., the history result does not. + await PlacesTestUtils.addVisits(["https://foo.bar/"]); + let extension = await SearchTestUtils.installSearchExtension( + { + name: "TestWww", + search_url: "https://www.foo.bar/", + }, + { skipUnload: true } + ); + let wwwTestEngine = Services.search.getEngineByName("TestWww"); + context = createContext("foo", { isPrivate: false }); + await check_results({ + context, + autofilled: "foo.bar/", + completed: "https://foo.bar/", + matches: [ + makeVisitResult(context, { + uri: "https://foo.bar/", + title: "test visit for https://foo.bar/", + heuristic: true, + providerName: "Autofill", + }), + makeSearchResult(context, { + engineName: wwwTestEngine.name, + engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS, + uri: UrlbarUtils.stripPublicSuffixFromHost( + wwwTestEngine.searchUrlDomain + ), + 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: "test visit for https://www.foo.bar/", + heuristic: true, + providerName: "Autofill", + }), + makeSearchResult(context, { + engineName: wwwTestEngine.name, + engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS, + uri: UrlbarUtils.stripPublicSuffixFromHost( + wwwTestEngine.searchUrlDomain + ), + providesSearchMode: true, + query: "", + providerName: "TabToSearch", + }), + ], + }); + await cleanupPlaces(); + + await extension.unload(); +}); + +// 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 extension1 = await SearchTestUtils.installSearchExtension( + { + name: "TestFooBar", + search_url: "https://foobar.com/", + }, + { skipUnload: true } + ); + let extension2 = await SearchTestUtils.installSearchExtension( + { + name: "TestFoo", + search_url: "https://foo.com/", + }, + { skipUnload: true } + ); + let fooBarTestEngine = Services.search.getEngineByName("TestFooBar"); + let fooTestEngine = Services.search.getEngineByName("TestFoo"); + + // 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: "test visit for https://foo.com/", + heuristic: true, + providerName: "Autofill", + }), + makeSearchResult(context, { + engineName: fooTestEngine.name, + engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS, + uri: UrlbarUtils.stripPublicSuffixFromHost( + fooTestEngine.searchUrlDomain + ), + providesSearchMode: true, + query: "", + providerName: "TabToSearch", + }), + makeVisitResult(context, { + uri: "https://foobar.com/", + title: "test visit for https://foobar.com/", + providerName: "Places", + }), + ], + }); + + // 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: "test visit for https://foobar.com/", + heuristic: true, + providerName: "Autofill", + }), + makeSearchResult(context, { + engineName: fooBarTestEngine.name, + engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS, + uri: UrlbarUtils.stripPublicSuffixFromHost( + fooBarTestEngine.searchUrlDomain + ), + providesSearchMode: true, + query: "", + providerName: "TabToSearch", + }), + ], + }); + + await cleanupPlaces(); + await extension1.unload(); + await extension2.unload(); +}); + +add_task(async function multipleEnginesForHostname() { + info( + "In case of multiple engines only one tab-to-search result should be returned" + ); + let extension = await SearchTestUtils.installSearchExtension( + { + name: "TestMaps", + search_url: "https://example.com/maps/", + }, + { skipUnload: true } + ); + + let context = createContext("examp", { isPrivate: false }); + let maxResultCount = UrlbarPrefs.get("maxRichResults"); + + // Add enough visits to autofill example.com. + for (let i = 0; i < maxResultCount; i++) { + await PlacesTestUtils.addVisits("https://example.com/"); + } + + // Add enough visits to other URLs matching our query to fill up the list of + // results. + let otherVisitResults = []; + for (let i = 0; i < maxResultCount; i++) { + let url = "https://mochi.test:8888/example/" + i; + await PlacesTestUtils.addVisits(url); + otherVisitResults.unshift( + makeVisitResult(context, { + uri: url, + title: "test visit for " + url, + }) + ); + } + + await check_results({ + context, + autofilled: "example.com/", + completed: "https://example.com/", + matches: [ + makeVisitResult(context, { + uri: "https://example.com/", + title: "test visit for https://example.com/", + heuristic: true, + providerName: "Autofill", + }), + makeSearchResult(context, { + engineName: testEngine.name, + engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS, + uri: UrlbarUtils.stripPublicSuffixFromHost(testEngine.searchUrlDomain), + providesSearchMode: true, + query: "", + providerName: "TabToSearch", + }), + // There should be `maxResultCount` - 2 other visit results. If this fails + // because there are actually `maxResultCount` - 3 other results, then the + // muxer is improperly including both TabToSearch results in its + // calculation of the total available result span instead of only one, so + // one fewer visit result appears than expected. + ...otherVisitResults.slice(0, maxResultCount - 2), + ], + }); + await cleanupPlaces(); + await extension.unload(); +}); + +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: "test visit for https://example.com/", + heuristic: true, + providerName: "Autofill", + }), + makeSearchResult(context, { + engineName: testEngine.name, + engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS, + uri: UrlbarUtils.stripPublicSuffixFromHost(testEngine.searchUrlDomain), + 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 extension = await SearchTestUtils.installSearchExtension( + { + name: "MyTest", + search_url: "https://test.mytest.it/", + }, + { skipUnload: true } + ); + let engine = Services.search.getEngineByName("MyTest"); + 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, + uri: UrlbarUtils.stripPublicSuffixFromHost(engine.searchUrlDomain), + providesSearchMode: true, + query: "", + providerName: "TabToSearch", + satisfiesAutofillThreshold: true, + }), + makeVisitResult(context, { + uri: "https://test.mytest.it/", + title: "test visit for https://test.mytest.it/", + providerName: "Places", + }), + ], + }); + await cleanupPlaces(); + await extension.unload(); +}); + +add_task(async function test_publicSuffixIsHost() { + info("Tab-to-search results does not appear in case we autofill a suffix."); + let extension = await SearchTestUtils.installSearchExtension( + { + name: "SuffixTest", + search_url: "https://somesuffix.com.mx/", + }, + { skipUnload: true } + ); + + // 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: "test visit for https://com.mx/", + heuristic: true, + providerName: "Autofill", + }), + ], + }); + await cleanupPlaces(); + await extension.unload(); +}); + +add_task(async function test_disabledEngine() { + info("Tab-to-search results does not appear for a Pref-disabled engine."); + let extension = await SearchTestUtils.installSearchExtension( + { + name: "Disabled", + search_url: "https://disabled.com/", + }, + { skipUnload: true } + ); + let engine = Services.search.getEngineByName("Disabled"); + 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: "test visit for https://disabled.com/", + heuristic: true, + providerName: "Autofill", + }), + makeSearchResult(context, { + engineName: engine.name, + engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS, + uri: UrlbarUtils.stripPublicSuffixFromHost(engine.searchUrlDomain), + 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: "test visit for https://disabled.com/", + heuristic: true, + providerName: "Autofill", + }), + ], + }); + Services.prefs.clearUserPref("browser.search.hiddenOneOffs"); + + await cleanupPlaces(); + await extension.unload(); +}); 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..d2391c0f43 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_providerTabToSearch_partialHost.js @@ -0,0 +1,214 @@ +/* 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"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarProviderAutofill: "resource:///modules/UrlbarProviderAutofill.sys.mjs", +}); + +add_task(async function setup() { + Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false); + Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", 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.urlbar.suggest.quickactions"); + Services.prefs.clearUserPref( + "browser.search.separatePrivateDefault.ui.enabled" + ); + Services.prefs.clearUserPref( + "browser.urlbar.tabToSearch.onboard.interactionsLeft" + ); + }); +}); + +add_task(async function test() { + let url = "https://en.example.com/"; + await SearchTestUtils.installSearchExtension( + { + name: "TestEngine", + search_url: url, + }, + { setAsDefault: true } + ); + + // 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: "TestEngine", + engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS, + 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/"; + await SearchTestUtils.installSearchExtension({ + name: "TestEngine2", + search_url: url2, + }); + + let engine2 = Services.search.getEngineByName("TestEngine2"); + // 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, + 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("Tab-to-search is not shown when an unrelated site is autofilled."); + let wikiUrl = "https://wikipedia.org/"; + await SearchTestUtils.installSearchExtension({ + name: "FakeWikipedia", + search_url: url, + }); + let wikiEngine = Services.search.getEngineByName("TestEngine"); + + // Make sure that wikiUrl will pass getTopHostOverThreshold. + await PlacesUtils.bookmarks.insert({ + url: wikiUrl, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "Wikipedia", + }); + + // Make sure an unrelated www site is autofilled. + let wwwUrl = "https://www.example.com"; + await PlacesUtils.bookmarks.insert({ + url: wwwUrl, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "Example", + }); + + let searchStr = "w"; + let context = createContext(searchStr, { + isPrivate: false, + sources: [UrlbarUtils.RESULT_SOURCE.BOOKMARKS], + }); + let host = await UrlbarProviderAutofill.getTopHostOverThreshold(context, [ + wikiEngine.searchUrlDomain, + ]); + Assert.equal( + host, + wikiEngine.searchUrlDomain, + "The search satisfies the autofill threshold requirement." + ); + await check_results({ + context, + autofilled: "www.example.com/", + completed: "https://www.example.com/", + matches: [ + makeVisitResult(context, { + uri: `${wwwUrl}/`, + title: "Example", + heuristic: true, + providerName: "Autofill", + }), + // Note that tab-to-search is not shown. + makeBookmarkResult(context, { + uri: wikiUrl, + title: "Wikipedia", + }), + makeBookmarkResult(context, { + uri: url2, + title: "bookmark", + }), + ], + }); + + info("Restricting to history should not autofill our bookmark"); + 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"); + + await cleanupPlaces(); +}); 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..8446ed0675 --- /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 provider = registerBasicTestProvider([match]); + let context = createContext(undefined, { providers: [provider.name] }); + 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..a631bfa204 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_providersManager_filtering.js @@ -0,0 +1,407 @@ +/* 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 provider = registerBasicTestProvider([match]); + let context = createContext(undefined, { providers: [provider.name] }); + 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: provider.name }); +}); + +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 provider = registerBasicTestProvider(matches); + let context = createContext(undefined, { providers: [provider.name] }); + 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(provider); +}); + +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 provider = registerBasicTestProvider(matches); + let context = createContext(`foo ${UrlbarTokenizer.RESTRICT.OPENPAGE}`, { + providers: [provider.name], + }); + 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(provider); +}); + +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 provider = registerBasicTestProvider([match, jsMatch]); + let context = createContext(undefined, { providers: [provider.name] }); + 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: [provider.name], + }); + 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: [provider.name] }); + 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(provider); +}); + +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 provider = 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); + } + } + } + let badProvider = new NoInvokeProvider(); + UrlbarProvidersManager.registerProvider(badProvider); + + let context = createContext(undefined, { + sources: [UrlbarUtils.RESULT_SOURCE.TABS], + providers: [provider.name, "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(provider); + UrlbarProvidersManager.unregisterProvider(badProvider); +}); + +add_task(async function test_filter_queryContext() { + let provider = 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"); + } + } + let badProvider = new NoInvokeProvider(); + UrlbarProvidersManager.registerProvider(badProvider); + + let context = createContext(undefined, { + providers: [provider.name], + }); + let controller = UrlbarTestUtils.newMockController(); + + await controller.startQuery(context, controller); + UrlbarProvidersManager.unregisterProvider(provider); + UrlbarProvidersManager.unregisterProvider(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 provider = registerBasicTestProvider( + matches, + undefined, + UrlbarUtils.PROVIDER_TYPE.HEURISTIC + ); + + let context = createContext(undefined, { + sources: [UrlbarUtils.RESULT_SOURCE.SEARCH], + providers: [provider.name], + }); + 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(provider); +}); + +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..b30b9352cd --- /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 provider = registerBasicTestProvider(matches); + let context = createContext(undefined, { providers: [provider.name] }); + 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..1d6171eac4 --- /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"; + +ChromeUtils.defineESModuleGetters(this, { + QueryScorer: "resource:///modules/UrlbarProviderInterventions.sys.mjs", +}); + +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..e49c966d76 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_query_url.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 PLACES_PROVIDERNAME = "Places"; + +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/", + fallbackTitle: "file.org", + heuristic: true, + }), + makeVisitResult(context, { + uri: "file:///c:/test.html", + title: "test visit for file:///c:/test.html", + iconUri: UrlbarUtils.ICON.DEFAULT, + providerName: PLACES_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "http://file.org/test/", + title: "test visit for http://file.org/test/", + providerName: PLACES_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/", + fallbackTitle: "file.org/", + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://file.org/test/", + title: "test visit for http://file.org/test/", + providerName: PLACES_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: "test visit for http://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: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { + uri: "file:///c:/test.html", + title: "test visit for file:///c:/test.html", + iconUri: UrlbarUtils.ICON.DEFAULT, + providerName: PLACES_PROVIDERNAME, + }), + ], + }); + await cleanupPlaces(); +}); diff --git a/browser/components/urlbar/tests/unit/test_quickactions.js b/browser/components/urlbar/tests/unit/test_quickactions.js new file mode 100644 index 0000000000..ddeb5a7561 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_quickactions.js @@ -0,0 +1,126 @@ +/* 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"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarProviderQuickActions: + "resource:///modules/UrlbarProviderQuickActions.sys.mjs", +}); + +let expectedMatch = (key, inputLength) => ({ + type: UrlbarUtils.RESULT_TYPE.DYNAMIC, + source: UrlbarUtils.RESULT_SOURCE.ACTIONS, + heuristic: false, + payload: { + results: [{ key }], + dynamicType: "quickactions", + helpUrl: UrlbarProviderQuickActions.helpUrl, + inputLength, + }, +}); + +testEngine_setup(); + +add_task(async function init() { + UrlbarPrefs.set("quickactions.enabled", true); + UrlbarPrefs.set("suggest.quickactions", true); + + UrlbarProviderQuickActions.addAction("newaction", { + commands: ["newaction"], + }); + + registerCleanupFunction(async () => { + UrlbarPrefs.clear("quickactions.enabled"); + UrlbarPrefs.clear("suggest.quickactions"); + UrlbarProviderQuickActions.removeAction("newaction"); + }); +}); + +add_task(async function nomatch() { + let context = createContext("this doesnt match", { + providers: [UrlbarProviderQuickActions.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [], + }); +}); + +add_task(async function quickactions_disabled() { + UrlbarPrefs.set("suggest.quickactions", false); + let context = createContext("new", { + providers: [UrlbarProviderQuickActions.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [], + }); +}); + +add_task(async function quickactions_match() { + UrlbarPrefs.set("suggest.quickactions", true); + let context = createContext("new", { + providers: [UrlbarProviderQuickActions.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [expectedMatch("newaction", 3)], + }); +}); + +add_task(async function duplicate_matches() { + UrlbarProviderQuickActions.addAction("testaction", { + commands: ["testaction", "test"], + }); + + let context = createContext("testaction", { + providers: [UrlbarProviderQuickActions.name], + isPrivate: false, + }); + + await check_results({ + context, + matches: [expectedMatch("testaction", 10)], + }); + + UrlbarProviderQuickActions.removeAction("testaction"); +}); + +add_task(async function remove_action() { + UrlbarProviderQuickActions.addAction("testaction", { + commands: ["testaction"], + }); + UrlbarProviderQuickActions.removeAction("testaction"); + + let context = createContext("test", { + providers: [UrlbarProviderQuickActions.name], + isPrivate: false, + }); + + await check_results({ + context, + matches: [], + }); +}); + +add_task(async function minimum_search_string() { + let searchString = "newa"; + for (let minimumSearchString of [0, 3]) { + UrlbarPrefs.set("quickactions.minimumSearchString", minimumSearchString); + for (let i = 1; i < 4; i++) { + let context = createContext(searchString.substring(0, i), { + providers: [UrlbarProviderQuickActions.name], + isPrivate: false, + }); + let matches = + i >= minimumSearchString ? [expectedMatch("newaction", i)] : []; + await check_results({ context, matches }); + } + } + UrlbarPrefs.clear("quickactions.minimumSearchString"); +}); diff --git a/browser/components/urlbar/tests/unit/test_remote_tabs.js b/browser/components/urlbar/tests/unit/test_remote_tabs.js new file mode 100644 index 0000000000..5b834c876e --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_remote_tabs.js @@ -0,0 +1,695 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim:set ts=2 sw=2 sts=2 et: + */ +"use strict"; + +const { Weave } = ChromeUtils.importESModule( + "resource://services-sync/main.sys.mjs" +); + +// A mock "Tabs" engine which autocomplete will use instead of the real +// engine. We pass a constructor that Sync creates. +function MockTabsEngine() { + this.clients = null; // We'll set this dynamically +} + +MockTabsEngine.prototype = { + name: "tabs", + + startTracking() {}, + getAllClients() { + return this.clients; + }, +}; + +// A clients engine that doesn't need to be a constructor. +let MockClientsEngine = { + getClientType(guid) { + Assert.ok(guid.endsWith("desktop") || guid.endsWith("mobile")); + return guid.endsWith("mobile") ? "phone" : "desktop"; + }, + remoteClientExists(id) { + return true; + }, + getClientName(id) { + return id.endsWith("mobile") ? "My Phone" : "My Desktop"; + }, +}; + +// Configure the singleton engine for a test. +function configureEngine(clients) { + // Configure the instance Sync created. + let engine = Weave.Service.engineManager.get("tabs"); + engine.clients = clients; + Weave.Service.clientsEngine = MockClientsEngine; + // Send an observer that pretends the engine just finished a sync. + Services.obs.notifyObservers(null, "weave:engine:sync:finish", "tabs"); +} + +testEngine_setup(); + +add_task(async function setup() { + // Tell Sync about the mocks. + Weave.Service.engineManager.register(MockTabsEngine); + + // Tell the Sync XPCOM service it is initialized. + let weaveXPCService = Cc["@mozilla.org/weave/service;1"].getService( + Ci.nsISupports + ).wrappedJSObject; + weaveXPCService.ready = true; + + registerCleanupFunction(async () => { + Services.prefs.clearUserPref("services.sync.username"); + Services.prefs.clearUserPref("services.sync.registerEngines"); + Services.prefs.clearUserPref("browser.urlbar.suggest.searches"); + Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions"); + await cleanupPlaces(); + }); + + Services.prefs.setCharPref("services.sync.username", "someone@somewhere.com"); + Services.prefs.setCharPref("services.sync.registerEngines", ""); + // Avoid hitting the network. + Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false); + Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false); +}); + +add_task(async function test_minimal() { + // The minimal client and tabs info we can get away with. + configureEngine([ + { + id: "desktop", + tabs: [ + { + urlHistory: ["http://example.com/"], + }, + ], + }, + ]); + + let query = "ex"; + let context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeRemoteTabResult(context, { + uri: "http://example.com/", + device: "My Desktop", + }), + ], + }); +}); + +add_task(async function test_maximal() { + // Every field that could possibly exist on a remote record. + configureEngine([ + { + id: "mobile", + tabs: [ + { + urlHistory: ["http://example.com/"], + title: "An Example", + icon: "http://favicon", + }, + ], + }, + ]); + + let query = "ex"; + let context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeRemoteTabResult(context, { + uri: "http://example.com/", + device: "My Phone", + title: "An Example", + iconUri: "moz-anno:favicon:http://favicon/", + }), + ], + }); +}); + +add_task(async function test_noShowIcons() { + Services.prefs.setBoolPref("services.sync.syncedTabs.showRemoteIcons", false); + configureEngine([ + { + id: "mobile", + tabs: [ + { + urlHistory: ["http://example.com/"], + title: "An Example", + icon: "http://favicon", + }, + ], + }, + ]); + + let query = "ex"; + let context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeRemoteTabResult(context, { + uri: "http://example.com/", + device: "My Phone", + title: "An Example", + // expecting the default favicon due to that pref. + iconUri: "", + }), + ], + }); + Services.prefs.clearUserPref("services.sync.syncedTabs.showRemoteIcons"); +}); + +add_task(async function test_dontMatchSyncedTabs() { + Services.prefs.setBoolPref("services.sync.syncedTabs.showRemoteTabs", false); + configureEngine([ + { + id: "mobile", + tabs: [ + { + urlHistory: ["http://example.com/"], + title: "An Example", + icon: "http://favicon", + }, + ], + }, + ]); + + let context = createContext("ex", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ], + }); + + Services.prefs.clearUserPref("services.sync.syncedTabs.showRemoteTabs"); +}); + +add_task(async function test_tabsDisabledInUrlbar() { + Services.prefs.setBoolPref("browser.urlbar.suggest.remotetab", false); + configureEngine([ + { + id: "mobile", + tabs: [ + { + urlHistory: ["http://example.com/"], + title: "An Example", + icon: "http://favicon", + }, + ], + }, + ]); + + let context = createContext("ex", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ], + }); + + Services.prefs.clearUserPref("browser.urlbar.suggest.remotetab"); +}); + +add_task(async function test_matches_title() { + // URL doesn't match search expression, should still match the title. + configureEngine([ + { + id: "mobile", + tabs: [ + { + urlHistory: ["http://foo.com/"], + title: "An Example", + }, + ], + }, + ]); + + let query = "ex"; + let context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeRemoteTabResult(context, { + uri: "http://foo.com/", + device: "My Phone", + title: "An Example", + }), + ], + }); +}); + +add_task(async function test_localtab_matches_override() { + // We have an open tab to the same page on a remote device, only "switch to + // tab" should appear as duplicate detection removed the remote one. + + // First set up Sync to have the page as a remote tab. + configureEngine([ + { + id: "mobile", + tabs: [ + { + urlHistory: ["http://foo.com/"], + title: "An Example", + }, + ], + }, + ]); + + // Set up Places to think the tab is open locally. + let uri = Services.io.newURI("http://foo.com/"); + await PlacesTestUtils.addVisits([{ uri, title: "An Example" }]); + await addOpenPages(uri, 1); + + let query = "ex"; + let context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeTabSwitchResult(context, { + uri: "http://foo.com/", + title: "An Example", + }), + ], + }); + + await removeOpenPages(uri, 1); + await cleanupPlaces(); +}); + +add_task(async function test_remotetab_matches_override() { + // If we have an history result to the same page, we should only get the + // remote tab match. + let url = "http://foo.remote.com/"; + // First set up Sync to have the page as a remote tab. + configureEngine([ + { + id: "mobile", + tabs: [ + { + urlHistory: [url], + title: "An Example", + }, + ], + }, + ]); + + // Set up Places to think the tab is in history. + await PlacesTestUtils.addVisits(url); + + let query = "ex"; + let context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeRemoteTabResult(context, { + uri: "http://foo.remote.com/", + device: "My Phone", + title: "An Example", + }), + ], + }); + + await cleanupPlaces(); +}); + +add_task(async function test_mixed_result_types() { + // In case we have many results, non-remote results should flex to the bottom. + let url = "http://foo.remote.com/"; + let tabs = Array(6) + .fill(0) + .map((e, i) => ({ + urlHistory: [`${url}${i}`], + title: "A title", + lastUsed: Math.floor(Date.now() / 1000) - i * 86400, // i days ago. + })); + // First set up Sync to have the page as a remote tab. + configureEngine([{ id: "mobile", tabs }]); + + // Register the page as an open tab. + let openTabUrl = url + "openpage/"; + let uri = Services.io.newURI(openTabUrl); + await PlacesTestUtils.addVisits([{ uri, title: "An Example" }]); + await addOpenPages(uri, 1); + + // Also add a local history result. + let historyUrl = url + "history/"; + await PlacesTestUtils.addVisits(historyUrl); + + let query = "rem"; + let context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeRemoteTabResult(context, { + uri: "http://foo.remote.com/0", + device: "My Phone", + title: "A title", + lastUsed: tabs[0].lastUsed, + }), + makeRemoteTabResult(context, { + uri: "http://foo.remote.com/1", + device: "My Phone", + title: "A title", + lastUsed: tabs[1].lastUsed, + }), + makeRemoteTabResult(context, { + uri: "http://foo.remote.com/2", + device: "My Phone", + title: "A title", + lastUsed: tabs[2].lastUsed, + }), + makeRemoteTabResult(context, { + uri: "http://foo.remote.com/3", + device: "My Phone", + title: "A title", + lastUsed: tabs[3].lastUsed, + }), + makeRemoteTabResult(context, { + uri: "http://foo.remote.com/4", + device: "My Phone", + title: "A title", + lastUsed: tabs[4].lastUsed, + }), + makeRemoteTabResult(context, { + uri: "http://foo.remote.com/5", + device: "My Phone", + title: "A title", + lastUsed: tabs[5].lastUsed, + }), + makeVisitResult(context, { + uri: historyUrl, + title: "test visit for " + historyUrl, + }), + makeTabSwitchResult(context, { + uri: openTabUrl, + title: "An Example", + }), + ], + }); + await removeOpenPages(uri, 1); + await cleanupPlaces(); +}); + +add_task(async function test_many_remotetab_results() { + let url = "http://foo.remote.com/"; + let tabs = Array(8) + .fill(0) + .map((e, i) => ({ + urlHistory: [`${url}${i}`], + title: "A title", + lastUsed: Math.floor(Date.now() / 1000) - i * 86400, // i days old. + })); + + // First set up Sync to have the page as a remote tab. + configureEngine([ + { + id: "mobile", + tabs, + }, + ]); + + let query = "rem"; + let context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeRemoteTabResult(context, { + uri: "http://foo.remote.com/0", + device: "My Phone", + title: "A title", + lastUsed: tabs[0].lastUsed, + }), + makeRemoteTabResult(context, { + uri: "http://foo.remote.com/1", + device: "My Phone", + title: "A title", + lastUsed: tabs[1].lastUsed, + }), + makeRemoteTabResult(context, { + uri: "http://foo.remote.com/2", + device: "My Phone", + title: "A title", + lastUsed: tabs[2].lastUsed, + }), + makeRemoteTabResult(context, { + uri: "http://foo.remote.com/3", + device: "My Phone", + title: "A title", + lastUsed: tabs[3].lastUsed, + }), + makeRemoteTabResult(context, { + uri: "http://foo.remote.com/4", + device: "My Phone", + title: "A title", + lastUsed: tabs[4].lastUsed, + }), + makeRemoteTabResult(context, { + uri: "http://foo.remote.com/5", + device: "My Phone", + title: "A title", + lastUsed: tabs[5].lastUsed, + }), + makeRemoteTabResult(context, { + uri: "http://foo.remote.com/6", + device: "My Phone", + title: "A title", + lastUsed: tabs[6].lastUsed, + }), + makeRemoteTabResult(context, { + uri: "http://foo.remote.com/7", + device: "My Phone", + title: "A title", + lastUsed: tabs[7].lastUsed, + }), + ], + }); +}); + +add_task(async function multiple_clients() { + let url = "http://foo.remote.com/"; + let mobileTabs = Array(2) + .fill(0) + .map((e, i) => ({ + urlHistory: [`${url}mobile/${i}`], + lastUsed: Date.now() / 1000 - 4 * 86400, // 4 days old (past threshold) + })); + + let desktopTabs = Array(3) + .fill(0) + .map((e, i) => ({ + urlHistory: [`${url}desktop/${i}`], + lastUsed: Date.now() / 1000 - 1, // Fresh tabs + })); + + // mobileTabs has the most recent tab, making it the most recent client. The + // rest of its tabs are stale. The tabs in desktopTabs are fresh, but not + // as fresh as the most recent tab in mobileTab. + mobileTabs.push({ + urlHistory: [`${url}mobile/fresh`], + lastUsed: Date.now() / 1000, + }); + + configureEngine([ + { + id: "mobile", + tabs: mobileTabs, + }, + { + id: "desktop", + tabs: desktopTabs, + }, + ]); + + // We expect that we will show the recent tab from mobileTabs, then all the + // tabs from desktopTabs, then the remaining tabs from mobileTabs. + let query = "rem"; + let context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeRemoteTabResult(context, { + uri: "http://foo.remote.com/mobile/fresh", + device: "My Phone", + lastUsed: mobileTabs[2].lastUsed, + }), + makeRemoteTabResult(context, { + uri: "http://foo.remote.com/desktop/0", + device: "My Desktop", + lastUsed: desktopTabs[0].lastUsed, + }), + makeRemoteTabResult(context, { + uri: "http://foo.remote.com/desktop/1", + device: "My Desktop", + lastUsed: desktopTabs[1].lastUsed, + }), + makeRemoteTabResult(context, { + uri: "http://foo.remote.com/desktop/2", + device: "My Desktop", + lastUsed: desktopTabs[2].lastUsed, + }), + makeRemoteTabResult(context, { + uri: "http://foo.remote.com/mobile/0", + device: "My Phone", + lastUsed: mobileTabs[0].lastUsed, + }), + makeRemoteTabResult(context, { + uri: "http://foo.remote.com/mobile/1", + device: "My Phone", + lastUsed: mobileTabs[1].lastUsed, + }), + ], + }); +}); + +add_task(async function test_restrictionCharacter() { + let url = "http://foo.remote.com/"; + let tabs = Array(5) + .fill(0) + .map((e, i) => ({ + urlHistory: [`${url}${i}`], + title: "A title", + lastUsed: Math.floor(Date.now() / 1000) - i, + })); + configureEngine([ + { + id: "mobile", + tabs, + }, + ]); + + // Also add an open page. + let openTabUrl = url + "openpage/"; + let uri = Services.io.newURI(openTabUrl); + await PlacesTestUtils.addVisits([{ uri, title: "An Example" }]); + await addOpenPages(uri, 1); + + // We expect the open tab to flex to the bottom. + let query = UrlbarTokenizer.RESTRICT.OPENPAGE; + let context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeRemoteTabResult(context, { + uri: "http://foo.remote.com/0", + device: "My Phone", + title: "A title", + lastUsed: tabs[0].lastUsed, + }), + makeRemoteTabResult(context, { + uri: "http://foo.remote.com/1", + device: "My Phone", + title: "A title", + lastUsed: tabs[1].lastUsed, + }), + makeRemoteTabResult(context, { + uri: "http://foo.remote.com/2", + device: "My Phone", + title: "A title", + lastUsed: tabs[2].lastUsed, + }), + makeRemoteTabResult(context, { + uri: "http://foo.remote.com/3", + device: "My Phone", + title: "A title", + lastUsed: tabs[3].lastUsed, + }), + makeRemoteTabResult(context, { + uri: "http://foo.remote.com/4", + device: "My Phone", + title: "A title", + lastUsed: tabs[4].lastUsed, + }), + makeTabSwitchResult(context, { + uri: openTabUrl, + title: "An Example", + }), + ], + }); + await removeOpenPages(uri, 1); + await cleanupPlaces(); +}); + +add_task(async function test_duplicate_remote_tabs() { + let url = "http://foo.remote.com/"; + let tabs = Array(3) + .fill(0) + .map((e, i) => ({ + urlHistory: [url], + title: "A title", + lastUsed: Math.floor(Date.now() / 1000), + })); + configureEngine([ + { + id: "mobile", + tabs, + }, + ]); + + // We expect the duplicate tabs to be deduped. + let query = UrlbarTokenizer.RESTRICT.OPENPAGE; + let context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeRemoteTabResult(context, { + uri: "http://foo.remote.com/", + device: "My Phone", + title: "A title", + lastUsed: tabs[0].lastUsed, + }), + ], + }); +}); diff --git a/browser/components/urlbar/tests/unit/test_resultGroups.js b/browser/components/urlbar/tests/unit/test_resultGroups.js new file mode 100644 index 0000000000..e385f7d2fb --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_resultGroups.js @@ -0,0 +1,1576 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests the muxer's result groups composition logic: child groups, +// `availableSpan`, `maxResultCount`, flex, etc. The purpose of this test is to +// check the composition logic, not every possible result type or group. + +"use strict"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + sinon: "resource://testing-common/Sinon.sys.mjs", +}); + +// The possible limit-related properties in result groups. +const LIMIT_KEYS = ["availableSpan", "maxResultCount"]; + +// Most of this test adds tasks using `add_resultGroupsLimit_tasks`. It works +// like this. Instead of defining `maxResultCount` or `availableSpan` in their +// result groups, tasks define a `limit` property. The value of this property is +// a number just like any of the values for the limit-related properties. At +// runtime, `add_resultGroupsLimit_tasks` adds multiple tasks, one for each key +// in `LIMIT_KEYS`. In each of these tasks, the `limit` property is replaced +// with the actual limit key. This allows us to run checks against each of the +// limit keys using essentially the same task. + +const MAX_RICH_RESULTS_PREF = "browser.urlbar.maxRichResults"; + +// For simplicity, most of the flex tests below assume that this is 10, so +// you'll need to update them if you change this. +const MAX_RESULTS = 10; + +let sandbox; + +add_task(async function setup() { + // Set a specific maxRichResults for sanity's sake. + Services.prefs.setIntPref(MAX_RICH_RESULTS_PREF, MAX_RESULTS); + + sandbox = lazy.sinon.createSandbox(); + registerCleanupFunction(() => { + sandbox.restore(); + }); +}); + +add_resultGroupsLimit_tasks({ + testName: "empty root", + resultGroups: {}, + providerResults: [...makeHistoryResults(1)], + expectedResultIndexes: [], +}); + +add_resultGroupsLimit_tasks({ + testName: "root with empty children", + resultGroups: { + children: [], + }, + providerResults: [...makeHistoryResults(1)], + expectedResultIndexes: [], +}); + +add_resultGroupsLimit_tasks({ + testName: "root no match", + resultGroups: { + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + }, + providerResults: [...makeHistoryResults(1)], + expectedResultIndexes: [], +}); + +add_resultGroupsLimit_tasks({ + testName: "children no match", + resultGroups: { + children: [{ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION }], + }, + providerResults: [...makeHistoryResults(1)], + expectedResultIndexes: [], +}); + +add_resultGroupsLimit_tasks({ + // The actual max result count on the root is always context.maxResults and + // limit is ignored, so we expect the result in this case. + testName: "root limit: 0", + resultGroups: { + limit: 0, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + providerResults: [...makeHistoryResults(1)], + expectedResultIndexes: [0], +}); + +add_resultGroupsLimit_tasks({ + // The actual max result count on the root is always context.maxResults and + // limit is ignored, so we expect the result in this case. + testName: "root limit: 0 with children", + resultGroups: { + limit: 0, + children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }], + }, + providerResults: [...makeHistoryResults(1)], + expectedResultIndexes: [0], +}); + +add_resultGroupsLimit_tasks({ + testName: "child limit: 0", + resultGroups: { + children: [ + { + limit: 0, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + ], + }, + providerResults: [...makeHistoryResults(1)], + expectedResultIndexes: [], +}); + +add_resultGroupsLimit_tasks({ + testName: "root group", + resultGroups: { + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + providerResults: [...makeHistoryResults(1)], + expectedResultIndexes: [...makeIndexRange(0, 1)], +}); + +add_resultGroupsLimit_tasks({ + testName: "root group multiple", + resultGroups: { + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + providerResults: [...makeHistoryResults(2)], + expectedResultIndexes: [...makeIndexRange(0, 2)], +}); + +add_resultGroupsLimit_tasks({ + testName: "child group multiple", + resultGroups: { + children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }], + }, + providerResults: [...makeHistoryResults(2)], + expectedResultIndexes: [0, 1], +}); + +add_resultGroupsLimit_tasks({ + testName: "simple limit", + resultGroups: { + children: [ + { + limit: 1, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + ], + }, + providerResults: [...makeHistoryResults(2)], + expectedResultIndexes: [0], +}); + +add_resultGroupsLimit_tasks({ + testName: "limit siblings", + resultGroups: { + children: [ + { + limit: 1, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + { group: UrlbarUtils.RESULT_GROUP.GENERAL }, + ], + }, + providerResults: [...makeHistoryResults(2)], + expectedResultIndexes: [0, 1], +}); + +add_resultGroupsLimit_tasks({ + testName: "limit nested", + resultGroups: { + children: [ + { + limit: 1, + children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }], + }, + ], + }, + providerResults: [...makeHistoryResults(2)], + expectedResultIndexes: [0], +}); + +add_resultGroupsLimit_tasks({ + testName: "limit nested siblings", + resultGroups: { + children: [ + { + limit: 1, + children: [ + { group: UrlbarUtils.RESULT_GROUP.GENERAL }, + { group: UrlbarUtils.RESULT_GROUP.GENERAL }, + ], + }, + ], + }, + providerResults: [...makeHistoryResults(2)], + expectedResultIndexes: [0], +}); + +add_resultGroupsLimit_tasks({ + testName: "limit nested uncle", + resultGroups: { + children: [ + { + limit: 1, + children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }], + }, + { group: UrlbarUtils.RESULT_GROUP.GENERAL }, + ], + }, + providerResults: [...makeHistoryResults(2)], + expectedResultIndexes: [0, 1], +}); + +add_resultGroupsLimit_tasks({ + testName: "limit nested override bad", + resultGroups: { + children: [ + { + limit: 1, + children: [ + { + limit: 99, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + ], + }, + ], + }, + providerResults: [...makeHistoryResults(2)], + expectedResultIndexes: [0], +}); + +add_resultGroupsLimit_tasks({ + testName: "limit nested override good", + resultGroups: { + children: [ + { + limit: 99, + children: [ + { + limit: 1, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + ], + }, + ], + }, + providerResults: [...makeHistoryResults(2)], + expectedResultIndexes: [0], +}); + +add_resultGroupsLimit_tasks({ + testName: "multiple groups", + resultGroups: { + children: [ + { group: UrlbarUtils.RESULT_GROUP.GENERAL }, + { group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION }, + ], + }, + providerResults: [ + ...makeRemoteSuggestionResults(2), + ...makeHistoryResults(2), + ], + expectedResultIndexes: [...makeIndexRange(2, 2), ...makeIndexRange(0, 2)], +}); + +add_resultGroupsLimit_tasks({ + testName: "multiple groups limit 1", + resultGroups: { + children: [ + { + limit: 1, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + { group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION }, + ], + }, + providerResults: [ + ...makeRemoteSuggestionResults(2), + ...makeHistoryResults(2), + ], + expectedResultIndexes: [...makeIndexRange(2, 1), ...makeIndexRange(0, 2)], +}); + +add_resultGroupsLimit_tasks({ + testName: "multiple groups limit 2", + resultGroups: { + children: [ + { group: UrlbarUtils.RESULT_GROUP.GENERAL }, + { + limit: 1, + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + }, + ], + }, + providerResults: [ + ...makeRemoteSuggestionResults(2), + ...makeHistoryResults(2), + ], + expectedResultIndexes: [...makeIndexRange(2, 2), ...makeIndexRange(0, 1)], +}); + +add_resultGroupsLimit_tasks({ + testName: "multiple groups limit 3", + resultGroups: { + children: [ + { + limit: 1, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + { group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION }, + { group: UrlbarUtils.RESULT_GROUP.GENERAL }, + ], + }, + providerResults: [ + ...makeRemoteSuggestionResults(2), + ...makeHistoryResults(2), + ], + expectedResultIndexes: [ + ...makeIndexRange(2, 1), + ...makeIndexRange(0, 2), + ...makeIndexRange(3, 1), + ], +}); + +add_resultGroupsLimit_tasks({ + testName: "multiple groups limit 4", + resultGroups: { + children: [ + { group: UrlbarUtils.RESULT_GROUP.GENERAL }, + { + limit: 1, + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + }, + { group: UrlbarUtils.RESULT_GROUP.GENERAL }, + ], + }, + providerResults: [ + ...makeRemoteSuggestionResults(2), + ...makeHistoryResults(2), + ], + expectedResultIndexes: [...makeIndexRange(2, 2), ...makeIndexRange(0, 1)], +}); + +add_resultGroupsLimit_tasks({ + testName: "multiple groups nested 1", + resultGroups: { + children: [ + { + children: [ + { group: UrlbarUtils.RESULT_GROUP.GENERAL }, + { group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION }, + ], + }, + ], + }, + providerResults: [ + ...makeRemoteSuggestionResults(2), + ...makeHistoryResults(2), + ], + expectedResultIndexes: [...makeIndexRange(2, 2), ...makeIndexRange(0, 2)], +}); + +add_resultGroupsLimit_tasks({ + testName: "multiple groups nested 2", + resultGroups: { + children: [ + { + children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }], + }, + { + children: [{ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION }], + }, + ], + }, + providerResults: [ + ...makeRemoteSuggestionResults(2), + ...makeHistoryResults(2), + ], + expectedResultIndexes: [...makeIndexRange(2, 2), ...makeIndexRange(0, 2)], +}); + +add_resultGroupsLimit_tasks({ + testName: "multiple groups nested limit 1", + resultGroups: { + children: [ + { + limit: 1, + children: [ + { group: UrlbarUtils.RESULT_GROUP.GENERAL }, + { group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION }, + ], + }, + ], + }, + providerResults: [ + ...makeRemoteSuggestionResults(2), + ...makeHistoryResults(2), + ], + expectedResultIndexes: [...makeIndexRange(2, 1)], +}); + +add_resultGroupsLimit_tasks({ + testName: "multiple groups nested limit 2", + resultGroups: { + children: [ + { + children: [ + { + limit: 1, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + ], + }, + { + children: [{ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION }], + }, + ], + }, + providerResults: [ + ...makeRemoteSuggestionResults(2), + ...makeHistoryResults(2), + ], + expectedResultIndexes: [...makeIndexRange(2, 1), ...makeIndexRange(0, 2)], +}); + +add_resultGroupsLimit_tasks({ + testName: "multiple groups nested limit 3", + resultGroups: { + children: [ + { + children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }], + }, + { + children: [ + { + limit: 1, + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + }, + ], + }, + ], + }, + providerResults: [ + ...makeRemoteSuggestionResults(2), + ...makeHistoryResults(2), + ], + expectedResultIndexes: [...makeIndexRange(2, 2), ...makeIndexRange(0, 1)], +}); + +add_resultGroupsLimit_tasks({ + testName: "multiple groups nested limit 4", + resultGroups: { + children: [ + { + children: [ + { + limit: 1, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + ], + }, + { + children: [{ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION }], + }, + { + children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }], + }, + ], + }, + providerResults: [ + ...makeRemoteSuggestionResults(2), + ...makeHistoryResults(2), + ], + expectedResultIndexes: [ + ...makeIndexRange(2, 1), + ...makeIndexRange(0, 2), + ...makeIndexRange(3, 1), + ], +}); + +add_resultGroupsLimit_tasks({ + testName: "multiple groups nested limit 5", + resultGroups: { + children: [ + { + children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }], + }, + { + children: [ + { + limit: 1, + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + }, + ], + }, + { + children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }], + }, + ], + }, + providerResults: [ + ...makeRemoteSuggestionResults(2), + ...makeHistoryResults(2), + ], + expectedResultIndexes: [...makeIndexRange(2, 2), ...makeIndexRange(0, 1)], +}); + +add_resultGroupsLimit_tasks({ + testName: "multiple groups nested limit 6", + resultGroups: { + children: [ + { + children: [ + { + limit: 1, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + ], + }, + { group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION }, + { group: UrlbarUtils.RESULT_GROUP.GENERAL }, + ], + }, + providerResults: [ + ...makeRemoteSuggestionResults(2), + ...makeHistoryResults(2), + ], + expectedResultIndexes: [ + ...makeIndexRange(2, 1), + ...makeIndexRange(0, 2), + ...makeIndexRange(3, 1), + ], +}); + +add_resultGroupsLimit_tasks({ + testName: "flex 1", + resultGroups: { + flexChildren: true, + children: [ + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + }, + ], + }, + providerResults: [ + ...makeRemoteSuggestionResults(MAX_RESULTS), + ...makeHistoryResults(MAX_RESULTS), + ], + expectedResultIndexes: [ + // general/history: round(10 * (1 / (1 + 1))) = 5 + ...makeIndexRange(MAX_RESULTS, 5), + // remote suggestions: round(10 * (1 / (1 + 1))) = 5 + ...makeIndexRange(0, 5), + ], +}); + +add_resultGroupsLimit_tasks({ + testName: "flex 2", + resultGroups: { + flexChildren: true, + children: [ + { + flex: 2, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + }, + ], + }, + providerResults: [ + ...makeRemoteSuggestionResults(MAX_RESULTS), + ...makeHistoryResults(MAX_RESULTS), + ], + expectedResultIndexes: [ + // general/history: round(10 * (2 / 3)) = 7 + ...makeIndexRange(MAX_RESULTS, 7), + // remote suggestions: round(10 * (1 / 3)) = 3 + ...makeIndexRange(0, 3), + ], +}); + +add_resultGroupsLimit_tasks({ + testName: "flex 3", + resultGroups: { + flexChildren: true, + children: [ + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + { + flex: 2, + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + }, + ], + }, + providerResults: [ + ...makeRemoteSuggestionResults(MAX_RESULTS), + ...makeHistoryResults(MAX_RESULTS), + ], + expectedResultIndexes: [ + // general/history: round(10 * (1 / 3)) = 3 + ...makeIndexRange(MAX_RESULTS, 3), + // remote suggestions: round(10 * (2 / 3)) = 7 + ...makeIndexRange(0, 7), + ], +}); + +add_resultGroupsLimit_tasks({ + testName: "flex 4", + resultGroups: { + flexChildren: true, + children: [ + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + }, + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY, + }, + ], + }, + providerResults: [ + ...makeFormHistoryResults(MAX_RESULTS), + ...makeRemoteSuggestionResults(MAX_RESULTS), + ...makeHistoryResults(MAX_RESULTS), + ], + expectedResultIndexes: [ + // general/history: round(10 * (1 / 3)) = 3, and then incremented to 4 so + // that the total result span is 10 instead of 9. This group is incremented + // because the fractional part of its unrounded ideal max result count is + // 0.33 (since 10 * (1 / 3) = 3.33), the same as the other two groups, and + // this group is first. + ...makeIndexRange(2 * MAX_RESULTS, 4), + // remote suggestions: round(10 * (1 / 3)) = 3 + ...makeIndexRange(MAX_RESULTS, 3), + // form history: round(10 * (1 / 3)) = 3 + // The first three form history results dupe the three remote suggestions, + // so they should not be included. + ...makeIndexRange(3, 3), + ], +}); + +add_resultGroupsLimit_tasks({ + testName: "flex 5", + resultGroups: { + flexChildren: true, + children: [ + { + flex: 2, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + }, + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY, + }, + ], + }, + providerResults: [ + ...makeFormHistoryResults(MAX_RESULTS), + ...makeRemoteSuggestionResults(MAX_RESULTS), + ...makeHistoryResults(MAX_RESULTS), + ], + expectedResultIndexes: [ + // general/history: round(10 * (2 / 4)) = 5 + ...makeIndexRange(2 * MAX_RESULTS, 5), + // remote suggestions: round(10 * (1 / 4)) = 3 + ...makeIndexRange(MAX_RESULTS, 3), + // form history: round(10 * (1 / 4)) = 3, but context.maxResults is 10, so 2 + // The first three form history results dupe the three remote suggestions, + // so they should not be included. + ...makeIndexRange(3, 2), + ], +}); + +add_resultGroupsLimit_tasks({ + testName: "flex 6", + resultGroups: { + flexChildren: true, + children: [ + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + { + flex: 2, + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + }, + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY, + }, + ], + }, + providerResults: [ + ...makeFormHistoryResults(MAX_RESULTS), + ...makeRemoteSuggestionResults(MAX_RESULTS), + ...makeHistoryResults(MAX_RESULTS), + ], + expectedResultIndexes: [ + // general/history: round(10 * (1 / 4)) = 3 + ...makeIndexRange(2 * MAX_RESULTS, 3), + // remote suggestions: round(10 * (2 / 4)) = 5 + ...makeIndexRange(MAX_RESULTS, 5), + // form history: round(10 * (1 / 4)) = 3, but context.maxResults is 10, so 2 + // The first five form history results dupe the five remote suggestions, so + // they should not be included. + ...makeIndexRange(5, 2), + ], +}); + +add_resultGroupsLimit_tasks({ + testName: "flex 7", + resultGroups: { + flexChildren: true, + children: [ + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + }, + { + flex: 2, + group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY, + }, + ], + }, + providerResults: [ + ...makeFormHistoryResults(MAX_RESULTS), + ...makeRemoteSuggestionResults(MAX_RESULTS), + ...makeHistoryResults(MAX_RESULTS), + ], + expectedResultIndexes: [ + // general/history: round(10 * (1 / 4)) = 3 + ...makeIndexRange(2 * MAX_RESULTS, 3), + // remote suggestions: round(10 * (1 / 4)) = 3, and then decremented to 2 so + // that the total result span is 10 instead of 11. This group is decremented + // because the fractional part of its unrounded ideal max result count is + // 0.5 (since 10 * (1 / 4) = 2.5), the same as the previous group, and the + // next group's fractional part is zero. + ...makeIndexRange(MAX_RESULTS, 2), + // form history: round(10 * (2 / 4)) = 5 + // The first 2 form history results dupe the three remote suggestions, so + // they should not be included. + ...makeIndexRange(2, 5), + ], +}); + +add_resultGroupsLimit_tasks({ + testName: "flex overfill 1", + resultGroups: { + flexChildren: true, + children: [ + { + flex: 2, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + }, + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY, + }, + ], + }, + providerResults: [ + ...makeFormHistoryResults(MAX_RESULTS), + ...makeHistoryResults(MAX_RESULTS), + ], + expectedResultIndexes: [ + // general/history: round(10 * (2 / (2 + 0 + 1))) = 7 + ...makeIndexRange(MAX_RESULTS, 7), + // form history: round(10 * (1 / (2 + 0 + 1))) = 3 + ...makeIndexRange(0, 3), + ], +}); + +add_resultGroupsLimit_tasks({ + testName: "flex overfill 2", + resultGroups: { + flexChildren: true, + children: [ + { + flex: 2, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + { + flex: 2, + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + }, + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY, + }, + ], + }, + providerResults: [ + ...makeFormHistoryResults(MAX_RESULTS), + ...makeRemoteSuggestionResults(1), + ...makeHistoryResults(MAX_RESULTS), + ], + expectedResultIndexes: [ + // general/history: round(9 * (2 / (2 + 0 + 1))) = 6 + ...makeIndexRange(MAX_RESULTS + 1, 6), + // remote suggestions + ...makeIndexRange(MAX_RESULTS, 1), + // form history: round(9 * (1 / (2 + 0 + 1))) = 3 + // The first form history result dupes the remote suggestion, so it should + // not be included. + ...makeIndexRange(1, 3), + ], +}); + +add_resultGroupsLimit_tasks({ + testName: "flex nested limit 1", + resultGroups: { + children: [ + { + limit: 5, + flexChildren: true, + children: [ + { + flex: 2, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + }, + ], + }, + ], + }, + providerResults: [ + ...makeRemoteSuggestionResults(MAX_RESULTS), + ...makeHistoryResults(MAX_RESULTS), + ], + expectedResultIndexes: [ + // general/history: round(5 * (2 / (2 + 1))) = 3 + ...makeIndexRange(MAX_RESULTS, 3), + // remote suggestions: round(5 * (1 / (2 + 1))) = 2 + ...makeIndexRange(0, 2), + ], +}); + +add_resultGroupsLimit_tasks({ + testName: "flex nested limit 2", + resultGroups: { + children: [ + { + limit: 7, + flexChildren: true, + children: [ + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + { + flex: 2, + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + }, + ], + }, + ], + }, + providerResults: [ + ...makeRemoteSuggestionResults(MAX_RESULTS), + ...makeHistoryResults(MAX_RESULTS), + ], + expectedResultIndexes: [ + // general: round(7 * (1 / (2 + 1))) = 2 + ...makeIndexRange(MAX_RESULTS, 2), + // remote suggestions: round(7 * (2 / (2 + 1))) = 5 + ...makeIndexRange(0, 5), + ], +}); + +add_resultGroupsLimit_tasks({ + testName: "flex nested limit 3", + resultGroups: { + children: [ + { + limit: 7, + flexChildren: true, + children: [ + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + { + flex: 2, + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + }, + ], + }, + { + limit: 3, + flexChildren: true, + children: [ + { + flex: 2, + group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY, + }, + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + ], + }, + ], + }, + providerResults: [ + ...makeRemoteSuggestionResults(MAX_RESULTS), + ...makeFormHistoryResults(MAX_RESULTS), + ...makeHistoryResults(MAX_RESULTS), + ], + expectedResultIndexes: [ + // general: round(7 * (1 / (2 + 1))) = 2 + ...makeIndexRange(2 * MAX_RESULTS, 2), + // remote suggestions: round(7 * (2 / (2 + 1))) = 5 + ...makeIndexRange(0, 5), + // form history: round(3 * (2 / (2 + 1))) = 2 + // The first five form history results dupe the five remote suggestions, so + // they should not be included. + ...makeIndexRange(MAX_RESULTS + 5, 2), + // general: round(3 * (1 / (2 + 1))) = 1 + ...makeIndexRange(2 * MAX_RESULTS + 2, 1), + ], +}); + +add_resultGroupsLimit_tasks({ + testName: "flex nested limit 4", + resultGroups: { + children: [ + { + limit: 7, + flexChildren: true, + children: [ + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + { + flex: 2, + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + }, + ], + }, + { + limit: 3, + children: [ + { group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY }, + { group: UrlbarUtils.RESULT_GROUP.GENERAL }, + ], + }, + ], + }, + providerResults: [ + ...makeRemoteSuggestionResults(MAX_RESULTS), + ...makeFormHistoryResults(MAX_RESULTS), + ...makeHistoryResults(MAX_RESULTS), + ], + expectedResultIndexes: [ + // general: round(7 * (1 / (2 + 1))) = 2 + ...makeIndexRange(2 * MAX_RESULTS, 2), + // remote suggestions: round(7 * (2 / (2 + 1))) = 5 + ...makeIndexRange(0, 5), + // form history: round(3 * (2 / (2 + 1))) = 2 + // The first five form history results dupe the five remote suggestions, so + // they should not be included. + ...makeIndexRange(MAX_RESULTS + 5, 3), + ], +}); + +add_resultGroupsLimit_tasks({ + testName: "flex nested limit 5", + resultGroups: { + children: [ + { + limit: 7, + flexChildren: true, + children: [ + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + { + flex: 2, + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + }, + ], + }, + { + limit: 3, + group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY, + }, + ], + }, + providerResults: [ + ...makeRemoteSuggestionResults(MAX_RESULTS), + ...makeFormHistoryResults(MAX_RESULTS), + ...makeHistoryResults(MAX_RESULTS), + ], + expectedResultIndexes: [ + // general: round(7 * (1 / (2 + 1))) = 2 + ...makeIndexRange(2 * MAX_RESULTS, 2), + // remote suggestions: round(7 * (2 / (2 + 1))) = 5 + ...makeIndexRange(0, 5), + // form history: round(3 * (2 / (2 + 1))) = 2 + // The first five form history results dupe the five remote suggestions, so + // they should not be included. + ...makeIndexRange(MAX_RESULTS + 5, 3), + ], +}); + +add_resultGroupsLimit_tasks({ + testName: "flex nested", + resultGroups: { + flexChildren: true, + children: [ + { + flex: 2, + flexChildren: true, + children: [ + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + { + flex: 2, + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + }, + ], + }, + { + flex: 1, + flexChildren: true, + children: [ + { + flex: 2, + group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY, + }, + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + ], + }, + ], + }, + providerResults: [ + ...makeRemoteSuggestionResults(MAX_RESULTS), + ...makeFormHistoryResults(MAX_RESULTS), + ...makeHistoryResults(MAX_RESULTS), + ], + expectedResultIndexes: [ + // outer 1: general & remote suggestions: round(10 * (2 / (2 + 1))) = 7 + // inner 1: general: round(7 * (1 / (2 + 1))) = 2 + ...makeIndexRange(2 * MAX_RESULTS, 2), + // inner 2: remote suggestions: round(7 * (2 / (2 + 1))) = 5 + ...makeIndexRange(0, 5), + + // outer 2: form history & general: round(10 * (1 / (2 + 1))) = 3 + // inner 1: form history: round(3 * (2 / (2 + 1))) = 2 + // The first five form history results dupe the five remote suggestions, so + // they should not be included. + ...makeIndexRange(MAX_RESULTS + 5, 2), + // inner 2: general: round(3 * (1 / (2 + 1))) = 1 + ...makeIndexRange(2 * MAX_RESULTS + 2, 1), + ], +}); + +add_resultGroupsLimit_tasks({ + testName: "flex nested overfill 1", + resultGroups: { + flexChildren: true, + children: [ + { + flex: 2, + flexChildren: true, + children: [ + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + { + flex: 2, + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + }, + ], + }, + { + flex: 1, + flexChildren: true, + children: [ + { + flex: 2, + group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY, + }, + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + ], + }, + ], + }, + providerResults: [ + ...makeRemoteSuggestionResults(MAX_RESULTS), + ...makeFormHistoryResults(MAX_RESULTS), + ], + expectedResultIndexes: [ + // outer 1: general & remote suggestions: round(10 * (2 / (2 + 1))) = 7 + // inner 1: general: no results + // inner 2: remote suggestions: round(7 * (2 / (2 + 0))) = 7 + ...makeIndexRange(0, 7), + + // outer 2: form history & general: round(10 * (1 / (2 + 1))) = 3 + // inner 1: form history: round(3 * (2 / (2 + 0))) = 3 + // The first seven form history results dupe the seven remote suggestions, + // so they should not be included. + ...makeIndexRange(MAX_RESULTS + 7, 3), + // inner 2: general: no results + ], +}); + +add_resultGroupsLimit_tasks({ + testName: "flex nested overfill 2", + resultGroups: { + flexChildren: true, + children: [ + { + flex: 2, + flexChildren: true, + children: [ + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + { + flex: 2, + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + }, + ], + }, + { + flex: 1, + flexChildren: true, + children: [ + { + flex: 2, + group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY, + }, + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + ], + }, + ], + }, + providerResults: [...makeFormHistoryResults(MAX_RESULTS)], + expectedResultIndexes: [ + // outer 1: general & remote suggestions: round(10 * (2 / (2 + 1))) = 7 + // inner 1: general: no results + // inner 2: remote suggestions: no results + + // outer 2: form history & general: round(10 * (1 / (0 + 1))) = 10 + // inner 1: form history: round(10 * (2 / (2 + 0))) = 10 + ...makeIndexRange(0, MAX_RESULTS), + // inner 2: general: no results + ], +}); + +add_resultGroupsLimit_tasks({ + testName: "flex nested overfill 3", + resultGroups: { + flexChildren: true, + children: [ + { + flex: 2, + flexChildren: true, + children: [ + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + { + flex: 2, + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + }, + ], + }, + { + flex: 1, + flexChildren: true, + children: [ + { + flex: 2, + group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY, + }, + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + ], + }, + ], + }, + providerResults: [...makeRemoteSuggestionResults(MAX_RESULTS)], + expectedResultIndexes: [ + // outer 1: general & remote suggestions: round(10 * (2 / (2 + 0))) = 10 + // inner 1: general: no results + // inner 2: remote suggestions: round(10 * (2 / (2 + 0))) = 10 + ...makeIndexRange(0, MAX_RESULTS), + + // outer 2: form history & general: round(10 * (1 / (2 + 1))) = 3 + // inner 1: form history: no results + // inner 2: general: no results + ], +}); + +add_resultGroupsLimit_tasks({ + testName: "limit ignored with flex", + resultGroups: { + flexChildren: true, + children: [ + { + limit: 1, + flex: 2, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + }, + ], + }, + providerResults: [ + ...makeRemoteSuggestionResults(MAX_RESULTS), + ...makeHistoryResults(MAX_RESULTS), + ], + expectedResultIndexes: [ + // general/history: round(10 * (2 / (2 + 1))) = 7 -- limit ignored + ...makeIndexRange(MAX_RESULTS, 7), + // remote suggestions: round(10 * (1 / (2 + 1))) = 3 + ...makeIndexRange(0, 3), + ], +}); + +add_resultGroupsLimit_tasks({ + testName: "resultSpan = 3 followed by others", + resultGroups: { + children: [ + { + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + { + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + }, + ], + }, + providerResults: [ + // max results remote suggestions + ...makeRemoteSuggestionResults(MAX_RESULTS), + // 1 history with resultSpan = 3 + Object.assign(makeHistoryResults(1)[0], { resultSpan: 3 }), + ], + expectedResultIndexes: [ + // general/history: 1 + ...makeIndexRange(MAX_RESULTS, 1), + // remote suggestions: maxResults - resultSpan of 3 = 10 - 3 = 7 + ...makeIndexRange(0, 7), + ], +}); + +add_resultGroups_task({ + testName: "maxResultCount: 1, availableSpan: 3", + resultGroups: { + children: [ + { + maxResultCount: 1, + availableSpan: 3, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + ], + }, + providerResults: [...makeHistoryResults(MAX_RESULTS)], + expectedResultIndexes: [0], +}); + +add_resultGroups_task({ + testName: "maxResultCount: 1, availableSpan: 3, resultSpan = 3", + resultGroups: { + children: [ + { + maxResultCount: 1, + availableSpan: 3, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + ], + }, + providerResults: [ + Object.assign(makeHistoryResults(1)[0], { resultSpan: 3 }), + Object.assign(makeHistoryResults(1)[0], { resultSpan: 3 }), + Object.assign(makeHistoryResults(1)[0], { resultSpan: 3 }), + ], + expectedResultIndexes: [0], +}); + +add_resultGroups_task({ + testName: "maxResultCount: 3, availableSpan: 1", + resultGroups: { + children: [ + { + maxResultCount: 3, + availableSpan: 1, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + ], + }, + providerResults: [...makeHistoryResults(MAX_RESULTS)], + expectedResultIndexes: [0], +}); + +add_resultGroups_task({ + testName: "maxResultCount: 3, availableSpan: 1, resultSpan = 3", + resultGroups: { + children: [ + { + maxResultCount: 3, + availableSpan: 1, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + ], + }, + providerResults: [Object.assign(makeHistoryResults(1)[0], { resultSpan: 3 })], + expectedResultIndexes: [], +}); + +add_resultGroups_task({ + testName: "availableSpan: 1", + resultGroups: { + children: [ + { + availableSpan: 1, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + ], + }, + providerResults: [...makeHistoryResults(MAX_RESULTS)], + expectedResultIndexes: [0], +}); + +add_resultGroups_task({ + testName: "availableSpan: 1, resultSpan = 3", + resultGroups: { + children: [ + { + availableSpan: 1, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + ], + }, + providerResults: [Object.assign(makeHistoryResults(1)[0], { resultSpan: 3 })], + expectedResultIndexes: [], +}); + +add_resultGroups_task({ + testName: "availableSpan: 3, resultSpan = 2 and resultSpan = 1", + resultGroups: { + children: [ + { + availableSpan: 3, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + ], + }, + providerResults: [ + makeHistoryResults(1)[0], + Object.assign(makeHistoryResults(1)[0], { resultSpan: 2 }), + makeHistoryResults(1)[0], + ], + expectedResultIndexes: [0, 1], +}); + +add_resultGroups_task({ + testName: "availableSpan: 3, resultSpan = 1 and resultSpan = 2", + resultGroups: { + children: [ + { + availableSpan: 3, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + ], + }, + providerResults: [ + Object.assign(makeHistoryResults(1)[0], { resultSpan: 2 }), + makeHistoryResults(1)[0], + makeHistoryResults(1)[0], + ], + expectedResultIndexes: [0, 1], +}); + +/** + * Adds a single test task. + * + * @param {object} options + * The options for the test + * @param {string} options.testName + * This name is logged with `info` as the task starts. + * @param {object} options.resultGroups + * browser.urlbar.resultGroups is set to this value as the task starts. + * @param {Array} options.providerResults + * Array of result objects that the test provider will add. + * @param {Array} options.expectedResultIndexes + * Array of indexes in `providerResults` of the expected final results. + */ +function add_resultGroups_task({ + testName, + resultGroups, + providerResults, + expectedResultIndexes, +}) { + let func = async () => { + info(`Running resultGroups test: ${testName}`); + info(`Setting result groups: ` + JSON.stringify(resultGroups)); + setResultGroups(resultGroups); + let provider = registerBasicTestProvider(providerResults); + let context = createContext("foo", { providers: [provider.name] }); + let controller = UrlbarTestUtils.newMockController(); + await UrlbarProvidersManager.startQuery(context, controller); + UrlbarProvidersManager.unregisterProvider(provider); + let expectedResults = expectedResultIndexes.map(i => providerResults[i]); + Assert.deepEqual(context.results, expectedResults); + setResultGroups(null); + }; + Object.defineProperty(func, "name", { value: testName }); + add_task(func); +} + +/** + * Adds test tasks for each of the keys in `LIMIT_KEYS`. + * + * @param {object} options + * The options for the test + * @param {string} options.testName + * The name of the test. + * @param {object} options.resultGroups + * The resultGroups object to set. + * @param {Array} options.providerResults + * The results to return from the test + * @param {Array} options.expectedResultIndexes + * Indexes of the expected results within {@link providerResults} + */ +function add_resultGroupsLimit_tasks({ + testName, + resultGroups, + providerResults, + expectedResultIndexes, +}) { + for (let key of LIMIT_KEYS) { + add_resultGroups_task({ + testName: `${testName} (limit: ${key})`, + resultGroups: replaceLimitWithKey(resultGroups, key), + providerResults, + expectedResultIndexes, + }); + } +} + +function replaceLimitWithKey(group, key) { + group = { ...group }; + if ("limit" in group) { + group[key] = group.limit; + delete group.limit; + } + for (let i = 0; i < group.children?.length; i++) { + group.children[i] = replaceLimitWithKey(group.children[i], key); + } + return group; +} + +function makeHistoryResults(count) { + let results = []; + for (let i = 0; i < count; i++) { + results.push( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://example.com/" + i } + ) + ); + } + return results; +} + +function makeRemoteSuggestionResults(count) { + let results = []; + for (let i = 0; i < count; i++) { + results.push( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.SEARCH, + UrlbarUtils.RESULT_SOURCE.SEARCH, + { + engine: "test", + query: "test", + suggestion: "test " + i, + lowerCaseSuggestion: "test " + i, + } + ) + ); + } + return results; +} + +function makeFormHistoryResults(count) { + let results = []; + for (let i = 0; i < count; i++) { + results.push( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.SEARCH, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { + engine: "test", + suggestion: "test " + i, + lowerCaseSuggestion: "test " + i, + } + ) + ); + } + return results; +} + +function makeIndexRange(startIndex, count) { + let indexes = []; + for (let i = startIndex; i < startIndex + count; i++) { + indexes.push(i); + } + return indexes; +} + +function setResultGroups(resultGroups) { + sandbox.restore(); + if (resultGroups) { + sandbox.stub(UrlbarPrefs, "resultGroups").get(() => resultGroups); + } +} 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..8135566547 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_search_engine_host.js @@ -0,0 +1,98 @@ +/* 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 SearchTestUtils.installSearchExtension({ + name: "MySearchEngine", + search_url: "https://my.search.com/", + }); + engine = Services.search.getEngineByName("MySearchEngine"); + registerCleanupFunction(async () => { + Services.prefs.clearUserPref("browser.urlbar.autoFill.searchEngines"); + Services.prefs.clearUserPref("browser.urlbar.suggest.searches"); + }); + + // 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 PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + ok( + (await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", { + url: 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/", + fallbackTitle: "my.search.com", + heuristic: true, + }), + makeSearchResult(context, { + engineName: engine.name, + engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS, + uri: UrlbarUtils.stripPublicSuffixFromHost(engine.searchUrlDomain), + providesSearchMode: true, + query: "", + providerName: "TabToSearch", + }), + makeVisitResult(context, { + uri: "http://my.search.com/samplepage/", + title: "test visit for http://my.search.com/samplepage/", + providerName: "Places", + }), + ], + }); + + await cleanupPlaces(); + Services.prefs.clearUserPref( + "browser.urlbar.tabToSearch.onboard.interactionsLeft" + ); +}); diff --git a/browser/components/urlbar/tests/unit/test_search_engine_restyle.js b/browser/components/urlbar/tests/unit/test_search_engine_restyle.js new file mode 100644 index 0000000000..5bfdd66416 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_search_engine_restyle.js @@ -0,0 +1,124 @@ +/* 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/. */ + +testEngine_setup(); + +const engineDomain = "s.example.com"; +add_task(async function setup() { + Services.prefs.setBoolPref("browser.urlbar.restyleSearches", true); + await SearchTestUtils.installSearchExtension({ + name: "MozSearch", + search_url: `https://${engineDomain}/search`, + }); + registerCleanupFunction(async () => { + Services.prefs.clearUserPref("browser.urlbar.restyleSearches"); + }); +}); + +add_task(async function test_searchEngine() { + let uri = Services.io.newURI(`https://${engineDomain}/search?q=Terms`); + await PlacesTestUtils.addVisits({ + uri, + title: "Terms - SearchEngine Search", + }); + + info("Past search terms should be styled."); + let context = createContext("term", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeFormHistoryResult(context, { + engineName: "MozSearch", + suggestion: "Terms", + }), + ], + }); + + info( + "Searching for a superset of the search string in history should not restyle." + ); + context = createContext("Terms Foo", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ], + }); + + info("Bookmarked past searches should not be restyled"); + await PlacesTestUtils.addBookmarkWithDetails({ + uri, + title: "Terms - SearchEngine Search", + }); + + context = createContext("term", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeBookmarkResult(context, { + uri: uri.spec, + title: "Terms - SearchEngine Search", + }), + ], + }); + + await PlacesUtils.bookmarks.eraseEverything(); + + info("Past search terms should not be styled if restyling is disabled"); + Services.prefs.setBoolPref("browser.urlbar.restyleSearches", false); + context = createContext("term", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { + uri: uri.spec, + title: "Terms - SearchEngine Search", + }), + ], + }); + Services.prefs.setBoolPref("browser.urlbar.restyleSearches", true); + + await cleanupPlaces(); +}); + +add_task(async function test_extraneousParameters() { + info("SERPs in history with extraneous parameters should not be restyled."); + let uri = Services.io.newURI( + `https://${engineDomain}/search?q=Terms&p=2&type=img` + ); + await PlacesTestUtils.addVisits({ + uri, + title: "Terms - SearchEngine Search", + }); + + let context = createContext("term", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { + uri: uri.spec, + title: "Terms - SearchEngine Search", + }), + ], + }); +}); 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..1a49ff2e7d --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_search_suggestions.js @@ -0,0 +1,2078 @@ +/* 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 lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + sinon: "resource://testing-common/Sinon.sys.mjs", +}); + +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 TAB_TO_SEARCH_PREF = "browser.urlbar.suggest.engines"; +const TRENDING_PREF = "browser.urlbar.trending.featureGate"; +const QUICKACTIONS_PREF = "browser.urlbar.suggest.quickactions"; +const MAX_RICH_RESULTS_PREF = "browser.urlbar.maxRichResults"; +const MAX_FORM_HISTORY_PREF = "browser.urlbar.maxHistoricalSearchSuggestions"; +const SHOW_SEARCH_SUGGESTIONS_FIRST_PREF = + "browser.urlbar.showSearchSuggestionsFirst"; +const SEARCH_STRING = "hello"; + +const MAX_RESULTS = Services.prefs.getIntPref(MAX_RICH_RESULTS_PREF, 10); + +var suggestionsFn; +var previousSuggestionsFn; +let port; +let sandbox; + +/** + * 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(); + sandbox.restore(); +} + +async function cleanUpSuggestions() { + await cleanup(); + if (previousSuggestionsFn) { + suggestionsFn = previousSuggestionsFn; + previousSuggestionsFn = null; + } +} + +function makeFormHistoryResults(context, count) { + let results = []; + for (let i = 0; i < count; i++) { + results.push( + makeFormHistoryResult(context, { + suggestion: `${SEARCH_STRING} world Form History ${i}`, + engineName: SUGGESTIONS_ENGINE_NAME, + }) + ); + } + return results; +} + +function makeRemoteSuggestionResults( + context, + { suggestionPrefix = SEARCH_STRING, query = undefined } = {} +) { + // The suggestions function in `setup` returns: + // [searchString, searchString + "foo", searchString + "bar"] + // But when the heuristic is a search result, the muxer discards suggestion + // results that match the search string, and therefore we expect only two + // remote suggestion results, the "foo" and "bar" ones. + return [ + makeSearchResult(context, { + query, + engineName: SUGGESTIONS_ENGINE_NAME, + suggestion: suggestionPrefix + " foo", + }), + makeSearchResult(context, { + query, + engineName: SUGGESTIONS_ENGINE_NAME, + suggestion: suggestionPrefix + " bar", + }), + ]; +} + +function setResultGroups(groups) { + sandbox.restore(); + sandbox.stub(UrlbarPrefs, "resultGroups").get(() => { + return { + children: [ + // heuristic + { + maxResultCount: 1, + children: [ + { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }, + { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_EXTENSION }, + { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_SEARCH_TIP }, + { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_OMNIBOX }, + { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_AUTOFILL }, + { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TOKEN_ALIAS_ENGINE }, + { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK }, + ], + }, + // extensions using the omnibox API + { + group: UrlbarUtils.RESULT_GROUP.OMNIBOX, + }, + ...groups, + ], + }; + }); +} + +add_task(async function setup() { + sandbox = lazy.sinon.createSandbox(); + + let engine = await addTestSuggestionsEngine(searchStr => { + return suggestionsFn(searchStr); + }); + port = engine.getSubmission("").uri.port; + + 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, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + Services.prefs.clearUserPref(PRIVATE_SEARCH_PREF); + Services.prefs.clearUserPref(TRENDING_PREF); + Services.prefs.clearUserPref(QUICKACTIONS_PREF); + Services.prefs.clearUserPref(TAB_TO_SEARCH_PREF); + sandbox.restore(); + }); + Services.search.setDefault(engine, Ci.nsISearchService.CHANGE_REASON_UNKNOWN); + Services.prefs.setBoolPref(PRIVATE_SEARCH_PREF, false); + Services.prefs.setBoolPref(TRENDING_PREF, false); + Services.prefs.setBoolPref(QUICKACTIONS_PREF, false); + // Tab-to-search engines can introduce unexpected results, espescially because + // they depend on real en-US engines. + Services.prefs.setBoolPref(TAB_TO_SEARCH_PREF, false); + + // Add MAX_RESULTS form history. + let context = createContext(SEARCH_STRING, { isPrivate: false }); + let entries = makeFormHistoryResults(context, MAX_RESULTS).map(r => ({ + value: r.payload.suggestion, + source: SUGGESTIONS_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: SUGGESTIONS_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: SUGGESTIONS_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: SUGGESTIONS_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: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ...makeFormHistoryResults(context, MAX_RESULTS - 3), + ...makeRemoteSuggestionResults(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: SUGGESTIONS_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: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ...makeFormHistoryResults(context, MAX_RESULTS - 3), + ...makeRemoteSuggestionResults(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: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ...makeFormHistoryResults(context, MAX_RESULTS - 3), + ...makeRemoteSuggestionResults(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: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ...makeFormHistoryResults(context, MAX_RESULTS - 3), + ...makeRemoteSuggestionResults(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: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ...makeFormHistoryResults(context, MAX_RESULTS - 3), + ...makeRemoteSuggestionResults(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: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ...makeFormHistoryResults(context, MAX_RESULTS - 3), + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + suggestion: "baz " + SEARCH_STRING, + }), + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + suggestion: "quux " + SEARCH_STRING, + }), + ], + }); + + await cleanUpSuggestions(); +}); + +add_task(async function remoteSuggestionsDupeSearchString() { + Services.prefs.setIntPref(MAX_FORM_HISTORY_PREF, 0); + + // Return remote suggestions with the trimmed search string, the uppercased + // search string, and the search string with a trailing space, plus the usual + // "foo" and "bar" suggestions. + setSuggestionsFn(searchStr => { + let suffixes = ["foo", "bar"]; + return [searchStr.trim(), searchStr.toUpperCase(), searchStr + " "].concat( + suffixes.map(s => searchStr + " " + s) + ); + }); + + // Do a search with a trailing space. All the variations of the search string + // with regard to spaces and case should be discarded from the remote + // suggestions, leaving only the usual "foo" and "bar" suggestions. + let query = SEARCH_STRING + " "; + let context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + query, + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ...makeRemoteSuggestionResults(context), + ], + }); + + await cleanUpSuggestions(); + Services.prefs.clearUserPref(MAX_FORM_HISTORY_PREF); +}); + +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: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ...makeFormHistoryResults(context, MAX_RESULTS - 3), + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + suggestion: "aaa", + }), + makeSearchResult(context, { + engineName: SUGGESTIONS_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: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ...makeFormHistoryResults(context, MAX_RESULTS - 5), + ...makeRemoteSuggestionResults(context), + 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`, + }), + ], + }); + + // 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: SUGGESTIONS_ENGINE_NAME, + alias: UrlbarTokenizer.RESTRICT.SEARCH, + query: SEARCH_STRING, + heuristic: true, + }), + ...makeFormHistoryResults(context, MAX_RESULTS - 3), + ...makeRemoteSuggestionResults(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: SUGGESTIONS_ENGINE_NAME, + query: "", + heuristic: true, + }), + ...makeFormHistoryResults(context, MAX_RESULTS - 1), + ], + }); + + // Also if followed by multiple spaces. + context = createContext(`${UrlbarTokenizer.RESTRICT.SEARCH} `, { + isPrivate: false, + }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + alias: UrlbarTokenizer.RESTRICT.SEARCH, + query: "", + heuristic: true, + }), + ...makeFormHistoryResults(context, MAX_RESULTS - 1), + ], + }); + + // 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: SUGGESTIONS_ENGINE_NAME, + query: "h", + heuristic: true, + }), + ...makeFormHistoryResults(context, MAX_RESULTS - 3), + ...makeRemoteSuggestionResults(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: SUGGESTIONS_ENGINE_NAME, + alias: UrlbarTokenizer.RESTRICT.SEARCH, + query: "h", + heuristic: true, + }), + ...makeFormHistoryResults(context, MAX_RESULTS - 3), + ...makeRemoteSuggestionResults(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: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ], + }); + + await cleanUpSuggestions(); +}); + +add_task(async function mixup_frecency() { + Services.prefs.setBoolPref(SUGGEST_PREF, true); + + // At most, we should have 22 results in this subtest. We set this to 30 to + // make we're not cutting off any results and we are actually getting 22. + Services.prefs.setIntPref(MAX_RICH_RESULTS_PREF, 30); + + // 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: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ...makeFormHistoryResults(context, MAX_RESULTS), + ...makeRemoteSuggestionResults(context), + 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`, + }), + 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 mixup. + setResultGroups([ + // 1 suggestion + { + maxResultCount: 1, + children: [ + { group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY }, + { group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION }, + ], + }, + // 5 general + { + maxResultCount: 5, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + // 1 suggestion + { + maxResultCount: 1, + children: [ + { group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY }, + { group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION }, + ], + }, + // remaining general + { group: UrlbarUtils.RESULT_GROUP.GENERAL }, + // remaining suggestions + { group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY }, + { group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION }, + ]); + + // 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: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ...makeFormHistoryResults(context, 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`, + }), + ...makeFormHistoryResults(context, 2).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`, + }), + ...makeFormHistoryResults(context, MAX_RESULTS).slice(2), + ...makeRemoteSuggestionResults(context), + ], + }); + + 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: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ...makeFormHistoryResults(context, MAX_RESULTS - 3), + ...makeRemoteSuggestionResults(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}/`, + fallbackTitle: `http://${SEARCH_STRING}/`, + iconUri: "", + heuristic: true, + }), + ...makeFormHistoryResults(context, MAX_RESULTS - 2), + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: false, + }), + ], + }); + + // 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: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ...makeFormHistoryResults(context, MAX_RESULTS - 3), + ...makeRemoteSuggestionResults(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}/`, + fallbackTitle: `http://${SEARCH_STRING}/`, + iconUri: "", + heuristic: true, + }), + ...makeFormHistoryResults(context, MAX_RESULTS - 2), + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: false, + }), + ], + }); + + context = createContext("somethingelse", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "http://somethingelse/", + fallbackTitle: "http://somethingelse/", + iconUri: "", + heuristic: true, + }), + makeSearchResult(context, { + engineName: SUGGESTIONS_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: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ...makeFormHistoryResults(context, MAX_RESULTS - 3), + ...makeRemoteSuggestionResults(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/", + fallbackTitle: "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/", + fallbackTitle: "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/", + fallbackTitle: "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", + fallbackTitle: "data:text/plain,Content", + iconUri: "", + heuristic: true, + }), + ], + }); + + context = createContext("a", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_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, + fallbackTitle: `http://${query}/`, + uri: `http://${query}/`, + iconUri: "", + heuristic: true, + }), + makeSearchResult(context, { query, engineName: SUGGESTIONS_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: SUGGESTIONS_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: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ...makeRemoteSuggestionResults(context, { + suggestionPrefix: query, + }), + ], + }); + } + + await cleanUpSuggestions(); +}); + +add_task(async function avoid_remote_url_suggestions_1() { + Services.prefs.setBoolPref(SUGGEST_PREF, true); + 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: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeFormHistoryResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + suggestion: `${query}.com`, + }), + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + suggestion: `${query}. com`, + }), + ], + }); + + await cleanUpSuggestions(); + await UrlbarTestUtils.formHistory.remove([`${query}.com`]); +}); + +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: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + suggestion: "htted", + }), + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + suggestion: "htteds", + }), + ], + }); + + context = createContext("ftp", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + suggestion: "ftped", + }), + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + suggestion: "ftpeds", + }), + ], + }); + + context = createContext("http", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + suggestion: "httped", + }), + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + suggestion: "httpeds", + }), + ], + }); + + context = createContext("http:", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ], + }); + + context = createContext("https", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + suggestion: "httpsed", + }), + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + suggestion: "httpseds", + }), + ], + }); + + context = createContext("https:", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ], + }); + + context = createContext("httpd", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + suggestion: "httpded", + }), + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + suggestion: "httpdeds", + }), + ], + }); + + // Check FTP disabled + context = createContext("ftp:", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ], + }); + + context = createContext("ftp:/", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ], + }); + + context = createContext("ftp://", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_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/", + fallbackTitle: "ftp://test/", + iconUri: "", + heuristic: true, + }), + ], + }); + + context = createContext("http:/", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ], + }); + + context = createContext("https:/", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ], + }); + + context = createContext("http://", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ], + }); + + context = createContext("https://", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_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/", + fallbackTitle: "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/", + fallbackTitle: "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/", + fallbackTitle: "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/", + fallbackTitle: "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/", + fallbackTitle: "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/", + fallbackTitle: "http://www.test.com/", + iconUri: "", + heuristic: true, + }), + ], + }); + + context = createContext("file", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + suggestion: "fileed", + }), + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + suggestion: "fileeds", + }), + ], + }); + + context = createContext("file:", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_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", + fallbackTitle: "file:///Users", + iconUri: "", + heuristic: true, + }), + ], + }); + + context = createContext("moz-test://", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ], + }); + + context = createContext("moz+test://", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ], + }); + + context = createContext("about", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + suggestion: "abouted", + }), + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + suggestion: "abouteds", + }), + ], + }); + + 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: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ...makeFormHistoryResults(context, MAX_RESULTS - 1), + ], + }); + + context = createContext(SEARCH_STRING, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ...makeFormHistoryResults(context, MAX_RESULTS - 1), + // 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: SUGGESTIONS_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); + + // `maxHistoricalSearchSuggestions` is no longer treated as a max count but as + // a boolean: If it's zero, then the user has opted out of form history so we + // shouldn't include any at all; if it's non-zero, then we include form + // history according to the limits specified in the muxer's result groups. + + // zero => no form history + Services.prefs.setIntPref(MAX_FORM_HISTORY_PREF, 0); + let context = createContext(SEARCH_STRING, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ...makeRemoteSuggestionResults(context), + ], + }); + + // non-zero => allow form history + Services.prefs.setIntPref(MAX_FORM_HISTORY_PREF, 1); + context = createContext(SEARCH_STRING, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ...makeFormHistoryResults(context, MAX_RESULTS - 3), + ...makeRemoteSuggestionResults(context), + ], + }); + + // non-zero => allow form history + Services.prefs.setIntPref(MAX_FORM_HISTORY_PREF, 2); + context = createContext(SEARCH_STRING, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ...makeFormHistoryResults(context, MAX_RESULTS - 3), + ...makeRemoteSuggestionResults(context), + ], + }); + + Services.prefs.clearUserPref(MAX_FORM_HISTORY_PREF); + + // 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 other form + // history results should not be included since they don't match; and both + // remote suggestions should be included. + let firstSuggestion = makeFormHistoryResults(context, 1)[0].payload + .suggestion; + context = createContext(firstSuggestion, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ...makeRemoteSuggestionResults(context, { + suggestionPrefix: firstSuggestion, + }), + ], + }); + + // Do the same search but in uppercase with a trailing space. We should get + // the same results, i.e., the form history result dupes the trimmed search + // string so it shouldn't be included. + let query = firstSuggestion.toUpperCase() + " "; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + query, + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ...makeRemoteSuggestionResults(context, { + suggestionPrefix: firstSuggestion.toUpperCase(), + }), + ], + }); + + // Add a form history entry that dupes the first remote suggestion and do a + // search that triggers both. The form history should be included but the + // remote suggestion should not since it dupes the form history. + let suggestionPrefix = "dupe"; + let dupeSuggestion = makeRemoteSuggestionResults(context, { + suggestionPrefix, + })[0].payload.suggestion; + Assert.ok(dupeSuggestion, "Sanity check: dupeSuggestion is defined"); + await UrlbarTestUtils.formHistory.add([dupeSuggestion]); + + context = createContext(suggestionPrefix, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeFormHistoryResult(context, { + suggestion: dupeSuggestion, + engineName: SUGGESTIONS_ENGINE_NAME, + }), + ...makeRemoteSuggestionResults(context, { suggestionPrefix }).slice(1), + ], + }); + + await UrlbarTestUtils.formHistory.remove([dupeSuggestion]); + + // Add these form history strings to use below. + let formHistoryStrings = ["foo", "FOO ", "foobar", "fooquux"]; + await UrlbarTestUtils.formHistory.add(formHistoryStrings); + + // Search for "foo". "foo" and "FOO " shouldn't be included since they dupe + // the heuristic. Both "foobar" and "fooquux" should be included even though + // the max form history count is only two and there are four matching form + // history results (including the discarded "foo" and "FOO "). + context = createContext("foo", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeFormHistoryResult(context, { + suggestion: "foobar", + engineName: SUGGESTIONS_ENGINE_NAME, + }), + makeFormHistoryResult(context, { + suggestion: "fooquux", + engineName: SUGGESTIONS_ENGINE_NAME, + }), + ...makeRemoteSuggestionResults(context, { + suggestionPrefix: "foo", + }), + ], + }); + + // 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. The "foo" remote suggestion should not be included since it + // dupes the "foo" form history. + 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: "test visit for http://foo.example.com/", + heuristic: true, + }), + makeFormHistoryResult(context, { + suggestion: "foo", + engineName: SUGGESTIONS_ENGINE_NAME, + }), + makeFormHistoryResult(context, { + suggestion: "foobar", + engineName: SUGGESTIONS_ENGINE_NAME, + }), + makeFormHistoryResult(context, { + suggestion: "fooquux", + engineName: SUGGESTIONS_ENGINE_NAME, + }), + ...makeRemoteSuggestionResults(context, { + suggestionPrefix: "foo", + }), + ], + }); + await PlacesUtils.history.clear(); + + // Add SERPs for "foobar", "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" and "fooBAR " SERPs depend on the result groups, see below. + let engine = await Services.search.getDefault(); + let serpURLs = ["foobar", "fooBAR ", "food"].map( + term => UrlbarUtils.getSearchQueryUrl(engine, term)[0] + ); + await PlacesTestUtils.addVisits(serpURLs); + + // First set showSearchSuggestionsFirst = false so that general results appear + // before suggestions, which means that the muxer visits the "foobar" and + // "fooBAR " SERPs before visiting the "foobar" form history, and so it + // doesn't see that these two SERPs dupe the form history. They are therefore + // included. + Services.prefs.setBoolPref(SHOW_SEARCH_SUGGESTIONS_FIRST_PREF, false); + context = createContext("foo", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { + uri: `http://localhost:${port}/search?q=food`, + title: `test visit for http://localhost:${port}/search?q=food`, + }), + makeVisitResult(context, { + uri: `http://localhost:${port}/search?q=fooBAR+`, + title: `test visit for http://localhost:${port}/search?q=fooBAR+`, + }), + makeVisitResult(context, { + uri: `http://localhost:${port}/search?q=foobar`, + title: `test visit for http://localhost:${port}/search?q=foobar`, + }), + makeFormHistoryResult(context, { + suggestion: "foobar", + engineName: SUGGESTIONS_ENGINE_NAME, + }), + makeFormHistoryResult(context, { + suggestion: "fooquux", + engineName: SUGGESTIONS_ENGINE_NAME, + }), + ...makeRemoteSuggestionResults(context, { + suggestionPrefix: "foo", + }), + ], + }); + + // Now clear showSearchSuggestionsFirst so that suggestions appear before + // general results. Now the muxer will see that the "foobar" and "fooBAR " + // SERPs dupe the "foobar" form history, so it will exclude them. + Services.prefs.clearUserPref(SHOW_SEARCH_SUGGESTIONS_FIRST_PREF); + context = createContext("foo", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeFormHistoryResult(context, { + suggestion: "foobar", + engineName: SUGGESTIONS_ENGINE_NAME, + }), + makeFormHistoryResult(context, { + suggestion: "fooquux", + engineName: SUGGESTIONS_ENGINE_NAME, + }), + ...makeRemoteSuggestionResults(context, { + suggestionPrefix: "foo", + }), + makeVisitResult(context, { + uri: `http://localhost:${port}/search?q=food`, + title: `test visit for http://localhost:${port}/search?q=food`, + }), + ], + }); + + await UrlbarTestUtils.formHistory.remove(formHistoryStrings); + + await cleanUpSuggestions(); + await PlacesUtils.history.clear(); +}); + +// When the heuristic is hidden, search results that match the heuristic should +// be included and not deduped. +add_task(async function hideHeuristic() { + UrlbarPrefs.set("experimental.hideHeuristic", true); + UrlbarPrefs.set("browser.search.suggest.enabled", true); + UrlbarPrefs.set("suggest.searches", true); + let context = createContext(SEARCH_STRING, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ...makeFormHistoryResults(context, MAX_RESULTS - 3), + makeSearchResult(context, { + query: SEARCH_STRING, + engineName: SUGGESTIONS_ENGINE_NAME, + suggestion: SEARCH_STRING, + }), + ...makeRemoteSuggestionResults(context), + ], + }); + await cleanUpSuggestions(); + UrlbarPrefs.clear("experimental.hideHeuristic"); +}); + +// When the heuristic is hidden, form history results that match the heuristic +// should be included and not deduped. +add_task(async function hideHeuristic_formHistory() { + UrlbarPrefs.set("experimental.hideHeuristic", true); + UrlbarPrefs.set("browser.search.suggest.enabled", true); + UrlbarPrefs.set("suggest.searches", true); + + // Search for exactly the suggestion of the first form history result. + // Expected results: + // + // * First form history should be included even though it dupes the heuristic + // * Other form history should not be included because they don't match the + // search string + // * The first remote suggestion that just echoes the search string should not + // be included because it dupes the first form history + // * The remaining remote suggestions should be included because they don't + // dupe anything + let context = createContext(SEARCH_STRING, { isPrivate: false }); + let firstFormHistory = makeFormHistoryResults(context, 1)[0]; + context = createContext(firstFormHistory.payload.suggestion, { + isPrivate: false, + }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + firstFormHistory, + ...makeRemoteSuggestionResults(context, { + suggestionPrefix: firstFormHistory.payload.suggestion, + }), + ], + }); + + // Add these form history strings to use below. + let formHistoryStrings = ["foo", "FOO ", "foobar", "fooquux"]; + await UrlbarTestUtils.formHistory.add(formHistoryStrings); + + // Search for "foo". Expected results: + // + // * "foo" form history should be included even though it dupes the heuristic + // * "FOO " form history should not be included because it dupes the "foo" + // form history + // * "foobar" and "fooqux" form history should be included because they don't + // dupe anything + // * "foo" remote suggestion should not be included because it dupes the "foo" + // form history + // * "foo foo" and "foo bar" remote suggestions should be included because + // they don't dupe anything + context = createContext("foo", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeFormHistoryResult(context, { + suggestion: "foo", + engineName: SUGGESTIONS_ENGINE_NAME, + }), + makeFormHistoryResult(context, { + suggestion: "foobar", + engineName: SUGGESTIONS_ENGINE_NAME, + }), + makeFormHistoryResult(context, { + suggestion: "fooquux", + engineName: SUGGESTIONS_ENGINE_NAME, + }), + ...makeRemoteSuggestionResults(context, { + suggestionPrefix: "foo", + }), + ], + }); + + // Add SERPs for "foo" and "food", and search for "foo". Expected results: + // + // * "foo" form history should be included even though it dupes the heuristic + // * "foobar" and "fooqux" form history should be included because they don't + // dupe anything + // * "foo" SERP depends on `showSearchSuggestionsFirst`, see below + // * "food" SERP should be include because it doesn't dupe anything + // * "foo" remote suggestion should not be included because it dupes the "foo" + // form history + // * "foo foo" and "foo bar" remote suggestions should be included because + // they don't dupe anything + let engine = await Services.search.getDefault(); + let serpURLs = ["foo", "food"].map( + term => UrlbarUtils.getSearchQueryUrl(engine, term)[0] + ); + await PlacesTestUtils.addVisits(serpURLs); + + // With `showSearchSuggestionsFirst = false` so that general results appear + // before suggestions, the muxer visits the "foo" (and "food") SERPs before + // visiting the "foo" form history, and so it doesn't see that the "foo" SERP + // dupes the form history. The SERP is therefore included. + UrlbarPrefs.set("showSearchSuggestionsFirst", false); + context = createContext("foo", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { + uri: `http://localhost:${port}/search?q=food`, + title: `test visit for http://localhost:${port}/search?q=food`, + }), + makeVisitResult(context, { + uri: `http://localhost:${port}/search?q=foo`, + title: `test visit for http://localhost:${port}/search?q=foo`, + }), + makeFormHistoryResult(context, { + suggestion: "foo", + engineName: SUGGESTIONS_ENGINE_NAME, + }), + makeFormHistoryResult(context, { + suggestion: "foobar", + engineName: SUGGESTIONS_ENGINE_NAME, + }), + makeFormHistoryResult(context, { + suggestion: "fooquux", + engineName: SUGGESTIONS_ENGINE_NAME, + }), + ...makeRemoteSuggestionResults(context, { + suggestionPrefix: "foo", + }), + ], + }); + + // Now clear `showSearchSuggestionsFirst` so that suggestions appear before + // general results. Now the muxer will see that the "foo" SERP dupes the "foo" + // form history, so it will exclude it. + UrlbarPrefs.clear("showSearchSuggestionsFirst"); + context = createContext("foo", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeFormHistoryResult(context, { + suggestion: "foo", + engineName: SUGGESTIONS_ENGINE_NAME, + }), + makeFormHistoryResult(context, { + suggestion: "foobar", + engineName: SUGGESTIONS_ENGINE_NAME, + }), + makeFormHistoryResult(context, { + suggestion: "fooquux", + engineName: SUGGESTIONS_ENGINE_NAME, + }), + ...makeRemoteSuggestionResults(context, { + suggestionPrefix: "foo", + }), + makeVisitResult(context, { + uri: `http://localhost:${port}/search?q=food`, + title: `test visit for http://localhost:${port}/search?q=food`, + }), + ], + }); + + await UrlbarTestUtils.formHistory.remove(formHistoryStrings); + + await cleanUpSuggestions(); + UrlbarPrefs.clear("experimental.hideHeuristic"); +}); 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..1f70a421fa --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_search_suggestions_aliases.js @@ -0,0 +1,364 @@ +/* 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 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; +let port; + +add_task(async function setup() { + engine = await addTestSuggestionsEngine(); + port = engine.getSubmission("").uri.port; + + // 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. + await SearchTestUtils.installSearchExtension( + { + name: DEFAULT_ENGINE_NAME, + search_url: "https://my.search.com/", + }, + { setAsDefault: true } + ); + + // 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:${port}/search?q=`, + 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:${port}/search?q=`, + 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:${port}/search?q=`, + 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..da43463a69 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_search_suggestions_tail.js @@ -0,0 +1,379 @@ +/* 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 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() { + 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, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + Services.prefs.clearUserPref(PRIVATE_SEARCH_PREF); + Services.prefs.clearUserPref(TAIL_SUGGESTIONS_PREF); + Services.prefs.clearUserPref(SUGGEST_ENABLED_PREF); + }); + Services.search.setDefault(engine, Ci.nsISearchService.CHANGE_REASON_UNKNOWN); + 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, Ci.nsISearchService.CHANGE_REASON_UNKNOWN); + + const query = "hello world"; + let context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + suggestion: query + " foo", + }), + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + suggestion: query + " bar", + }), + ], + }); + + Services.search.setDefault( + tailEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + 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: TAIL_SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeSearchResult(context, { + engineName: TAIL_SUGGESTIONS_ENGINE_NAME, + suggestion: query + "oronto", + tail: "toronto", + }), + makeSearchResult(context, { + engineName: TAIL_SUGGESTIONS_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: TAIL_SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeSearchResult(context, { + engineName: TAIL_SUGGESTIONS_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: TAIL_SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeSearchResult(context, { + engineName: TAIL_SUGGESTIONS_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", + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + // 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: TAIL_SUGGESTIONS_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: TAIL_SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeSearchResult(context, { + engineName: TAIL_SUGGESTIONS_ENGINE_NAME, + suggestion: tQuery + "oronto", + tail: "toronto", + }), + makeSearchResult(context, { + engineName: TAIL_SUGGESTIONS_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: TAIL_SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeFormHistoryResult(context, { + engineName: TAIL_SUGGESTIONS_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: TAIL_SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeSearchResult(context, { + engineName: TAIL_SUGGESTIONS_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: TAIL_SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ], + }); + + Services.prefs.setBoolPref(TAIL_SUGGESTIONS_PREF, oldPrefValue); + await cleanUpSuggestions(); +}); diff --git a/browser/components/urlbar/tests/unit/test_special_search.js b/browser/components/urlbar/tests/unit/test_special_search.js new file mode 100644 index 0000000000..863196909a --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_special_search.js @@ -0,0 +1,543 @@ +/* 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 bug 395161 that allows special searches that restrict results to + * history/bookmark/tagged items and title/url matches. + * + * Test 485122 by making sure results don't have tags when restricting result + * to just history either by default behavior or dynamic query restrict. + */ + +testEngine_setup(); + +function setSuggestPrefsToFalse() { + Services.prefs.setBoolPref("browser.urlbar.suggest.history", false); + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false); +} + +const TRANSITION_TYPED = PlacesUtils.history.TRANSITION_TYPED; + +add_task(async function test_special_searches() { + let uri1 = Services.io.newURI("http://url/"); + let uri2 = Services.io.newURI("http://url/2"); + let uri3 = Services.io.newURI("http://foo.bar/"); + let uri4 = Services.io.newURI("http://foo.bar/2"); + let uri5 = Services.io.newURI("http://url/star"); + let uri6 = Services.io.newURI("http://url/star/2"); + let uri7 = Services.io.newURI("http://foo.bar/star"); + let uri8 = Services.io.newURI("http://foo.bar/star/2"); + let uri9 = Services.io.newURI("http://url/tag"); + let uri10 = Services.io.newURI("http://url/tag/2"); + let uri11 = Services.io.newURI("http://foo.bar/tag"); + let uri12 = Services.io.newURI("http://foo.bar/tag/2"); + await PlacesTestUtils.addVisits([ + { uri: uri11, title: "title", transition: TRANSITION_TYPED }, + { uri: uri6, title: "foo.bar" }, + { uri: uri4, title: "foo.bar", transition: TRANSITION_TYPED }, + { uri: uri3, title: "title" }, + { uri: uri2, title: "foo.bar" }, + { uri: uri1, title: "title", transition: TRANSITION_TYPED }, + ]); + + await PlacesTestUtils.addBookmarkWithDetails({ + uri: uri12, + title: "foo.bar", + tags: ["foo.bar"], + }); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: uri11, + title: "title", + tags: ["foo.bar"], + }); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: uri10, + title: "foo.bar", + tags: ["foo.bar"], + }); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: uri9, + title: "title", + tags: ["foo.bar"], + }); + await PlacesTestUtils.addBookmarkWithDetails({ uri: uri8, title: "foo.bar" }); + await PlacesTestUtils.addBookmarkWithDetails({ uri: uri7, title: "title" }); + await PlacesTestUtils.addBookmarkWithDetails({ uri: uri6, title: "foo.bar" }); + await PlacesTestUtils.addBookmarkWithDetails({ uri: uri5, title: "title" }); + + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + // Order of frecency when not restricting, descending: + // uri11 + // uri1 + // uri4 + // uri6 + // uri5 + // uri7 + // uri8 + // uri9 + // uri10 + // uri12 + // uri2 + // uri3 + + // Test restricting searches. + + info("History restrict"); + let context = createContext(UrlbarTokenizer.RESTRICT.HISTORY, { + isPrivate: false, + }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { uri: uri11.spec, title: "title" }), + makeVisitResult(context, { uri: uri1.spec, title: "title" }), + makeVisitResult(context, { uri: uri4.spec, title: "foo.bar" }), + makeVisitResult(context, { uri: uri6.spec, title: "foo.bar" }), + makeVisitResult(context, { uri: uri2.spec, title: "foo.bar" }), + makeVisitResult(context, { uri: uri3.spec, title: "title" }), + ], + }); + + info("Star restrict"); + context = createContext(UrlbarTokenizer.RESTRICT.BOOKMARK, { + isPrivate: false, + }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeBookmarkResult(context, { + uri: uri11.spec, + title: "title", + }), + makeBookmarkResult(context, { uri: uri6.spec, title: "foo.bar" }), + makeBookmarkResult(context, { uri: uri5.spec, title: "title" }), + makeBookmarkResult(context, { uri: uri7.spec, title: "title" }), + makeBookmarkResult(context, { uri: uri8.spec, title: "foo.bar" }), + makeBookmarkResult(context, { + uri: uri9.spec, + title: "title", + }), + makeBookmarkResult(context, { + uri: uri10.spec, + title: "foo.bar", + }), + makeBookmarkResult(context, { + uri: uri12.spec, + title: "foo.bar", + }), + ], + }); + + info("Tag restrict"); + context = createContext(UrlbarTokenizer.RESTRICT.TAG, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeBookmarkResult(context, { + uri: uri11.spec, + title: "title", + }), + makeBookmarkResult(context, { + uri: uri9.spec, + title: "title", + }), + makeBookmarkResult(context, { + uri: uri10.spec, + title: "foo.bar", + }), + makeBookmarkResult(context, { + uri: uri12.spec, + title: "foo.bar", + }), + ], + }); + + info("Special as first word"); + context = createContext(`${UrlbarTokenizer.RESTRICT.HISTORY} foo bar`, { + isPrivate: false, + }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + query: "foo bar", + alias: UrlbarTokenizer.RESTRICT.HISTORY, + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + heuristic: true, + }), + makeVisitResult(context, { uri: uri11.spec, title: "title" }), + makeVisitResult(context, { uri: uri4.spec, title: "foo.bar" }), + makeVisitResult(context, { uri: uri6.spec, title: "foo.bar" }), + makeVisitResult(context, { uri: uri2.spec, title: "foo.bar" }), + makeVisitResult(context, { uri: uri3.spec, title: "title" }), + ], + }); + + info("Special as last word"); + context = createContext(`foo bar ${UrlbarTokenizer.RESTRICT.HISTORY}`, { + isPrivate: false, + }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { uri: uri11.spec, title: "title" }), + makeVisitResult(context, { uri: uri4.spec, title: "foo.bar" }), + makeVisitResult(context, { uri: uri6.spec, title: "foo.bar" }), + makeVisitResult(context, { uri: uri2.spec, title: "foo.bar" }), + makeVisitResult(context, { uri: uri3.spec, title: "title" }), + ], + }); + + // Test restricting and matching searches with a term. + + info(`foo ${UrlbarTokenizer.RESTRICT.HISTORY} -> history`); + context = createContext(`foo ${UrlbarTokenizer.RESTRICT.HISTORY}`, { + isPrivate: false, + }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { uri: uri11.spec, title: "title" }), + makeVisitResult(context, { uri: uri4.spec, title: "foo.bar" }), + makeVisitResult(context, { uri: uri6.spec, title: "foo.bar" }), + makeVisitResult(context, { uri: uri2.spec, title: "foo.bar" }), + makeVisitResult(context, { uri: uri3.spec, title: "title" }), + ], + }); + + info(`foo ${UrlbarTokenizer.RESTRICT.BOOKMARK} -> is star`); + context = createContext(`foo ${UrlbarTokenizer.RESTRICT.BOOKMARK}`, { + isPrivate: false, + }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeBookmarkResult(context, { + uri: uri11.spec, + title: "title", + tags: ["foo.bar"], + }), + makeBookmarkResult(context, { uri: uri6.spec, title: "foo.bar" }), + makeBookmarkResult(context, { uri: uri7.spec, title: "title" }), + makeBookmarkResult(context, { uri: uri8.spec, title: "foo.bar" }), + makeBookmarkResult(context, { + uri: uri9.spec, + title: "title", + tags: ["foo.bar"], + }), + makeBookmarkResult(context, { + uri: uri10.spec, + title: "foo.bar", + tags: ["foo.bar"], + }), + makeBookmarkResult(context, { + uri: uri12.spec, + title: "foo.bar", + tags: ["foo.bar"], + }), + ], + }); + + info(`foo ${UrlbarTokenizer.RESTRICT.TITLE} -> in title`); + context = createContext(`foo ${UrlbarTokenizer.RESTRICT.TITLE}`, { + isPrivate: false, + }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { uri: uri11.spec, title: "title" }), + makeVisitResult(context, { uri: uri4.spec, title: "foo.bar" }), + makeBookmarkResult(context, { uri: uri6.spec, title: "foo.bar" }), + makeBookmarkResult(context, { uri: uri8.spec, title: "foo.bar" }), + makeVisitResult(context, { uri: uri9.spec, title: "title" }), + makeVisitResult(context, { uri: uri10.spec, title: "foo.bar" }), + makeVisitResult(context, { uri: uri12.spec, title: "foo.bar" }), + makeVisitResult(context, { uri: uri2.spec, title: "foo.bar" }), + ], + }); + + info(`foo ${UrlbarTokenizer.RESTRICT.URL} -> in url`); + context = createContext(`foo ${UrlbarTokenizer.RESTRICT.URL}`, { + isPrivate: false, + }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { uri: uri11.spec, title: "title" }), + makeVisitResult(context, { uri: uri4.spec, title: "foo.bar" }), + makeBookmarkResult(context, { uri: uri7.spec, title: "title" }), + makeBookmarkResult(context, { uri: uri8.spec, title: "foo.bar" }), + makeVisitResult(context, { uri: uri12.spec, title: "foo.bar" }), + makeVisitResult(context, { uri: uri3.spec, title: "title" }), + ], + }); + + info(`foo ${UrlbarTokenizer.RESTRICT.TAG} -> is tag`); + context = createContext(`foo ${UrlbarTokenizer.RESTRICT.TAG}`, { + isPrivate: false, + }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeBookmarkResult(context, { + uri: uri11.spec, + title: "title", + tags: ["foo.bar"], + }), + makeBookmarkResult(context, { + uri: uri9.spec, + title: "title", + tags: ["foo.bar"], + }), + makeBookmarkResult(context, { + uri: uri10.spec, + title: "foo.bar", + tags: ["foo.bar"], + }), + makeBookmarkResult(context, { + uri: uri12.spec, + title: "foo.bar", + tags: ["foo.bar"], + }), + ], + }); + + // Test conflicting restrictions. + + info( + `conflict ${UrlbarTokenizer.RESTRICT.TITLE} ${UrlbarTokenizer.RESTRICT.URL} -> url wins` + ); + await PlacesTestUtils.addVisits([ + { + uri: `http://conflict.com/${UrlbarTokenizer.RESTRICT.TITLE}`, + title: "test", + }, + { + uri: "http://conflict.com/", + title: `test${UrlbarTokenizer.RESTRICT.TITLE}`, + }, + ]); + context = createContext( + `conflict ${UrlbarTokenizer.RESTRICT.TITLE} ${UrlbarTokenizer.RESTRICT.URL}`, + { isPrivate: false } + ); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { + uri: `http://conflict.com/${UrlbarTokenizer.RESTRICT.TITLE}`, + title: "test", + }), + ], + }); + + info( + `conflict ${UrlbarTokenizer.RESTRICT.HISTORY} ${UrlbarTokenizer.RESTRICT.BOOKMARK} -> bookmark wins` + ); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "http://bookmark.conflict.com/", + title: `conflict ${UrlbarTokenizer.RESTRICT.HISTORY}`, + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + context = createContext( + `conflict ${UrlbarTokenizer.RESTRICT.HISTORY} ${UrlbarTokenizer.RESTRICT.BOOKMARK}`, + { isPrivate: false } + ); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeBookmarkResult(context, { + uri: "http://bookmark.conflict.com/", + title: `conflict ${UrlbarTokenizer.RESTRICT.HISTORY}`, + }), + ], + }); + + info( + `conflict ${UrlbarTokenizer.RESTRICT.BOOKMARK} ${UrlbarTokenizer.RESTRICT.TAG} -> tag wins` + ); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "http://tag.conflict.com/", + title: `conflict ${UrlbarTokenizer.RESTRICT.BOOKMARK}`, + tags: ["one"], + }); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "http://nontag.conflict.com/", + title: `conflict ${UrlbarTokenizer.RESTRICT.BOOKMARK}`, + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + context = createContext( + `conflict ${UrlbarTokenizer.RESTRICT.BOOKMARK} ${UrlbarTokenizer.RESTRICT.TAG}`, + { isPrivate: false } + ); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeBookmarkResult(context, { + uri: "http://tag.conflict.com/", + title: `conflict ${UrlbarTokenizer.RESTRICT.BOOKMARK}`, + }), + ], + }); + + // Disable autoFill for the next tests, see test_autoFill_default_behavior.js + // for specific tests. + Services.prefs.setBoolPref("browser.urlbar.autoFill", false); + + // Test default usage by setting certain browser.urlbar.suggest.* prefs + info("foo -> default history"); + setSuggestPrefsToFalse(); + Services.prefs.setBoolPref("browser.urlbar.suggest.history", true); + context = createContext("foo", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { uri: uri11.spec, title: "title" }), + makeVisitResult(context, { uri: uri4.spec, title: "foo.bar" }), + makeVisitResult(context, { uri: uri6.spec, title: "foo.bar" }), + makeVisitResult(context, { uri: uri2.spec, title: "foo.bar" }), + makeVisitResult(context, { uri: uri3.spec, title: "title" }), + ], + }); + + info("foo -> default history, is star"); + setSuggestPrefsToFalse(); + Services.prefs.setBoolPref("browser.urlbar.suggest.history", true); + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true); + // The purpose of this test is to verify what is being sent by ProviderPlaces. + // It will send 10 results, but the heuristic result pushes the last result + // out of the panel. We set maxRichResults to a high value to test the full + // output of ProviderPlaces. + Services.prefs.setIntPref("browser.urlbar.maxRichResults", 20); + context = createContext("foo", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeBookmarkResult(context, { + uri: uri11.spec, + title: "title", + tags: ["foo.bar"], + }), + makeVisitResult(context, { uri: uri4.spec, title: "foo.bar" }), + makeBookmarkResult(context, { uri: uri6.spec, title: "foo.bar" }), + makeBookmarkResult(context, { uri: uri7.spec, title: "title" }), + makeBookmarkResult(context, { uri: uri8.spec, title: "foo.bar" }), + makeBookmarkResult(context, { + uri: uri9.spec, + title: "title", + tags: ["foo.bar"], + }), + makeBookmarkResult(context, { + uri: uri10.spec, + title: "foo.bar", + tags: ["foo.bar"], + }), + makeBookmarkResult(context, { + uri: uri12.spec, + title: "foo.bar", + tags: ["foo.bar"], + }), + makeVisitResult(context, { uri: uri2.spec, title: "foo.bar" }), + makeVisitResult(context, { uri: uri3.spec, title: "title" }), + ], + }); + Services.prefs.clearUserPref("browser.urlbar.maxRichResults"); + + info("foo -> is star"); + setSuggestPrefsToFalse(); + Services.prefs.setBoolPref("browser.urlbar.suggest.history", false); + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true); + context = createContext("foo", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeBookmarkResult(context, { + uri: uri11.spec, + title: "title", + tags: ["foo.bar"], + }), + makeBookmarkResult(context, { uri: uri6.spec, title: "foo.bar" }), + makeBookmarkResult(context, { uri: uri7.spec, title: "title" }), + makeBookmarkResult(context, { uri: uri8.spec, title: "foo.bar" }), + makeBookmarkResult(context, { + uri: uri9.spec, + title: "title", + tags: ["foo.bar"], + }), + makeBookmarkResult(context, { + uri: uri10.spec, + title: "foo.bar", + tags: ["foo.bar"], + }), + makeBookmarkResult(context, { + uri: uri12.spec, + title: "foo.bar", + tags: ["foo.bar"], + }), + ], + }); + + await cleanupPlaces(); +}); diff --git a/browser/components/urlbar/tests/unit/test_suggestedIndex.js b/browser/components/urlbar/tests/unit/test_suggestedIndex.js new file mode 100644 index 0000000000..f2f7fcc0d6 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_suggestedIndex.js @@ -0,0 +1,562 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests results with suggestedIndex and resultSpan. + +"use strict"; + +const MAX_RESULTS = 10; + +add_task(async function suggestedIndex() { + // Initialize maxRichResults for sanity. + UrlbarPrefs.set("maxRichResults", MAX_RESULTS); + + let tests = [ + // no result spans > 1 + { + desc: "{ suggestedIndex: 0 }", + suggestedIndexes: [0], + expected: indexes([10, 1], [0, 9]), + }, + { + desc: "{ suggestedIndex: 1 }", + suggestedIndexes: [1], + expected: indexes([0, 1], [10, 1], [1, 8]), + }, + { + desc: "{ suggestedIndex: -1 }", + suggestedIndexes: [-1], + expected: indexes([0, 9], [10, 1]), + }, + { + desc: "{ suggestedIndex: -2 }", + suggestedIndexes: [-2], + expected: indexes([0, 8], [10, 1], [8, 1]), + }, + { + desc: "{ suggestedIndex: 0 }, { suggestedIndex: -1 }", + suggestedIndexes: [0, -1], + expected: indexes([10, 1], [0, 8], [11, 1]), + }, + { + desc: "{ suggestedIndex: 1 }, { suggestedIndex: -1 }", + suggestedIndexes: [1, -1], + expected: indexes([0, 1], [10, 1], [1, 7], [11, 1]), + }, + { + desc: "{ suggestedIndex: 1 }, { suggestedIndex: -2 }", + suggestedIndexes: [1, -2], + expected: indexes([0, 1], [10, 1], [1, 6], [11, 1], [7, 1]), + }, + { + desc: "{ suggestedIndex: 0 }, resultCount < max", + suggestedIndexes: [0], + resultCount: 5, + expected: indexes([5, 1], [0, 5]), + }, + { + desc: "{ suggestedIndex: 1 }, resultCount < max", + suggestedIndexes: [1], + resultCount: 5, + expected: indexes([0, 1], [5, 1], [1, 4]), + }, + { + desc: "{ suggestedIndex: -1 }, resultCount < max", + suggestedIndexes: [-1], + resultCount: 5, + expected: indexes([0, 5], [5, 1]), + }, + { + desc: "{ suggestedIndex: -2 }, resultCount < max", + suggestedIndexes: [-2], + resultCount: 5, + expected: indexes([0, 4], [5, 1], [4, 1]), + }, + { + desc: "{ suggestedIndex: 0 }, { suggestedIndex: -1 }, resultCount < max", + suggestedIndexes: [0, -1], + resultCount: 5, + expected: indexes([5, 1], [0, 5], [6, 1]), + }, + { + desc: "{ suggestedIndex: 1 }, { suggestedIndex: -1 }, resultCount < max", + suggestedIndexes: [1, -1], + resultCount: 5, + expected: indexes([0, 1], [5, 1], [1, 4], [6, 1]), + }, + { + desc: "{ suggestedIndex: 0 }, { suggestedIndex: -2 }, resultCount < max", + suggestedIndexes: [0, -2], + resultCount: 5, + expected: indexes([5, 1], [0, 4], [6, 1], [4, 1]), + }, + { + desc: "{ suggestedIndex: 1 }, { suggestedIndex: -2 }, resultCount < max", + suggestedIndexes: [1, -2], + resultCount: 5, + expected: indexes([0, 1], [5, 1], [1, 3], [6, 1], [4, 1]), + }, + + // one suggestedIndex with result span > 1 + { + desc: "{ suggestedIndex: 0, resultSpan: 2 }", + suggestedIndexes: [0], + spansByIndex: { 10: 2 }, + expected: indexes([10, 1], [0, 8]), + }, + { + desc: "{ suggestedIndex: 0, resultSpan: 3 }", + suggestedIndexes: [0], + spansByIndex: { 10: 3 }, + expected: indexes([10, 1], [0, 7]), + }, + { + desc: "{ suggestedIndex: 1, resultSpan: 2 }", + suggestedIndexes: [1], + spansByIndex: { 10: 2 }, + expected: indexes([0, 1], [10, 1], [1, 7]), + }, + { + desc: "suggestedIndex: 1, resultSpan:: 3 }", + suggestedIndexes: [1], + spansByIndex: { 10: 3 }, + expected: indexes([0, 1], [10, 1], [1, 6]), + }, + { + desc: "{ suggestedIndex: -1, resultSpan 2 }", + suggestedIndexes: [-1], + spansByIndex: { 10: 2 }, + expected: indexes([0, 8], [10, 1]), + }, + { + desc: "{ suggestedIndex: -1, resultSpan: 3 }", + suggestedIndexes: [-1], + spansByIndex: { 10: 3 }, + expected: indexes([0, 7], [10, 1]), + }, + { + desc: "{ suggestedIndex: 0, resultSpan: 2 }, { suggestedIndex: -1 }", + suggestedIndexes: [0, -1], + spansByIndex: { 10: 2 }, + expected: indexes([10, 1], [0, 7], [11, 1]), + }, + { + desc: "{ suggestedIndex: 0, resultSpan: 3 }, { suggestedIndex: -1 }", + suggestedIndexes: [0, -1], + spansByIndex: { 10: 3 }, + expected: indexes([10, 1], [0, 6], [11, 1]), + }, + { + desc: "{ suggestedIndex: 0 }, { suggestedIndex: -1, resultSpan: 2 }", + suggestedIndexes: [0, -1], + spansByIndex: { 11: 2 }, + expected: indexes([10, 1], [0, 7], [11, 1]), + }, + { + desc: "{ suggestedIndex: 0 }, { suggestedIndex: -1, resultSpan: 3 }", + suggestedIndexes: [0, -1], + spansByIndex: { 11: 3 }, + expected: indexes([10, 1], [0, 6], [11, 1]), + }, + { + desc: "{ suggestedIndex: 1, resultSpan: 2 }, { suggestedIndex: -1 }", + suggestedIndexes: [1, -1], + spansByIndex: { 10: 2 }, + expected: indexes([0, 1], [10, 1], [1, 6], [11, 1]), + }, + { + desc: "{ suggestedIndex: 1, resultSpan: 3 }, { suggestedIndex: -1 }", + suggestedIndexes: [1, -1], + spansByIndex: { 10: 3 }, + expected: indexes([0, 1], [10, 1], [1, 5], [11, 1]), + }, + { + desc: "{ suggestedIndex: 1 }, { suggestedIndex: -1, resultSpan: 2 }", + suggestedIndexes: [1, -1], + spansByIndex: { 11: 2 }, + expected: indexes([0, 1], [10, 1], [1, 6], [11, 1]), + }, + { + desc: "{ suggestedIndex: 1 }, { suggestedIndex: -1, resultSpan: 3 }", + suggestedIndexes: [1, -1], + spansByIndex: { 11: 3 }, + expected: indexes([0, 1], [10, 1], [1, 5], [11, 1]), + }, + { + desc: "{ suggestedIndex: 0, resultSpan: 2 }, { suggestedIndex: -2 }", + suggestedIndexes: [0, -2], + spansByIndex: { 10: 2 }, + expected: indexes([10, 1], [0, 6], [11, 1], [6, 1]), + }, + { + desc: "{ suggestedIndex: 0, resultSpan: 3 }, { suggestedIndex: -2 }", + suggestedIndexes: [0, -2], + spansByIndex: { 10: 3 }, + expected: indexes([10, 1], [0, 5], [11, 1], [5, 1]), + }, + { + desc: "{ suggestedIndex: 1, resultSpan: 2 }, { suggestedIndex: -2 }", + suggestedIndexes: [1, -2], + spansByIndex: { 10: 2 }, + expected: indexes([0, 1], [10, 1], [1, 5], [11, 1], [6, 1]), + }, + { + desc: "{ suggestedIndex: 1, resultSpan: 3 }, { suggestedIndex: -2 }", + suggestedIndexes: [1, -2], + spansByIndex: { 10: 3 }, + expected: indexes([0, 1], [10, 1], [1, 4], [11, 1], [5, 1]), + }, + { + desc: "{ suggestedIndex: 0 }, { suggestedIndex: -2, resultSpan: 2 }", + suggestedIndexes: [0, -2], + spansByIndex: { 11: 2 }, + expected: indexes([10, 1], [0, 6], [11, 1], [6, 1]), + }, + { + desc: "{ suggestedIndex: 0 }, { suggestedIndex: -2, resultSpan: 3 }", + suggestedIndexes: [0, -2], + spansByIndex: { 11: 3 }, + expected: indexes([10, 1], [0, 5], [11, 1], [5, 1]), + }, + { + desc: "{ suggestedIndex: 1 }, { suggestedIndex: -2, resultSpan: 2 }", + suggestedIndexes: [1, -2], + spansByIndex: { 11: 2 }, + expected: indexes([0, 1], [10, 1], [1, 5], [11, 1], [6, 1]), + }, + { + desc: "{ suggestedIndex: 1 }, { suggestedIndex: -2, resultSpan: 3 }", + suggestedIndexes: [1, -2], + spansByIndex: { 11: 3 }, + expected: indexes([0, 1], [10, 1], [1, 4], [11, 1], [5, 1]), + }, + { + desc: "{ suggestedIndex: 0, resultSpan: 2 }, resultCount < max", + suggestedIndexes: [0], + spansByIndex: { 5: 2 }, + resultCount: 5, + expected: indexes([5, 1], [0, 5]), + }, + { + desc: "{ suggestedIndex: 1, resultSpan: 2 }, resultCount < max", + suggestedIndexes: [1], + spansByIndex: { 5: 2 }, + resultCount: 5, + expected: indexes([0, 1], [5, 1], [1, 4]), + }, + { + desc: "{ suggestedIndex: -1, resultSpan: 2 }, resultCount < max", + suggestedIndexes: [-1], + spansByIndex: { 5: 2 }, + resultCount: 5, + expected: indexes([0, 5], [5, 1]), + }, + { + desc: "{ suggestedIndex: -2, resultSpan: 2 }, resultCount < max", + suggestedIndexes: [-2], + spansByIndex: { 5: 2 }, + resultCount: 5, + expected: indexes([0, 4], [5, 1], [4, 1]), + }, + { + desc: "{ suggestedIndex: 0, resultSpan: 2 }, { suggestedIndex: -1 }, resultCount < max", + suggestedIndexes: [0, -1], + spansByIndex: { 5: 2 }, + resultCount: 5, + expected: indexes([5, 1], [0, 5], [6, 1]), + }, + { + desc: "{ suggestedIndex: 1, resultSpan: 2 }, { suggestedIndex: -1 }, resultCount < max", + suggestedIndexes: [1, -1], + spansByIndex: { 5: 2 }, + resultCount: 5, + expected: indexes([0, 1], [5, 1], [1, 4], [6, 1]), + }, + { + desc: "{ suggestedIndex: 0, resultSpan: 2 }, { suggestedIndex: -2 }, resultCount < max", + suggestedIndexes: [0, -2], + spansByIndex: { 5: 2 }, + resultCount: 5, + expected: indexes([5, 1], [0, 4], [6, 1], [4, 1]), + }, + { + desc: "{ suggestedIndex: 0 }, { suggestedIndex: -1, resultSpan: 2 }, resultCount < max", + suggestedIndexes: [0, -1], + spansByIndex: { 6: 2 }, + resultCount: 5, + expected: indexes([5, 1], [0, 5], [6, 1]), + }, + { + desc: "{ suggestedIndex: 0 }, { suggestedIndex: -2, resultSpan: 2 }, resultCount < max", + suggestedIndexes: [0, -2], + spansByIndex: { 6: 2 }, + resultCount: 5, + expected: indexes([5, 1], [0, 4], [6, 1], [4, 1]), + }, + + // two suggestedIndexes with result span > 1 + { + desc: "{ suggestedIndex: 0, resultSpan: 2 }, { suggestedIndex: -1, resultSpan: 2 }", + suggestedIndexes: [0, -1], + spansByIndex: { 10: 2, 11: 2 }, + expected: indexes([10, 1], [0, 6], [11, 1]), + }, + { + desc: "{ suggestedIndex: 0, resultSpan: 3 }, { suggestedIndex: -1, resultSpan: 2 }", + suggestedIndexes: [0, -1], + spansByIndex: { 10: 3, 11: 2 }, + expected: indexes([10, 1], [0, 5], [11, 1]), + }, + { + desc: "{ suggestedIndex: 0, resultSpan: 2 }, { suggestedIndex: -1, resultSpan: 3 }", + suggestedIndexes: [0, -1], + spansByIndex: { 10: 2, 11: 3 }, + expected: indexes([10, 1], [0, 5], [11, 1]), + }, + { + desc: "{ suggestedIndex: 1, resultSpan: 2 }, { suggestedIndex: -1, resultSpan: 2 }", + suggestedIndexes: [1, -1], + spansByIndex: { 10: 2, 11: 2 }, + expected: indexes([0, 1], [10, 1], [1, 5], [11, 1]), + }, + { + desc: "{ suggestedIndex: 1, resultSpan: 3 }, { suggestedIndex: -1, resultSpan: 2 }", + suggestedIndexes: [1, -1], + spansByIndex: { 10: 3, 11: 2 }, + expected: indexes([0, 1], [10, 1], [1, 4], [11, 1]), + }, + { + desc: "{ suggestedIndex: 1, resultSpan: 2 }, { suggestedIndex: -1, resultSpan: 3 }", + suggestedIndexes: [1, -1], + spansByIndex: { 10: 2, 11: 3 }, + expected: indexes([0, 1], [10, 1], [1, 4], [11, 1]), + }, + { + desc: "{ suggestedIndex: 0, resultSpan: 2 }, { suggestedIndex: -2, resultSpan: 2 }", + suggestedIndexes: [0, -2], + spansByIndex: { 10: 2, 11: 2 }, + expected: indexes([10, 1], [0, 5], [11, 1], [5, 1]), + }, + { + desc: "{ suggestedIndex: 0, resultSpan: 3 }, { suggestedIndex: -2, resultSpan: 2 }", + suggestedIndexes: [0, -2], + spansByIndex: { 10: 3, 11: 2 }, + expected: indexes([10, 1], [0, 4], [11, 1], [4, 1]), + }, + { + desc: "{ suggestedIndex: 0, resultSpan: 2 }, { suggestedIndex: -2, resultSpan: 3 }", + suggestedIndexes: [0, -2], + spansByIndex: { 10: 2, 11: 3 }, + expected: indexes([10, 1], [0, 4], [11, 1], [4, 1]), + }, + { + desc: "{ suggestedIndex: 1, resultSpan: 2 }, { suggestedIndex: -2, resultSpan: 2 }", + suggestedIndexes: [1, -2], + spansByIndex: { 10: 2, 11: 2 }, + expected: indexes([0, 1], [10, 1], [1, 4], [11, 1], [5, 1]), + }, + { + desc: "{ suggestedIndex: 1, resultSpan: 3 }, { suggestedIndex: -2, resultSpan: 2 }", + suggestedIndexes: [1, -2], + spansByIndex: { 10: 3, 11: 2 }, + expected: indexes([0, 1], [10, 1], [1, 3], [11, 1], [4, 1]), + }, + { + desc: "{ suggestedIndex: 1, resultSpan: 2 }, { suggestedIndex: -2, resultSpan: 3 }", + suggestedIndexes: [1, -2], + spansByIndex: { 10: 2, 11: 3 }, + expected: indexes([0, 1], [10, 1], [1, 3], [11, 1], [4, 1]), + }, + { + desc: "{ suggestedIndex: 0, resultSpan: 2 }, { suggestedIndex: -1, resultSpan: 2 }, resultCount < max", + suggestedIndexes: [0, -1], + spansByIndex: { 5: 2, 6: 2 }, + resultCount: 5, + expected: indexes([5, 1], [0, 5], [6, 1]), + }, + { + desc: "{ suggestedIndex: 1, resultSpan: 2 }, { suggestedIndex: -1, resultSpan: 2 }, resultCount < max", + suggestedIndexes: [1, -1], + spansByIndex: { 5: 2, 6: 2 }, + resultCount: 5, + expected: indexes([0, 1], [5, 1], [1, 4], [6, 1]), + }, + { + desc: "{ suggestedIndex: 0, resultSpan: 2 }, { suggestedIndex: -2, resultSpan: 2 }, resultCount < max", + suggestedIndexes: [0, -2], + spansByIndex: { 5: 2, 6: 2 }, + resultCount: 5, + expected: indexes([5, 1], [0, 4], [6, 1], [4, 1]), + }, + + // one suggestedIndex plus other result with resultSpan > 1 + { + desc: "{ suggestedIndex: 0 }, { resultSpan: 2 } A", + suggestedIndexes: [0], + spansByIndex: { 0: 2 }, + expected: indexes([10, 1], [0, 8]), + }, + { + desc: "{ suggestedIndex: 0 }, { resultSpan: 2 } B", + suggestedIndexes: [0], + spansByIndex: { 8: 2 }, + expected: indexes([10, 1], [0, 8]), + }, + { + desc: "{ suggestedIndex: 0 }, { resultSpan: 2 } C", + suggestedIndexes: [0], + spansByIndex: { 9: 2 }, + expected: indexes([10, 1], [0, 9]), + }, + { + desc: "{ suggestedIndex: 1 }, { resultSpan: 2 } A", + suggestedIndexes: [1], + spansByIndex: { 0: 2 }, + expected: indexes([0, 1], [10, 1], [1, 7]), + }, + { + desc: "{ suggestedIndex: 1 }, { resultSpan: 2 } B", + suggestedIndexes: [1], + spansByIndex: { 8: 2 }, + expected: indexes([0, 1], [10, 1], [1, 7]), + }, + { + desc: "{ suggestedIndex: -1 }, { resultSpan: 2 }", + suggestedIndexes: [-1], + spansByIndex: { 0: 2 }, + expected: indexes([0, 8], [10, 1]), + }, + { + desc: "{ suggestedIndex: -2 }, { resultSpan: 2 }", + suggestedIndexes: [-2], + spansByIndex: { 0: 2 }, + expected: indexes([0, 7], [10, 1], [7, 1]), + }, + + // miscellaneous + { + desc: "no suggestedIndex, last result has resultSpan = 2", + suggestedIndexes: [], + spansByIndex: { 9: 2 }, + expected: indexes([0, 9]), + }, + { + desc: "{ suggestedIndex: -1 }, last result has resultSpan = 2", + suggestedIndexes: [-1], + spansByIndex: { 9: 2 }, + expected: indexes([0, 9], [10, 1]), + }, + { + desc: "no suggestedIndex, index 8 result has resultSpan = 2", + suggestedIndexes: [], + spansByIndex: { 8: 2 }, + expected: indexes([0, 9]), + }, + { + desc: "{ suggestedIndex: -1 }, index 8 result has resultSpan = 2", + suggestedIndexes: [-1], + spansByIndex: { 8: 2 }, + expected: indexes([0, 8], [10, 1]), + }, + ]; + + for (let test of tests) { + info("Running test: " + JSON.stringify(test)); + await doSuggestedIndexTest(test); + } +}); + +/** + * Sets up a provider with some results with suggested indexes and result spans, + * performs a search, and then checks the results. + * + * @param {object} options + * Options for the test. + * @param {Array} options.suggestedIndexes + * For each of the indexes in this array, a new result with the given + * suggestedIndex will be returned by the provider. + * @param {Array} options.expected + * The indexes of the expected results within the array of results returned by + * the provider. + * @param {object} [options.spansByIndex] + * Maps indexes within the array of results returned by the provider to result + * spans to set on those results. + * @param {number} [options.resultCount] + * Aside from the results with suggested indexes, this is the number of + * results that the provider will return. + */ +async function doSuggestedIndexTest({ + suggestedIndexes, + expected, + spansByIndex = {}, + resultCount = MAX_RESULTS, +}) { + // Make resultCount history results. + let results = []; + for (let i = 0; i < resultCount; i++) { + results.push( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { + url: "http://example.com/" + i, + } + ) + ); + } + + // Make the suggested-index results. + for (let suggestedIndex of suggestedIndexes) { + results.push( + Object.assign( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { + url: "http://example.com/si " + suggestedIndex, + } + ), + { suggestedIndex } + ) + ); + } + + // Set resultSpan on each result as indicated by spansByIndex. + for (let [index, span] of Object.entries(spansByIndex)) { + results[index].resultSpan = span; + } + + // Set up the provider, etc. + let provider = registerBasicTestProvider(results); + let context = createContext(undefined, { providers: [provider.name] }); + let controller = UrlbarTestUtils.newMockController(); + + // Finally, search and check the results. + let expectedResults = expected.map(i => results[i]); + await UrlbarProvidersManager.startQuery(context, controller); + Assert.deepEqual(context.results, expectedResults); +} + +/** + * Helper that generates an array of indexes. Pass in [index, length] tuples. + * Each tuple will produce the indexes starting from `index` to `index + length` + * (not including the index at `index + length`). + * + * Examples: + * + * indexes([0, 5]) => [0, 1, 2, 3, 4] + * indexes([0, 1], [4, 3], [8, 2]) => [0, 4, 5, 6, 8, 9] + * + * @param {Array} pairs + * [index, length] tuples as described above. + * @returns {Array} + * An array of indexes. + */ +function indexes(...pairs) { + return pairs.reduce((indexesArray, [start, len]) => { + for (let i = start; i < start + len; i++) { + indexesArray.push(i); + } + return indexesArray; + }, []); +} diff --git a/browser/components/urlbar/tests/unit/test_suggestedIndexRelativeToGroup.js b/browser/components/urlbar/tests/unit/test_suggestedIndexRelativeToGroup.js new file mode 100644 index 0000000000..ede2f9cc66 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_suggestedIndexRelativeToGroup.js @@ -0,0 +1,596 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests results with `suggestedIndex` and `isSuggestedIndexRelativeToGroup`. + +"use strict"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + sinon: "resource://testing-common/Sinon.sys.mjs", +}); + +const MAX_RESULTS = 10; +const MAX_RICH_RESULTS_PREF = "browser.urlbar.maxRichResults"; + +// Default result groups used in the tests below. +const RESULT_GROUPS = { + children: [ + { + group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT, + flexChildren: true, + children: [ + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY, + }, + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + }, + ], + }, + ], +}; + +let sandbox; +add_task(function setuo() { + sandbox = lazy.sinon.createSandbox(); + registerCleanupFunction(() => { + sandbox.restore(); + }); +}); + +add_task(async function test() { + // Set a specific maxRichResults for sanity's sake. + Services.prefs.setIntPref(MAX_RICH_RESULTS_PREF, MAX_RESULTS); + + // Create the default non-suggestedIndex results we'll use for tests that + // don't specify `otherResults`. + let basicResults = [ + ...makeHistoryResults(), + ...makeFormHistoryResults(), + ...makeRemoteSuggestionResults(), + ]; + + // Test cases follow. Each object in `tests` has the following properties: + // + // * {string} desc + // * {object} suggestedIndexResults + // Describes the suggestedIndex results the test provider should return. + // Properties: + // * {number} suggestedIndex + // * {UrlbarUtils.RESULT_GROUP} group + // This will force the result to have the given group. + // * {array} expected + // Describes the expected results the muxer should return, i.e., the search + // results. Each object in the array describes a slice of expected results. + // The size of the slice is defined by the `count` property. + // * {UrlbarUtils.RESULT_GROUP} group + // The expected group of the results in the slice. + // * {number} count + // The number of results in the slice. + // * {number} [offset] + // Can be used to offset the starting index of the slice in the results. + // * {array} [otherResults] + // An array of results besides the group-relative suggestedIndex results + // that the provider should return. If not specified `basicResults` is used. + // * {array} [resultGroups] + // The result groups to use. If not specified `RESULT_GROUPS` is used. + let tests = [ + { + desc: "First result in GENERAL", + suggestedIndexResults: [ + { + suggestedIndex: 0, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + ], + expected: [ + { + group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY, + count: 4, + }, + { + group: UrlbarUtils.RESULT_GROUP.GENERAL, + suggestedIndex: 0, + }, + { + group: UrlbarUtils.RESULT_GROUP.GENERAL, + count: 2, + }, + { + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + // The muxer will remove the first 4 remote suggestions because they + // dupe the earlier form history. + offset: 4, + count: 3, + }, + ], + }, + + { + desc: "Last result in GENERAL", + suggestedIndexResults: [ + { + suggestedIndex: -1, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + ], + expected: [ + { + group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY, + count: 4, + }, + { + group: UrlbarUtils.RESULT_GROUP.GENERAL, + count: 2, + }, + { + group: UrlbarUtils.RESULT_GROUP.GENERAL, + suggestedIndex: -1, + }, + { + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + // The muxer will remove the first 4 remote suggestions because they + // dupe the earlier form history. + offset: 4, + count: 3, + }, + ], + }, + + { + desc: "First result in GENERAL_PARENT", + suggestedIndexResults: [ + { + suggestedIndex: 0, + group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT, + }, + ], + expected: [ + { + group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT, + suggestedIndex: 0, + }, + { + group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY, + count: 3, + }, + { + group: UrlbarUtils.RESULT_GROUP.GENERAL, + count: 3, + }, + { + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + // The muxer will remove the first 3 remote suggestions because they + // dupe the earlier form history. + offset: 3, + count: 3, + }, + ], + }, + + { + desc: "Last result in GENERAL_PARENT", + suggestedIndexResults: [ + { + suggestedIndex: -1, + group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT, + }, + ], + expected: [ + { + group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY, + count: 3, + }, + { + group: UrlbarUtils.RESULT_GROUP.GENERAL, + count: 3, + }, + { + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + // The muxer will remove the first 3 remote suggestions because they + // dupe the earlier form history. + offset: 3, + count: 3, + }, + { + group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT, + suggestedIndex: -1, + }, + ], + }, + + { + desc: "First and last results in GENERAL", + suggestedIndexResults: [ + { + suggestedIndex: 0, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + { + suggestedIndex: -1, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + ], + expected: [ + { + group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY, + count: 4, + }, + { + group: UrlbarUtils.RESULT_GROUP.GENERAL, + suggestedIndex: 0, + }, + { + group: UrlbarUtils.RESULT_GROUP.GENERAL, + count: 1, + }, + { + group: UrlbarUtils.RESULT_GROUP.GENERAL, + suggestedIndex: -1, + }, + { + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + // The muxer will remove the first 4 remote suggestions because they + // dupe the earlier form history. + offset: 4, + count: 3, + }, + ], + }, + + { + desc: "First and last results in GENERAL_PARENT", + suggestedIndexResults: [ + { + suggestedIndex: 0, + group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT, + }, + { + suggestedIndex: -1, + group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT, + }, + ], + expected: [ + { + group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT, + suggestedIndex: 0, + }, + { + group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY, + count: 3, + }, + { + group: UrlbarUtils.RESULT_GROUP.GENERAL, + count: 3, + }, + { + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + // The muxer will remove the first 3 remote suggestions because they + // dupe the earlier form history. + offset: 3, + count: 2, + }, + { + group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT, + suggestedIndex: -1, + }, + ], + }, + + { + desc: "First result in GENERAL_PARENT, first result in GENERAL", + suggestedIndexResults: [ + { + suggestedIndex: 0, + group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT, + }, + { + suggestedIndex: 0, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + ], + expected: [ + { + group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT, + suggestedIndex: 0, + }, + { + group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY, + count: 3, + }, + { + group: UrlbarUtils.RESULT_GROUP.GENERAL, + suggestedIndex: 0, + }, + { + group: UrlbarUtils.RESULT_GROUP.GENERAL, + count: 2, + }, + { + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + // The muxer will remove the first 3 remote suggestions because they + // dupe the earlier form history. + offset: 3, + count: 3, + }, + ], + }, + + { + desc: "Results in sibling group, no other results in same group", + otherResults: makeFormHistoryResults(), + suggestedIndexResults: [ + { + suggestedIndex: -1, + group: UrlbarUtils.RESULT_GROUP.GENERAL, + }, + ], + expected: [ + { + group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY, + count: 9, + }, + { + group: UrlbarUtils.RESULT_GROUP.GENERAL, + suggestedIndex: -1, + }, + ], + }, + + { + desc: "Results in sibling group, no other results in same group, has child group", + resultGroups: { + flexChildren: true, + children: [ + { + flex: 2, + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + }, + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT, + children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }], + }, + ], + }, + otherResults: makeRemoteSuggestionResults(), + suggestedIndexResults: [ + { + suggestedIndex: -1, + group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT, + }, + ], + expected: [ + { + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + count: 9, + }, + { + group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT, + suggestedIndex: -1, + }, + ], + }, + + { + desc: "Complex group nesting with global suggestedIndex with resultSpan", + resultGroups: { + children: [ + { + maxResultCount: 1, + children: [{ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }], + }, + { + flexChildren: true, + children: [ + { + flex: 2, + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + }, + { + flex: 1, + group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT, + children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }], + }, + ], + }, + ], + }, + otherResults: [ + // heuristic + Object.assign( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.SEARCH, + UrlbarUtils.RESULT_SOURCE.SEARCH, + { + engine: "test", + suggestion: "foo", + lowerCaseSuggestion: "foo", + } + ), + { + heuristic: true, + group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST, + } + ), + // global suggestedIndex with resultSpan = 2 + Object.assign( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.SEARCH, + UrlbarUtils.RESULT_SOURCE.SEARCH, + { + engine: "test", + } + ), + { + suggestedIndex: 1, + resultSpan: 2, + } + ), + // remote suggestions + ...makeRemoteSuggestionResults(), + ], + suggestedIndexResults: [ + { + suggestedIndex: -1, + group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT, + }, + ], + expected: [ + { + group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST, + count: 1, + }, + { + group: UrlbarUtils.RESULT_GROUP.SUGGESTED_INDEX, + suggestedIndex: 1, + resultSpan: 2, + count: 1, + }, + { + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + count: 6, + }, + { + group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT, + suggestedIndex: -1, + }, + ], + }, + ]; + + let controller = UrlbarTestUtils.newMockController(); + + for (let { + desc, + suggestedIndexResults, + expected, + resultGroups, + otherResults, + } of tests) { + info(`Running test: ${desc}`); + + setResultGroups(resultGroups || RESULT_GROUPS); + + // Make the array of all results and do a search. + let results = (otherResults || basicResults).concat( + makeSuggestedIndexResults(suggestedIndexResults) + ); + let provider = registerBasicTestProvider(results); + let context = createContext(undefined, { providers: [provider.name] }); + await UrlbarProvidersManager.startQuery(context, controller); + + // Make the list of expected results. + let expectedResults = []; + for (let { group, offset, count, suggestedIndex } of expected) { + // Find the index in `results` of the expected result. + let index = results.findIndex( + r => + UrlbarUtils.getResultGroup(r) == group && + r.suggestedIndex === suggestedIndex + ); + Assert.notEqual( + index, + -1, + "Sanity check: Expected result is in `results`" + ); + if (offset) { + index += offset; + } + + // Extract the expected number of results from `results` and append them + // to the expected results array. + count = count || 1; + expectedResults.push(...results.slice(index, index + count)); + } + + Assert.deepEqual(context.results, expectedResults); + + UrlbarProvidersManager.unregisterProvider(provider); + } +}); + +function makeHistoryResults(count = MAX_RESULTS) { + let results = []; + for (let i = 0; i < count; i++) { + results.push( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://example.com/" + i } + ) + ); + } + return results; +} + +function makeRemoteSuggestionResults(count = MAX_RESULTS) { + let results = []; + for (let i = 0; i < count; i++) { + results.push( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.SEARCH, + UrlbarUtils.RESULT_SOURCE.SEARCH, + { + engine: "test", + query: "test", + suggestion: "test " + i, + lowerCaseSuggestion: "test " + i, + } + ) + ); + } + return results; +} + +function makeFormHistoryResults(count = MAX_RESULTS) { + let results = []; + for (let i = 0; i < count; i++) { + results.push( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.SEARCH, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { + engine: "test", + suggestion: "test " + i, + lowerCaseSuggestion: "test " + i, + } + ) + ); + } + return results; +} + +function makeSuggestedIndexResults(objects) { + return objects.map(({ suggestedIndex, group }) => + Object.assign( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + url: "http://example.com/si " + suggestedIndex, + } + ), + { + group, + suggestedIndex, + isSuggestedIndexRelativeToGroup: true, + } + ) + ); +} + +function setResultGroups(resultGroups) { + sandbox.restore(); + if (resultGroups) { + sandbox.stub(UrlbarPrefs, "resultGroups").get(() => resultGroups); + } +} diff --git a/browser/components/urlbar/tests/unit/test_tab_matches.js b/browser/components/urlbar/tests/unit/test_tab_matches.js new file mode 100644 index 0000000000..f4061bf3db --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_tab_matches.js @@ -0,0 +1,354 @@ +/* -*- 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/. */ + +testEngine_setup(); + +add_task(async function test_tab_matches() { + Services.prefs.setBoolPref("browser.urlbar.autoFill", false); + Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.urlbar.suggest.searches"); + Services.prefs.clearUserPref("browser.urlbar.autoFill"); + }); + + let uri1 = Services.io.newURI("http://abc.com/"); + let uri2 = Services.io.newURI("http://xyz.net/"); + let uri3 = Services.io.newURI("about:mozilla"); + let uri4 = Services.io.newURI("data:text/html,test"); + let uri5 = Services.io.newURI("http://foobar.org"); + await PlacesTestUtils.addVisits([ + { + uri: uri5, + title: "foobar.org - much better than ABC, definitely better than XYZ", + }, + { uri: uri2, title: "xyz.net - we're better than ABC" }, + { uri: uri1, title: "ABC rocks" }, + ]); + await addOpenPages(uri1, 1); + // Pages that cannot be registered in history. + await addOpenPages(uri3, 1); + await addOpenPages(uri4, 1); + + info("basic tab match"); + let context = createContext("abc.com", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + uri: "http://abc.com/", + title: "ABC rocks", + heuristic: true, + }), + makeTabSwitchResult(context, { + uri: "http://abc.com/", + title: "ABC rocks", + }), + ], + }); + + info("three results, one tab match"); + context = createContext("abc", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeTabSwitchResult(context, { + uri: "http://abc.com/", + title: "ABC rocks", + }), + makeVisitResult(context, { + uri: uri2.spec, + title: "xyz.net - we're better than ABC", + }), + makeVisitResult(context, { + uri: uri5.spec, + title: "foobar.org - much better than ABC, definitely better than XYZ", + }), + ], + }); + + info("three results, both normal results are tab matches"); + await addOpenPages(uri2, 1); + context = createContext("abc", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeTabSwitchResult(context, { + uri: "http://abc.com/", + title: "ABC rocks", + }), + makeTabSwitchResult(context, { + uri: "http://xyz.net/", + title: "xyz.net - we're better than ABC", + }), + makeVisitResult(context, { + uri: uri5.spec, + title: "foobar.org - much better than ABC, definitely better than XYZ", + }), + ], + }); + + info("a container tab is not visible in 'switch to tab'"); + await addOpenPages(uri5, 1, /* userContextId: */ 3); + context = createContext("abc", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeTabSwitchResult(context, { + uri: "http://abc.com/", + title: "ABC rocks", + }), + makeTabSwitchResult(context, { + uri: "http://xyz.net/", + title: "xyz.net - we're better than ABC", + }), + makeVisitResult(context, { + uri: uri5.spec, + title: "foobar.org - much better than ABC, definitely better than XYZ", + }), + ], + }); + + info( + "a container tab should not see 'switch to tab' for other container tabs" + ); + context = createContext("abc", { isPrivate: false, userContextId: 3 }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { + uri: uri1.spec, + title: "ABC rocks", + }), + makeVisitResult(context, { + uri: uri2.spec, + title: "xyz.net - we're better than ABC", + }), + makeTabSwitchResult(context, { + uri: "http://foobar.org/", + title: "foobar.org - much better than ABC, definitely better than XYZ", + }), + ], + }); + + info("a different container tab should not see any 'switch to tab'"); + context = createContext("abc", { isPrivate: false, userContextId: 2 }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { uri: uri1.spec, title: "ABC rocks" }), + makeVisitResult(context, { + uri: uri2.spec, + title: "xyz.net - we're better than ABC", + }), + makeVisitResult(context, { + uri: uri5.spec, + title: "foobar.org - much better than ABC, definitely better than XYZ", + }), + ], + }); + + info( + "three results, both normal results are tab matches, one has multiple tabs" + ); + await addOpenPages(uri2, 5); + context = createContext("abc", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeTabSwitchResult(context, { + uri: "http://abc.com/", + title: "ABC rocks", + }), + makeTabSwitchResult(context, { + uri: "http://xyz.net/", + title: "xyz.net - we're better than ABC", + }), + makeVisitResult(context, { + uri: uri5.spec, + title: "foobar.org - much better than ABC, definitely better than XYZ", + }), + ], + }); + + info("three results, no tab matches"); + await removeOpenPages(uri1, 1); + await removeOpenPages(uri2, 6); + context = createContext("abc", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { + uri: uri1.spec, + title: "ABC rocks", + }), + makeVisitResult(context, { + uri: uri2.spec, + title: "xyz.net - we're better than ABC", + }), + makeVisitResult(context, { + uri: uri5.spec, + title: "foobar.org - much better than ABC, definitely better than XYZ", + }), + ], + }); + + info("tab match search with restriction character"); + await addOpenPages(uri1, 1); + context = createContext(UrlbarTokenizer.RESTRICT.OPENPAGE + " abc", { + isPrivate: false, + }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + query: "abc", + alias: UrlbarTokenizer.RESTRICT.OPENPAGE, + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + heuristic: true, + }), + makeTabSwitchResult(context, { + uri: "http://abc.com/", + title: "ABC rocks", + }), + ], + }); + + info("tab match with not-addable pages"); + context = createContext("mozilla", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeTabSwitchResult(context, { + uri: "about:mozilla", + title: "about:mozilla", + }), + ], + }); + + info("tab match with not-addable pages, no boundary search"); + context = createContext("ut:mo", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeTabSwitchResult(context, { + uri: "about:mozilla", + title: "about:mozilla", + }), + ], + }); + + info("tab match with not-addable pages and restriction character"); + context = createContext(UrlbarTokenizer.RESTRICT.OPENPAGE + " mozilla", { + isPrivate: false, + }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + query: "mozilla", + alias: UrlbarTokenizer.RESTRICT.OPENPAGE, + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + heuristic: true, + }), + makeTabSwitchResult(context, { + uri: "about:mozilla", + title: "about:mozilla", + }), + ], + }); + + info("tab match with not-addable pages and only restriction character"); + context = createContext(UrlbarTokenizer.RESTRICT.OPENPAGE, { + isPrivate: false, + }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeTabSwitchResult(context, { + uri: "data:text/html,test", + title: "data:text/html,test", + iconUri: UrlbarUtils.ICON.DEFAULT, + }), + makeTabSwitchResult(context, { + uri: "about:mozilla", + title: "about:mozilla", + }), + makeTabSwitchResult(context, { + uri: "http://abc.com/", + title: "ABC rocks", + }), + ], + }); + + info("tab match should not return tags as part of the title"); + // Bookmark one of the pages, and add tags to it, to check they don't appear + // in the title. + let bm = await PlacesUtils.bookmarks.insert({ + url: uri1, + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + }); + PlacesUtils.tagging.tagURI(uri1, ["test-tag"]); + context = createContext("abc.com", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + uri: "http://abc.com/", + title: "ABC rocks", + heuristic: true, + }), + makeTabSwitchResult(context, { + uri: "http://abc.com/", + title: "ABC rocks", + }), + ], + }); + await PlacesUtils.bookmarks.remove(bm); + + await cleanupPlaces(); +}); diff --git a/browser/components/urlbar/tests/unit/test_tags_caseInsensitivity.js b/browser/components/urlbar/tests/unit/test_tags_caseInsensitivity.js new file mode 100644 index 0000000000..f7994326ee --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_tags_caseInsensitivity.js @@ -0,0 +1,137 @@ +/* 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/. */ + +testEngine_setup(); + +/** + * Checks the results of a search for `searchTerm`. + * + * @param {Array} uris + * A 2-element array containing [{string} uri, {array} tags}], where `tags` + * is a comma-separated list of the tags expected to appear in the search. + * @param {string} searchTerm + * The term to search for + */ +async function ensure_tag_results(uris, searchTerm) { + print("Searching for '" + searchTerm + "'"); + let context = createContext(searchTerm, { isPrivate: false }); + let urlbarResults = []; + for (let [uri, tags] of uris) { + urlbarResults.push( + makeBookmarkResult(context, { + uri, + title: "A title", + tags, + }) + ); + } + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ...urlbarResults, + ], + }); +} + +const uri1 = "http://site.tld/1"; +const uri2 = "http://site.tld/2"; +const uri3 = "http://site.tld/3"; +const uri4 = "http://site.tld/4"; +const uri5 = "http://site.tld/5"; +const uri6 = "http://site.tld/6"; + +/** + * Properly tags a uri adding it to bookmarks. + * + * @param {string} url + * The URI to tag. + * @param {Array} tags + * The tags to add. + */ +async function tagURI(url, tags) { + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url, + title: "A title", + }); + PlacesUtils.tagging.tagURI(Services.io.newURI(url), tags); +} + +/** + * Test bug #408221 + */ +add_task(async function test_tags_search_case_insensitivity() { + // always search in history + bookmarks, no matter what the default is + Services.prefs.setBoolPref("browser.urlbar.suggest.history", true); + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmarks", true); + Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.urlbar.suggest.history"); + Services.prefs.clearUserPref("browser.urlbar.suggest.bookmarks"); + Services.prefs.clearUserPref("browser.urlbar.suggest.searches"); + }); + + await tagURI(uri6, ["muD"]); + await tagURI(uri6, ["baR"]); + await tagURI(uri5, ["mud"]); + await tagURI(uri5, ["bar"]); + await tagURI(uri4, ["MUD"]); + await tagURI(uri4, ["BAR"]); + await tagURI(uri3, ["foO"]); + await tagURI(uri2, ["FOO"]); + await tagURI(uri1, ["Foo"]); + + await ensure_tag_results( + [ + [uri1, ["Foo"]], + [uri2, ["Foo"]], + [uri3, ["Foo"]], + ], + "foo" + ); + await ensure_tag_results( + [ + [uri1, ["Foo"]], + [uri2, ["Foo"]], + [uri3, ["Foo"]], + ], + "Foo" + ); + await ensure_tag_results( + [ + [uri1, ["Foo"]], + [uri2, ["Foo"]], + [uri3, ["Foo"]], + ], + "foO" + ); + await ensure_tag_results( + [ + [uri4, ["BAR", "MUD"]], + [uri5, ["BAR", "MUD"]], + [uri6, ["BAR", "MUD"]], + ], + "bar mud" + ); + await ensure_tag_results( + [ + [uri4, ["BAR", "MUD"]], + [uri5, ["BAR", "MUD"]], + [uri6, ["BAR", "MUD"]], + ], + "BAR MUD" + ); + await ensure_tag_results( + [ + [uri4, ["BAR", "MUD"]], + [uri5, ["BAR", "MUD"]], + [uri6, ["BAR", "MUD"]], + ], + "Bar Mud" + ); +}); diff --git a/browser/components/urlbar/tests/unit/test_tags_extendedUnicode.js b/browser/components/urlbar/tests/unit/test_tags_extendedUnicode.js new file mode 100644 index 0000000000..596b439be5 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_tags_extendedUnicode.js @@ -0,0 +1,66 @@ +/* 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 autocomplete for non-English URLs that match the tag bug 416214. Also + * test bug 417441 by making sure escaped ascii characters like "+" remain + * escaped. + * + * - add a visit for a page with a non-English URL + * - add a tag for the page + * - search for the tag + * - test number of matches (should be exactly one) + * - make sure the url is decoded + */ + +testEngine_setup(); + +add_task(async function test_tag_match_url() { + Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.urlbar.suggest.searches"); + }); + info( + "Make sure tag matches return the right url as well as '+' remain escaped" + ); + let uri1 = Services.io.newURI("http://escaped/ユニコード"); + let uri2 = Services.io.newURI("http://asciiescaped/blocking-firefox3%2B"); + await PlacesTestUtils.addVisits([ + { uri: uri1, title: "title" }, + { uri: uri2, title: "title" }, + ]); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: uri1, + title: "title", + tags: ["superTag"], + style: ["bookmark-tag"], + }); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: uri2, + title: "title", + tags: ["superTag"], + style: ["bookmark-tag"], + }); + let context = createContext("superTag", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeBookmarkResult(context, { + uri: uri2.spec, + title: "title", + tags: ["superTag"], + }), + makeBookmarkResult(context, { + uri: uri1.spec, + title: "title", + tags: ["superTag"], + }), + ], + }); + await cleanupPlaces(); +}); diff --git a/browser/components/urlbar/tests/unit/test_tags_general.js b/browser/components/urlbar/tests/unit/test_tags_general.js new file mode 100644 index 0000000000..c2c620c152 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_tags_general.js @@ -0,0 +1,207 @@ +/* -*- 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/. */ + +testEngine_setup(); + +/** + * Checks the results of a search for `searchTerm`. + * + * @param {Array} uris + * A 2-element array containing [{string} uri, {array} tags}], where `tags` + * is a comma-separated list of the tags expected to appear in the search. + * @param {string} searchTerm + * The term to search for + */ +async function ensure_tag_results(uris, searchTerm) { + print("Searching for '" + searchTerm + "'"); + let context = createContext(searchTerm, { isPrivate: false }); + let urlbarResults = []; + for (let [uri, tags] of uris) { + urlbarResults.push( + makeBookmarkResult(context, { + uri, + title: "A title", + tags, + }) + ); + } + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ...urlbarResults, + ], + }); +} + +var uri1 = "http://site.tld/1/aaa"; +var uri2 = "http://site.tld/2/bbb"; +var uri3 = "http://site.tld/3/aaa"; +var uri4 = "http://site.tld/4/bbb"; +var uri5 = "http://site.tld/5/aaa"; +var uri6 = "http://site.tld/6/bbb"; + +var tests = [ + () => + ensure_tag_results( + [ + [uri1, ["foo"]], + [uri4, ["foo bar"]], + [uri6, ["foo bar cheese"]], + ], + "foo" + ), + () => ensure_tag_results([[uri1, ["foo"]]], "foo aaa"), + () => + ensure_tag_results( + [ + [uri4, ["foo bar"]], + [uri6, ["foo bar cheese"]], + ], + "foo bbb" + ), + () => + ensure_tag_results( + [ + [uri2, ["bar"]], + [uri4, ["foo bar"]], + [uri5, ["bar cheese"]], + [uri6, ["foo bar cheese"]], + ], + "bar" + ), + () => ensure_tag_results([[uri5, ["bar cheese"]]], "bar aaa"), + () => + ensure_tag_results( + [ + [uri2, ["bar"]], + [uri4, ["foo bar"]], + [uri6, ["foo bar cheese"]], + ], + "bar bbb" + ), + () => + ensure_tag_results( + [ + [uri3, ["cheese"]], + [uri5, ["bar cheese"]], + [uri6, ["foo bar cheese"]], + ], + "cheese" + ), + () => + ensure_tag_results( + [ + [uri3, ["cheese"]], + [uri5, ["bar cheese"]], + ], + "chees aaa" + ), + () => ensure_tag_results([[uri6, ["foo bar cheese"]]], "chees bbb"), + () => + ensure_tag_results( + [ + [uri4, ["foo bar"]], + [uri6, ["foo bar cheese"]], + ], + "fo bar" + ), + () => ensure_tag_results([], "fo bar aaa"), + () => + ensure_tag_results( + [ + [uri4, ["foo bar"]], + [uri6, ["foo bar cheese"]], + ], + "fo bar bbb" + ), + () => + ensure_tag_results( + [ + [uri4, ["foo bar"]], + [uri6, ["foo bar cheese"]], + ], + "ba foo" + ), + () => ensure_tag_results([], "ba foo aaa"), + () => + ensure_tag_results( + [ + [uri4, ["foo bar"]], + [uri6, ["foo bar cheese"]], + ], + "ba foo bbb" + ), + () => + ensure_tag_results( + [ + [uri5, ["bar cheese"]], + [uri6, ["foo bar cheese"]], + ], + "ba chee" + ), + () => ensure_tag_results([[uri5, ["bar cheese"]]], "ba chee aaa"), + () => ensure_tag_results([[uri6, ["foo bar cheese"]]], "ba chee bbb"), + () => + ensure_tag_results( + [ + [uri5, ["bar cheese"]], + [uri6, ["foo bar cheese"]], + ], + "cheese bar" + ), + () => ensure_tag_results([[uri5, ["bar cheese"]]], "cheese bar aaa"), + () => ensure_tag_results([[uri6, ["foo bar cheese"]]], "chees bar bbb"), + () => ensure_tag_results([[uri6, ["foo bar cheese"]]], "cheese bar foo"), + () => ensure_tag_results([], "foo bar cheese aaa"), + () => ensure_tag_results([[uri6, ["foo bar cheese"]]], "foo bar cheese bbb"), +]; + +/** + * Properly tags a uri adding it to bookmarks. + * + * @param {string} url + * The URI to tag. + * @param {Array} tags + * The tags to add. + */ +async function tagURI(url, tags) { + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url, + title: "A title", + }); + PlacesUtils.tagging.tagURI(Services.io.newURI(url), tags); +} + +/** + * Test history autocomplete + */ +add_task(async function test_history_autocomplete_tags() { + // always search in history + bookmarks, no matter what the default is + Services.prefs.setBoolPref("browser.urlbar.suggest.history", true); + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmarks", true); + Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.urlbar.suggest.history"); + Services.prefs.clearUserPref("browser.urlbar.suggest.bookmarks"); + Services.prefs.clearUserPref("browser.urlbar.suggest.searches"); + }); + + await tagURI(uri6, ["foo bar cheese"]); + await tagURI(uri5, ["bar cheese"]); + await tagURI(uri4, ["foo bar"]); + await tagURI(uri3, ["cheese"]); + await tagURI(uri2, ["bar"]); + await tagURI(uri1, ["foo"]); + + for (let tagTest of tests) { + await tagTest(); + } +}); diff --git a/browser/components/urlbar/tests/unit/test_tags_matchBookmarkTitles.js b/browser/components/urlbar/tests/unit/test_tags_matchBookmarkTitles.js new file mode 100644 index 0000000000..98d12ebe32 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_tags_matchBookmarkTitles.js @@ -0,0 +1,42 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Test bug 416211 to make sure results that match the tag show the bookmark + * title instead of the page title. + */ + +testEngine_setup(); + +add_task(async function test_tag_match_has_bookmark_title() { + Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.urlbar.suggest.searches"); + }); + + info("Make sure the tag match gives the bookmark title"); + let uri = Services.io.newURI("http://theuri/"); + await PlacesTestUtils.addVisits({ uri, title: "Page title" }); + await PlacesTestUtils.addBookmarkWithDetails({ + uri, + title: "Bookmark title", + tags: ["superTag"], + }); + let context = createContext("superTag", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeBookmarkResult(context, { + uri: uri.spec, + title: "Bookmark title", + tags: ["superTag"], + }), + ], + }); + await cleanupPlaces(); +}); diff --git a/browser/components/urlbar/tests/unit/test_tags_returnedInSearches.js b/browser/components/urlbar/tests/unit/test_tags_returnedInSearches.js new file mode 100644 index 0000000000..d5f18278fd --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_tags_returnedInSearches.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/. */ + +/** + * Test bug 418257 by making sure tags are returned with the title as part of + * the "comment" if there are tags even if we didn't match in the tags. They + * are separated from the title by a endash. + */ + +testEngine_setup(); + +add_task(async function test() { + Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false); + Services.prefs.setBoolPref("browser.urlbar.autoFill", false); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.urlbar.suggest.searches"); + Services.prefs.clearUserPref("browser.urlbar.autoFill"); + }); + + let uri1 = Services.io.newURI("http://page1"); + let uri2 = Services.io.newURI("http://page2"); + let uri3 = Services.io.newURI("http://page3"); + let uri4 = Services.io.newURI("http://page4"); + await PlacesTestUtils.addVisits([ + { uri: uri1, title: "tagged" }, + { uri: uri2, title: "tagged" }, + { uri: uri3, title: "tagged" }, + { uri: uri4, title: "tagged" }, + ]); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: uri1, + title: "tagged", + tags: ["tag1"], + }); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: uri2, + title: "tagged", + tags: ["tag1", "tag2"], + }); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: uri3, + title: "tagged", + tags: ["tag1", "tag3"], + }); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: uri4, + title: "tagged", + tags: ["tag1", "tag2", "tag3"], + }); + info("Make sure tags come back in the title when matching tags"); + let context = createContext("page1 tag", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeBookmarkResult(context, { + uri: uri1.spec, + title: "tagged", + tags: ["tag1"], + }), + ], + }); + + info("Check tags in title for page2"); + context = createContext("page2 tag", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeBookmarkResult(context, { + uri: uri2.spec, + title: "tagged", + tags: ["tag1", "tag2"], + }), + ], + }); + + info("Tags do not appear when not matching the tag"); + context = createContext("page3", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeBookmarkResult(context, { + uri: uri3.spec, + title: "tagged", + tags: [], + }), + ], + }); + + info("Extra test just to make sure we match the title"); + context = createContext("tag2", { isPrivate: true }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeBookmarkResult(context, { + uri: uri4.spec, + title: "tagged", + tags: ["tag2"], + }), + makeBookmarkResult(context, { + uri: uri2.spec, + title: "tagged", + tags: ["tag2"], + }), + ], + }); + + await cleanupPlaces(); +}); 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..835d1a5909 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_tokenizer.js @@ -0,0 +1,449 @@ +/* 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..5f0b340b53 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_trimming.js @@ -0,0 +1,171 @@ +/* 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/", + fallbackTitle: "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: "test visit for 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/", + fallbackTitle: "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: "test visit for 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/", + fallbackTitle: "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: "test visit for http://www.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.HISTORY, + uri: "https://www.mozilla.org/%E5%95%8A-test", + title: "test visit for https://www.mozilla.org/%E5%95%8A-test", + iconUri: "page-icon:https://www.mozilla.org/%E5%95%8A-test", + heuristic: true, + }), + ], + }); + await cleanupPlaces(); +}); diff --git a/browser/components/urlbar/tests/unit/test_unitConversion.js b/browser/components/urlbar/tests/unit/test_unitConversion.js new file mode 100644 index 0000000000..fc0620e7aa --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_unitConversion.js @@ -0,0 +1,504 @@ +/* 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"; + +/** + * Unit test for unit conversion module. + */ + +ChromeUtils.defineESModuleGetters(this, { + UrlbarProviderUnitConversion: + "resource:///modules/UrlbarProviderUnitConversion.sys.mjs", +}); + +const TEST_DATA = [ + { + category: "angle", + cases: [ + { queryString: "1 d to d", expected: "1 deg" }, + { queryString: "-1 d to d", expected: "-1 deg" }, + { queryString: "1 d in d", expected: "1 deg" }, + { queryString: "1 d = d", expected: "1 deg" }, + { queryString: "1 D=D", expected: "1 deg" }, + { queryString: "1 d to degree", expected: "1 deg" }, + { queryString: "2 d to degree", expected: "2 deg" }, + { + queryString: "1 d to radian", + expected: `${round(Math.PI / 180)} radian`, + }, + { + queryString: "2 d to radian", + expected: `${round((Math.PI / 180) * 2)} radian`, + }, + { queryString: "1 d to rad", expected: `${round(Math.PI / 180)} radian` }, + { queryString: "1 d to r", expected: `${round(Math.PI / 180)} radian` }, + { queryString: "1 d to gradian", expected: `${round(1 / 0.9)} gradian` }, + { queryString: "1 d to g", expected: `${round(1 / 0.9)} gradian` }, + { queryString: "1 d to minute", expected: "60 min" }, + { queryString: "1 d to min", expected: "60 min" }, + { queryString: "1 d to m", expected: "60 min" }, + { queryString: "1 d to second", expected: "3,600 sec" }, + { queryString: "1 d to sec", expected: "3,600 sec" }, + { queryString: "1 d to s", expected: "3,600 sec" }, + { queryString: "1 d to sign", expected: `${round(1 / 30)} sign` }, + { queryString: "1 d to mil", expected: `${round(1 / 0.05625)} mil` }, + { + queryString: "1 d to revolution", + expected: `${round(1 / 360)} revolution`, + }, + { queryString: "1 d to circle", expected: `${round(1 / 360)} circle` }, + { queryString: "1 d to turn", expected: `${round(1 / 360)} turn` }, + { queryString: "1 d to quadrant", expected: `${round(1 / 90)} quadrant` }, + { + queryString: "1 d to rightangle", + expected: `${round(1 / 90)} rightangle`, + }, + { queryString: "1 d to sextant", expected: `${round(1 / 60)} sextant` }, + { queryString: "1 degree to d", expected: "1 deg" }, + { queryString: "1 radian to d", expected: `${round(180 / Math.PI)} deg` }, + { + queryString: "1 r to g", + expected: `${round(180 / Math.PI / 0.9)} gradian`, + }, + ], + }, + { + category: "force", + cases: [ + { queryString: "1 n to n", expected: "1 newton" }, + { queryString: "-1 n to n", expected: "-1 newton" }, + { queryString: "1 n in n", expected: "1 newton" }, + { queryString: "1 n = n", expected: "1 newton" }, + { queryString: "1 N=N", expected: "1 newton" }, + { queryString: "1 n to newton", expected: "1 newton" }, + { queryString: "1 n to kilonewton", expected: "0.001 kilonewton" }, + { queryString: "1 n to kn", expected: "0.001 kilonewton" }, + { + queryString: "1 n to gram-force", + expected: `${round(101.9716213)} gram-force`, + }, + { + queryString: "1 n to gf", + expected: `${round(101.9716213)} gram-force`, + }, + { + queryString: "1 n to kilogram-force", + expected: `${round(0.1019716213)} kilogram-force`, + }, + { + queryString: "1 n to kgf", + expected: `${round(0.1019716213)} kilogram-force`, + }, + { + queryString: "1 n to ton-force", + expected: `${round(0.0001019716213)} ton-force`, + }, + { + queryString: "1 n to tf", + expected: `${round(0.0001019716213)} ton-force`, + }, + { + queryString: "1 n to exanewton", + expected: `${round(1.0e-18)} exanewton`, + }, + { queryString: "1 n to en", expected: `${round(1.0e-18)} exanewton` }, + { + queryString: "1 n to petanewton", + expected: `${round(1.0e-15)} petanewton`, + }, + { queryString: "1 n to PN", expected: `${round(1.0e-15)} petanewton` }, + { queryString: "1 n to Pn", expected: `${round(1.0e-15)} petanewton` }, + { + queryString: "1 n to teranewton", + expected: `${round(1.0e-12)} teranewton`, + }, + { queryString: "1 n to tn", expected: `${round(1.0e-12)} teranewton` }, + { + queryString: "1 n to giganewton", + expected: `${round(1.0e-9)} giganewton`, + }, + { queryString: "1 n to gn", expected: `${round(1.0e-9)} giganewton` }, + { queryString: "1 n to meganewton", expected: "0.000001 meganewton" }, + { queryString: "1 n to MN", expected: "0.000001 meganewton" }, + { queryString: "1 n to Mn", expected: "0.000001 meganewton" }, + { queryString: "1 n to hectonewton", expected: "0.01 hectonewton" }, + { queryString: "1 n to hn", expected: "0.01 hectonewton" }, + { queryString: "1 n to dekanewton", expected: "0.1 dekanewton" }, + { queryString: "1 n to dan", expected: "0.1 dekanewton" }, + { queryString: "1 n to decinewton", expected: "10 decinewton" }, + { queryString: "1 n to dn", expected: "10 decinewton" }, + { queryString: "1 n to centinewton", expected: "100 centinewton" }, + { queryString: "1 n to cn", expected: "100 centinewton" }, + { queryString: "1 n to millinewton", expected: "1000 millinewton" }, + { queryString: "1 n to mn", expected: "1000 millinewton" }, + { queryString: "1 n to micronewton", expected: "1000000 micronewton" }, + { queryString: "1 n to µn", expected: "1000000 micronewton" }, + { + queryString: "1 n to nanonewton", + expected: "1000000000 nanonewton", + }, + { queryString: "1 n to nn", expected: "1000000000 nanonewton" }, + { + queryString: "1 n to piconewton", + expected: "1000000000000 piconewton", + }, + { queryString: "1 n to pn", expected: "1000000000000 piconewton" }, + { + queryString: "1 n to femtonewton", + expected: "1000000000000000 femtonewton", + }, + { queryString: "1 n to fn", expected: "1000000000000000 femtonewton" }, + { + queryString: "1 n to attonewton", + expected: "1000000000000000000 attonewton", + }, + { queryString: "1 n to an", expected: "1000000000000000000 attonewton" }, + { queryString: "1 n to dyne", expected: "100000 dyne" }, + { queryString: "1 n to dyn", expected: "100000 dyne" }, + { queryString: "1 n to joule/meter", expected: "1 joule/meter" }, + { queryString: "1 n to j/m", expected: "1 joule/meter" }, + { + queryString: "1 n to joule/centimeter", + expected: "100 joule/centimeter", + }, + { queryString: "1 n to j/cm", expected: "100 joule/centimeter" }, + { + queryString: "1 n to ton-force-short", + expected: `${round(0.0001124045)} ton-force-short`, + }, + { + queryString: "1 n to short", + expected: `${round(0.0001124045)} ton-force-short`, + }, + { + queryString: "1 n to ton-force-long", + expected: `${round(0.0001003611)} ton-force-long`, + }, + { + queryString: "1 n to tonf", + expected: `${round(0.0001003611)} ton-force-long`, + }, + { + queryString: "1 n to kip-force", + expected: `${round(0.0002248089)} kip-force`, + }, + { + queryString: "1 n to kipf", + expected: `${round(0.0002248089)} kip-force`, + }, + { + queryString: "1 n to pound-force", + expected: `${round(0.2248089431)} pound-force`, + }, + { + queryString: "1 n to lbf", + expected: `${round(0.2248089431)} pound-force`, + }, + { + queryString: "1 n to ounce-force", + expected: `${round(3.5969430896)} ounce-force`, + }, + { + queryString: "1 n to ozf", + expected: `${round(3.5969430896)} ounce-force`, + }, + { + queryString: "1 n to poundal", + expected: `${round(7.2330138512)} poundal`, + }, + { queryString: "1 n to pdl", expected: `${round(7.2330138512)} poundal` }, + { queryString: "1 n to pond", expected: `${round(101.9716213)} pond` }, + { queryString: "1 n to p", expected: `${round(101.9716213)} pond` }, + { + queryString: "1 n to kilopond", + expected: `${round(0.1019716213)} kilopond`, + }, + { queryString: "1 n to kp", expected: `${round(0.1019716213)} kilopond` }, + { queryString: "1 kilonewton to n", expected: "1000 newton" }, + ], + }, + { + category: "length", + cases: [ + { queryString: "1 meter to meter", expected: "1 m" }, + { queryString: "-1 meter to meter", expected: "-1 m" }, + { queryString: "1 meter in meter", expected: "1 m" }, + { queryString: "1 meter = meter", expected: "1 m" }, + { queryString: "1 METER=METER", expected: "1 m" }, + { queryString: "1 m to meter", expected: "1 m" }, + { queryString: "1 m to nanometer", expected: "1000000000 nanometer" }, + { queryString: "1 m to micrometer", expected: "1000000 micrometer" }, + { queryString: "1 m to millimeter", expected: "1,000 mm" }, + { queryString: "1 m to mm", expected: "1,000 mm" }, + { queryString: "1 m to centimeter", expected: "100 cm" }, + { queryString: "1 m to cm", expected: "100 cm" }, + { queryString: "1 m to kilometer", expected: "0.001 km" }, + { queryString: "1 m to km", expected: "0.001 km" }, + { queryString: "1 m to mile", expected: `${round(0.0006213689)} mi` }, + { queryString: "1 m to mi", expected: `${round(0.0006213689)} mi` }, + { queryString: "1 m to yard", expected: `${round(1.0936132983)} yd` }, + { queryString: "1 m to yd", expected: `${round(1.0936132983)} yd` }, + { queryString: "1 m to foot", expected: `${round(3.280839895)} ft` }, + { queryString: "1 m to ft", expected: `${round(3.280839895)} ft` }, + { queryString: "1 m to inch", expected: `${round(39.37007874)} in` }, + { queryString: "1 inch to m", expected: `${round(1 / 39.37007874)} m` }, + ], + }, + { + category: "mass", + cases: [ + { queryString: "1 kg to kg", expected: "1 kg" }, + { queryString: "-1 kg to kg", expected: "-1 kg" }, + { queryString: "1 kg in kg", expected: "1 kg" }, + { queryString: "1 kg = kg", expected: "1 kg" }, + { queryString: "1 KG=KG", expected: "1 kg" }, + { queryString: "1 kg to kilogram", expected: "1 kg" }, + { queryString: "1 kg to gram", expected: "1,000 g" }, + { queryString: "1 kg to g", expected: "1,000 g" }, + { queryString: "1 kg to milligram", expected: "1000000 milligram" }, + { queryString: "1 kg to mg", expected: "1000000 milligram" }, + { queryString: "1 kg to ton", expected: "0.001 ton" }, + { queryString: "1 kg to t", expected: "0.001 ton" }, + { + queryString: "1 kg to longton", + expected: `${round(0.0009842073)} longton`, + }, + { + queryString: "1 kg to l.t.", + expected: `${round(0.0009842073)} longton`, + }, + { + queryString: "1 kg to l/t", + expected: `${round(0.0009842073)} longton`, + }, + { + queryString: "1 kg to shortton", + expected: `${round(0.0011023122)} shortton`, + }, + { + queryString: "1 kg to s.t.", + expected: `${round(0.0011023122)} shortton`, + }, + { + queryString: "1 kg to s/t", + expected: `${round(0.0011023122)} shortton`, + }, + { + queryString: "1 kg to pound", + expected: `${round(2.2046244202)} lb`, + }, + { queryString: "1 kg to lbs", expected: `${round(2.2046244202)} lb` }, + { + queryString: "1 kg to lb", + expected: `${round(2.2046244202)} lb`, + }, + { + queryString: "1 kg to ounce", + expected: `${round(35.273990723)} oz`, + }, + { queryString: "1 kg to oz", expected: `${round(35.273990723)} oz` }, + { queryString: "1 kg to carat", expected: "5000 carat" }, + { queryString: "1 kg to ffd", expected: "5000 ffd" }, + { queryString: "1 ffd to kg", expected: `${round(1 / 5000)} kg` }, + ], + }, + { + category: "temperature", + cases: [ + { queryString: "0 c to c", expected: "0°C" }, + { queryString: "0 c in c", expected: "0°C" }, + { queryString: "0 c = c", expected: "0°C" }, + { queryString: "0 C=C", expected: "0°C" }, + { queryString: "0 c to celsius", expected: "0°C" }, + { queryString: "0 c to kelvin", expected: "273.15 kelvin" }, + { queryString: "0 c to k", expected: "273.15 kelvin" }, + { queryString: "10 c to k", expected: "283.15 kelvin" }, + { queryString: "0 c to fahrenheit", expected: "32°F" }, + { queryString: "0 c to f", expected: "32°F" }, + { queryString: "10 c to f", expected: `${round(10 * 1.8 + 32)}°F` }, + { + queryString: "10 f to kelvin", + expected: `${round((10 - 32) / 1.8 + 273.15)} kelvin`, + }, + { queryString: "-10 c to f", expected: "14°F" }, + ], + }, + { + category: "timezone", + cases: [ + { queryString: "0 utc to utc", expected: "00:00 UTC" }, + { queryString: "0 utc in utc", expected: "00:00 UTC" }, + { queryString: "0 utc = utc", expected: "00:00 UTC" }, + { queryString: "0 UTC=UTC", expected: "00:00 UTC" }, + { queryString: "11 pm utc to utc", expected: "11:00 PM UTC" }, + { queryString: "11 am utc to utc", expected: "11:00 AM UTC" }, + { queryString: "11:30 utc to utc", expected: "11:30 UTC" }, + { queryString: "11:30 PM utc to utc", expected: "11:30 PM UTC" }, + { queryString: "1 utc to idlw", expected: "13:00 IDLW" }, + { queryString: "1 pm utc to idlw", expected: "1:00 AM IDLW" }, + { queryString: "1 am utc to idlw", expected: "1:00 PM IDLW" }, + { queryString: "1 utc to idlw", expected: "13:00 IDLW" }, + { queryString: "1 PM utc to idlw", expected: "1:00 AM IDLW" }, + { queryString: "0 utc to nt", expected: "13:00 NT" }, + { queryString: "0 utc to hst", expected: "14:00 HST" }, + { queryString: "0 utc to akst", expected: "15:00 AKST" }, + { queryString: "0 utc to pst", expected: "16:00 PST" }, + { queryString: "0 utc to akdt", expected: "16:00 AKDT" }, + { queryString: "0 utc to mst", expected: "17:00 MST" }, + { queryString: "0 utc to pdt", expected: "17:00 PDT" }, + { queryString: "0 utc to cst", expected: "18:00 CST" }, + { queryString: "0 utc to mdt", expected: "18:00 MDT" }, + { queryString: "0 utc to est", expected: "19:00 EST" }, + { queryString: "0 utc to cdt", expected: "19:00 CDT" }, + { queryString: "0 utc to edt", expected: "20:00 EDT" }, + { queryString: "0 utc to ast", expected: "20:00 AST" }, + { queryString: "0 utc to guy", expected: "21:00 GUY" }, + { queryString: "0 utc to adt", expected: "21:00 ADT" }, + { queryString: "0 utc to at", expected: "22:00 AT" }, + { queryString: "0 utc to gmt", expected: "00:00 GMT" }, + { queryString: "0 utc to z", expected: "00:00 Z" }, + { queryString: "0 utc to wet", expected: "00:00 WET" }, + { queryString: "0 utc to west", expected: "01:00 WEST" }, + { queryString: "0 utc to cet", expected: "01:00 CET" }, + { queryString: "0 utc to bst", expected: "01:00 BST" }, + { queryString: "0 utc to ist", expected: "01:00 IST" }, + { queryString: "0 utc to cest", expected: "02:00 CEST" }, + { queryString: "0 utc to eet", expected: "02:00 EET" }, + { queryString: "0 utc to eest", expected: "03:00 EEST" }, + { queryString: "0 utc to msk", expected: "03:00 MSK" }, + { queryString: "0 utc to msd", expected: "04:00 MSD" }, + { queryString: "0 utc to zp4", expected: "04:00 ZP4" }, + { queryString: "0 utc to zp5", expected: "05:00 ZP5" }, + { queryString: "0 utc to zp6", expected: "06:00 ZP6" }, + { queryString: "0 utc to wast", expected: "07:00 WAST" }, + { queryString: "0 utc to awst", expected: "08:00 AWST" }, + { queryString: "0 utc to wst", expected: "08:00 WST" }, + { queryString: "0 utc to jst", expected: "09:00 JST" }, + { queryString: "0 utc to acst", expected: "09:30 ACST" }, + { queryString: "0 utc to aest", expected: "10:00 AEST" }, + { queryString: "0 utc to acdt", expected: "10:30 ACDT" }, + { queryString: "0 utc to aedt", expected: "11:00 AEDT" }, + { queryString: "0 utc to nzst", expected: "12:00 NZST" }, + { queryString: "0 utc to idle", expected: "12:00 IDLE" }, + { queryString: "0 utc to nzd", expected: "13:00 NZD" }, + { queryString: "9:00 jst to utc", expected: "00:00 UTC" }, + { queryString: "8:00 jst to utc", expected: "23:00 UTC" }, + { queryString: "8:00 am jst to utc", expected: "11:00 PM UTC" }, + { queryString: "9:00 jst to pdt", expected: "17:00 PDT" }, + { queryString: "12 pm pst to cet", expected: "9:00 PM CET" }, + { queryString: "12 am pst to cet", expected: "9:00 AM CET" }, + { queryString: "12:30 pm pst to cet", expected: "9:30 PM CET" }, + { queryString: "12:30 am pst to cet", expected: "9:30 AM CET" }, + { queryString: "23 pm pst to cet", expected: "8:00 AM CET" }, + { queryString: "23:30 pm pst to cet", expected: "8:30 AM CET" }, + { + queryString: "10:00 JST to here", + timezone: "UTC", + expected: "01:00 UTC-000", + }, + { + queryString: "1:00 to JST", + timezone: "UTC", + expected: "10:00 JST", + }, + { + queryString: "1 am to JST", + timezone: "UTC", + expected: "10:00 AM JST", + }, + { + queryString: "now to JST", + timezone: "UTC", + assertResult: output => { + const outputRegexResult = /([0-9]+):([0-9]+)/.exec(output); + const outputMinutes = + parseInt(outputRegexResult[1]) * 60 + + parseInt(outputRegexResult[2]); + const nowDate = new Date(); + // Apply JST time difference. + nowDate.setHours(nowDate.getHours() + 9); + let nowMinutes = nowDate.getHours() * 60 + nowDate.getMinutes(); + // When we cross the day between the unit converter calculation and the + // assertion here. + nowMinutes = + outputMinutes > nowMinutes ? nowMinutes + 1440 : nowMinutes; + Assert.lessOrEqual(nowMinutes - outputMinutes, 1); + }, + }, + { + queryString: "now to here", + timezone: "UTC", + assertResult: output => { + const outputRegexResult = /([0-9]+):([0-9]+)/.exec(output); + const outputMinutes = + parseInt(outputRegexResult[1]) * 60 + + parseInt(outputRegexResult[2]); + const nowDate = new Date(); + let nowMinutes = nowDate.getHours() * 60 + nowDate.getMinutes(); + nowMinutes = + outputMinutes > nowMinutes ? nowMinutes + 1440 : nowMinutes; + Assert.lessOrEqual(nowMinutes - outputMinutes, 1); + }, + }, + ], + }, + { + category: "invalid", + cases: [ + { queryString: "1 to cm" }, + { queryString: "1cm to newton" }, + { queryString: "1cm to foo" }, + { queryString: "0:00:00 utc to jst" }, + ], + }, +]; + +add_task(function () { + // Enable unit conversion. + Services.prefs.setBoolPref("browser.urlbar.unitConversion.enabled", true); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.urlbar.unitConversion.enabled"); + }); + + for (const { category, cases } of TEST_DATA) { + for (const { queryString, timezone, expected, assertResult } of cases) { + info(`Test "${queryString}" in ${category}`); + + let originalTimezone; + if (timezone) { + originalTimezone = Cu.getJSTestingFunctions().getTimeZone(); + info(`Set timezone ${timezone}`); + Cu.getJSTestingFunctions().setTimeZone(timezone); + } + + const context = createContext(queryString); + const isActive = UrlbarProviderUnitConversion.isActive(context); + Assert.equal(isActive, !!expected || !!assertResult); + + if (isActive) { + UrlbarProviderUnitConversion.startQuery(context, (module, result) => { + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.DYNAMIC); + Assert.equal(result.source, UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL); + Assert.equal(result.suggestedIndex, 1); + Assert.equal(result.payload.input, queryString); + + if (expected) { + Assert.equal(result.payload.output, expected); + } else { + assertResult(result.payload.output); + } + }); + } + + if (originalTimezone) { + Cu.getJSTestingFunctions().setTimeZone(originalTimezone); + } + } + } +}); + +function round(number) { + return parseFloat(number.toPrecision(10)); +} diff --git a/browser/components/urlbar/tests/unit/test_word_boundary_search.js b/browser/components/urlbar/tests/unit/test_word_boundary_search.js new file mode 100644 index 0000000000..f015a29fb5 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_word_boundary_search.js @@ -0,0 +1,403 @@ +/* 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 to make sure matches against the url, title, tags are first made on word + * boundaries, instead of in the middle of words, and later are extended to the + * whole words. For this test it is critical to check sorting of the matches. + * + * Make sure we don't try matching one after a CamelCase because the upper-case + * isn't really a word boundary. (bug 429498) + */ + +testEngine_setup(); + +var katakana = ["\u30a8", "\u30c9"]; // E, Do +var ideograph = ["\u4efb", "\u5929", "\u5802"]; // Nin Ten Do + +add_task(async function test_escape() { + Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", false); + Services.prefs.setBoolPref("browser.urlbar.autoFill", false); + Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false); + Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.urlbar.suggest.searches"); + Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions"); + Services.prefs.clearUserPref("browser.urlbar.autoFill"); + Services.prefs.clearUserPref("browser.urlbar.autoFill.searchEngines"); + }); + + await PlacesTestUtils.addVisits([ + { uri: "http://matchme/", title: "title1" }, + { uri: "http://dontmatchme/", title: "title1" }, + { uri: "http://title/1", title: "matchme2" }, + { uri: "http://title/2", title: "dontmatchme3" }, + { uri: "http://tag/1", title: "title1" }, + { uri: "http://tag/2", title: "title1" }, + { uri: "http://crazytitle/", title: "!@#$%^&*()_+{}|:<>?word" }, + { uri: "http://katakana/", title: katakana.join("") }, + { uri: "http://ideograph/", title: ideograph.join("") }, + { uri: "http://camel/pleaseMatchMe/", title: "title1" }, + ]); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "http://tag/1", + title: "title1", + tags: ["matchme2"], + }); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "http://tag/2", + title: "title1", + tags: ["dontmatchme3"], + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + info("Match 'match' at the beginning or after / or on a CamelCase"); + let context = createContext("match", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeBookmarkResult(context, { + uri: "http://tag/1", + title: "title1", + tags: ["matchme2"], + }), + makeVisitResult(context, { + uri: "http://camel/pleaseMatchMe/", + title: "title1", + }), + makeVisitResult(context, { uri: "http://title/1", title: "matchme2" }), + makeVisitResult(context, { uri: "http://matchme/", title: "title1" }), + makeBookmarkResult(context, { + uri: "http://tag/2", + title: "title1", + tags: ["dontmatchme3"], + }), + makeVisitResult(context, { + uri: "http://title/2", + title: "dontmatchme3", + }), + makeVisitResult(context, { uri: "http://dontmatchme/", title: "title1" }), + ], + }); + + info("Match 'dont' at the beginning or after /"); + context = createContext("dont", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeBookmarkResult(context, { + uri: "http://tag/2", + title: "title1", + tags: ["dontmatchme3"], + }), + makeVisitResult(context, { + uri: "http://title/2", + title: "dontmatchme3", + }), + makeVisitResult(context, { uri: "http://dontmatchme/", title: "title1" }), + ], + }); + + info("Match 'match' at the beginning or after / or on a CamelCase"); + context = createContext("2", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeBookmarkResult(context, { + uri: "http://tag/2", + title: "title1", + }), + makeBookmarkResult(context, { + uri: "http://tag/1", + title: "title1", + tags: ["matchme2"], + }), + makeVisitResult(context, { + uri: "http://title/2", + title: "dontmatchme3", + }), + makeVisitResult(context, { uri: "http://title/1", title: "matchme2" }), + ], + }); + + info("Match 't' at the beginning or after /"); + context = createContext("t", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeBookmarkResult(context, { + uri: "http://tag/2", + title: "title1", + tags: ["dontmatchme3"], + }), + makeBookmarkResult(context, { + uri: "http://tag/1", + title: "title1", + tags: ["matchme2"], + }), + makeVisitResult(context, { + uri: "http://camel/pleaseMatchMe/", + title: "title1", + }), + makeVisitResult(context, { + uri: "http://title/2", + title: "dontmatchme3", + }), + makeVisitResult(context, { uri: "http://title/1", title: "matchme2" }), + makeVisitResult(context, { uri: "http://dontmatchme/", title: "title1" }), + makeVisitResult(context, { uri: "http://matchme/", title: "title1" }), + makeVisitResult(context, { + uri: "http://katakana/", + title: katakana.join(""), + }), + makeVisitResult(context, { + uri: "http://crazytitle/", + title: "!@#$%^&*()_+{}|:<>?word", + }), + ], + }); + + info("Match 'word' after many consecutive word boundaries"); + context = createContext("word", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://crazytitle/", + title: "!@#$%^&*()_+{}|:<>?word", + }), + ], + }); + + info("Match a word boundary '/' for everything"); + context = createContext("/", { isPrivate: false }); + // UNIX platforms can search for a file:// URL by typing a forward slash. + let heuristicSlashResult = + AppConstants.platform == "win" + ? makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }) + : makeVisitResult(context, { + uri: "file:///", + fallbackTitle: "file:///", + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + heuristic: true, + }); + await check_results({ + context, + matches: [ + heuristicSlashResult, + makeBookmarkResult(context, { + uri: "http://tag/2", + title: "title1", + }), + makeBookmarkResult(context, { + uri: "http://tag/1", + title: "title1", + }), + makeVisitResult(context, { + uri: "http://camel/pleaseMatchMe/", + title: "title1", + }), + makeVisitResult(context, { + uri: "http://ideograph/", + title: ideograph.join(""), + }), + makeVisitResult(context, { + uri: "http://katakana/", + title: katakana.join(""), + }), + makeVisitResult(context, { + uri: "http://crazytitle/", + title: "!@#$%^&*()_+{}|:<>?word", + }), + makeVisitResult(context, { + uri: "http://title/2", + title: "dontmatchme3", + }), + makeVisitResult(context, { uri: "http://title/1", title: "matchme2" }), + makeVisitResult(context, { uri: "http://dontmatchme/", title: "title1" }), + ], + }); + + info("Match word boundaries '()_' that are among word boundaries"); + context = createContext("()_", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://crazytitle/", + title: "!@#$%^&*()_+{}|:<>?word", + }), + ], + }); + + info("Katakana characters form a string, so match the beginning"); + context = createContext(katakana[0], { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://katakana/", + title: katakana.join(""), + }), + ], + }); + + /* + info("Middle of a katakana word shouldn't be matched"); + await check_autocomplete({ + search: katakana[1], + matches: [ ], + }); +*/ + + info("Ideographs are treated as words so 'nin' is one word"); + context = createContext(ideograph[0], { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://ideograph/", + title: ideograph.join(""), + }), + ], + }); + + info("Ideographs are treated as words so 'ten' is another word"); + context = createContext(ideograph[1], { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://ideograph/", + title: ideograph.join(""), + }), + ], + }); + + info("Ideographs are treated as words so 'do' is yet another word"); + context = createContext(ideograph[2], { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://ideograph/", + title: ideograph.join(""), + }), + ], + }); + + info("Match in the middle. Should just be sorted by frecency."); + context = createContext("ch", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeBookmarkResult(context, { + uri: "http://tag/2", + title: "title1", + tags: ["dontmatchme3"], + }), + makeBookmarkResult(context, { + uri: "http://tag/1", + title: "title1", + tags: ["matchme2"], + }), + makeVisitResult(context, { + uri: "http://camel/pleaseMatchMe/", + title: "title1", + }), + makeVisitResult(context, { + uri: "http://title/2", + title: "dontmatchme3", + }), + makeVisitResult(context, { uri: "http://title/1", title: "matchme2" }), + makeVisitResult(context, { uri: "http://dontmatchme/", title: "title1" }), + makeVisitResult(context, { uri: "http://matchme/", title: "title1" }), + ], + }); + + // Also this test should just be sorted by frecency. + info( + "Don't match one character after a camel-case word boundary (bug 429498). Should just be sorted by frecency." + ); + context = createContext("atch", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeBookmarkResult(context, { + uri: "http://tag/2", + title: "title1", + tags: ["dontmatchme3"], + }), + makeBookmarkResult(context, { + uri: "http://tag/1", + title: "title1", + tags: ["matchme2"], + }), + makeVisitResult(context, { + uri: "http://camel/pleaseMatchMe/", + title: "title1", + }), + makeVisitResult(context, { + uri: "http://title/2", + title: "dontmatchme3", + }), + makeVisitResult(context, { uri: "http://title/1", title: "matchme2" }), + makeVisitResult(context, { uri: "http://dontmatchme/", title: "title1" }), + makeVisitResult(context, { uri: "http://matchme/", title: "title1" }), + ], + }); + + 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..18a608e5f3 --- /dev/null +++ b/browser/components/urlbar/tests/unit/xpcshell.ini @@ -0,0 +1,99 @@ +[DEFAULT] +skip-if = toolkit == 'android' # bug 1730213 +head = head.js +firefox-appdir = browser +support-files = + data/engine.xml + +[test_000_frecency.js] +[test_about_urls.js] +[test_autofill_adaptiveHistory.js] +[test_autofill_bookmarked.js] +[test_autofill_do_not_trim.js] +[test_autofill_functional.js] +[test_autofill_origins.js] +[test_autofill_origins_alt_frecency.js] +prefs = places.frecency.origins.alternative.featureGate=true +[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_calculator.js] +[test_casing.js] +[test_dedupe_prefix.js] +[test_dedupe_switchTab.js] +[test_download_embed_bookmarks.js] +[test_empty_search.js] +[test_encoded_urls.js] +[test_escaping_badEscapedURI.js] +[test_escaping_escapeSelf.js] +[test_exposure.js] +[test_frecency.js] +[test_frecency_alternative_nimbus.js] +[test_heuristic_cancel.js] +[test_hideSponsoredHistory.js] +[test_keywords.js] +skip-if = os == 'linux' # bug 1474616 +[test_l10nCache.js] +[test_local_suggest_prefs.js] +[test_match_javascript.js] +[test_multi_word_search.js] +[test_muxer.js] +[test_protocol_ignore.js] +[test_protocol_swap.js] +[test_providerAliasEngines.js] +[test_providerHeuristicFallback.js] +[test_providerHistoryUrlHeuristic.js] +[test_providerKeywords.js] +[test_providerOmnibox.js] +[test_providerOpenTabs.js] +skip-if = + os == "mac" && debug # Bug 1781972 + os == "win" && debug # Bug 1781972 +[test_providerPlaces.js] +[test_providerPlaces_duplicate_entries.js] +[test_providerPlaces_nonEnglish.js] +[test_providerPreloaded.js] +[test_providersManager.js] +[test_providersManager_filtering.js] +[test_providersManager_maxResults.js] +[test_providerTabToSearch.js] +[test_providerTabToSearch_partialHost.js] +[test_query_url.js] +[test_queryScorer.js] +[test_quickactions.js] +[test_remote_tabs.js] +skip-if = !sync +[test_resultGroups.js] +[test_search_engine_host.js] +[test_search_engine_restyle.js] +[test_search_suggestions.js] +[test_search_suggestions_aliases.js] +[test_search_suggestions_tail.js] +[test_special_search.js] +[test_suggestedIndex.js] +[test_suggestedIndexRelativeToGroup.js] +[test_tab_matches.js] +[test_tags_caseInsensitivity.js] +[test_tags_extendedUnicode.js] +[test_tags_general.js] +[test_tags_matchBookmarkTitles.js] +[test_tags_returnedInSearches.js] +[test_tokenizer.js] +[test_trimming.js] +[test_unitConversion.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.js] +[test_UrlbarUtils_addToUrlbarHistory.js] +[test_UrlbarUtils_getShortcutOrURIAndPostData.js] +[test_UrlbarUtils_getTokenMatches.js] +[test_UrlbarUtils_unEscapeURIForUI.js] +[test_word_boundary_search.js] -- cgit v1.2.3