From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- .../urlbar/tests/UrlbarTestUtils.sys.mjs | 1581 ++++++++ .../urlbar/tests/browser-tips/README.txt | 7 + .../urlbar/tests/browser-tips/browser.toml | 31 + .../tests/browser-tips/browser_interventions.js | 273 ++ .../urlbar/tests/browser-tips/browser_picks.js | 200 + .../tests/browser-tips/browser_searchTips.js | 645 ++++ .../browser-tips/browser_searchTips_interaction.js | 691 ++++ .../urlbar/tests/browser-tips/browser_selection.js | 261 ++ .../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 | 759 ++++ .../urlbar/tests/browser-tips/slow-page.html | 7 + .../browser-tips/suppress-tips/active-update.xml | 1 + .../tests/browser-tips/suppress-tips/browser.toml | 24 + .../suppress-tips/browser_suppressTips.js | 128 + .../suppress-tips/config_localhost_update_url.json | 5 + .../suppress-tips/updates/0/update.status | 1 + .../tests/browser-updateResults/browser.toml | 27 + .../browser_appendSpanCount.js | 183 + .../browser_noUpdateResultsFromOtherProviders.js | 128 + .../browser_suggestedIndex_10_search_10_url.js | 1102 ++++++ .../browser_suggestedIndex_10_search_5_url.js | 661 ++++ .../browser_suggestedIndex_10_url_10_search.js | 1165 ++++++ .../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 | 1178 ++++++ .../urlbar/tests/browser-updateResults/head.js | 552 +++ .../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.toml | 692 ++++ .../browser/browser_UrlbarInput_formatValue.js | 187 + .../browser_UrlbarInput_formatValue_detachedTab.js | 92 + .../browser_UrlbarInput_formatValue_strikeout.js | 62 + .../browser/browser_UrlbarInput_hiddenFocus.js | 21 + .../tests/browser/browser_UrlbarInput_overflow.js | 159 + .../browser/browser_UrlbarInput_overflow_resize.js | 58 + .../browser/browser_UrlbarInput_privateFeature.js | 74 + .../browser/browser_UrlbarInput_searchTerms.js | 275 ++ ...owser_UrlbarInput_searchTerms_backgroundTabs.js | 63 + .../browser_UrlbarInput_searchTerms_modifiedUrl.js | 104 + .../browser_UrlbarInput_searchTerms_moveTab.js | 136 + .../browser_UrlbarInput_searchTerms_popup.js | 145 + .../browser_UrlbarInput_searchTerms_revert.js | 170 + .../browser_UrlbarInput_searchTerms_searchBar.js | 104 + .../browser_UrlbarInput_searchTerms_searchMode.js | 85 + .../browser_UrlbarInput_searchTerms_strings.js | 79 + ...rowser_UrlbarInput_searchTerms_stringsUnsafe.js | 133 + .../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 | 150 + .../tests/browser/browser_aboutHomeLoading.js | 228 ++ .../browser_acknowledgeFeedbackAndDismissal.js | 361 ++ .../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 | 272 ++ .../tests/browser/browser_autoFill_canonize.js | 65 + .../browser/browser_autoFill_caretNotAtEnd.js | 34 + ...owser_autoFill_clear_properly_on_accent_char.js | 185 + .../tests/browser/browser_autoFill_firstResult.js | 201 + .../urlbar/tests/browser/browser_autoFill_paste.js | 39 + .../tests/browser/browser_autoFill_placeholder.js | 894 +++++ .../tests/browser/browser_autoFill_preserve.js | 257 ++ .../tests/browser/browser_autoFill_trimURLs.js | 181 + .../urlbar/tests/browser/browser_autoFill_typed.js | 174 + .../urlbar/tests/browser/browser_autoFill_undo.js | 51 + .../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 | 76 + .../browser/browser_autocomplete_enter_race.js | 198 + .../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 | 193 + .../urlbar/tests/browser/browser_blanking.js | 58 + .../urlbar/tests/browser/browser_blobIcons.js | 133 + .../browser/browser_bufferer_onQueryResults.js | 82 + .../urlbar/tests/browser/browser_calculator.js | 33 + .../urlbar/tests/browser/browser_canonizeURL.js | 284 ++ .../urlbar/tests/browser/browser_caret_position.js | 362 ++ .../tests/browser/browser_click_row_border.js | 36 + .../urlbar/tests/browser/browser_clipboard.js | 349 ++ .../tests/browser/browser_closePanelOnClick.js | 51 + .../urlbar/tests/browser/browser_content_opener.js | 23 + .../tests/browser/browser_contextualsearch.js | 125 + .../browser/browser_copy_and_paste_first_result.js | 46 + .../tests/browser/browser_copy_during_load.js | 51 + .../urlbar/tests/browser/browser_copying.js | 738 ++++ .../urlbar/tests/browser/browser_customizeMode.js | 73 + .../urlbar/tests/browser/browser_cutting.js | 16 + .../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 | 89 + .../urlbar/tests/browser/browser_dragdropURL.js | 106 + .../urlbar/tests/browser/browser_dynamicResults.js | 998 +++++ .../browser/browser_editAndEnterWithSlowQuery.js | 476 +++ .../tests/browser/browser_edit_invalid_url.js | 91 + .../urlbar/tests/browser/browser_engagement.js | 210 ++ .../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 + .../browser/browser_heuristicNotAddedFirst.js | 159 + .../urlbar/tests/browser/browser_hideHeuristic.js | 514 +++ .../tests/browser/browser_ime_composition.js | 328 ++ .../urlbar/tests/browser/browser_inputHistory.js | 676 ++++ .../tests/browser/browser_inputHistory_autofill.js | 210 ++ .../browser/browser_inputHistory_emptystring.js | 97 + .../browser/browser_keepStateAcrossTabSwitches.js | 235 ++ .../urlbar/tests/browser/browser_keyword.js | 234 ++ .../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 | 352 ++ .../browser/browser_locationBarExternalLoad.js | 94 + .../browser_locationchange_urlbar_edit_dos.js | 67 + .../urlbar/tests/browser/browser_middleClick.js | 279 ++ .../browser/browser_move_tab_to_new_window.js | 120 + .../tests/browser/browser_new_tab_urlbar_reset.js | 39 + .../browser_observers_for_strip_on_share.js | 81 + .../urlbar/tests/browser/browser_oneOffs.js | 999 +++++ .../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 | 74 + .../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 | 70 + .../browser/browser_privateBrowsingWindowChange.js | 51 + .../tests/browser/browser_queryContextCache.js | 490 +++ .../urlbar/tests/browser/browser_quickactions.js | 737 ++++ .../tests/browser/browser_quickactions_devtools.js | 176 + .../browser/browser_quickactions_screenshot.js | 170 + .../browser/browser_quickactions_tab_refocus.js | 194 + .../urlbar/tests/browser/browser_raceWithTabs.js | 86 + .../urlbar/tests/browser/browser_recentsearches.js | 138 + .../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 | 218 ++ .../tests/browser/browser_restoreEmptyInput.js | 64 + .../urlbar/tests/browser/browser_resultSpan.js | 254 ++ .../urlbar/tests/browser/browser_result_menu.js | 260 ++ .../tests/browser/browser_result_menu_general.js | 416 +++ .../tests/browser/browser_result_onSelection.js | 73 + .../browser/browser_results_format_displayValue.js | 76 + .../browser/browser_retainedResultsOnFocus.js | 438 +++ .../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 | 219 ++ .../tests/browser/browser_searchMode_indicator.js | 377 ++ .../browser_searchMode_indicator_clickthrough.js | 106 + .../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 | 581 +++ .../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 + .../tests/browser/browser_search_continuation.js | 113 + .../browser_search_history_from_history_panel.js | 97 + .../tests/browser/browser_selectStaleResults.js | 329 ++ .../browser/browser_selectionKeyNavigation.js | 200 + .../browser/browser_separatePrivateDefault.js | 223 ++ ...owser_separatePrivateDefault_differentEngine.js | 354 ++ .../browser/browser_shortcuts_add_search_engine.js | 243 ++ .../urlbar/tests/browser/browser_slow_heuristic.js | 84 + .../tests/browser/browser_speculative_connect.js | 199 + ...ser_speculative_connect_not_with_client_cert.js | 230 ++ .../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 | 197 + .../browser/browser_strip_on_share_telemetry.js | 98 + .../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 | 122 + .../browser/browser_switchToTab_closed_tab.js | 90 + .../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 | 174 + .../urlbar/tests/browser/browser_tabToSearch.js | 647 ++++ .../urlbar/tests/browser/browser_textruns.js | 55 + .../urlbar/tests/browser/browser_tokenAlias.js | 861 +++++ .../urlbar/tests/browser/browser_top_sites.js | 478 +++ .../tests/browser/browser_top_sites_private.js | 171 + .../urlbar/tests/browser/browser_typed_value.js | 69 + .../urlbar/tests/browser/browser_unitConversion.js | 88 + .../browser/browser_updateForDomainCompletion.js | 22 + .../browser_url_formatted_correctly_on_load.js | 54 + .../tests/browser/browser_urlbar_annotation.js | 333 ++ .../tests/browser/browser_urlbar_selection.js | 307 ++ .../tests/browser/browser_urlbar_telemetry.js | 1218 ++++++ .../browser/browser_urlbar_telemetry_autofill.js | 684 ++++ .../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 | 321 ++ .../browser_urlbar_telemetry_quickactions.js | 133 + .../browser/browser_urlbar_telemetry_remotetab.js | 185 + .../browser/browser_urlbar_telemetry_searchmode.js | 592 +++ .../browser_urlbar_telemetry_tabtosearch.js | 418 +++ .../tests/browser/browser_urlbar_telemetry_tip.js | 130 + .../browser/browser_urlbar_telemetry_topsite.js | 133 + .../browser/browser_urlbar_telemetry_zeroPrefix.js | 266 ++ .../urlbar/tests/browser/browser_userTypedValue.js | 50 + .../tests/browser/browser_valueOnTabSwitch.js | 166 + .../tests/browser/browser_view_emptyResultSet.js | 40 + .../browser/browser_view_removedSelectedElement.js | 87 + .../tests/browser/browser_view_resultDisplay.js | 354 ++ .../browser/browser_view_resultTypes_display.js | 317 ++ .../tests/browser/browser_view_selectionByMouse.js | 567 +++ .../browser/browser_waitForLoadStartOrTimeout.js | 33 + .../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_copying_home.html | 1 + .../urlbar/tests/browser/file_urlbar_edit_dos.html | 18 + .../urlbar/tests/browser/file_userTypedValue.html | 1 + .../components/urlbar/tests/browser/head-common.js | 153 + browser/components/urlbar/tests/browser/head.js | 248 ++ .../urlbar/tests/browser/mixed_active.html | 14 + 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 + .../browser/search-engines/basic/manifest.json | 20 + .../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 + .../components/urlbar/tests/browser/wait-a-bit.sjs | 11 + .../tests/engagementTelemetry/browser/browser.toml | 87 + .../browser_glean_telemetry_abandonment_groups.js | 235 ++ ...wser_glean_telemetry_abandonment_interaction.js | 61 + ..._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 + ...lemetry_abandonment_search_engine_default_id.js | 19 + ...wser_glean_telemetry_abandonment_search_mode.js | 54 + .../browser_glean_telemetry_abandonment_tips.js | 99 + ...rowser_glean_telemetry_engagement_edge_cases.js | 221 ++ .../browser_glean_telemetry_engagement_groups.js | 292 ++ ...owser_glean_telemetry_engagement_interaction.js | 90 + ..._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 + ...elemetry_engagement_search_engine_default_id.js | 19 + ...owser_glean_telemetry_engagement_search_mode.js | 63 + ...r_glean_telemetry_engagement_selected_result.js | 974 +++++ .../browser_glean_telemetry_engagement_tips.js | 173 + .../browser_glean_telemetry_engagement_type.js | 118 + .../browser/browser_glean_telemetry_exposure.js | 136 + .../browser_glean_telemetry_exposure_edge_cases.js | 539 +++ .../browser_glean_telemetry_impression_groups.js | 258 ++ ...owser_glean_telemetry_impression_interaction.js | 68 + ..._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 + ...elemetry_impression_search_engine_default_id.js | 28 + ...owser_glean_telemetry_impression_search_mode.js | 72 + .../browser_glean_telemetry_impression_timing.js | 91 + .../browser_glean_telemetry_record_preferences.js | 74 + .../engagementTelemetry/browser/head-exposure.js | 47 + .../engagementTelemetry/browser/head-groups.js | 339 ++ .../browser/head-interaction.js | 340 ++ .../browser/head-n_chars_n_words.js | 56 + .../tests/engagementTelemetry/browser/head-sap.js | 66 + .../browser/head-search_engine_default_id.js | 43 + .../browser/head-search_mode.js | 93 + .../tests/engagementTelemetry/browser/head.js | 473 +++ .../tests/quicksuggest/MerinoTestUtils.sys.mjs | 809 ++++ .../quicksuggest/QuickSuggestTestUtils.sys.mjs | 915 +++++ .../quicksuggest/RemoteSettingsServer.sys.mjs | 619 ++++ .../urlbar/tests/quicksuggest/browser/browser.toml | 68 + .../quicksuggest/browser/browser_quicksuggest.js | 166 + .../browser/browser_quicksuggest_addons.js | 443 +++ .../browser/browser_quicksuggest_block.js | 252 ++ .../browser/browser_quicksuggest_configuration.js | 2099 +++++++++++ .../browser/browser_quicksuggest_indexes.js | 410 ++ .../browser/browser_quicksuggest_mdn.js | 230 ++ .../browser/browser_quicksuggest_merinoSessions.js | 138 + .../browser_quicksuggest_onboardingDialog.js | 1569 ++++++++ .../browser/browser_quicksuggest_pocket.js | 435 +++ .../browser/browser_quicksuggest_yelp.js | 429 +++ .../browser/browser_telemetry_dynamicWikipedia.js | 116 + .../browser/browser_telemetry_gleanEmptyStrings.js | 221 ++ .../browser_telemetry_impressionEdgeCases.js | 482 +++ .../browser_telemetry_navigationalSuggestions.js | 346 ++ .../browser/browser_telemetry_nonsponsored.js | 236 ++ .../browser/browser_telemetry_other.js | 298 ++ .../browser/browser_telemetry_sponsored.js | 408 ++ .../browser/browser_telemetry_weather.js | 158 + .../tests/quicksuggest/browser/browser_weather.js | 426 +++ .../urlbar/tests/quicksuggest/browser/head.js | 693 ++++ .../browser/searchSuggestionEngine.sjs | 57 + .../browser/searchSuggestionEngine.xml | 11 + .../tests/quicksuggest/browser/subdialog.xhtml | 14 + .../urlbar/tests/quicksuggest/unit/head.js | 911 +++++ .../tests/quicksuggest/unit/test_merinoClient.js | 647 ++++ .../unit/test_merinoClient_sessions.js | 402 ++ .../tests/quicksuggest/unit/test_quicksuggest.js | 1661 +++++++++ .../quicksuggest/unit/test_quicksuggest_addons.js | 558 +++ .../unit/test_quicksuggest_dynamicWikipedia.js | 103 + .../unit/test_quicksuggest_impressionCaps.js | 3907 ++++++++++++++++++++ .../quicksuggest/unit/test_quicksuggest_mdn.js | 190 + .../quicksuggest/unit/test_quicksuggest_merino.js | 574 +++ .../unit/test_quicksuggest_merinoSessions.js | 173 + .../unit/test_quicksuggest_migrate_v1.js | 490 +++ .../unit/test_quicksuggest_migrate_v2.js | 1355 +++++++ .../unit/test_quicksuggest_nonUniqueKeywords.js | 285 ++ .../unit/test_quicksuggest_offlineDefault.js | 127 + .../quicksuggest/unit/test_quicksuggest_pocket.js | 531 +++ .../test_quicksuggest_positionInSuggestions.js | 487 +++ .../unit/test_quicksuggest_scoreMap.js | 670 ++++ .../unit/test_quicksuggest_topPicks.js | 192 + .../quicksuggest/unit/test_quicksuggest_yelp.js | 842 +++++ .../tests/quicksuggest/unit/test_rust_ingest.js | 244 ++ .../tests/quicksuggest/unit/test_suggestionsMap.js | 293 ++ .../urlbar/tests/quicksuggest/unit/test_weather.js | 1402 +++++++ .../quicksuggest/unit/test_weather_keywords.js | 1503 ++++++++ .../urlbar/tests/quicksuggest/unit/xpcshell.toml | 51 + .../components/urlbar/tests/unit/data/engine.xml | 10 + browser/components/urlbar/tests/unit/head.js | 1173 ++++++ .../urlbar/tests/unit/test_000_frecency.js | 245 ++ .../unit/test_UrlbarController_integration.js | 106 + .../tests/unit/test_UrlbarController_telemetry.js | 253 ++ .../tests/unit/test_UrlbarController_unit.js | 389 ++ .../urlbar/tests/unit/test_UrlbarPrefs.js | 447 +++ .../urlbar/tests/unit/test_UrlbarQueryContext.js | 73 + .../unit/test_UrlbarQueryContext_restrictSource.js | 113 + .../urlbar/tests/unit/test_UrlbarSearchUtils.js | 462 +++ .../unit/test_UrlbarUtils_addToUrlbarHistory.js | 63 + .../unit/test_UrlbarUtils_copySnakeKeysToCamel.js | 226 ++ ...test_UrlbarUtils_getShortcutOrURIAndPostData.js | 249 ++ .../tests/unit/test_UrlbarUtils_getTokenMatches.js | 294 ++ .../tests/unit/test_UrlbarUtils_skippableTimer.js | 89 + .../unit/test_UrlbarUtils_unEscapeURIForUI.js | 36 + .../urlbar/tests/unit/test_about_urls.js | 176 + .../tests/unit/test_autofill_adaptiveHistory.js | 1443 ++++++++ .../urlbar/tests/unit/test_autofill_bookmarked.js | 151 + .../urlbar/tests/unit/test_autofill_do_not_trim.js | 140 + .../urlbar/tests/unit/test_autofill_functional.js | 147 + .../urlbar/tests/unit/test_autofill_origins.js | 1041 ++++++ .../tests/unit/test_autofill_originsAndQueries.js | 2471 +++++++++++++ .../unit/test_autofill_origins_alt_frecency.js | 272 ++ .../tests/unit/test_autofill_prefix_fallback.js | 76 + .../unit/test_autofill_search_engine_aliases.js | 85 + .../urlbar/tests/unit/test_autofill_urls.js | 916 +++++ .../unit/test_avoid_stripping_to_empty_tokens.js | 117 + .../urlbar/tests/unit/test_calculator.js | 46 + .../components/urlbar/tests/unit/test_casing.js | 370 ++ .../tests/unit/test_dedupe_embedded_url_param.js | 226 ++ .../urlbar/tests/unit/test_dedupe_prefix.js | 277 ++ .../urlbar/tests/unit/test_dedupe_switchTab.js | 34 + .../urlbar/tests/unit/test_dont_autofill_cases.js | 59 + .../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 | 271 ++ .../components/urlbar/tests/unit/test_frecency.js | 403 ++ .../tests/unit/test_frecency_alternative_nimbus.js | 77 + .../urlbar/tests/unit/test_heuristic_cancel.js | 238 ++ .../urlbar/tests/unit/test_hideSponsoredHistory.js | 104 + ...y_bookmark_results_on_search_service_failure.js | 116 + .../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 | 153 + .../urlbar/tests/unit/test_multi_word_search.js | 126 + browser/components/urlbar/tests/unit/test_muxer.js | 731 ++++ .../urlbar/tests/unit/test_pages_alt_frecency.js | 85 + .../urlbar/tests/unit/test_protocol_ignore.js | 42 + .../urlbar/tests/unit/test_protocol_swap.js | 302 ++ .../urlbar/tests/unit/test_providerAliasEngines.js | 146 + .../tests/unit/test_providerHeuristicFallback.js | 775 ++++ .../tests/unit/test_providerHistoryUrlHeuristic.js | 197 + .../urlbar/tests/unit/test_providerKeywords.js | 407 ++ .../urlbar/tests/unit/test_providerOmnibox.js | 887 +++++ .../urlbar/tests/unit/test_providerOpenTabs.js | 80 + .../urlbar/tests/unit/test_providerPlaces.js | 250 ++ .../unit/test_providerPlaces_duplicate_entries.js | 42 + .../tests/unit/test_providerPlaces_nonEnglish.js | 43 + .../tests/unit/test_providerRecentSearches.js | 167 + .../urlbar/tests/unit/test_providerTabToSearch.js | 536 +++ .../unit/test_providerTabToSearch_partialHost.js | 214 ++ .../urlbar/tests/unit/test_providersManager.js | 74 + .../tests/unit/test_providersManager_filtering.js | 405 ++ .../tests/unit/test_providersManager_maxResults.js | 37 + .../urlbar/tests/unit/test_queryScorer.js | 405 ++ .../components/urlbar/tests/unit/test_query_url.js | 123 + .../urlbar/tests/unit/test_quickactions.js | 127 + .../urlbar/tests/unit/test_remote_tabs.js | 695 ++++ .../urlbar/tests/unit/test_resultGroups.js | 1576 ++++++++ .../urlbar/tests/unit/test_richsuggestions.js | 66 + .../tests/unit/test_richsuggestions_order.js | 76 + .../tests/unit/test_search_engine_restyle.js | 124 + .../urlbar/tests/unit/test_search_suggestions.js | 2077 +++++++++++ .../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 | 599 +++ .../unit/test_suggestedIndexRelativeToGroup.js | 645 ++++ .../urlbar/tests/unit/test_tab_matches.js | 366 ++ .../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 | 503 +++ .../urlbar/tests/unit/test_word_boundary_search.js | 401 ++ browser/components/urlbar/tests/unit/xpcshell.toml | 201 + 482 files changed, 128260 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.toml 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-tips/suppress-tips/active-update.xml create mode 100644 browser/components/urlbar/tests/browser-tips/suppress-tips/browser.toml create mode 100644 browser/components/urlbar/tests/browser-tips/suppress-tips/browser_suppressTips.js create mode 100644 browser/components/urlbar/tests/browser-tips/suppress-tips/config_localhost_update_url.json create mode 100644 browser/components/urlbar/tests/browser-tips/suppress-tips/updates/0/update.status create mode 100644 browser/components/urlbar/tests/browser-updateResults/browser.toml create mode 100644 browser/components/urlbar/tests/browser-updateResults/browser_appendSpanCount.js create mode 100644 browser/components/urlbar/tests/browser-updateResults/browser_noUpdateResultsFromOtherProviders.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.toml 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_formatValue_strikeout.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_strings.js create mode 100644 browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_stringsUnsafe.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_clear_properly_on_accent_char.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_blobIcons.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_clipboard.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_and_paste_first_result.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_editAndEnterWithSlowQuery.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_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_move_tab_to_new_window.js create mode 100644 browser/components/urlbar/tests/browser/browser_new_tab_urlbar_reset.js create mode 100644 browser/components/urlbar/tests/browser/browser_observers_for_strip_on_share.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_screenshot.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_recentsearches.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_menu_general.js create mode 100644 browser/components/urlbar/tests/browser/browser_result_onSelection.js create mode 100644 browser/components/urlbar/tests/browser/browser_results_format_displayValue.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_continuation.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_slow_heuristic.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_strip_on_share_telemetry.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_closed_tab.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_url_formatted_correctly_on_load.js create mode 100644 browser/components/urlbar/tests/browser/browser_urlbar_annotation.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_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_removedSelectedElement.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_waitForLoadStartOrTimeout.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_copying_home.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/mixed_active.html 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/search-engines/basic/manifest.json 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/browser/wait-a-bit.sjs create mode 100644 browser/components/urlbar/tests/engagementTelemetry/browser/browser.toml 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_engine_default_id.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_engine_default_id.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_exposure_edge_cases.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_engine_default_id.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_engine_default_id.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/quicksuggest/MerinoTestUtils.sys.mjs create mode 100644 browser/components/urlbar/tests/quicksuggest/QuickSuggestTestUtils.sys.mjs create mode 100644 browser/components/urlbar/tests/quicksuggest/RemoteSettingsServer.sys.mjs create mode 100644 browser/components/urlbar/tests/quicksuggest/browser/browser.toml 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_mdn.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_quicksuggest_pocket.js create mode 100644 browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_yelp.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_gleanEmptyStrings.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_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_mdn.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_pocket.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_scoreMap.js create mode 100644 browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_topPicks.js create mode 100644 browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_yelp.js create mode 100644 browser/components/urlbar/tests/quicksuggest/unit/test_rust_ingest.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.toml 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_copySnakeKeysToCamel.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_skippableTimer.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_urls.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_embedded_url_param.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_dont_autofill_cases.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_history_bookmark_results_on_search_service_failure.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_pages_alt_frecency.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_providerRecentSearches.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_richsuggestions.js create mode 100644 browser/components/urlbar/tests/unit/test_richsuggestions_order.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.toml (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..a8e422526c --- /dev/null +++ b/browser/components/urlbar/tests/UrlbarTestUtils.sys.mjs @@ -0,0 +1,1581 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +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", + BrowserUIUtils: "resource:///modules/BrowserUIUtils.sys.mjs", + BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.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", +}); + +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(); + } + if (win.gURLBar.view.oneOffSearchButtons._rebuilding) { + await new Promise(resolve => + win.gURLBar.view.oneOffSearchButtons.addEventListener( + "rebuild", + resolve, + { + once: true, + } + ) + ); + } + 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 + * @param {boolean} [options.reopenOnBlur] Whether this method should repoen + * the view if the input is blurred before the query finishes. This is + * necessary to work around spurious blurs in CI, which close the view + * and cancel the query, defeating the typical use of this method where + * your test waits for the query to finish. However, this behavior + * isn't always desired, for example if your test intentionally blurs + * the input before the query finishes. In that case, pass false. + */ + async promiseAutocompleteResultPopup({ + window, + value, + waitForFocus, + fireInputEvent = true, + selectionStart = -1, + selectionEnd = -1, + reopenOnBlur = true, + } = {}) { + if (this.SimpleTest) { + await this.SimpleTest.promiseFocus(window); + } else { + await new Promise(resolve => waitForFocus(resolve, window)); + } + + const setup = () => { + window.gURLBar.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._setValue(value, false); + 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. + if (reopenOnBlur) { + window.gURLBar.inputField.addEventListener("blur", setup, { once: true }); + } + const result = await this.promiseSearchComplete(window); + if (reopenOnBlur) { + window.gURLBar.inputField.removeEventListener("blur", setup); + } + 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("Waiting for the urlbar view to open"); + await new Promise(resolve => { + win.gURLBar.controller.addQueryListener({ + onViewOpen() { + win.gURLBar.controller.removeQueryListener(this); + resolve(); + }, + }); + }); + this.info("Urlbar view 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) { + let closePromise = new Promise(resolve => { + if (!win.gURLBar.view.isOpen) { + resolve(); + return; + } + win.gURLBar.controller.addQueryListener({ + onViewClose() { + win.gURLBar.controller.removeQueryListener(this); + resolve(); + }, + }); + }); + if (closeFn) { + this.info("Awaiting custom close function"); + await closeFn(); + this.info("Done awaiting custom close function"); + } else { + this.info("Closing the view directly"); + win.gURLBar.view.close(); + } + this.info("Waiting for the view to close"); + await closePromise; + this.info("Urlbar view 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); + }, + + /** + * Removes the scheme from an url according to user prefs. + * + * @param {string} url + * The url that is supposed to be sanitizied. + * @param {{removeSingleTrailingSlash: (boolean)}} options + * removeSingleTrailingSlash: Remove trailing slash, when trimming enabled. + * @returns {string} + * The sanitized URL. + */ + trimURL(url, { removeSingleTrailingSlash = true } = {}) { + if (!lazy.UrlbarPrefs.get("trimURLs")) { + return url; + } + + let sanitizedURL = url; + if (removeSingleTrailingSlash) { + sanitizedURL = + lazy.BrowserUIUtils.removeSingleTrailingSlashFromURL(sanitizedURL); + } + + if (lazy.UrlbarPrefs.get("trimHttps")) { + sanitizedURL = sanitizedURL.replace("https://", ""); + } else { + sanitizedURL = sanitizedURL.replace("http://", ""); + } + + // Remove empty emphasis markers in case the protocol was trimmed. + sanitizedURL = sanitizedURL.replace("<>", ""); + + return sanitizedURL; + }, + + /** + * Returns the trimmed protocol with slashes. + * + * @returns {string} The trimmed protocol including slashes. Returns an empty + * string, when the protocol trimming is disabled. + */ + getTrimmedProtocolWithSlashes() { + if (Services.prefs.getBoolPref("browser.urlbar.trimURLs")) { + return Services.prefs.getBoolPref("browser.urlbar.trimHttps") + ? "https://" + : "http://"; // eslint-disable-this-line @microsoft/sdl/no-insecure-url + } + return ""; + }, + + /** + * 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) { + if (win.gURLBar.focused) { + win.gURLBar.select(); + } else { + this.EventUtils.synthesizeMouseAtCenter(win.gURLBar.inputField, {}, win); + await lazy.TestUtils.waitForCondition(() => win.gURLBar.focused); + } + if (text.length > 1) { + // Set most of the string directly instead of going through sendString, + // so that we don't make life unnecessarily hard for consumers by + // possibly starting multiple searches. + win.gURLBar._setValue( + text.substr(0, text.length - 1), + false /* allowTrim = */ + ); + } + this.EventUtils.sendString(text.substr(-1, 1), win); + }, + + /** + * Checks the urlbar value fomatting for a given URL. + * + * @param {window} win + * The input in this window will be tested. + * @param {string} urlFormatString + * The URL to test. The parts the are expected to be de-emphasized should be + * wrapped in "<" and ">" chars. + * @param {object} [options] + * Options object. + * @param {string} [options.clobberedURLString] + * Normally the URL is de-emphasized in-place, thus it's enough to pass + * urlString. In some cases however the formatter may decide to replace + * the URL with a fixed one, because it can't properly guess a host. In + * that case clobberedURLString is the expected de-emphasized value. The + * parts the are expected to be de-emphasized should be wrapped in "<" + * and ">" chars. + * @param {string} [options.additionalMsg] + * Additional message to use for Assert.equal. + * @param {int} [options.selectionType] + * The selectionType for which the input should be checked. + */ + checkFormatting( + win, + urlFormatString, + { + clobberedURLString = null, + additionalMsg = null, + selectionType = Ci.nsISelectionController.SELECTION_URLSECONDARY, + } = {} + ) { + let selectionController = win.gURLBar.editor.selectionController; + let selection = selectionController.getSelection(selectionType); + 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; + this.Assert.equal( + result, + clobberedURLString || urlFormatString, + "Correct part of the URL is de-emphasized" + + (additionalMsg ? ` (${additionalMsg})` : "") + ); + }, +}; + +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. + * If there's no results, the query will be completed after this timeout. + * @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. + * @param {Function} [options.delayResultsPromise] + * If given, we'll await on this before returning results. + */ + constructor({ + results, + name = "TestProvider" + Services.uuid.generateUUID(), + type = UrlbarUtils.PROVIDER_TYPE.PROFILE, + priority = 0, + addTimeout = 0, + onCancel = null, + onSelection = null, + onEngagement = null, + delayResultsPromise = null, + } = {}) { + if (delayResultsPromise && addTimeout) { + throw new Error( + "Can't provide both `addTimeout` and `delayResultsPromise`" + ); + } + super(); + this.results = results; + this.priority = priority; + this.addTimeout = addTimeout; + this.delayResultsPromise = delayResultsPromise; + this._name = name; + this._type = type; + this._onCancel = onCancel; + this._onSelection = onSelection; + this._onEngagement = onEngagement; + + // As this has been a common source of mistakes, auto-upgrade the provider + // type to heuristic if any result is heuristic. + if (!type && this.results?.some(r => r.heuristic)) { + this.type = UrlbarUtils.PROVIDER_TYPE.HEURISTIC; + } + } + + get name() { + return this._name; + } + + get type() { + return this._type; + } + + getPriority(context) { + return this.priority; + } + + isActive(context) { + return true; + } + + async startQuery(context, addCallback) { + if (!this.results.length && this.addTimeout) { + await new Promise(resolve => lazy.setTimeout(resolve, this.addTimeout)); + } + if (this.delayResultsPromise) { + await this.delayResultsPromise; + } + 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) { + this._onCancel?.(); + } + + onSelection(result, element) { + this._onSelection?.(result, element); + } + + onEngagement(state, queryContext, details, controller) { + this._onEngagement?.(state, queryContext, details, controller); + } +} + +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.toml b/browser/components/urlbar/tests/browser-tips/browser.toml new file mode 100644 index 0000000000..8d1e832529 --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/browser.toml @@ -0,0 +1,31 @@ +[DEFAULT] +support-files = ["head.js"] +prefs = ["browser.bookmarks.testing.skipDefaultBookmarksImport=true"] + +["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..7288898710 --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/browser_interventions.js @@ -0,0 +1,273 @@ +/* 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. + let useOldClearHistoryDialog = Services.prefs.getBoolPref( + "privacy.sanitize.useOldClearHistoryDialog" + ); + let dialogURL = useOldClearHistoryDialog + ? "chrome://browser/content/sanitize.xhtml" + : "chrome://browser/content/sanitize_v2.xhtml"; + 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", dialogURL, { + 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..ba0ff69357 --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/browser_picks.js @@ -0,0 +1,200 @@ +/* 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(); +}); + +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("menu") : 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) { + 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 + ); + // 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: "urlbar-result-menu-tip-get-help", + }, + } + ); +} 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..a82a2d658b --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/browser_searchTips.js @@ -0,0 +1,645 @@ +/* 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", + HttpServer: "resource://testing-common/httpd.sys.mjs", + UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs", + UrlbarProviderSearchTips: + "resource:///modules/UrlbarProviderSearchTips.sys.mjs", +}); + +// 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, + ], + // Set following prefs so tips are actually shown. + ["browser.laterrun.bookkeeping.profileCreationTime", 0], + ["browser.laterrun.bookkeeping.updateAppliedTime", 0], + ], + }); + + // 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 () => { + 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.startLoadingURIString( + 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.startLoadingURIString(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..a5cee02dae --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/browser_searchTips_interaction.js @@ -0,0 +1,691 @@ +/* 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", + HttpServer: "resource://testing-common/httpd.sys.mjs", + UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs", + UrlbarProviderSearchTips: + "resource:///modules/UrlbarProviderSearchTips.sys.mjs", + UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs", +}); + +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, + ], + // Set following prefs so tips are actually shown. + ["browser.laterrun.bookkeeping.profileCreationTime", 0], + ["browser.laterrun.bookkeeping.updateAppliedTime", 0], + ], + }); + + // 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 () => { + 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; + 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 + ); + + 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); +}); + +// 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 setDefaultEngine("Google"); + await BrowserTestUtils.withNewTab("about:blank", async () => { + await withDNSRedirect("www.google.com", "/", async url => { + BrowserTestUtils.startLoadingURIString(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 + ); + + 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(); +}); + +// 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.showSearchTerms.featureGate", true]], + }); + + await setDefaultEngine("Example"); + + await BrowserTestUtils.withNewTab("about:blank", async () => { + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + SEARCH_SERP_URL + ); + BrowserTestUtils.startLoadingURIString( + 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 + ); + + 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 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 + ); + + 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); +}); + +// 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 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 + ); + + 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); +}); + +// 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 setDefaultEngine("Google"); + await BrowserTestUtils.withNewTab("about:blank", async () => { + await withDNSRedirect("www.google.com", "/", async url => { + BrowserTestUtils.startLoadingURIString(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 + ); + + 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(); +}); + +// 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.showSearchTerms.featureGate", true]], + }); + + await setDefaultEngine("Example"); + await BrowserTestUtils.withNewTab("about:blank", async () => { + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + SEARCH_SERP_URL + ); + BrowserTestUtils.startLoadingURIString( + 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 + ); + + 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(); +}); + +// 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 setDefaultEngine("Google"); + await BrowserTestUtils.withNewTab("about:blank", async () => { + await withDNSRedirect("www.google.com", "/", async url => { + BrowserTestUtils.startLoadingURIString(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 + ); + + 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(); +}); + +// 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.showSearchTerms.featureGate", true]], + }); + await setDefaultEngine("Example"); + await BrowserTestUtils.withNewTab("about:blank", async () => { + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + SEARCH_SERP_URL + ); + BrowserTestUtils.startLoadingURIString( + 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 + ); + + 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.startLoadingURIString(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.startLoadingURIString(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 () => { + // We intentionally turn off this a11y check, because the following + // click is purposefully targeting a non-interactive element to dismiss + // the opened URL Bar with a mouse which can be done by assistive + // technology and keyboard by pressing `Esc` key, this rule check shall + // be ignored by a11y_checks suite. + AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false }); + await EventUtils.synthesizeMouseAtCenter(spring, {}); + AccessibilityUtils.resetEnv(); + }); + 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..6bb9b15b1c --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/browser_selection.js @@ -0,0 +1,261 @@ +/* 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", + helpUrl: "http://example.com/", + isBlockable: true, + blockL10n: { id: "urlbar-result-menu-remove-from-history" }, + } + ), + 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), + 2, + "Selected element index" + ); + + EventUtils.synthesizeKey("KEY_Tab"); + Assert.ok( + UrlbarTestUtils.getSelectedElement(window).classList.contains( + "urlbarView-button-menu" + ), + "The selected element should be the tip menu button." + ); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 1, + "getSelectedRowIndex should return 1 even though the menu button is selected." + ); + Assert.equal( + UrlbarTestUtils.getSelectedElementIndex(window), + 3, + "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.isVisible( + 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( + "urlbarView-button-menu" + ), + "The selected element should be the tip menu 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( + "urlbarView-button-menu" + ), + "The selected element should be the tip menu 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( + "urlbarView-button-menu" + ), + "The selected element should be the tip menu 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", + helpUrl: "http://example.com/", + isBlockable: true, + blockL10n: { id: "urlbar-result-menu-remove-from-history" }, + } + ), + 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), + 2, + "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..8b806ca5b7 --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/head.js @@ -0,0 +1,759 @@ +/* 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, { + HttpServer: "resource://testing-common/httpd.sys.mjs", + 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", +}); + +ChromeUtils.defineLazyGetter(this, "UrlbarTestUtils", () => { + const { UrlbarTestUtils: module } = ChromeUtils.importESModule( + "resource://testing-common/UrlbarTestUtils.sys.mjs" + ); + module.init(this); + return module; +}); + +ChromeUtils.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"); + } + + Assert.ok(element._buttons.has("menu"), "Tip has a menu 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, + "Number of results is greater than or equal to 2" + ); + let result = context.results[1]; + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.TIP, "Result type"); + 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"); + } + + let menuButton = element._buttons.get("menu"); + Assert.ok(menuButton, "Menu button exists"); + Assert.ok(BrowserTestUtils.isVisible(menuButton), "Menu 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, "View is not open"); + 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, "Number of results"); + let result = await UrlbarTestUtils.getDetailsOfResultAt(win, 0); + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.TIP, "Result type"); + 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, "Result is heuristic"); + Assert.equal(result.displayed.title, title, "Title"); + Assert.equal( + result.element.row._buttons.get("0").textContent, + expectedTip == UrlbarProviderSearchTips.TIP_TYPE.PERSIST + ? `Got it` + : `Okay, Got It`, + "Button text" + ); + Assert.ok( + !result.element.row._buttons.has("help"), + "Buttons in row does not include 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-tips/suppress-tips/active-update.xml b/browser/components/urlbar/tests/browser-tips/suppress-tips/active-update.xml new file mode 100644 index 0000000000..6e32eb1be2 --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/suppress-tips/active-update.xml @@ -0,0 +1 @@ + diff --git a/browser/components/urlbar/tests/browser-tips/suppress-tips/browser.toml b/browser/components/urlbar/tests/browser-tips/suppress-tips/browser.toml new file mode 100644 index 0000000000..24c805b2fe --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/suppress-tips/browser.toml @@ -0,0 +1,24 @@ +# This test environment is copied from "browser/components/tests/browser/whats_new_page/". + +[DEFAULT] +skip-if = [ + "verify", + "os == 'win' && msix", # Updater is disabled in MSIX builds; what's new pages therefore have no meaning. +] +reason = "This is a startup test. Verify runs tests multiple times after startup." +support-files = [ + "../head.js", + "active-update.xml", + "updates/0/update.status", + "config_localhost_update_url.json", +] +prefs = [ + "app.update.altUpdateDirPath='/browser/components/urlbar/tests/browser-tips/suppress-tips'", + "app.update.disabledForTesting=false", + "browser.aboutwelcome.enabled=false", + "browser.startup.homepage_override.mstone='60.0'", + "browser.startup.upgradeDialog.enabled=false", + "browser.policies.alternatePath='/browser/components/urlbar/tests/browser-tips/suppress-tips/config_localhost_update_url.json'", +] + +["browser_suppressTips.js"] diff --git a/browser/components/urlbar/tests/browser-tips/suppress-tips/browser_suppressTips.js b/browser/components/urlbar/tests/browser-tips/suppress-tips/browser_suppressTips.js new file mode 100644 index 0000000000..44fa912356 --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/suppress-tips/browser_suppressTips.js @@ -0,0 +1,128 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that the browser tips are suppressed correctly. + +"use strict"; + +/* import-globals-from ../head.js */ + +ChromeUtils.defineESModuleGetters(this, { + LaterRun: "resource:///modules/LaterRun.sys.mjs", + UrlbarProviderSearchTips: + "resource:///modules/UrlbarProviderSearchTips.sys.mjs", +}); + +const LAST_UPDATE_THRESHOLD_HOURS = 24; + +add_setup(async function () { + await PlacesUtils.history.clear(); + + await SpecialPowers.pushPrefEnv({ + set: [ + [ + `browser.urlbar.tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.ONBOARD}`, + 0, + ], + ], + }); + + registerCleanupFunction(() => { + resetSearchTipsProvider(); + Services.prefs.clearUserPref( + "browser.laterrun.bookkeeping.profileCreationTime" + ); + Services.prefs.clearUserPref( + "browser.laterrun.bookkeeping.updateAppliedTime" + ); + }); +}); + +add_task(async function updateApplied() { + // Check the update time. + Assert.notEqual( + Services.prefs.getIntPref( + "browser.laterrun.bookkeeping.updateAppliedTime", + 0 + ), + 0, + "updateAppliedTime pref should be updated when booting this test" + ); + Assert.equal( + LaterRun.hoursSinceUpdate, + 0, + "LaterRun.hoursSinceUpdate is 0 since one hour should not have passed from starting this test" + ); + + // To not suppress the tip by profile creation. + Services.prefs.setIntPref( + "browser.laterrun.bookkeeping.profileCreationTime", + secondsBasedOnNow(LAST_UPDATE_THRESHOLD_HOURS + 0.5) + ); + + // The test harness will use the current tab and remove the tab's history. + // Since the page that is tested is opened prior to the test harness taking + // over the current tab the active-update.xml specifies two pages to open by + // having 'https://example.com/|https://example.com/' for the value of openURL + // and then uses the first tab for the test. + gBrowser.selectedTab = gBrowser.tabs[0]; + // The test harness also changes the page to about:blank so go back to the + // page that was originally opened. + gBrowser.goBack(); + // Wait for the page to go back to the original page. + await TestUtils.waitForCondition( + () => gBrowser.selectedBrowser?.currentURI?.spec == "https://example.com/", + "Waiting for the expected page to reopen" + ); + gBrowser.removeTab(gBrowser.selectedTab); + + // Check whether the tip is suppressed by update. + await checkTab(window, "about:newtab"); + + // Clean up. + const alternatePath = Services.prefs.getCharPref( + "app.update.altUpdateDirPath" + ); + const testRoot = Services.prefs.getCharPref("mochitest.testRoot"); + let relativePath = alternatePath.substring("".length); + if (AppConstants.platform == "win") { + relativePath = relativePath.replace(/\//g, "\\"); + } + const updateDir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + updateDir.initWithPath(testRoot + relativePath); + const updatesFile = updateDir.clone(); + updatesFile.append("updates.xml"); + await TestUtils.waitForCondition( + () => updatesFile.exists(), + "Waiting until the updates.xml file exists" + ); + updatesFile.remove(false); +}); + +add_task(async function profileAge() { + // To not suppress the tip by profile creation and update. + Services.prefs.setIntPref( + "browser.laterrun.bookkeeping.profileCreationTime", + secondsBasedOnNow(LAST_UPDATE_THRESHOLD_HOURS + 0.5) + ); + Services.prefs.setIntPref( + "browser.laterrun.bookkeeping.updateAppliedTime", + secondsBasedOnNow(LAST_UPDATE_THRESHOLD_HOURS + 0.5) + ); + await checkTab( + window, + "about:newtab", + UrlbarProviderSearchTips.TIP_TYPE.ONBOARD + ); + + // To suppress the tip by profile creation. + Services.prefs.setIntPref( + "browser.laterrun.bookkeeping.profileCreationTime", + secondsBasedOnNow() + ); + await checkTab(window, "about:newtab"); +}); + +function secondsBasedOnNow(howManyHoursAgo = 0) { + return Math.floor(Date.now() / 1000 - howManyHoursAgo * 60 * 60); +} diff --git a/browser/components/urlbar/tests/browser-tips/suppress-tips/config_localhost_update_url.json b/browser/components/urlbar/tests/browser-tips/suppress-tips/config_localhost_update_url.json new file mode 100644 index 0000000000..4766b6a3fd --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/suppress-tips/config_localhost_update_url.json @@ -0,0 +1,5 @@ +{ + "policies": { + "AppUpdateURL": "http://127.0.0.1:8888/update.xml" + } +} diff --git a/browser/components/urlbar/tests/browser-tips/suppress-tips/updates/0/update.status b/browser/components/urlbar/tests/browser-tips/suppress-tips/updates/0/update.status new file mode 100644 index 0000000000..774a5c0df4 --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/suppress-tips/updates/0/update.status @@ -0,0 +1 @@ +succeeded diff --git a/browser/components/urlbar/tests/browser-updateResults/browser.toml b/browser/components/urlbar/tests/browser-updateResults/browser.toml new file mode 100644 index 0000000000..6b003d50ed --- /dev/null +++ b/browser/components/urlbar/tests/browser-updateResults/browser.toml @@ -0,0 +1,27 @@ +# 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"] +prefs = ["browser.bookmarks.testing.skipDefaultBookmarksImport=true"] + +["browser_appendSpanCount.js"] + +["browser_noUpdateResultsFromOtherProviders.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..60838d0c8b --- /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.isVisible(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.isVisible(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_noUpdateResultsFromOtherProviders.js b/browser/components/urlbar/tests/browser-updateResults/browser_noUpdateResultsFromOtherProviders.js new file mode 100644 index 0000000000..2757cccbeb --- /dev/null +++ b/browser/components/urlbar/tests/browser-updateResults/browser_noUpdateResultsFromOtherProviders.js @@ -0,0 +1,128 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test makes sure that when the view updates itself it doesn't try to +// update a row with a new result from a different provider. We avoid that +// because it's a cause of results flickering. + +"use strict"; + +add_task(async function test() { + // This slow provider is used to delay the end of the query. + let slowProvider = new UrlbarTestUtils.TestProvider({ + results: [], + priority: 10, + addTimeout: 1000, + }); + + // We'll run a first query with this provider to generate results, that should + // be overriden by results from the second provider. + let firstProvider = new UrlbarTestUtils.TestProvider({ + results: [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "https://mozilla.org/c" } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "https://mozilla.org/d" } + ), + ], + priority: 10, + }); + + // Then we'll run a second query with this provider, the results should not + // immediately replace the ones from the first provider, but rather be + // appended, until the query is done or the stale timer elapses. + let secondProvider = new UrlbarTestUtils.TestProvider({ + results: [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "https://mozilla.org/c" } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "https://mozilla.org/d" } + ), + ], + priority: 10, + }); + + UrlbarProvidersManager.registerProvider(slowProvider); + UrlbarProvidersManager.registerProvider(firstProvider); + function cleanup() { + UrlbarProvidersManager.unregisterProvider(slowProvider); + UrlbarProvidersManager.unregisterProvider(firstProvider); + UrlbarProvidersManager.unregisterProvider(secondProvider); + } + registerCleanupFunction(cleanup); + + // Execute the first query. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "moz", + }); + + // Now run the second query but don't wait for it to finish, we want to + // observe the view contents along the way. + UrlbarProvidersManager.unregisterProvider(firstProvider); + UrlbarProvidersManager.registerProvider(secondProvider); + let hasAtLeast4Children = BrowserTestUtils.waitForMutationCondition( + UrlbarTestUtils.getResultsContainer(window), + { childList: true }, + () => UrlbarTestUtils.getResultCount(window) == 4 + ); + let queryPromise = UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "moz", + }); + await hasAtLeast4Children; + // At this point we have the old results marked as "stale", and the new ones. + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 4, + "There should be 4 results" + ); + Assert.ok( + UrlbarTestUtils.getRowAt(window, 0).hasAttribute("stale"), + "Should be stale" + ); + Assert.ok( + UrlbarTestUtils.getRowAt(window, 1).hasAttribute("stale"), + "Should be stale" + ); + Assert.ok( + !UrlbarTestUtils.getRowAt(window, 2).hasAttribute("stale"), + "Should not be stale" + ); + Assert.ok( + !UrlbarTestUtils.getRowAt(window, 3).hasAttribute("stale"), + "Should not be stale" + ); + + // Now wait for the query end, this should remove stale results. + await queryPromise; + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "There should be 2 results" + ); + Assert.ok( + !UrlbarTestUtils.getRowAt(window, 0).hasAttribute("stale"), + "Should not be stale" + ); + Assert.ok( + !UrlbarTestUtils.getRowAt(window, 1).hasAttribute("stale"), + "Should not be stale" + ); + + await UrlbarTestUtils.promisePopupClose(window); + gURLBar.handleRevert(); + + cleanup(); +}); 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..c35526e592 --- /dev/null +++ b/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_10_url_10_search.js @@ -0,0 +1,1165 @@ +/* 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: UrlbarUtils.RESULT_TYPE.URL }, + ], +}); + +// 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: UrlbarUtils.RESULT_TYPE.URL }, + { + 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: UrlbarUtils.RESULT_TYPE.URL }, + { + 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: UrlbarUtils.RESULT_TYPE.URL }, + ], +}); + +// 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..692d4da276 --- /dev/null +++ b/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_5_url_5_search.js @@ -0,0 +1,1178 @@ +/* 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: 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: { + 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: UrlbarUtils.RESULT_TYPE.URL }, + { + 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: UrlbarUtils.RESULT_TYPE.URL }, + { + 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: UrlbarUtils.RESULT_TYPE.URL }, + { + count: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + suggestedIndex: -2, + }, + { count: 1, type: UrlbarUtils.RESULT_TYPE.URL }, + ], +}); + +// 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..3d46d83018 --- /dev/null +++ b/browser/components/urlbar/tests/browser-updateResults/head.js @@ -0,0 +1,552 @@ +/* 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", +}); + +ChromeUtils.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", + helpUrl: "http://example.com/", + isBlockable: true, + blockL10n: { id: "urlbar-result-menu-remove-from-history" }, + } + ), + { 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, + helpUrl: "http://example.com/", + isBlockable: true, + blockL10n: { id: "urlbar-result-menu-remove-from-history" }, + } + ) + ); + 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.isVisible(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.toml b/browser/components/urlbar/tests/browser/browser.toml new file mode 100644 index 0000000000..a77a831fab --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser.toml @@ -0,0 +1,692 @@ +[DEFAULT] +support-files = [ + "dummy_page.html", + "head.js", + "head-common.js", +] + +prefs = [ + "browser.bookmarks.testing.skipDefaultBookmarksImport=true", + "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_formatValue_strikeout.js"] +support-files = ["mixed_active.html"] + +["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_strings.js"] + +["browser_UrlbarInput_searchTerms_stringsUnsafe.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"] +skip-if = [ + "os == 'linux' && asan", # Bug 1834810 + "os == 'linux' && debug", # Bug 1834810 + "os == 'win' && asan", # Bug 1834810 + "os == 'win' && debug", # Bug 1834810 +] + +["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_clear_properly_on_accent_char.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", +] +skip-if = ["a11y_checks"] # Test times out (bug 1854660) + +["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_blobIcons.js"] + +["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_clipboard.js"] + +["browser_closePanelOnClick.js"] + +["browser_content_opener.js"] + +["browser_contextualsearch.js"] + +["browser_copy_and_paste_first_result.js"] + +["browser_copy_during_load.js"] +support-files = ["slow-page.sjs"] + +["browser_copying.js"] +https_first_disabled = true +support-files = [ + "authenticate.sjs", + "file_copying_home.html", + "wait-a-bit.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_editAndEnterWithSlowQuery.js"] + +["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_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 + +["browser_locationBarExternalLoad.js"] + +["browser_locationchange_urlbar_edit_dos.js"] +support-files = ["file_urlbar_edit_dos.html"] + +["browser_middleClick.js"] +fail-if = ["a11y_checks"] # Bug 1854660 clicked element may not be focusable and/or labeled + +["browser_move_tab_to_new_window.js"] + +["browser_new_tab_urlbar_reset.js"] + +["browser_observers_for_strip_on_share.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 + "a11y_checks", # Bugs 1858041 and 1854661 to investigate intermittent a11y_checks results +] + +["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"] + +["browser_quickactions_devtools.js"] + +["browser_quickactions_screenshot.js"] + +["browser_quickactions_tab_refocus.js"] + +["browser_raceWithTabs.js"] + +["browser_recentsearches.js"] +support-files = ["search-engines"] + +["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_menu_general.js"] + +["browser_result_onSelection.js"] + +["browser_results_format_displayValue.js"] + +["browser_retainedResultsOnFocus.js"] + +["browser_revert.js"] + +["browser_search_continuation.js"] +support-files = ["search-engines", "../../../search/test/browser/trendingSuggestionEngine.sjs"] + +["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", # Bug 1671045, Bug 1849098 + "os == 'linux' && (debug || tsan || asan)", # Bug 1849098 + "os == 'win' && debug", # Bug 1849098 +] + +["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_slow_heuristic.js"] + +["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_strip_on_share_telemetry.js"] + +["browser_suggestedIndex.js"] + +["browser_suppressFocusBorder.js"] + +["browser_switchTab_closesUrlbarPopup.js"] + +["browser_switchTab_currentTab.js"] + +["browser_switchTab_decodeuri.js"] + +["browser_switchTab_inputHistory.js"] + +["browser_switchTab_override.js"] + +["browser_switchToTabHavingURI_aOpenParams.js"] + +["browser_switchToTab_chiclet.js"] + +["browser_switchToTab_closed_tab.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_url_formatted_correctly_on_load.js"] + +["browser_urlbar_annotation.js"] +support-files = ["redirect_to.sjs"] + +["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_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_removedSelectedElement.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_waitForLoadStartOrTimeout.js"] +https_first_disabled = true + +["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..307767fa96 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_formatValue.js @@ -0,0 +1,187 @@ +/* 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} urlFormatString The URL to test. + * @param {string} [clobberedURLString] 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 clobberedURLString is + * the expected de-emphasized value. + */ +async function testVal(urlFormatString, clobberedURLString = null) { + let str = urlFormatString.replace(/[<>]/g, ""); + + info("Setting the value property directly"); + gURLBar.value = str; + gBrowser.selectedBrowser.focus(); + UrlbarTestUtils.checkFormatting(window, urlFormatString, { + clobberedURLString, + }); + + info("Simulating user input"); + await UrlbarTestUtils.inputIntoURLBar(window, str); + Assert.equal( + gURLBar.editor.rootElement.textContent, + str, + "URL is not highlighted" + ); + gBrowser.selectedBrowser.focus(); + UrlbarTestUtils.checkFormatting(window, urlFormatString, { + clobberedURLString, + additionalMsg: "with input simulation", + }); +} + +add_task(async function () { + const PREF_FORMATTING = "browser.urlbar.formatting.enabled"; + const PREF_TRIM_HTTPS = "browser.urlbar.trimHttps"; + + registerCleanupFunction(function () { + Services.prefs.clearUserPref(PREF_FORMATTING); + Services.prefs.clearUserPref(PREF_TRIM_HTTPS); + gURLBar.setURI(); + }); + + Services.prefs.setBoolPref(PREF_TRIM_HTTPS, false); + + gBrowser.selectedBrowser.focus(); + + await testVal("mozilla.org"); + await testVal("mözilla.org"); + await testVal("mozilla.imaginatory"); + + await testVal("mozilla.org"); + await testVal("mozilla.org"); + await testVal("mozilla.org"); + await testVal("mozilla.org"); + await testVal("mozilla.org"); + await testVal("mozilla.org"); + await testVal("mozilla.com"); + await testVal("mozilla.com"); + await testVal("mozilla.com"); + + await testVal("mozilla.org"); + await testVal("mozilla.org"); + + await testVal("mozilla.org"); + await testVal("mozilla.org"); + await testVal("mozilla.org"); + await testVal("mozilla.org"); + await testVal("mozilla.org"); + await testVal("mozilla.org"); + + await testVal("mozilla.org< >"); + await testVal("mozilla.org< >"); + // RTL characters in domain change order of domain and suffix. Domain should + // be highlighted correctly. + await testVal("اختبار.اختبار"); + + await testVal("mozilla.org"); + await testVal("mozilla.org"); + await testVal("mozilla.org"); + await testVal("mozilla.org"); + await testVal("mozilla.org"); + await testVal("mozilla.org"); + await testVal("foo.bar"); + await testVal("foo.bar<#mozilla.org>"); + await testVal("foo.bar"); + await testVal("foo.bar"); + await testVal("foo.bar<#x@mozilla.org>"); + await testVal("foo.bar<#@x@mozilla.org>"); + await testVal("foo.bar"); + await testVal("foo.bar"); + await testVal("mozilla.org"); + await testVal("mozilla.org"); + await testVal("mozilla.org"); + await testVal("mozilla.org"); + await testVal("mozilla.org"); + await testVal( + "foopy:\\blah@somewhere.com//whatever/", + "foopy" + ); + + await testVal("mozilla.org<:666/file.ext>"); + await testVal("mozilla.org<:666/file.ext>"); + await 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]", + ]; + for (let IP of IPs) { + await testVal(IP); + await testVal(IP + ""); + await testVal(IP + "<:666/file.ext>"); + await testVal("" + IP); + await testVal(`${IP}`); + await testVal(`${IP}<:666/file.ext>`); + await testVal(`${IP}<:666/file.ext>`); + await testVal(`user:\\pass@${IP}/`, `user`); + } + + await testVal("mailto:admin@mozilla.org"); + await testVal("gopher://mozilla.org/"); + await testVal("about:config"); + await testVal("jar:http://mozilla.org/example.jar!/"); + await testVal("view-source:http://mozilla.org/"); + await testVal("foo9://mozilla.org/"); + await testVal("foo+://mozilla.org/"); + await testVal("foo.://mozilla.org/"); + await testVal("foo-://mozilla.org/"); + + // Disable formatting. + Services.prefs.setBoolPref(PREF_FORMATTING, false); + + await testVal("https://mozilla.org"); +}); + +add_task(async function test_url_formatting_after_visiting_bookmarks() { + SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.trimURLs", true], + ["browser.urlbar.trimHttps", true], + ["browser.urlbar.formatting.enabled", true], + ], + }); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "https://something.example.com/test", + }); + await search({ + searchString: "something", + valueBefore: "something", + valueAfter: "something.example.com/", + placeholderAfter: "something.example.com/", + }); + EventUtils.sendKey("DOWN"); + EventUtils.sendKey("RETURN"); + await BrowserTestUtils.browserLoaded(gBrowser, false, null, true); + + UrlbarTestUtils.checkFormatting(window, "example.com"); + SpecialPowers.popPrefEnv(); +}); 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..fcb1357095 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_formatValue_detachedTab.js @@ -0,0 +1,92 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +async function detachTab(tab) { + 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(); + + return win; +} + +add_task(async function detach() { + // After detaching a tab into a new window, the input value in the new window + // should be formatted. + + // 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; + } + + let originalTab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: "https://example.com/original-tab", + }); + + let tabToDetach = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: "https://example.com/detach", + }); + + let win = await detachTab(tabToDetach); + + UrlbarTestUtils.checkFormatting( + win, + UrlbarTestUtils.trimURL("example.com") + ); + await BrowserTestUtils.closeWindow(win); + + UrlbarTestUtils.checkFormatting( + window, + UrlbarTestUtils.trimURL("example.com") + ); + gBrowser.removeTab(originalTab); +}); + +add_task(async function detach_emptyTab() { + // After detaching an empty tab into a new window, the input value in the + // original window should be formatted. + + let originalTab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: "https://example.com/original-tab", + }); + + let emptyTab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: "about:blank", + }); + gURLBar.focus(); + ok(gURLBar.focused, "urlbar is focused"); + is(gURLBar.value, "", "urlbar is empty"); + + let focusPromise = BrowserTestUtils.waitForEvent( + originalTab.linkedBrowser, + "focus" + ); + let win = await detachTab(emptyTab); + await BrowserTestUtils.closeWindow(win); + await focusPromise; + + ok(!gURLBar.focused, "urlbar is not focused"); + UrlbarTestUtils.checkFormatting( + window, + UrlbarTestUtils.trimURL("example.com") + ); + gBrowser.removeTab(originalTab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_formatValue_strikeout.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_formatValue_strikeout.js new file mode 100644 index 0000000000..2dd236525e --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_formatValue_strikeout.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "mixed_active.html"; + +/** + * Tests a given url. + * The de-emphasized parts must be wrapped in "<" and ">" chars. + * + * @param {string} urlFormatString The URL to test. + * @param {string} [clobberedURLString] 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 clobberedURLString is + * the expected de-emphasized value. + */ +async function testVal(urlFormatString, clobberedURLString = null) { + let str = urlFormatString.replace(/[<>]/g, ""); + + info("Setting the value property directly"); + gURLBar.value = str; + gBrowser.selectedBrowser.focus(); + UrlbarTestUtils.checkFormatting(window, urlFormatString, { + clobberedURLString, + selectionType: Ci.nsISelectionController.SELECTION_URLSTRIKEOUT, + }); +} + +add_task(async function test_strikeout_on_no_https_trimming() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.trimHttps", false], + ["security.mixed_content.block_active_content", false], + ], + }); + await BrowserTestUtils.withNewTab(TEST_URL, function () { + testVal("://example.com/mixed_active.html"); + }); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_no_strikeout_on_https_trimming() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.trimHttps", true], + ["security.mixed_content.block_active_content", false], + ], + }); + await BrowserTestUtils.withNewTab(TEST_URL, function () { + testVal( + "https://example.com/mixed_active.html", + "example.com/mixed_active.html" + ); + }); + await SpecialPowers.popPrefEnv(); +}); 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..f191cae321 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow.js @@ -0,0 +1,159 @@ +/* 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 () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.trimHttps", false]], + }); + // 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..46afdc5856 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms.js @@ -0,0 +1,275 @@ +/* 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 }; +} + +// 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.startLoadingURIString( + 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.startLoadingURIString(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..4182e3bf3d --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_modifiedUrl.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 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.startLoadingURIString(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.startLoadingURIString(tab.linkedBrowser, expectedSearchUrl); + await browserLoadedPromise; + + Assert.equal( + gURLBar.value, + UrlbarTestUtils.trimURL(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..790e950c65 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_moveTab.js @@ -0,0 +1,136 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + 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], + ["browser.urlbar.tipShownCount.searchTip_persist", 999], + ], + }); + + 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.startLoadingURIString(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..ee5bfb7dfc --- /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, + UrlbarTestUtils.trimURL(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..0fb9f2e7fb --- /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, + UrlbarTestUtils.trimURL(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, + UrlbarTestUtils.trimURL(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..d6793f4d0f --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_searchMode.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/. */ + +// 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, + UrlbarTestUtils.trimURL(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_strings.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_strings.js new file mode 100644 index 0000000000..866cb38760 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_strings.js @@ -0,0 +1,79 @@ +/* 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 whether certain patterns of search terms will show +// in the Urlbar as a search term. + +ChromeUtils.defineESModuleGetters(this, { + UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs", +}); + +let defaultTestEngine; + +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}", + }, + { setAsDefault: true } + ); + defaultTestEngine = Services.search.getEngineByName("MozSearch"); + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + }); +}); + +// 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 search_strings() { + const searches = [ + // Single word + "chocolate", + // Word with space + "chocolate cake", + // Allowable special characters. + "chocolate;,?@&=+$-_!~*'()#cake", + // Period used after the first word. + "what is 255.255.255.255", + // Protocol used after the first word. + "what is https://", + // Search with special characters + '"chocolate cake" -recipes', + "window.location how to use", + "http", + "https", + // Long string within threshold. + "h".repeat(UrlbarUtils.MAX_TEXT_LENGTH), + ]; + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + for (let searchString of searches) { + info("Search for term:", searchString); + let [searchUrl] = UrlbarUtils.getSearchQueryUrl( + defaultTestEngine, + searchString + ); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + searchUrl + ); + BrowserTestUtils.startLoadingURIString(gBrowser, searchUrl); + await browserLoadedPromise; + assertSearchStringIsInUrlbar(searchString); + + info("Check that no formatting is applied."); + UrlbarTestUtils.checkFormatting(window, searchString); + } + + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_stringsUnsafe.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_stringsUnsafe.js new file mode 100644 index 0000000000..09743b3ec2 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_stringsUnsafe.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/. */ + +// This test checks whether certain patterns of search terms won't show +// in the Urlbar as a search term. + +// Can regularly cause a timeout error on Mac verify mode. +requestLongerTimeout(5); + +ChromeUtils.defineESModuleGetters(this, { + UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs", +}); + +let defaultTestEngine, tab; + +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}", + }, + { setAsDefault: true } + ); + defaultTestEngine = Services.search.getEngineByName("MozSearch"); + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + }); +}); + +async function checkSearchString(searchString, isIpv6) { + info("Search for term:", searchString); + let [searchUrl] = UrlbarUtils.getSearchQueryUrl( + defaultTestEngine, + searchString + ); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + searchUrl + ); + BrowserTestUtils.startLoadingURIString(gBrowser, searchUrl); + await browserLoadedPromise; + + // decodeURI is necessary for matching square brackets in IPV6. + let expectedUrl = isIpv6 ? decodeURI(searchUrl) : searchUrl; + + if (UrlbarPrefs.get("trimHttps") && expectedUrl.startsWith("https://")) { + expectedUrl = expectedUrl.slice("https://".length); + } + + Assert.equal(gURLBar.value, expectedUrl, "The full URL should be in URL bar"); + Assert.equal( + gBrowser.userTypedValue, + null, + `There should not be a userTypedValue for ${searchString}` + ); + Assert.equal( + gBrowser.selectedBrowser.searchTerms, + "", + "searchTerms should be empty." + ); +} + +add_task(async function unsafe_search_strings() { + tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + const searches = [ + "example.org", + "www.example.org", + " www.example.org ", + "www.example.org/path", + "https://", + "https://example", + "https://example.org", + "https://example.org/path", + "https:// example.org/", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://", + "http://example", + "http://example.org", + "http://example.org/path", + "http:// example.org/path", + "file://example", + // Some protocols can be fixed up. + "ttp://example", + "htp://example", + "ttps://example", + "tps://example", + "ps://example", + "htps://example", + // Protocol fixup with a space and path. + "ttp:// example.org/path", + "htp:// example.org/path", + "ttps:// example.org/path", + "tps:// example.org/path", + "ps:// example.org/path", + "htps:// example.org/path", + // Variations of spaces. + "https ://example.org", + "https: //example.org", + "https:/ /example.org", + "https://\texample.org", + "https://\r\nexample.org", + // URL without protocols. + "www.example.org", + "www.example.org/path", + "www.example.org/path path", + "www. example.org/path", + // Long string exceeding threshold. + "h".repeat(UrlbarUtils.MAX_TEXT_LENGTH + 1), + ]; + for (let searchString of searches) { + await checkSearchString(searchString, false); + } + + const ipV6Searches = [ + "[2001:db8:85a3:8d3:1319:8a2e:370:7348]/example", + // Includes a space. + "[2001:db8:85a3:8d3:1319:8a2e:370:7348]/path path", + ]; + for (let searchString of ipV6Searches) { + await checkSearchString(searchString, true); + } + + 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..4bc4a3935d --- /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.startLoadingURIString( + 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..f4afeded40 --- /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, + UrlbarTestUtils.trimURL("http://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, + UrlbarTestUtils.trimURL("http://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.startLoadingURIString( + tab.linkedBrowser, + "http://test1.example.com" + ); + tab.linkedBrowser.stop(); + is( + gURLBar.value, + UrlbarTestUtils.trimURL("http://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, + UrlbarTestUtils.trimURL("http://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..484ac22007 --- /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.value, + UrlbarTestUtils.trimURL(longURL), + "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..b96017435e --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_trimURLs.js @@ -0,0 +1,150 @@ +function testValues(trimmedProtocol, notTrimmedProtocol) { + testVal(trimmedProtocol + "mozilla.org/", "mozilla.org"); + testVal( + notTrimmedProtocol + "mozilla.org/", + notTrimmedProtocol + "mozilla.org" + ); + testVal(trimmedProtocol + "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(trimmedProtocol + "mozilla.imaginatory/"); + testVal(trimmedProtocol + "www.mozilla.org/", "www.mozilla.org"); + testVal(trimmedProtocol + "sub.mozilla.org/", "sub.mozilla.org"); + testVal( + trimmedProtocol + "sub1.sub2.sub3.mozilla.org/", + "sub1.sub2.sub3.mozilla.org" + ); + testVal(trimmedProtocol + "mozilla.org/file.ext", "mozilla.org/file.ext"); + testVal(trimmedProtocol + "mozilla.org/sub/", "mozilla.org/sub/"); + + testVal(trimmedProtocol + "ftp.mozilla.org/", "ftp.mozilla.org"); + testVal(trimmedProtocol + "ftp1.mozilla.org/", "ftp1.mozilla.org"); + testVal(trimmedProtocol + "ftp42.mozilla.org/", "ftp42.mozilla.org"); + testVal(trimmedProtocol + "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( + notTrimmedProtocol + "user:pass@mozilla.org/", + notTrimmedProtocol + "user:pass@mozilla.org" + ); + testVal( + notTrimmedProtocol + "user@mozilla.org/", + notTrimmedProtocol + "user@mozilla.org" + ); + + 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/"); +} + +add_task(async function () { + const PREF_TRIM_URLS = "browser.urlbar.trimURLs"; + const PREF_TRIM_HTTPS = "browser.urlbar.trimHttps"; + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + registerCleanupFunction(async function () { + BrowserTestUtils.removeTab(tab); + Services.prefs.clearUserPref(PREF_TRIM_URLS); + Services.prefs.clearUserPref(PREF_TRIM_HTTPS); + gURLBar.setURI(); + }); + + Services.prefs.setBoolPref(PREF_TRIM_HTTPS, false); + + // Avoid search service sync init warnings due to URIFixup, when running the + // test alone. + await Services.search.init(); + + Services.prefs.setBoolPref(PREF_TRIM_URLS, true); + + testValues("http://", "https://"); + Services.prefs.setBoolPref(PREF_TRIM_HTTPS, true); + testValues("https://", "http://"); + Services.prefs.setBoolPref(PREF_TRIM_HTTPS, false); + + // 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"); + + 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" + ); + + // 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_TRIM_URLS, false); + + testVal("http://mozilla.org/"); + + Services.prefs.setBoolPref(PREF_TRIM_URLS, true); + + let promiseLoaded = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + "http://example.com/" + ); + BrowserTestUtils.startLoadingURIString(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..427a7419c8 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_aboutHomeLoading.js @@ -0,0 +1,228 @@ +/* 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_setup(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.startLoadingURIString(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); + let currentBrowser; + + 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; + currentBrowser = win.gBrowser.selectedBrowser; + is(win.gBrowser.visibleTabs.length, 2, "2 tabs opened"); + + // We need to load *something* here otherwise SessionStore will refuse to save this + // window when it closes as there is no user interaction, no tab history, and all the + // tab URIs are in the ignore list. + let loadPromise = BrowserTestUtils.browserLoaded( + currentBrowser, + false, + "about:logo" + ); + BrowserTestUtils.startLoadingURIString(currentBrowser, "about:logo"); + await loadPromise; + + let homeButton = win.document.getElementById("home-button"); + ok(BrowserTestUtils.isVisible(homeButton), "home-button is visible"); + + let changeListener; + let locationChangePromise = new Promise(resolve => { + changeListener = { + onLocationChange() { + is(win.gURLBar.value, "", "URL bar value should stay empty."); + resolve(); + }, + }; + win.gBrowser.addProgressListener(changeListener); + }); + homeButton.click(); + info("Waiting for location change to about:home"); + await locationChangePromise; + win.gBrowser.removeProgressListener(changeListener); + + 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..5ad8dfc75d --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_acknowledgeFeedbackAndDismissal.js @@ -0,0 +1,361 @@ +/* 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 command that dismisses a single result. +const DISMISS_ONE_COMMAND = "dismiss-one"; + +// The command that dismisses all results of a particular type. +const DISMISS_ALL_COMMAND = "dismiss-all"; + +// 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({ + command: DISMISS_ONE_COMMAND, + 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, + command: DISMISS_ONE_COMMAND, + 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, + command: DISMISS_ONE_COMMAND, + shouldBeSelected: true, + }); +}); + +// Tests dismissal of all results of a particular type. +add_task(async function acknowledgeDismissal_all() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await doDismissTest({ + command: DISMISS_ALL_COMMAND, + shouldBeSelected: false, + }); +}); + +/** + * Does a dismissal test: + * + * 1. Clicks a 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 {string} options.command + * One of: DISMISS_ONE_COMMAND, DISMISS_ALL_COMMAND + * @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({ command, 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 command. + await UrlbarTestUtils.openResultMenuAndClickItem(window, command, { + resultIndex, + openByMouse: true, + }); + + Assert.equal( + gTestProvider.commandCount[command], + 1, + "One dismissal should have happened" + ); + gTestProvider.commandCount[command] = 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.equal( + details.displayed.title, + command == DISMISS_ONE_COMMAND + ? "Thanks for your feedback. You won’t see this suggestion again." + : "Thanks for your feedback. You won’t see these suggestions anymore.", + "Tip text should be correct for the dismiss type" + ); + 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) { + // The l10n values aren't important. + return [ + { + name: FEEDBACK_COMMAND, + l10n: { + id: "firefox-suggest-weather-command-inaccurate-location", + }, + }, + { + name: DISMISS_ONE_COMMAND, + l10n: { + id: "firefox-suggest-weather-command-not-interested", + }, + }, + { + name: DISMISS_ALL_COMMAND, + l10n: { + id: "firefox-suggest-weather-command-not-interested", + }, + }, + ]; + } + + onEngagement(state, queryContext, details, controller) { + 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]++; + + switch (selType) { + case FEEDBACK_COMMAND: + controller.view.acknowledgeFeedback(details.result); + break; + case DISMISS_ONE_COMMAND: + details.result.acknowledgeDismissalL10n = { + id: "firefox-suggest-dismissal-acknowledgment-one", + }; + controller.removeResult(details.result); + break; + case DISMISS_ALL_COMMAND: + details.result.acknowledgeDismissalL10n = { + id: "firefox-suggest-dismissal-acknowledgment-all", + }; + controller.removeResult(details.result); + break; + } + } + } +} + +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..cfcaccfdd5 --- /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.isVisible(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.isVisible(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.isVisible(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.isVisible(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.isVisible(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.isVisible(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.isVisible(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.isVisible(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.isVisible(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.isVisible(elt)); + + Assert.ok(!popup.parentNode.getMenuItem("add-engine-menu")); + + elt = popup.parentNode.getMenuItem("add-engine-0"); + Assert.ok(BrowserTestUtils.isVisible(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.isVisible(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.isVisible(separator)); + + info("Engines should appear in sub menu"); + let menu = popup.parentNode.getMenuItem("add-engine-menu"); + Assert.ok(BrowserTestUtils.isVisible(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.isVisible(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.isVisible(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.isVisible(separator)); + + info("Engines should appear in sub menu"); + let menu = popup.parentNode.getMenuItem("add-engine-menu"); + Assert.ok(BrowserTestUtils.isVisible(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.isVisible(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.isVisible(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.isVisible(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.isVisible(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.isVisible(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..6df60f6941 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autoFill_backspaced.js @@ -0,0 +1,272 @@ +/* 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, + UrlbarTestUtils.trimURL(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: "http://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..fec11a9c8f --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autoFill_canonize.js @@ -0,0 +1,65 @@ +/* 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 () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.autoFill", true]], + }); + registerCleanupFunction(async function () { + gURLBar.handleRevert(); + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); + }); + + // Add a typed visit, so it will be autofilled. + await PlacesTestUtils.addVisits({ + uri: "https://example.com/", + transition: Ci.nsINavHistoryService.TRANSITION_TYPED, + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + await test_autocomplete({ + desc: "ENTER on the autofilled part should use autofill", + typed: "exam", + autofilled: "example.com/", + modified: UrlbarTestUtils.trimURL("https://example.com"), + waitForUrl: "https://example.com/", + keys: [["KEY_Enter"]], + }); + + await test_autocomplete({ + desc: "CTRL+ENTER on the autofilled part should bypass autofill", + typed: "exam", + autofilled: "example.com/", + modified: UrlbarTestUtils.trimURL("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..23382a70da --- /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.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_clear_properly_on_accent_char.js b/browser/components/urlbar/tests/browser/browser_autoFill_clear_properly_on_accent_char.js new file mode 100644 index 0000000000..a65da338a8 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autoFill_clear_properly_on_accent_char.js @@ -0,0 +1,185 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(async function () { + await cleanUp(); +}); + +add_task(async function test_autoFill_clear_properly_on_accent_char() { + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "", + url: "https://example.com", + }); + + await search({ + searchString: "e", + valueBefore: "e", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }); + + // Simulate macos accent character insertion. First the character is selected and + // then replaced by the accentuated character. + gURLBar.selectionStart = 0; + gURLBar.selectionEnd = 1; + EventUtils.sendChar("è", window); + + await UrlbarTestUtils.promiseSearchComplete(window); + + is(gURLBar.value, "è", "No auto complete for accent char."); + + await cleanUp(); +}); + +add_task(async function dont_clear_placeholder_if_autofill_accepted() { + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "", + url: "https://abc.yz", + }); + + let selectionChangedPromise = waitForSelectionChange({ times: 2 }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "abc", + }); + await UrlbarTestUtils.promiseSearchComplete(window); + + // PromiseAutoCompleteResultPopup fires one input event and two + // selectionchange events. If we don't wait for them to be fired before + // entering navigation keys, the selection gets messed up. + await selectionChangedPromise; + + Assert.equal(gURLBar.value, "abc.yz/", "autofilled value is as expected"); + info("Synthesizing keys"); + await sendNavigationKey("KEY_ArrowRight"); + await sendNavigationKey("KEY_ArrowLeft"); + await sendNavigationKey("KEY_ArrowLeft"); + await sendNavigationKey("KEY_ArrowLeft"); + + EventUtils.sendChar("x"); + is(gURLBar.value, "abc.xyz/", "No auto complete for accent char."); + + await cleanUp(); +}); + +add_task(async function dont_clear_placeholder_after_selection_change() { + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "", + url: "https://mozilla.org/", + }); + + let userTypedValue = "mo"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: userTypedValue, + }); + + Assert.equal( + gURLBar.value, + "mozilla.org/", + "autofilled value is as expected" + ); + + info("Simulate mouse click to change caret position."); + let selectionChangedPromise = waitForSelectionChange(); + is( + gURLBar.selectionStart, + userTypedValue.length, + " SelectionStart at the beginning of the placeholder" + ); + is( + gURLBar.selectionEnd, + gURLBar.value.length, + " Selection at the end of the placeholder" + ); + gURLBar.selectionStart = 1; + gURLBar.selectionEnd = 1; + + await selectionChangedPromise; + await UrlbarTestUtils.promiseSearchComplete(window); + + EventUtils.sendChar("o", window); + + await UrlbarTestUtils.promiseSearchComplete(window); + + is( + gURLBar.value, + "moozilla.org/", + "Autofill was not cleared and new character was inserted." + ); + + await cleanUp(); +}); + +add_task(async function modify_autofilled_selection() { + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "", + url: "https://developer.mozilla.org/en-US/", + }); + + let userTypedValue = "d"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: userTypedValue, + }); + + Assert.equal( + gURLBar.value, + "developer.mozilla.org/", + "autofilled value is as expected" + ); + await sendNavigationKey("KEY_ArrowDown"); + + let selectionChangedPromise = waitForSelectionChange(); + gURLBar.selectionStart = gURLBar.value.length - 6; + gURLBar.selectionEnd = gURLBar.value.length - 1; + + await selectionChangedPromise; + await UrlbarTestUtils.promiseSearchComplete(window); + + EventUtils.sendChar("j", window); + + await UrlbarTestUtils.promiseSearchComplete(window); + is( + gURLBar.value, + UrlbarTestUtils.trimURL("https://developer.mozilla.org/j/"), + "gURLBar contains correct modified autofilled value" + ); +}); + +async function cleanUp() { + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +} + +async function sendNavigationKey(key) { + let selectionChangePromise = waitForSelectionChange(); + EventUtils.synthesizeKey(key); + await selectionChangePromise; +} + +async function waitForSelectionChange(options = { times: 1 }) { + let observedSelectionChanges = 0; + + function handler(event, resolve) { + observedSelectionChanges += 1; + if (observedSelectionChanges == options.times) { + resolve(); + } + } + + await new Promise(resolve => { + gURLBar.addEventListener("selectionchange", event => + handler(event, resolve) + ); + }); + + gURLBar.removeEventListener("selectionchange", handler); +} 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..ba7ef20df6 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autoFill_firstResult.js @@ -0,0 +1,201 @@ +/* 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/"]); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + // 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..7e0d76c8cc --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autoFill_paste.js @@ -0,0 +1,39 @@ +/* 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/"]); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + 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..fd475f31c6 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autoFill_placeholder.js @@ -0,0 +1,894 @@ +/* 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(); +}); + +/** + * 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); + } + } + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); +} + +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..5e941e9ede --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autoFill_trimURLs.js @@ -0,0 +1,181 @@ +/* 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 () { + SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.trimURLs", true], + ["browser.urlbar.trimHttps", false], + ["browser.urlbar.autoFill", true], + ], + }); + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + gURLBar.handleRevert(); + }); + + 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) { + await UrlbarTestUtils.inputIntoURLBar(window, searchtext); + await UrlbarTestUtils.promiseSearchComplete(window); +} + +async function promiseTestResult(test) { + info(`Searching for '${test.search}'`); + + await promiseSearch(test.search); + + Assert.equal( + gURLBar.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.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.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..6f6ac57648 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autoFill_typed.js @@ -0,0 +1,174 @@ +/* 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/"]); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + // 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"]); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + // 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..8abe846754 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autoFill_undo.js @@ -0,0 +1,51 @@ +/* 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/"]); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + // 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..bfe491fc61 --- /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.startLoadingURIString( + 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..4fa60f6bf3 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autocomplete_edit_completed.js @@ -0,0 +1,76 @@ +/* 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 () { + SpecialPowers.pushPrefEnv({ set: [["browser.urlbar.trimHttps", false]] }); + 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..63a0958e0f --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autocomplete_enter_race.js @@ -0,0 +1,198 @@ +/* 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! +const IS_UPGRADING_SCHEMELESS = SpecialPowers.getBoolPref( + "dom.security.https_first_schemeless" +); +// eslint-disable-next-line @microsoft/sdl/no-insecure-url +const DEFAULT_URL_SCHEME = IS_UPGRADING_SCHEMELESS ? "https://" : "http://"; + +add_setup(async function () { + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: DEFAULT_URL_SCHEME + "/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, + DEFAULT_URL_SCHEME + "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, + DEFAULT_URL_SCHEME + "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, + DEFAULT_URL_SCHEME + "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..21c97405a6 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_bestMatch.js @@ -0,0 +1,193 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests best match rows in 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", "urlbarView-button-menu"]; + + 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; + + 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 button = row._buttons.get("menu"); + 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..f68c4d894a --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_blanking.js @@ -0,0 +1,58 @@ +/* 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, + UrlbarTestUtils.trimURL(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, + UrlbarTestUtils.trimURL(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, + UrlbarTestUtils.trimURL(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_blobIcons.js b/browser/components/urlbar/tests/browser/browser_blobIcons.js new file mode 100644 index 0000000000..701519c97f --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_blobIcons.js @@ -0,0 +1,133 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests `Blob` icon management in the view. + +"use strict"; + +// `URL.createObjectURL()` should be called the first time a blob icon is shown +// while the view is open, and `revokeObjectURL()` should be called when the +// view is closed. +add_task(async function test() { + let sandbox = sinon.createSandbox(); + registerCleanupFunction(() => sandbox.restore()); + + // Spy on `URL.createObjectURL()` and `revokeObjectURL()`. + let spies = ["createObjectURL", "revokeObjectURL"].reduce((memo, name) => { + memo[name] = sandbox.spy(Cu.getGlobalForObject(gURLBar.view).URL, name); + return memo; + }, {}); + + // Do a search and close the view. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await UrlbarTestUtils.promisePopupClose(window); + + // No blob URLs should have been created or revoked since no results that have + // blob icons were matched. + checkCallCounts(spies, { + createObjectURL: 0, + revokeObjectURL: 0, + }); + + // Create a test provider that returns a result with a blob icon. + let provider = new UrlbarTestUtils.TestProvider({ + results: [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + url: "https://example.com/", + iconBlob: new Blob([new Uint8Array([])]), + } + ), + ], + }); + UrlbarProvidersManager.registerProvider(provider); + + // Do some searches. + await doSearches(provider, spies, { + createObjectURL: 1, + revokeObjectURL: 0, + }); + + // Closing the view should cause `revokeObjectURL()` to be called. + await UrlbarTestUtils.promisePopupClose(window); + checkCallCounts(spies, { + createObjectURL: 1, + revokeObjectURL: 1, + }); + + // Do some more searches. + await doSearches(provider, spies, { + createObjectURL: 2, + revokeObjectURL: 1, + }); + + // Close the view. + await UrlbarTestUtils.promisePopupClose(window); + checkCallCounts(spies, { + createObjectURL: 2, + revokeObjectURL: 2, + }); + + // Remove the provider, do another search, and close the view. Since no + // results with blob icons are matched, the call counts should not change. + UrlbarProvidersManager.unregisterProvider(provider); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await UrlbarTestUtils.promisePopupClose(window); + checkCallCounts(spies, { + createObjectURL: 2, + revokeObjectURL: 2, + }); + + sandbox.restore(); +}); + +async function doSearches(provider, spies, expectedCountsByName) { + let previousImage; + for (let i = 0; i < 3; i++) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test " + i, + }); + + let result = await getTestResult(provider); + Assert.ok(result, "Test result should be present"); + Assert.ok(result.image, "Row has an icon with a src"); + Assert.ok(result.image.startsWith("blob:"), "Row icon src is a blob URL"); + if (i > 0) { + Assert.equal( + result.image, + previousImage, + "Blob URL should be the same as in previous searches" + ); + } + previousImage = result.image; + + // `createObjectURL()` should be called only once across all searches since + // the view remains open the whole time. + checkCallCounts(spies, expectedCountsByName); + } +} + +async function getTestResult(provider) { + for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + if (result.result.providerName == provider.name) { + return result; + } + } + return null; +} + +function checkCallCounts(spies, expectedCountsByName) { + for (let [name, count] of Object.entries(expectedCountsByName)) { + Assert.strictEqual(spies[name].callCount, count, "Spy call count: " + name); + } +} 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..7447f44ffd --- /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 = Promise.withResolvers(); + let waitFirstSearchResults = Promise.withResolvers(); + 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..fbbb7c01d1 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_canonizeURL.js @@ -0,0 +1,284 @@ +/* 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. + */ + +requestLongerTimeout(2); + +const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml"; + +add_task(async function checkCtrlWorks() { + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(); + }); + + // We do not want schemeless HTTPS-First interfering with this test, + // that interaction is already tested in dom/security/test/https-first/browser_schemeless.js + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_first_schemeless", false]], + }); + + 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 + ); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + await UrlbarTestUtils.inputIntoURLBar(win, inputValue); + 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.value.length; + win.gURLBar.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: "https://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", "https://example.com/", {}], + // search alias + ["@goo", "https://www.goo.com/", { ctrlKey: true }], + ]; + + for (let [inputValue, expectedURL, options] of testcases) { + let promiseLoad = BrowserTestUtils.waitForDocLoadAndStopIt( + expectedURL, + win.gBrowser.selectedBrowser + ); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + win.gURLBar.select(); + let autofillPromise = BrowserTestUtils.waitForEvent( + win.gURLBar.inputField, + "select" + ); + 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..6a8a8b18f8 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_caret_position.js @@ -0,0 +1,362 @@ +/* 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() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.trimHttps", false]], + }); + 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_clipboard.js b/browser/components/urlbar/tests/browser/browser_clipboard.js new file mode 100644 index 0000000000..f6127ef8d9 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_clipboard.js @@ -0,0 +1,349 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Browser test for clipboard suggestion. + */ + +"use strict"; + +const { UrlbarProviderClipboard, CLIPBOARD_IMPRESSION_LIMIT } = + ChromeUtils.importESModule( + "resource:///modules/UrlbarProviderClipboard.sys.mjs" + ); + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.clipboard.featureGate", true], + ["browser.urlbar.suggest.clipboard", true], + ], + }); + registerCleanupFunction(() => { + SpecialPowers.clipboardCopyString(""); + }); +}); + +async function searchEmptyStringAndGetFirstRow() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + Assert.ok(gURLBar.view.isOpen, "UrlbarView should be open."); + return UrlbarTestUtils.getRowAt(window, 0); +} + +async function checkClipboardSuggestionAbsent(startIdx) { + for (let i = startIdx; i < UrlbarTestUtils.getResultCount(window); i++) { + const row = await UrlbarTestUtils.getRowAt(window, i); + Assert.notEqual( + row.result.providerName, + UrlbarProviderClipboard.name, + `Clipboard suggestion should be absent (checking index ${i})` + ); + } +} + +add_task(async function testFormattingOfClipboardSuggestion() { + let unicodeURL = "https://пример.com/"; + let punycodeURL = "https://xn--e1afmkfd.com/"; + + SpecialPowers.clipboardCopyString(unicodeURL); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:home" }, + async browser => { + let { result } = await searchEmptyStringAndGetFirstRow(); + + Assert.equal( + result.providerName, + UrlbarProviderClipboard.name, + "The first result is a clipboard valid url suggestion." + ); + Assert.equal( + result.payload.url, + punycodeURL, + "The Clipboard suggestion URL should not be decoded." + ); + Assert.equal( + result.payload.fallbackTitle, + unicodeURL, + "The Clipboard suggestion fallback title should be decoded." + ); + } + ); +}); +// Verifies that a valid URL copied to the clipboard results in the +// display of a corresponding suggestion in the URL bar as the first +// suggestion with accurate URL and icon. Also ensures that engaging +// with a clipboard suggestion leads to navigation to the copied URL +// and subsequent absence of the suggestion upon refocusing the URL bar. +add_task(async function testUserEngagementWithClipboardSuggestion() { + const validURL = "https://example.com/"; + SpecialPowers.clipboardCopyString(validURL); + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:home" }, + async browser => { + let { result } = await searchEmptyStringAndGetFirstRow(); + let onLoad = BrowserTestUtils.browserLoaded(browser, false); + + Assert.equal( + result.providerName, + UrlbarProviderClipboard.name, + "The first result is a clipboard valid url suggestion." + ); + Assert.equal( + result.payload.url, + validURL, + "The Clipboard suggestion URL and the valid URL should match." + ); + Assert.equal( + result.icon, + "chrome://global/skin/icons/clipboard.svg", + "Clipboard suggestion icon" + ); + await checkClipboardSuggestionAbsent(1); + + // Focus and select the clipbaord result. + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_Enter"); + await onLoad; + + Assert.equal( + gBrowser.selectedBrowser.currentURI.spec, + validURL, + "Navigated to the validURL webpage after selecting the clipboard result." + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + await checkClipboardSuggestionAbsent(0); + } + ); +}); + +// This test confirms that dismissing the result from the result menu +// button after copying a valid URL dismisses the clipboard suggestion, +// and the suggestion does not reappear upon refocusing the URL bar. +add_task(async function testDismissClipboardSuggestion() { + SpecialPowers.clipboardCopyString("https://example.com/2"); + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:home" }, + async () => { + const resultIndex = 0; + const command = "dismiss"; + let row = await searchEmptyStringAndGetFirstRow(); + + Assert.equal( + row.result.providerName, + UrlbarProviderClipboard.name, + "Clipboard suggestion should be present" + ); + await checkClipboardSuggestionAbsent(1); + await UrlbarTestUtils.openResultMenuAndClickItem(window, command, { + resultIndex, + }); + Assert.ok( + gURLBar.view.isOpen, + "The view should remain open after clicking the command" + ); + Assert.ok( + !row.hasAttribute("feedback-acknowledgement"), + "Row should not have feedback acknowledgement after clicking command" + ); + await UrlbarTestUtils.promisePopupClose(window, () => { + gURLBar.blur(); + }); + + // Do the same search again. The suggestion should not appear. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + await checkClipboardSuggestionAbsent(0); + } + ); +}); + +// The test validates that the clipboard suggestion is displayed for +// the first two URL bar openings after copying a valid URL, but is +// suppressed on the third opening of URL bar. +add_task(async function testClipboardSuggestionLimit() { + SpecialPowers.clipboardCopyString("https://example.com/3"); + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:home" }, + async () => { + for (let i = 0; i < CLIPBOARD_IMPRESSION_LIMIT; i++) { + const { result } = await searchEmptyStringAndGetFirstRow(); + Assert.equal( + result.providerName, + UrlbarProviderClipboard.name, + "Clipboard suggestion should be present as the first suggestion." + ); + await checkClipboardSuggestionAbsent(1); + await UrlbarTestUtils.promisePopupClose(window, () => { + gURLBar.blur(); + }); + } + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + await checkClipboardSuggestionAbsent(0); + } + ); +}); + +// This test ensures that copying non-URL content to the clipboard +// results in the absence of a clipboard suggestion when opening +// the URL bar. +add_task(async function testNonUrlClipboardSuggestion() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:home" }, + async () => { + const malformedURLs = [ + "plain text", + "ftp://example.com", + "https://example.com[invalid]", + // Testing http because it is considered as a valid URL. + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://", + "https://example.com some text", + "https://example.com/ some text", + ]; + for (let i = 0; i < malformedURLs.length; i++) { + SpecialPowers.clipboardCopyString(malformedURLs[i]); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + Assert.ok(gURLBar.view.isOpen, "UrlbarView should be open."); + await checkClipboardSuggestionAbsent(0); + await UrlbarTestUtils.promisePopupClose(window, () => { + gURLBar.blur(); + }); + } + } + ); +}); + +// This test verifies that clipboard suggestions are displayed +// based on the toggled state of the 'clipboard.featureGate' preference. +add_task(async function testClipboardFeatureGateToggle() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.clipboard.featureGate", false], + ["browser.urlbar.suggest.clipboard", true], + ], + }); + SpecialPowers.clipboardCopyString("https://example.com/4"); + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:home" }, + async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + await checkClipboardSuggestionAbsent(0); + await UrlbarTestUtils.promisePopupClose(window, () => { + gURLBar.blur(); + }); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.clipboard.featureGate", true]], + }); + const { result } = await searchEmptyStringAndGetFirstRow(); + Assert.equal( + result.providerName, + UrlbarProviderClipboard.name, + "Clipboard suggestion should be present as the first suggestion." + ); + await checkClipboardSuggestionAbsent(1); + } + ); +}); + +// This test confirms that clipboard suggestions are presented based on +// the state of the 'suggest.clipboard' preference toggle. +add_task(async function testClipboardSuggestToggle() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.clipboard.featureGate", true], + ["browser.urlbar.suggest.clipboard", false], + ], + }); + SpecialPowers.clipboardCopyString("https://example.com/5"); + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:home" }, + async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + await checkClipboardSuggestionAbsent(0); + await UrlbarTestUtils.promisePopupClose(window, () => { + gURLBar.blur(); + }); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.clipboard", true]], + }); + const { result } = await searchEmptyStringAndGetFirstRow(); + Assert.equal( + result.providerName, + UrlbarProviderClipboard.name, + "Clipboard suggestion should be present as the first suggestion." + ); + await checkClipboardSuggestionAbsent(1); + } + ); +}); + +add_task(async function testScalarAndStopWatchTelemetry() { + SpecialPowers.clipboardCopyString("https://example.com/6"); + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:home" }, + async () => { + Services.telemetry.clearScalars(); + let histogram = Services.telemetry.getHistogramById( + "FX_URLBAR_PROVIDER_CLIPBOARD_READ_TIME_MS" + ); + histogram.clear(); + Assert.equal( + Object.values(histogram.snapshot().values).length, + 0, + "histogram is empty before search" + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + waitForFocus, + fireInputEvent: true, + }); + + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_Enter"); + }); + + const scalars = TelemetryTestUtils.getProcessScalars( + "parent", + true, + true + ); + + TelemetryTestUtils.assertKeyedScalar( + scalars, + `urlbar.picked.clipboard`, + 0, + 1 + ); + + Assert.greater( + Object.values(histogram.snapshot().values).length, + 0, + "histogram updated after search" + ); + } + ); +}); 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..c61bb35bb6 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_closePanelOnClick.js @@ -0,0 +1,51 @@ +/* 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_setup(function () { + // We intentionally turn off this a11y check, because the following + // clicks is purposefully targeting non-interactive elements to dismiss + // the opened URL Bar with a mouse which can be done by assistive + // technology and keyboard by pressing `Esc` key, this rule check shall + // be ignored by a11y_checks suite. + AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false }); + + registerCleanupFunction(async () => { + // Usually, the AccessibilityUtils environment should be reset right after + // the click, but in this case there are no other testable interactions + // between iterations of the use case task besides those clicks that we are + // setting the environment with. + AccessibilityUtils.resetEnv(); + }); +}); + +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..60e489a542 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_contextualsearch.js @@ -0,0 +1,125 @@ +/* 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.startLoadingURIString( + 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.startLoadingURIString( + 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_and_paste_first_result.js b/browser/components/urlbar/tests/browser/browser_copy_and_paste_first_result.js new file mode 100644 index 0000000000..236ad49671 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_copy_and_paste_first_result.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(async function () { + registerCleanupFunction(async function () { + gURLBar.handleRevert(); + await PlacesUtils.history.clear(); + }); + SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.autoFill", true], + ["browser.urlbar.suggest.quickactions", false], + ], + }); + + await PlacesUtils.history.clear(); + await PlacesTestUtils.addVisits([ + "https://example.com/", + "https://example.com/foo", + ]); +}); + +add_task(async function () { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + }); + Assert.equal( + gURLBar.value, + "example.com/", + "autofilled value is as expected" + ); + + await UrlbarTestUtils.promisePopupClose(window); + + goDoCommand("cmd_selectAll"); + goDoCommand("cmd_copy"); + goDoCommand("cmd_paste"); + Assert.equal( + gURLBar.inputField.value, + "https://example.com/", + "pasted value contains scheme" + ); +}); 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..111df58fd1 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_copying.js @@ -0,0 +1,738 @@ +/* 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 () { + await test_copy_values(trimHttpTests, false); + await test_copy_values(trimHttpsTests, true); +}); + +async function test_copy_values(testValues, trimHttps) { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + registerCleanupFunction(function () { + gBrowser.removeTab(tab); + gURLBar.setURI(); + }); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.trimURLs", true], + ["browser.urlbar.trimHttps", trimHttps], + // avoid prompting about phishing + ["network.http.phishy-userpass-length", 32], + ], + }); + + for (let testCase of testValues) { + if (testCase.setup) { + await testCase.setup(); + } + + if (testCase.loadURL) { + info(`Loading : ${testCase.loadURL}`); + let expectedLoad = testCase.expectedLoad || testCase.loadURL; + BrowserTestUtils.startLoadingURIString( + 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 trimHttpTests = [ + // 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", + }, +]; + +var trimHttpsTests = [ + // pageproxystate="invalid" + { + setURL: "https://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: "https://example.com/", + expectedURL: "example.com", + copyExpected: "https://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: "https://example.com/foo", + expectedURL: "example.com/foo", + copyExpected: "https://example.com/foo", + }, + { + copyVal: "/foo", + copyExpected: "https://example.com", + }, + { + copyVal: ".com/foo", + copyExpected: "example", + }, + // Test that partially selected URL is copied with encoded spaces + { + loadURL: "https://example.com/%20space/test", + expectedURL: "example.com/ space/test", + copyExpected: "https://example.com/%20space/test", + }, + { + copyVal: "/test", + copyExpected: "https://example.com/%20space", + }, + { + copyVal: "", + copyExpected: "https://example.com/%20space/test", + }, + { + loadURL: "https://example.com/%20foo%20bar%20baz/", + expectedURL: "example.com/ foo bar baz/", + copyExpected: "https://example.com/%20foo%20bar%20baz/", + }, + { + copyVal: " baz/", + copyExpected: "https://example.com/%20foo%20bar", + }, + { + copyVal: "example. baz/", + copyExpected: "com/ foo bar", + }, + // Test escaping + { + loadURL: "https://example.com/()%28%29%C3%A9", + expectedURL: "example.com/()()\xe9", + copyExpected: "https://example.com/()%28%29%C3%A9", + }, + { + copyVal: ")()\xe9", + copyExpected: "https://example.com/(", + }, + { + copyVal: "e)()\xe9", + copyExpected: "xample.com/(", + }, + + { + loadURL: "https://example.com/%C3%A9%C3%A9", + expectedURL: "example.com/\xe9\xe9", + copyExpected: "https://example.com/%C3%A9%C3%A9", + }, + { + copyVal: "e\xe9", + copyExpected: "xample.com/\xe9", + }, + { + copyVal: "\xe9", + copyExpected: "https://example.com/%C3%A9", + } /* + { + // Note: it seems BrowserTestUtils.loadURI fails for unicode domains + loadURL: "https://sub2.xn--lt-uia.mochi.test:8888/foo", + expectedURL: "sub2.ält.mochi.test:8888/foo", + copyExpected: "https://sub2.ält.mochi.test:8888/foo", + }, + { + copyVal: "soo", + copyExpected: "ub2.ält.mochi.test:8888/f", + }, + { + copyVal: "oo", + copyExpected: "https://sub2.%C3%A4lt.mochi.test:8888/f", + },*/, + + { + loadURL: "https://example.com/?%C3%B7%C3%B7", + expectedURL: "example.com/?\xf7\xf7", + copyExpected: "https://example.com/?%C3%B7%C3%B7", + }, + { + copyVal: "e\xf7", + copyExpected: "xample.com/?\xf7", + }, + { + copyVal: "\xf7", + copyExpected: "https://example.com/?%C3%B7", + }, + { + loadURL: "https://example.com/a%20test", + expectedURL: "example.com/a test", + copyExpected: "https://example.com/a%20test", + }, + { + loadURL: "https://example.com/a%E3%80%80test", + expectedURL: "example.com/a%E3%80%80test", + copyExpected: "https://example.com/a%E3%80%80test", + }, + { + loadURL: "https://example.com/a%20%C2%A0test", + expectedURL: "example.com/a %C2%A0test", + copyExpected: "https://example.com/a%20%C2%A0test", + }, + { + loadURL: "https://example.com/%20%20%20", + expectedURL: "example.com/%20%20%20", + copyExpected: "https://example.com/%20%20%20", + }, + { + loadURL: "https://example.com/%E3%80%80%E3%80%80", + expectedURL: "example.com/%E3%80%80%E3%80%80", + copyExpected: "https://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: "https://example.com/%E3%80%80%E3%80%80", + expectedURL: "example.com/%E3%80%80%E3%80%80", + copyExpected: "https://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: + "https://example.com/%D0%B1%D0%B8%D0%BE%D0%B3%D1%80%D0%B0%D1%84%D0%B8%D1%8F", + expectedURL: "example.com/биография", + copyExpected: "https://example.com/биография", + }, + { + copyVal: "ография", + copyExpected: "https://example.com/%D0%B1%D0%B8", + }, + { + async setup() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.decodeURLsOnCopy", true]], + }); + }, + async cleanup() { + await SpecialPowers.popPrefEnv(); + }, + loadURL: "http://example.com/", + expectedURL: "http://example.com", + copyExpected: "http://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(); + } + info(`Target Value ${targetValue}`); + return SimpleTest.promiseClipboardChange(targetValue, () => + goDoCommand("cmd_copy") + ); +} + +add_task(async function includingProtocol() { + await PlacesUtils.history.clear(); + await PlacesTestUtils.clearInputHistory(); + SpecialPowers.pushPrefEnv({ set: [["browser.urlbar.trimHttps", true]] }); + + await PlacesTestUtils.addVisits(["https://example.com/"]); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + // If the url is autofilled, the protocol should be included in the copied + // value. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "example", + fireInputEvent: true, + }); + Assert.ok( + (await UrlbarTestUtils.getDetailsOfResultAt(window, 0)).autofill, + "The first result should be aufotill suggestion" + ); + + window.goDoCommand("cmd_selectAll"); + await SimpleTest.promiseClipboardChange("https://example.com/", () => + goDoCommand("cmd_copy") + ); + Assert.ok(true, "Expected value is copied"); + + // Then, when adding some more characters, should not be included. + gURLBar.selectionStart = gURLBar.value.length; + gURLBar.selectionEnd = gURLBar.value.length; + EventUtils.synthesizeKey("x"); + await UrlbarTestUtils.promiseSearchComplete(window); + Assert.ok( + !(await UrlbarTestUtils.getDetailsOfResultAt(window, 0)).autofill, + "The first result should not be aufotill suggestion" + ); + + window.goDoCommand("cmd_selectAll"); + await SimpleTest.promiseClipboardChange("example.com/x", () => + goDoCommand("cmd_copy") + ); + Assert.ok(true, "Expected value is copied"); + + await PlacesUtils.history.clear(); + await PlacesTestUtils.clearInputHistory(); +}); + +add_task(async function loadingPageInBlank() { + const home = `${TEST_BASE_URL}file_copying_home.html`; + const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, home); + const onNewTabCreated = waitForNewTabWithLoadRequest(); + SpecialPowers.spawn(tab.linkedBrowser, [], function () { + content.document.querySelector("a").click(); + }); + const newtab = await onNewTabCreated; + await BrowserTestUtils.waitForCondition( + () => + newtab.linkedBrowser.browsingContext.mostRecentLoadingSessionHistoryEntry + ); + gURLBar.focus(); + window.goDoCommand("cmd_selectAll"); + await SimpleTest.promiseClipboardChange( + "https://example.com/browser/browser/components/urlbar/tests/browser/wait-a-bit.sjs", + () => goDoCommand("cmd_copy") + ); + Assert.ok(true, "Expected value is copied"); + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(newtab); +}); + +async function waitForNewTabWithLoadRequest() { + return new Promise(resolve => + gBrowser.addTabsProgressListener({ + onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) { + if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) { + gBrowser.removeTabsProgressListener(this); + resolve(gBrowser.getTabForBrowser(aBrowser)); + } + }, + }) + ); +} 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..87e1b01695 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_cutting.js @@ -0,0 +1,16 @@ +/* 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() { + await UrlbarTestUtils.inputIntoURLBar(window, "https://example.com/"); + gURLBar.selectionStart = 4; + gURLBar.selectionEnd = 5; + goDoCommand("cmd_cut"); + is( + gURLBar.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..577d39b587 --- /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 = UrlbarTestUtils.getTrimmedProtocolWithSlashes() + 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.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 = UrlbarTestUtils.getTrimmedProtocolWithSlashes() + 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.value, + UrlbarTestUtils.trimURL(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..f1f85c4cd0 --- /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.value, "ug1105244.example.com/"); + sendDelete(); + Assert.equal(gURLBar.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..64a086b0cb --- /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.isVisible(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.isHidden(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..215f21bd3f --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_downArrowKeySearch.js @@ -0,0 +1,89 @@ +/* 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 () => { + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + 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, + UrlbarTestUtils.trimURL("http://example.com/", { + removeSingleTrailingSlash: false, + }) + ); + 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..976ae3b9cb --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_dynamicResults.js @@ -0,0 +1,998 @@ +/* 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 IS_UPGRADING_SCHEMELESS = SpecialPowers.getBoolPref( + "dom.security.https_first_schemeless" +); +// eslint-disable-next-line @microsoft/sdl/no-insecure-url +const DEFAULT_URL_SCHEME = IS_UPGRADING_SCHEMELESS ? "https://" : "http://"; +const DUMMY_PAGE = + DEFAULT_URL_SCHEME + + "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]" + ); + Assert.ok( + !row.hasAttribute("has-url"), + "Row should not have has-url since view template does not contain .urlbarView-url" + ); + 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.startLoadingURIString( + 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()); +}); + +// View templates that contain a top-level `.urlbarView-url` element should +// cause `has-url` to be set on `.urlbarView-row`. +add_task(async function hasUrlTopLevel() { + await doAttributesTest({ + viewTemplate: { + name: "url", + tag: "span", + classList: ["urlbarView-url"], + }, + viewUpdate: { + url: { + textContent: "https://example.com/", + }, + }, + expectedAttributes: { + "has-url": true, + }, + }); +}); + +// View templates that contain a descendant `.urlbarView-url` element should +// cause `has-url` to be set on `.urlbarView-row`. +add_task(async function hasUrlDescendant() { + await doAttributesTest({ + viewTemplate: { + children: [ + { + children: [ + { + children: [ + { + name: "url", + tag: "span", + classList: ["urlbarView-url"], + }, + ], + }, + ], + }, + ], + }, + viewUpdate: { + url: { + textContent: "https://example.com/", + }, + }, + expectedAttributes: { + "has-url": true, + }, + }); +}); + +// View templates that contain a top-level `.urlbarView-action` element should +// cause `has-action` to be set on `.urlbarView-row`. +add_task(async function hasActionTopLevel() { + await doAttributesTest({ + viewTemplate: { + name: "action", + tag: "span", + classList: ["urlbarView-action"], + }, + viewUpdate: { + action: { + textContent: "Some action text", + }, + }, + expectedAttributes: { + "has-action": true, + }, + }); +}); + +// View templates that contain a descendant `.urlbarView-action` element should +// cause `has-action` to be set on `.urlbarView-row`. +add_task(async function hasActionDescendant() { + await doAttributesTest({ + viewTemplate: { + children: [ + { + children: [ + { + children: [ + { + name: "action", + tag: "span", + classList: ["urlbarView-action"], + }, + ], + }, + ], + }, + ], + }, + viewUpdate: { + action: { + textContent: "Some action text", + }, + }, + expectedAttributes: { + "has-action": true, + }, + }); +}); + +// View templates that contain descendant `.urlbarView-url` and +// `.urlbarView-action` elements should cause `has-url` and `has-action` to be +// set on `.urlbarView-row`. +add_task(async function hasUrlAndActionDescendant() { + await doAttributesTest({ + viewTemplate: { + children: [ + { + children: [ + { + children: [ + { + name: "url", + tag: "span", + classList: ["urlbarView-url"], + }, + ], + }, + { + name: "action", + tag: "span", + classList: ["urlbarView-action"], + }, + ], + }, + ], + }, + viewUpdate: { + url: { + textContent: "https://example.com/", + }, + action: { + textContent: "Some action text", + }, + }, + expectedAttributes: { + "has-url": true, + "has-action": true, + }, + }); +}); + +async function doAttributesTest({ + viewTemplate, + viewUpdate, + expectedAttributes, +}) { + expectedAttributes = { + "has-url": false, + "has-action": false, + ...expectedAttributes, + }; + + let provider = new TestProvider(); + provider.getViewTemplate = () => viewTemplate; + provider.getViewUpdate = () => viewUpdate; + + 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, + "Sanity check: The expected row is present" + ); + for (let [name, expected] of Object.entries(expectedAttributes)) { + Assert.equal( + row.hasAttribute(name), + expected, + "Row should have attribute as expected: " + name + ); + } + + await UrlbarTestUtils.promisePopupClose(window); + }, provider); +} + +/** + * 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(state, queryContext, details, controller) { + 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_editAndEnterWithSlowQuery.js b/browser/components/urlbar/tests/browser/browser_editAndEnterWithSlowQuery.js new file mode 100644 index 0000000000..63e799e178 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_editAndEnterWithSlowQuery.js @@ -0,0 +1,476 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test when a user enters a different URL than the result being selected. + +"use strict"; + +const ORIGINAL_CHUNK_RESULTS_DELAY = + UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS; + +add_setup(async function setup() { + let suggestionsEngine = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + "searchSuggestionEngine.xml", + }); + await SearchTestUtils.installSearchExtension( + { + name: "Test", + keyword: "@test", + }, + { setAsDefault: true } + ); + await Services.search.moveEngine(suggestionsEngine, 0); + + registerCleanupFunction(async () => { + UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS = + ORIGINAL_CHUNK_RESULTS_DELAY; + UrlbarPrefs.clear("delay"); + }); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.quickactions", false], + ["browser.urlbar.trimHttps", false], + [ + "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features", + false, + ], + ], + }); +}); + +add_task(async function test_url_type() { + const testCases = [ + { + testURL: "https://example.com/123", + displayedURL: "https://example.com/123", + trimURLs: true, + }, + { + testURL: "https://example.com/123", + displayedURL: "https://example.com/123", + trimURLs: false, + }, + { + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + testURL: "http://example.com/123", + displayedURL: "example.com/123", + trimURLs: true, + }, + { + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + testURL: "http://example.com/123", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + displayedURL: "http://example.com/123", + trimURLs: false, + }, + ]; + + for (const { testURL, displayedURL, trimURLs } of testCases) { + info("Setup: " + JSON.stringify({ testURL, displayedURL, trimURLs })); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.trimURLs", trimURLs]], + }); + await PlacesTestUtils.addVisits([testURL]); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + info("Show results"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exa", + fireInputEvent: true, + }); + + info("Find target result"); + let targetRowIndex = await findTargetRowIndex( + result => + result.type == UrlbarUtils.RESULT_TYPE.URL && result.url == testURL + ); + + info("Select a visit suggestion"); + UrlbarTestUtils.setSelectedRowIndex(window, targetRowIndex); + Assert.equal(window.gURLBar.value, displayedURL); + + info("Change the delay time to avoid updating results"); + const DELAY = 10000; + UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS = DELAY; + UrlbarPrefs.set("delay", DELAY); + + info("Edit text on the URL bar"); + window.gURLBar.setSelectionRange( + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER + ); + EventUtils.synthesizeKey("KEY_Backspace"); + Assert.ok(gURLBar.valueIsTyped); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), targetRowIndex); + let selectedResult = UrlbarTestUtils.getSelectedRow(window).result; + Assert.equal(selectedResult.type, UrlbarUtils.RESULT_TYPE.URL); + + info("Enter before updating"); + let loadingURL = testURL.substring(0, testURL.length - 1); + let onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + loadingURL + ); + EventUtils.synthesizeKey("KEY_Enter"); + await onLoad; + Assert.equal(gBrowser.currentURI.spec, loadingURL); + + info("Clean up"); + await PlacesUtils.history.clear(); + UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS = + ORIGINAL_CHUNK_RESULTS_DELAY; + UrlbarPrefs.clear("delay"); + await SpecialPowers.popPrefEnv(); + } +}); + +add_task(async function test_search_type() { + info("Show results"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "123", + fireInputEvent: true, + }); + await UrlbarTestUtils.enterSearchMode(window); + + info("Find target result"); + let targetRowIndex = await findTargetRowIndex( + result => + result.type == UrlbarUtils.RESULT_TYPE.SEARCH && + result.url == "http://mochi.test:8888/?terms=123foo" + ); + + info("Select a search suggestion"); + UrlbarTestUtils.setSelectedRowIndex(window, targetRowIndex); + Assert.equal(window.gURLBar.value, "123foo"); + + info("Change the delay time to avoid updating results"); + const DELAY = 10000; + UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS = DELAY; + UrlbarPrefs.set("delay", DELAY); + + info("Edit text on the URL bar"); + window.gURLBar.setSelectionRange( + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER + ); + EventUtils.synthesizeKey("KEY_Backspace"); + Assert.ok(gURLBar.valueIsTyped); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), targetRowIndex); + let selectedResult = UrlbarTestUtils.getSelectedRow(window).result; + Assert.equal(selectedResult.type, UrlbarUtils.RESULT_TYPE.SEARCH); + + info("Enter before updating"); + let loadingURL = "http://mochi.test:8888/?terms=123fo"; + let onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + loadingURL + ); + EventUtils.synthesizeKey("KEY_Enter"); + await onLoad; + Assert.equal(gBrowser.currentURI.spec, loadingURL); + + info("Clean up"); + await PlacesUtils.history.clear(); + UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS = ORIGINAL_CHUNK_RESULTS_DELAY; + UrlbarPrefs.clear("delay"); +}); + +add_task(async function test_keyword_type() { + info("Setup"); + await PlacesUtils.keywords.insert({ + keyword: "keyword", + url: "https://example.com/?q=%s", + }); + + info("Show results"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "keyword 123", + fireInputEvent: true, + }); + + info("Find target result"); + let targetRowIndex = await findTargetRowIndex( + result => + result.type == UrlbarUtils.RESULT_TYPE.KEYWORD && + result.url == "https://example.com/?q=123" + ); + + info("Select a search suggestion"); + UrlbarTestUtils.setSelectedRowIndex(window, targetRowIndex); + Assert.equal(window.gURLBar.value, "keyword 123"); + + info("Change the delay time to avoid updating results"); + const DELAY = 10000; + UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS = DELAY; + UrlbarPrefs.set("delay", DELAY); + + info("Edit text on the URL bar"); + window.gURLBar.setSelectionRange( + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER + ); + EventUtils.synthesizeKey("KEY_Backspace"); + Assert.ok(gURLBar.valueIsTyped); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), targetRowIndex); + let selectedResult = UrlbarTestUtils.getSelectedRow(window).result; + Assert.equal(selectedResult.type, UrlbarUtils.RESULT_TYPE.KEYWORD); + + info("Enter before updating"); + let loadingURL = "https://example.com/?q=12"; + let onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + loadingURL + ); + EventUtils.synthesizeKey("KEY_Enter"); + await onLoad; + Assert.equal(gBrowser.currentURI.spec, loadingURL); + + info("Clean up"); + await PlacesUtils.history.clear(); + await PlacesUtils.keywords.remove("keyword"); + UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS = ORIGINAL_CHUNK_RESULTS_DELAY; + UrlbarPrefs.clear("delay"); +}); + +add_task(async function test_dynamic_type() { + info("Setup"); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.unitConversion.enabled", true]], + }); + + info("Show results"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "12 cm to mm", + fireInputEvent: true, + }); + + info("Find target result"); + let targetRowIndex = await findTargetRowIndex( + result => result.type == UrlbarUtils.RESULT_TYPE.DYNAMIC + ); + + info("Select a dynamic suggestion"); + UrlbarTestUtils.setSelectedRowIndex(window, targetRowIndex); + Assert.equal(window.gURLBar.value, "12 cm to mm"); + + info("Change the delay time to avoid updating results"); + const DELAY = 10000; + UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS = DELAY; + UrlbarPrefs.set("delay", DELAY); + + info("Edit text on the URL bar"); + window.gURLBar.setSelectionRange( + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER + ); + EventUtils.synthesizeKey("KEY_Backspace"); + Assert.ok(gURLBar.valueIsTyped); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), targetRowIndex); + let selectedResult = UrlbarTestUtils.getSelectedRow(window).result; + Assert.equal(selectedResult.type, UrlbarUtils.RESULT_TYPE.DYNAMIC); + + info("Enter before updating"); + // TODO: We need to show the dynamic result with different word here. + let loadingURL = "https://example.com/?q=12+cm+to+m"; + let onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + loadingURL + ); + EventUtils.synthesizeKey("KEY_Enter"); + await onLoad; + Assert.equal(gBrowser.currentURI.spec, loadingURL); + + info("Clean up"); + await PlacesUtils.history.clear(); + await SpecialPowers.popPrefEnv(); + UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS = ORIGINAL_CHUNK_RESULTS_DELAY; + UrlbarPrefs.clear("delay"); +}); + +add_task(async function test_omnibox_type() { + info("Setup"); + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["tabs"], + omnibox: { + keyword: "omnibox", + }, + }, + background() { + /* global browser */ + browser.omnibox.setDefaultSuggestion({ + description: "doit", + }); + browser.omnibox.onInputEntered.addListener(text => { + browser.tabs.update({ url: `https://example.com/${text}` }); + }); + browser.omnibox.onInputChanged.addListener((text, suggest) => { + suggest([]); + }); + }, + }); + await extension.startup(); + + info("Show results"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "omnibox 123", + fireInputEvent: true, + }); + + info("Find target result"); + let targetRowIndex = await findTargetRowIndex( + result => result.type == UrlbarUtils.RESULT_TYPE.OMNIBOX + ); + + info("Select an omnibox suggestion"); + UrlbarTestUtils.setSelectedRowIndex(window, targetRowIndex); + Assert.equal(window.gURLBar.value, "omnibox 123"); + + info("Change the delay time to avoid updating results"); + const DELAY = 10000; + UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS = DELAY; + UrlbarPrefs.set("delay", DELAY); + + info("Edit text on the URL bar"); + window.gURLBar.setSelectionRange( + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER + ); + EventUtils.synthesizeKey("KEY_Backspace"); + Assert.ok(gURLBar.valueIsTyped); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), targetRowIndex); + let selectedResult = UrlbarTestUtils.getSelectedRow(window).result; + Assert.equal(selectedResult.type, UrlbarUtils.RESULT_TYPE.OMNIBOX); + Assert.ok(selectedResult.heuristic); + + info("Enter before updating"); + // As this result is heuristic, should pick as it is. + let loadingURL = "https://example.com/123"; + let onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + loadingURL + ); + EventUtils.synthesizeKey("KEY_Enter"); + await onLoad; + Assert.equal(gBrowser.currentURI.spec, loadingURL); + + info("Clean up"); + await PlacesUtils.history.clear(); + await extension.unload(); + UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS = ORIGINAL_CHUNK_RESULTS_DELAY; + UrlbarPrefs.clear("delay"); +}); + +add_task(async function test_heuristic() { + const testCases = [ + { + testResult: new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { url: "https://example.com/123" } + ), + loadingURL: "https://example.com/123", + displayedValue: "https://example.com/123", + }, + { + testResult: new UrlbarResult( + UrlbarUtils.RESULT_TYPE.SEARCH, + UrlbarUtils.RESULT_SOURCE.SEARCH, + { + engine: Services.search.defaultEngine.name, + query: "heuristic_search", + } + ), + loadingURL: "https://example.com/?q=heuristic_search", + displayedValue: "heuristic_search", + }, + ]; + + for (const { testResult, loadingURL, displayedValue } of testCases) { + info("Setup: " + JSON.stringify(testResult)); + testResult.heuristic = true; + let provider = new UrlbarTestUtils.TestProvider({ + results: [testResult], + name: "TestProviderHeuristic", + priority: Infinity, + }); + UrlbarProvidersManager.registerProvider(provider); + + info("Show results"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "any query", + fireInputEvent: true, + }); + + info("Select a visit suggestion"); + const targetRowIndex = 0; + UrlbarTestUtils.setSelectedRowIndex(window, targetRowIndex); + Assert.equal(window.gURLBar.value, displayedValue); + + info("Change the delay time to avoid updating results"); + const DELAY = 10000; + UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS = DELAY; + UrlbarPrefs.set("delay", DELAY); + + info("Edit text on the URL bar"); + window.gURLBar.setSelectionRange( + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER + ); + EventUtils.synthesizeKey("KEY_Backspace"); + Assert.ok(gURLBar.valueIsTyped); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), targetRowIndex); + let selectedResult = UrlbarTestUtils.getSelectedRow(window).result; + Assert.equal(selectedResult, testResult); + Assert.equal( + window.gURLBar.value, + displayedValue.substring(0, displayedValue.length - 1) + ); + + info("Enter before updating"); + let spy = sinon.spy(UrlbarUtils, "getHeuristicResultFor"); + let onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + loadingURL + ); + EventUtils.synthesizeKey("KEY_Enter"); + await onLoad; + Assert.equal(gBrowser.currentURI.spec, loadingURL); + spy.restore(); + Assert.ok(!spy.called, "getHeuristicResultFor should not be called"); + + info("Clean up"); + UrlbarProvidersManager.unregisterProvider(provider); + UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS = + ORIGINAL_CHUNK_RESULTS_DELAY; + UrlbarPrefs.clear("delay"); + } +}); + +async function findTargetRowIndex(finder) { + for ( + let i = 0, count = UrlbarTestUtils.getResultCount(window); + i < count; + i++ + ) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + if (finder(result)) { + return i; + } + } + + throw new Error("Target not found"); +} 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..b1998b6f55 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_engagement.js @@ -0,0 +1,210 @@ +/* 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 [state, queryContext, details, controller] = await startPromise; + Assert.equal( + controller.input.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()) ?? {}; + + [state, queryContext, details, controller] = await endPromise; + Assert.equal(controller.input.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..5fa301c027 --- /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 = "http://example.com/\xF7?\xF7"; +const START_VALUE = "http://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, + UrlbarTestUtils.trimURL(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, + UrlbarTestUtils.trimURL(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, + UrlbarTestUtils.trimURL(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, + 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.value, + UrlbarTestUtils.trimURL(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..b750080d41 --- /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, + UrlbarTestUtils.trimURL(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, + UrlbarTestUtils.trimURL(url), + "URL bar visible value should be correct when the page loads from about:newtab" + ); + is( + gURLBar.untrimmedValue, + url, + "URL bar value should be correct when the page loads from about:newtab" + ); + let identityBox = document.getElementById("identity-box"); + ok( + identityBox.classList.contains("verifiedDomain"), + "Identity box should know we're doing SSL when the page loads from about:newtab" + ); + is( + gURLBar.getAttribute("pageproxystate"), + "valid", + "URL bar is in valid page proxy state when SSL page with hash loads from about:newtab" + ); + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_heuristicNotAddedFirst.js b/browser/components/urlbar/tests/browser/browser_heuristicNotAddedFirst.js new file mode 100644 index 0000000000..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..5f76157f9d --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_hideHeuristic.js @@ -0,0 +1,514 @@ +/* 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], + ["dom.security.https_first_schemeless", 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..5d04f51411 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_ime_composition.js @@ -0,0 +1,328 @@ +/* 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 PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + 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..0791f9da20 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_inputHistory.js @@ -0,0 +1,676 @@ +/* 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(); +} + +async function isPageInInputHistory(url) { + let db = await PlacesUtils.promiseDBConnection(); + let rows = await db.executeCached( + `SELECT 1 + FROM moz_inputhistory i + JOIN moz_places h ON h.id = i.place_id + WHERE h.url = :url`, + { url } + ); + return rows?.length > 0; +} + +async function isInputHistoryUrlInResults(url) { + for (let i = 0; i < UrlbarTestUtils.getResultCount(window); ++i) { + const result = await UrlbarTestUtils.getRowAt(window, i).result; + if (result.providerName == "InputHistory") { + if (result.payload.url == url) { + return true; + } + } + } + return false; +} + +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); +}); + +add_task(async function test_adaptive_dismiss() { + info("Check dismissing an adaptive history result"); + + 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 = UrlbarTestUtils.getRowAt(window, 1).result; + Assert.equal(result.payload.url, url1, "Check result #1 URL"); + Assert.equal(result.providerName, "InputHistory", "Check result #1 provider"); + result = UrlbarTestUtils.getRowAt(window, 2).result; + Assert.equal(result.payload.url, url2, "Check result #2 URL"); + Assert.equal(result.providerName, "InputHistory", "Check result #2 provider"); + let waitForHistoryRemoval = + PlacesTestUtils.waitForNotification("page-removed"); + await UrlbarTestUtils.openResultMenuAndClickItem(window, "dismiss", { + resultIndex: 1, + }); + await waitForHistoryRemoval; + Assert.ok( + gURLBar.view.isOpen, + "The view should remain open after clicking the command" + ); + + Assert.ok( + !(await isInputHistoryUrlInResults(url1)), + "Check result has been removed" + ); + Assert.strictEqual( + await PlacesUtils.history.fetch(url1), + null, + "The removed page should not be in browsing history" + ); + Assert.ok( + !(await isPageInInputHistory(url1)), + "The removed page should not be in input history" + ); + + Assert.ok( + await isInputHistoryUrlInResults(url2), + "Check result has been retained" + ); + Assert.notStrictEqual( + await PlacesUtils.history.fetch(url2), + null, + "The non removed page should still be in history" + ); + Assert.ok( + await isPageInInputHistory(url2), + "The non removed page should still be in input history" + ); +}); + +add_task(async function test_bookmarked_adaptive_dismiss() { + info("Check dismissing a bookmarked adaptive history result"); + + let url = "http://mysite.tld/"; + + info("Sanity check: adaptive history is shown for a normal search."); + await PlacesUtils.history.clear(); + await bumpScore(url, "site", { visits: 3, picks: 3 }, true); + await PlacesUtils.bookmarks.insert({ + url, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "si", + }); + let result = UrlbarTestUtils.getRowAt(window, 1).result; + Assert.equal(result.payload.url, url, "Check result #1 URL"); + Assert.equal(result.providerName, "InputHistory", "Check result #1 provider"); + + let waitForHistoryRemoval = + PlacesTestUtils.waitForNotification("page-removed"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_Delete", { shiftKey: true }); + await waitForHistoryRemoval; + Assert.ok( + gURLBar.view.isOpen, + "The view should remain open after removing history" + ); + + Assert.ok( + !(await isInputHistoryUrlInResults(url)), + "Check result has been removed" + ); + Assert.ok( + !(await PlacesUtils.history.hasVisits(url)), + "The removed page should not be in browsing history" + ); + Assert.ok( + !(await isPageInInputHistory(url)), + "The removed page should be in input history" + ); +}); 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..5c8ad73491 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_inputHistory_autofill.js @@ -0,0 +1,210 @@ +/* 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 PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + 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 PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + 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 PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + 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..421c01fb69 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_inputHistory_emptystring.js @@ -0,0 +1,97 @@ +/* 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.startLoadingURIString( + 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..6ad6ce43e6 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_keepStateAcrossTabSwitches.js @@ -0,0 +1,235 @@ +/* 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() { + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_first_schemeless", false]], + }); + let input = "http://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, UrlbarTestUtils.trimURL(input), "Text is still in URL bar"); + await BrowserTestUtils.switchTab(gBrowser, tab.previousElementSibling); + await BrowserTestUtils.switchTab(gBrowser, tab); + is( + gURLBar.value, + UrlbarTestUtils.trimURL(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. Use a test-url that uses a protocol that + // is not trimmed. + const webpageTabURL = + UrlbarTestUtils.getTrimmedProtocolWithSlashes() == "https://" + ? "http://example.com" + : "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..04568cc1b5 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_keyword.js @@ -0,0 +1,234 @@ +/* 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, + "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.KEYWORD, + "Result should be a keyword" + ); + Assert.equal(result.keyword, "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.KEYWORD, + "Result should be a keyword" + ); + Assert.equal(result.keyword, "?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"); +}); 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..cd00646cbd --- /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 = Promise.withResolvers(); + 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.startLoadingURIString(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..670b9741f4 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_locationBarCommand.js @@ -0,0 +1,352 @@ +/* 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, or clicking the Go button, with various key + * combinations in the urlbar. + */ + +const TEST_VALUE = "http://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 typeAndCommand("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 = TEST_VALUE + "/"; + let newWindowPromise = BrowserTestUtils.waitForNewWindow({ + url: destinationURL, + }); + await typeAndCommand("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, + UrlbarTestUtils.trimURL(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 typeAndCommand("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 typeAndCommand("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, + UrlbarTestUtils.trimURL(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 typeAndCommand(type, details); + await loadStartedPromise; + + info("URL should be loaded in the current tab"); + is( + gURLBar.value, + UrlbarTestUtils.trimURL(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 typeAndCommand(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, + UrlbarTestUtils.trimURL(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(); + } +}); + +add_task(async function go_button_after_tab_switch() { + // Add a new tab. + let tab = await promiseOpenNewTab(); + + await UrlbarTestUtils.inputIntoURLBar(window, TEST_VALUE); + await BrowserTestUtils.switchTab(gBrowser, gBrowser.visibleTabs[0]); + isnot( + gURLBar.value, + UrlbarTestUtils.trimURL(TEST_VALUE), + "Urlbar does not have the entered value after switching to a different tab" + ); + await BrowserTestUtils.switchTab(gBrowser, tab); + is( + gURLBar.value, + UrlbarTestUtils.trimURL(TEST_VALUE), + "Urlbar still has the entered value restored after switching back to the new tab" + ); + + // Trigger a load and check it occurs in the current tab. + let loadStartedPromise = promiseLoadStarted(); + await triggerCommand("click"); + await loadStartedPromise; + + info("URL should be loaded in the current tab"); + is( + gURLBar.value, + UrlbarTestUtils.trimURL(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(); +}); + +async function typeAndCommand(eventType, details = {}) { + await UrlbarTestUtils.inputIntoURLBar(window, TEST_VALUE); + await triggerCommand(eventType, details); +} + +async function triggerCommand(eventType, details = {}) { + Assert.equal( + await UrlbarTestUtils.promiseUserContextId(window), + gBrowser.selectedTab.getAttribute("usercontextid"), + "userContextId must be the same as the originating tab" + ); + + switch (eventType) { + case "click": + ok( + gURLBar.hasAttribute("usertyping"), + "usertyping attribute must be set for the go button to be visible" + ); + EventUtils.synthesizeMouseAtCenter(gURLBar.goButton, details); + break; + case "keypress": + EventUtils.synthesizeKey("KEY_Enter", details); + break; + default: + 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 = BrowserTestUtils.switchTab(gBrowser, 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..b2d567cff4 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_middleClick.js @@ -0,0 +1,279 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test for middle click behavior. + */ + +add_setup(async () => { + 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: UrlbarTestUtils.trimURL("https://example.com"), + }, + { + isMiddleMousePastePrefOn: true, + isLoadInBackground: false, + startPagePref: "about:home", + expectedURLBarFocus: true, + expectedURLBarValue: "", + }, + { + isMiddleMousePastePrefOn: true, + isLoadInBackground: false, + startPagePref: "https://example.com", + expectedURLBarFocus: false, + expectedURLBarValue: UrlbarTestUtils.trimURL("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); +}); + +add_task(async function test_middleClickOnComponentNotHandlingPasteEvent() { + Services.prefs.setBoolPref("middlemouse.paste", true); + + info("Set initial value"); + SpecialPowers.clipboardCopyString("test\nsample"); + gURLBar.value = ""; + gURLBar.focus(); + + info("Middle click on a component that does not handle paste event"); + const allTabsButton = document.getElementById("alltabs-button"); + const onMiddleClick = new Promise(r => + allTabsButton.addEventListener("auxclick", r, { once: true }) + ); + let pastedOnURLBar = false; + gURLBar.addEventListener("paste", () => { + pastedOnURLBar = true; + }); + EventUtils.synthesizeMouseAtCenter(allTabsButton, { button: 1 }); + await onMiddleClick; + + Assert.equal(gURLBar.value, "", "URLBar has no pasted value"); + Assert.ok(!pastedOnURLBar, "URLBar should not receive paste event"); +}); + +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_move_tab_to_new_window.js b/browser/components/urlbar/tests/browser/browser_move_tab_to_new_window.js new file mode 100644 index 0000000000..3dfaedec81 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_move_tab_to_new_window.js @@ -0,0 +1,120 @@ +/* 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 ensure that if the urlbar has a user typed value and the user + moves the tab into a new window, the user typed value moves with it. +*/ + +add_setup(async function () { + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); + await PlacesTestUtils.addVisits(["https://example.com/"]); + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + }); +}); + +async function moveTabIntoNewWindowAndBack(url = "about:blank") { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + + info("Replace urlbar value with a user typed value."); + gURLBar.value = "hello world"; + UrlbarTestUtils.fireInputEvent(window); + Assert.equal( + gBrowser.userTypedValue, + "hello world", + "The user typed value should be replaced with hello world." + ); + + info("Move the tab into its own window."); + let newWindow = gBrowser.replaceTabWithWindow(tab); + let swapDocShellPromise = BrowserTestUtils.waitForEvent( + tab.linkedBrowser, + "SwapDocShells" + ); + await swapDocShellPromise; + Assert.equal( + newWindow.gURLBar.value, + "hello world", + "The value of the urlbar should have been moved." + ); + + info("Return that tab back to its original window and select it."); + tab = newWindow.gBrowser.selectedTab; + swapDocShellPromise = BrowserTestUtils.waitForEvent( + tab.linkedBrowser, + "SwapDocShells" + ); + gBrowser.adoptTab(newWindow.gBrowser.selectedTab, 1, true); + await swapDocShellPromise; + Assert.equal( + gURLBar.value, + "hello world", + "The value of the urlbar should have been moved." + ); + + // Clean up. + await BrowserTestUtils.removeTab(gBrowser.selectedTab); +} + +add_task(async function move_newtab_with_value() { + info("Open a new tab."); + await moveTabIntoNewWindowAndBack(); +}); + +add_task(async function move_loaded_page_with_value() { + info("Open a new tab and load a URL."); + await moveTabIntoNewWindowAndBack("https://www.example.com/"); +}); + +add_task(async function move_tab_into_new_window_and_open_new_tab() { + info("Open a new tab."); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + info("Move the new tab into a new window."); + let swapDocShellPromise = BrowserTestUtils.waitForEvent( + tab.linkedBrowser, + "SwapDocShells" + ); + let newWindow = gBrowser.replaceTabWithWindow(tab); + await swapDocShellPromise; + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + info("Type in the urlbar to open it and see an autofill suggestion."); + await UrlbarTestUtils.promisePopupOpen(newWindow, async () => { + newWindow.gURLBar.focus(); + EventUtils.synthesizeKey("ex", {}, newWindow); + }); + + let details = await UrlbarTestUtils.getDetailsOfResultAt(newWindow, 0); + Assert.equal(details.autofill, true, "Heuristic result should be Autofill."); + Assert.equal( + details.result.autofill.value, + "example.com/", + "Autofill value is as expected." + ); + + info("Open an about:newtab page while address bar is focused."); + let tab2 = await BrowserTestUtils.openNewForegroundTab( + newWindow.gBrowser, + "about:newtab", + false + ); + + // To be certain autoOpen isn't triggered, wait a brief amount of time + // following the tab switch event. + /* eslint-disable mozilla/no-arbitrary-setTimeout */ + await new Promise(resolve => setTimeout(resolve, 100)); + + Assert.equal(newWindow.gURLBar.value, "", "Urlbar should be empty."); + Assert.equal( + newWindow.gURLBar.view.isOpen, + false, + "Urlbar view should be closed." + ); + + await BrowserTestUtils.removeTab(tab2); + 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_observers_for_strip_on_share.js b/browser/components/urlbar/tests/browser/browser_observers_for_strip_on_share.js new file mode 100644 index 0000000000..02b404926b --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_observers_for_strip_on_share.js @@ -0,0 +1,81 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let listService; + +const QPS_PREF = "privacy.query_stripping.enabled"; +const STRIP_ON_SHARE_PREF = "privacy.query_stripping.strip_on_share.enabled"; + +// Tests for the observers for both QPS and Strip on Share +add_setup(async function () { + // 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(); +}); + +// Test if Strip on share observers are registered/unregistered depending if the +// Strip on Share Pref is enabled/disabled regardless of the state of QPS Pref +add_task( + async function checkStripOnShareObserversForVaryingStatesOfQPSAndStripOnShare() { + for (let queryStrippingEnabled of [false, true]) { + for (let stripOnShareEnabled of [false, true]) { + await SpecialPowers.pushPrefEnv({ + set: [ + [QPS_PREF, queryStrippingEnabled], + [STRIP_ON_SHARE_PREF, stripOnShareEnabled], + ], + }); + + let areObserservesRegistered; + await BrowserTestUtils.waitForCondition(function () { + areObserservesRegistered = listService.testHasStripOnShareObservers(); + return areObserservesRegistered == stripOnShareEnabled; + }, "waiting for init of URLQueryStrippingListService ensuring observers have time to register if they need"); + + if (!stripOnShareEnabled) { + Assert.ok(!areObserservesRegistered, "Observers are unregistered"); + } else { + Assert.ok(areObserservesRegistered, "Observers are registered"); + } + + await SpecialPowers.popPrefEnv(); + } + } + } +); + +// Test if QPS observers are registered/unregistered depending if the QPS +// Pref is enabled/disabled regardless of the state of Strip on Share Pref +add_task( + async function checkQPSObserversForVaryingStatesOfQPSAndStripOnShare() { + for (let queryStrippingEnabled of [false, true]) { + for (let stripOnShareEnabled of [false, true]) { + await SpecialPowers.pushPrefEnv({ + set: [ + [QPS_PREF, queryStrippingEnabled], + [STRIP_ON_SHARE_PREF, stripOnShareEnabled], + ], + }); + + let areObserservesRegistered; + await BrowserTestUtils.waitForCondition(function () { + areObserservesRegistered = listService.testHasQPSObservers(); + return areObserservesRegistered == queryStrippingEnabled; + }, "waiting for init of URLQueryStrippingListService ensuring observers have time to register if they need"); + + if (!queryStrippingEnabled) { + Assert.ok(!areObserservesRegistered, "Observers are unregistered"); + } else { + Assert.ok(areObserservesRegistered, "Observers are registered"); + } + + await SpecialPowers.popPrefEnv(); + } + } + } +); 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..0c04f1e321 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_oneOffs.js @@ -0,0 +1,999 @@ +/* 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; + +ChromeUtils.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 = UrlbarTestUtils.trimURL( + "http://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, + UrlbarTestUtils.trimURL( + "http://example.com/browser_urlbarOneOffs.js/?" + (gMaxResults - i - 1) + ) + ); + Assert.ok( + !BrowserTestUtils.isVisible(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.isVisible(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.isVisible(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.isVisible(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, + UrlbarTestUtils.trimURL( + "http://example.com/browser_urlbarOneOffs.js/?" + (gMaxResults - i - 1) + ) + ); + Assert.ok( + !BrowserTestUtils.isVisible(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.isVisible(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: [ + ...UrlbarUtils.LOCAL_SEARCH_MODES.map(m => [ + `browser.urlbar.${m.pref}`, + false, + ]), + ], + }); + engines.forEach(e => { + e.hideOneOffButton = e.name !== defaultEngine.name; + }); + + 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(); + engines.forEach(e => { + e.hideOneOffButton = false; + }); +}); + +// 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: [ + ...UrlbarUtils.LOCAL_SEARCH_MODES.map(m => [ + `browser.urlbar.${m.pref}`, + false, + ]), + ], + }); + engines.forEach(e => { + e.hideOneOffButton = true; + }); + 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(); + engines.forEach(e => { + e.hideOneOffButton = false; + }); +}); + +// 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(); + + engines.forEach(e => { + e.hideOneOffButton = true; + }); + + 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(); + engines.forEach(e => { + e.hideOneOffButton = false; + }); +}); + +/** + * 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..4ae083c51f --- /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; + +ChromeUtils.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..8f7f058dd8 --- /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"; + +ChromeUtils.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.isVisible(resultDetails.element.separator), + !!actionText, + "The title separator is " + (actionText ? "visible" : "hidden") + ); + Assert.equal( + BrowserTestUtils.isVisible(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.isVisible(resultDetails.element.separator), + "The restyled result's title separator should be visible" + ); + Assert.ok( + BrowserTestUtils.isVisible(resultDetails.element.action), + "The restyled result's action text should be visible" + ); + + if (engine) { + Assert.equal( + resultDetails.image, + engine.getIconURL() || 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..375dd6e9ae --- /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"; + +ChromeUtils.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..3d68b08f73 --- /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.isVisible(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.isVisible(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..3e7732e158 --- /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.ok(gURLBar.hasAttribute("usertyping")); + Assert.ok(BrowserTestUtils.isVisible(gURLBar.goButton)); + } else { + Assert.ok(!gURLBar.hasAttribute("usertyping")); + Assert.ok(BrowserTestUtils.isHidden(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..09d94f79e7 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_paste_then_switch_tab.js @@ -0,0 +1,74 @@ +/* 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: UrlbarTestUtils.trimURL("https://example.com"), + }, + { + input: "http:\n//\nexample.\ncom", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + expected: UrlbarTestUtils.trimURL("http://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..e096c6fdf6 --- /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"); + await 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..96c43326a9 --- /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, + UrlbarTestUtils.trimURL(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..2f8e871bfe --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_primary_selection_safe_on_new_tab.js @@ -0,0 +1,70 @@ +/* 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.selectionStart, 0); + Assert.equal(gURLBar.selectionEnd, gURLBar.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..88409e253d --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_queryContextCache.js @@ -0,0 +1,490 @@ +/* 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; + + let cachedContext = getContext(); + Assert.equal( + !!cachedContext, + cached, + "Context is present or not in cache as expected for search string: " + + JSON.stringify(searchString) + ); + // Our payload schema validator allows for explicit undefined properties, + // thus we must transform them for stringify. + Assert.deepEqual( + cachedContext, + JSON.parse(JSON.stringify(cachedContext, (k, v) => v ?? null)), + "The query context should be made of serializable properties" + ); + + // 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..ccf045d9e8 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_quickactions.js @@ -0,0 +1,737 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test QuickActions. + */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + AppConstants: "resource://gre/modules/AppConstants.sys.mjs", + UpdateService: "resource://gre/modules/UpdateService.sys.mjs", + UrlbarProviderQuickActions: + "resource:///modules/UrlbarProviderQuickActions.sys.mjs", +}); + +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", + }); + + await clickQuickActionOneoffButton(); + + 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.startLoadingURIString( + 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-button" + ); + 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"); +}); + +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], + ["screenshots.browser.component.enabled", true], + ], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "screenshot", + }); + Assert.ok( + !window.document.querySelector( + ".urlbarView-row[dynamicType=quickactions] .urlbarView-quickaction-button" + ), + "Screenshot button is not suggested" + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "> screenshot", + }); + Assert.ok( + window.document.querySelector( + ".urlbarView-row[dynamicType=quickactions] .urlbarView-quickaction-button" + ), + "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-button" + ), + "Screenshot button is not suggested" + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "> screenshot", + }); + Assert.ok( + !window.document.querySelector( + ".urlbarView-row[dynamicType=quickactions] .urlbarView-quickaction-button" + ), + "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.startLoadingURIString( + 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.startLoadingURIString( + 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.startLoadingURIString( + 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.startLoadingURIString( + 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.startLoadingURIString( + 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() { + let useOldClearHistoryDialog = Services.prefs.getBoolPref( + "privacy.sanitize.useOldClearHistoryDialog" + ); + let dialogURL = useOldClearHistoryDialog + ? "chrome://browser/content/sanitize.xhtml" + : "chrome://browser/content/sanitize_v2.xhtml"; + await doAlertDialogTest({ + input: "clear", + dialogContentURI: dialogURL, + }); +}); + +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-button" + ).length; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: " ", + }); + const countForWhitespace = window.document.querySelectorAll( + ".urlbarView-quickaction-button" + ).length; + Assert.equal( + countForEmpty, + countForWhitespace, + "Count of quick actions of empty and whitespace are same" + ); + await SpecialPowers.popPrefEnv(); +}); + +async function clickQuickActionOneoffButton() { + const 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", + }); +} 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..1e1e92fb31 --- /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.startLoadingURIString(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 = 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_screenshot.js b/browser/components/urlbar/tests/browser/browser_quickactions_screenshot.js new file mode 100644 index 0000000000..c81442f0f5 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_quickactions_screenshot.js @@ -0,0 +1,170 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * QuickActions tests that touch screenshot functionality. + */ + +"use strict"; + +requestLongerTimeout(3); + +const DUMMY_PAGE = + "https://example.com/browser/browser/base/content/test/general/dummy_page.html"; + +async function isScreenshotInitialized() { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + let screenshotsChild = content.windowGlobalChild.getActor( + "ScreenshotsComponent" + ); + return screenshotsChild?.overlay?.initialized; + }); +} + +async function clickQuickActionOneoffButton() { + const 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", + }); +} + +add_setup(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.quickactions.enabled", true], + ["browser.urlbar.suggest.quickactions", true], + ["browser.urlbar.shortcuts.quickactions", true], + ], + }); +}); + +add_task(async function test_screenshot() { + await SpecialPowers.pushPrefEnv({ + set: [["screenshots.browser.component.enabled", true]], + }); + + BrowserTestUtils.startLoadingURIString(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 search_mode_on_webpage() { + const tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "https://example.com" + ); + + info("Show result by click"); + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}, window); + }); + await UrlbarTestUtils.promiseSearchComplete(window); + + info("Enter quick action search mode"); + await clickQuickActionOneoffButton(); + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0); + Assert.ok(true, "Actions are shown when we enter actions search mode."); + + info("Trigger the screenshot mode"); + const initialActionButtons = window.document.querySelectorAll( + ".urlbarView-row[dynamicType=quickactions] .urlbarView-quickaction-button" + ); + let screenshotButton; + for (let i = 0; i < initialActionButtons.length; i++) { + const item = initialActionButtons.item(i); + if (item.dataset.key === "screenshot") { + screenshotButton = item; + break; + } + } + EventUtils.synthesizeMouseAtCenter(screenshotButton, {}, 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" + ); + + info("Check the urlbar state"); + Assert.equal(gURLBar.value, UrlbarTestUtils.trimURL("https://example.com")); + Assert.equal(gURLBar.getAttribute("pageproxystate"), "valid"); + + info("Show result again"); + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}, window); + }); + await UrlbarTestUtils.promiseSearchComplete(window); + + info("Enter quick action search mode again"); + await clickQuickActionOneoffButton(); + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0); + const finalActionButtons = window.document.querySelectorAll( + ".urlbarView-row[dynamicType=quickactions] .urlbarView-quickaction-button" + ); + + info("Check the action buttons and the urlbar"); + Assert.equal( + finalActionButtons.length, + initialActionButtons.length, + "The same buttons as initially displayed will display" + ); + Assert.equal(gURLBar.value, ""); + + info("Clean up"); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); + EventUtils.synthesizeKey("KEY_Escape"); + BrowserTestUtils.removeTab(tab); + await PlacesUtils.history.clear(); +}); 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..abac861931 --- /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.startLoadingURIString(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_recentsearches.js b/browser/components/urlbar/tests/browser/browser_recentsearches.js new file mode 100644 index 0000000000..e0ba5f684f --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_recentsearches.js @@ -0,0 +1,138 @@ +/* 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 CONFIG_DEFAULT = [ + { + webExtension: { id: "basic@search.mozilla.org" }, + appliesTo: [{ included: { everywhere: true } }], + default: "yes", + }, +]; + +const TOP_SITES = [ + "https://example-1.com/", + "https://example-2.com/", + "https://example-3.com/", +]; + +SearchTestUtils.init(this); + +add_setup(async () => { + // Use engines in test directory + let searchExtensions = getChromeDir(getResolvedURI(gTestPath)); + searchExtensions.append("search-engines"); + await SearchTestUtils.useMochitestEngines(searchExtensions); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.searches", true], + ["browser.urlbar.suggest.recentsearches", true], + ["browser.urlbar.recentsearches.featureGate", true], + // Disable UrlbarProviderSearchTips + [ + "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features", + false, + ], + ], + }); + + SearchTestUtils.useMockIdleService(); + await SearchTestUtils.updateRemoteSettingsConfig(CONFIG_DEFAULT); + Services.telemetry.clearScalars(); + + registerCleanupFunction(async () => { + let settingsWritten = SearchTestUtils.promiseSearchNotification( + "write-settings-to-disk-complete" + ); + await SearchTestUtils.updateRemoteSettingsConfig(); + await settingsWritten; + await UrlbarTestUtils.formHistory.clear(); + }); +}); + +add_task(async () => { + let tab = await BrowserTestUtils.openNewForegroundTab( + window.gBrowser, + "data:text/html," + ); + + info("Perform a search that will be added to search history."); + let browserLoaded = BrowserTestUtils.browserLoaded( + window.gBrowser.selectedBrowser + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "Bob Vylan", + }); + + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Enter", {}, window); + }); + await browserLoaded; + + info("Now check that is shown in search history."); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 1, + "Previous search shown" + ); + let { result } = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal(result.providerName, "RecentSearches"); + + info("Selecting the recent search should be indicated in telemetry."); + browserLoaded = BrowserTestUtils.browserLoaded( + window.gBrowser.selectedBrowser + ); + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, window); + EventUtils.synthesizeKey("KEY_Enter", {}, window); + }); + await browserLoaded; + + let scalars = TelemetryTestUtils.getProcessScalars("parent", true, true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "urlbar.picked.recent_search", + 0, + 1 + ); + await BrowserTestUtils.removeTab(tab); +}); + +// Ensure that top sites are shown above recent searches, even if trending +// suggestions are disabled. +add_task(async () => { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.trending", false], + ["browser.urlbar.suggest.topsites", true], + ["browser.newtabpage.activity-stream.default.sites", TOP_SITES.join(",")], + ], + }); + await updateTopSites(sites => sites && sites.length); + + let tab = await BrowserTestUtils.openNewForegroundTab( + window.gBrowser, + "data:text/html," + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + + let count = UrlbarTestUtils.getResultCount(window); + let { result } = await UrlbarTestUtils.getDetailsOfResultAt( + window, + count - 1 + ); + Assert.equal(result.providerName, "RecentSearches"); + + await BrowserTestUtils.removeTab(tab); +}); 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..ae8dec3da6 --- /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, + UrlbarTestUtils.trimURL(currentURI), + "The URL bar shows the content URI. aIsSelectedTab:" + aIsSelectedTab + ); + + if (!aIsSelectedTab) { + // If this was a background request, go on a foreground request. + BrowserTestUtils.startLoadingURIString( + 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..d4d64f81cb --- /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.startLoadingURIString(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..b9e97044e4 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_remove_match.js @@ -0,0 +1,218 @@ +/* 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(); +}); 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..ccbe247598 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_result_menu.js @@ -0,0 +1,260 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +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 = (state, queryContext, details, controller) => { + onEngagementCallCount++; + 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_menu_general.js b/browser/components/urlbar/tests/browser/browser_result_menu_general.js new file mode 100644 index 0000000000..ece48de20a --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_result_menu_general.js @@ -0,0 +1,416 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// General tests for the result menu that aren't related to specific result +// types. + +"use strict"; + +const MAX_RESULTS = UrlbarPrefs.get("maxRichResults"); +const RESULT_URL = "https://example.com/test"; +const RESULT_HELP_URL = "https://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("https://example.com/" + i); + } + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + }); +}); + +// Sets `helpUrl` on a result payload and makes sure the result menu ends up +// with a help command. +add_task(async function help() { + let provider = registerTestProvider(1); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "example", + window, + }); + + await assertIsTestResult(1); + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + let menuButton = result.element.row._buttons.get("menu"); + Assert.ok(menuButton, "Sanity check: menu button should exist"); + + let menuitem = await UrlbarTestUtils.openResultMenuAndGetItem({ + window, + command: "help", + resultIndex: 1, + openByMouse: true, + }); + Assert.ok(menuitem, "Help menu item should exist"); + + let l10nAttrs = document.l10n.getAttributes(menuitem); + Assert.deepEqual( + l10nAttrs, + { id: "urlbar-result-menu-tip-get-help", args: null }, + "The l10n ID attribute was correctly set" + ); + + // The result menu needs to be closed before calling + // `openResultMenuAndClickItem()` below; otherwise it will wait on a + // `popupshown` event that will never come. + gURLBar.view.resultMenu.hidePopup(true); + + // We assume clicking "help" will load a page in a new tab. + let loadPromise = BrowserTestUtils.waitForNewTab(gBrowser); + + await UrlbarTestUtils.openResultMenuAndClickItem(window, "help", { + resultIndex: 1, + openByMouse: true, + }); + + info("Waiting for load"); + await loadPromise; + await TestUtils.waitForTick(); + Assert.equal( + gBrowser.currentURI.spec, + RESULT_HELP_URL, + "The load URL should be the help URL" + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + UrlbarProvidersManager.unregisterProvider(provider); +}); + +// (SHIFT+)TABs through a result with a menu 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 menu 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 = MAX_RESULTS * 2 - 2; + + // Arrow down to the main part of the result. + EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: MAX_RESULTS - 1 }); + assertMainPartSelected(numSelectable - 1); + + // TAB to the menu 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.isVisible( + 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 menu 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 - 3, "previous result"); + + await UrlbarTestUtils.promisePopupClose(window); + UrlbarProvidersManager.unregisterProvider(provider); +}); + +// Picks the main part of the test result with the keyboard. +add_task(async function pick_mainPart_keyboard() { + await doPickTest({ pickHelp: false, useKeyboard: true }); +}); + +// Picks the help command with the keyboard. +add_task(async function pick_help_keyboard() { + await doPickTest({ pickHelp: true, useKeyboard: true }); +}); + +// Picks the main part of the test result with the mouse. +add_task(async function pick_mainPart_mouse() { + await doPickTest({ pickHelp: false, useKeyboard: false }); +}); + +// Picks the help command with the mouse. +add_task(async function pick_help_mouse() { + await doPickTest({ pickHelp: true, useKeyboard: false }); +}); + +async function doPickTest({ pickHelp, 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(index * 2 - 1); + } + + // Pick the result. The appropriate URL should load. + let loadPromise = pickHelp + ? BrowserTestUtils.waitForNewTab(gBrowser) + : BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + await Promise.all([ + loadPromise, + UrlbarTestUtils.promisePopupClose(window, async () => { + if (pickHelp) { + await UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "h", { + openByMouse: !useKeyboard, + resultIndex: index, + }); + } else if (useKeyboard) { + EventUtils.synthesizeKey("KEY_Enter"); + } else { + let result = await UrlbarTestUtils.getDetailsOfResultAt( + window, + index + ); + EventUtils.synthesizeMouseAtCenter(result.element.row._content, {}); + } + }), + ]); + Assert.equal( + gBrowser.selectedBrowser.currentURI.spec, + pickHelp ? RESULT_HELP_URL : RESULT_URL, + "Expected URL should have loaded" + ); + + if (pickHelp) { + 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 URL. + * + * @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: "urlbar-result-menu-tip-get-help", + }, + } + ), + { 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 menu + * 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; + Assert.ok(row._buttons.get("menu"), "The result should have a menu button"); + 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 result 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 menu button is selected. + * + * @param {number} expectedSelectedElementIndex + * The expected selected element index. + */ +function assertButtonSelected(expectedSelectedElementIndex) { + assertSelection( + expectedSelectedElementIndex, + "urlbarView-button-menu", + "menu 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_result_onSelection.js b/browser/components/urlbar/tests/browser/browser_result_onSelection.js new file mode 100644 index 0000000000..2a5f8c3760 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_result_onSelection.js @@ -0,0 +1,73 @@ +/* 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", + helpUrl: "http://example.com/", + isBlockable: true, + blockL10n: { id: "urlbar-result-menu-remove-from-history" }, + } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { + url: "http://mozilla.org/2", + helpUrl: "http://example.com/", + isBlockable: true, + blockL10n: { id: "urlbar-result-menu-remove-from-history" }, + } + ), + 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: 5, + }); + EventUtils.synthesizeKey("KEY_ArrowDown"); + ok( + UrlbarTestUtils.getOneOffSearchButtons(window).selectedButton, + "a one off button is selected" + ); + + Assert.equal(selectionCount, 6, "Number of elements selected in the view."); + UrlbarProvidersManager.unregisterProvider(provider); +}); diff --git a/browser/components/urlbar/tests/browser/browser_results_format_displayValue.js b/browser/components/urlbar/tests/browser/browser_results_format_displayValue.js new file mode 100644 index 0000000000..d0ec3d3818 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_results_format_displayValue.js @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_receive_punycode_result() { + let url = "https://www.اختبار.اختبار.org:5000/"; + + // eslint-disable-next-line jsdoc/require-jsdoc + class ResultWithHighlightsProvider extends UrlbarTestUtils.TestProvider { + startQuery(context, addCallback) { + let result = Object.assign( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + ...UrlbarResult.payloadAndSimpleHighlights(context.tokens, { + url: [url, UrlbarUtils.HIGHLIGHT.TYPED], + }) + ), + { suggestedIndex: 0 } + ); + addCallback(this, result); + } + + getViewUpdate(result, idsByName) { + return {}; + } + } + let provider = new ResultWithHighlightsProvider(); + + registerCleanupFunction(async () => { + UrlbarProvidersManager.unregisterProvider(provider); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + gURLBar.handleRevert(); + }); + UrlbarProvidersManager.registerProvider(provider); + + info("Open the result popup"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "org", + window, + fireInputEvent: true, + }); + let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0); + is(row.result.type, UrlbarUtils.RESULT_TYPE.URL, "row.result.type"); + is( + row.result.payload.displayUrl, + "اختبار.اختبار.org:5000", + "Result is trimmed and formatted correctly." + ); + is( + row.result.payload.title, + "www.اختبار.اختبار.org:5000", + "Result is trimmed and formatted correctly." + ); + + let firstRow = document.querySelector(".urlbarView-row"); + let firstRowUrl = firstRow.querySelector(".urlbarView-url"); + + is( + firstRowUrl.innerHTML.charAt(0), + "\u200e", + "UrlbarView row url contains LRM" + ); + // Tests if highlights are correct after inserting lrm symbol + is( + firstRowUrl.querySelector("strong")?.innerText, + "org", + "Correct part of url is highlighted" + ); + is( + firstRow.querySelector(".urlbarView-title strong")?.innerText, + "org", + "Correct part of title is highlighted" + ); +}); 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..3cc26a5757 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_retainedResultsOnFocus.js @@ -0,0 +1,438 @@ +/* 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.startLoadingURIString( + 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..96c9b7212f --- /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 PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + 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..c979e86235 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_heuristic.js @@ -0,0 +1,219 @@ +/* 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: "https://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 promise = waitForLoadStartOrTimeout(); + EventUtils.synthesizeKey("KEY_Enter"); + await Assert.rejects(promise, /timed out/, "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, + "https://example.com/bookmark", + "Result URL is our bookmark URL" + ); + Assert.ok(!result.heuristic, "Result should not be heuristic"); + + // Press enter. Nothing should happen. + let promise = waitForLoadStartOrTimeout(); + EventUtils.synthesizeKey("KEY_Enter"); + await Assert.rejects(promise, /timed out/, "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, + "https://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, + "https://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, + "https://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..707a4ea38e --- /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, + UrlbarTestUtils.trimURL("http://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..214448ee61 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_indicator_clickthrough.js @@ -0,0 +1,106 @@ +/* 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.isVisible(indicator)); + const indicatorCloseButton = document.getElementById( + "urlbar-search-mode-indicator-close" + ); + Assert.ok(!BrowserTestUtils.isVisible(indicatorCloseButton)); + const labelBox = document.getElementById("urlbar-label-box"); + Assert.ok(!BrowserTestUtils.isVisible(labelBox)); + + await UrlbarTestUtils.enterSearchMode(window); + Assert.ok(BrowserTestUtils.isVisible(indicator)); + Assert.ok(BrowserTestUtils.isVisible(indicatorCloseButton)); + Assert.ok(!BrowserTestUtils.isVisible(labelBox)); + + info("Blur the urlbar"); + gURLBar.blur(); + Assert.ok(BrowserTestUtils.isVisible(indicator)); + Assert.ok(BrowserTestUtils.isVisible(indicatorCloseButton)); + Assert.ok(!BrowserTestUtils.isVisible(labelBox)); + Assert.notEqual( + document.activeElement, + gURLBar.inputField, + "URL Bar should not be focused" + ); + + info("Focus the urlbar clicking on the indicator"); + // We intentionally turn off a11y_checks for the following click, because + // it is send to send a focus on the URL Bar with the mouse, while other + // ways to focus it are accessible for users of assistive technology and + // keyboards, thus this test can be excluded from the accessibility tests. + AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false }); + EventUtils.synthesizeMouseAtCenter(indicator, {}); + AccessibilityUtils.resetEnv(); + Assert.ok(BrowserTestUtils.isVisible(indicator)); + Assert.ok(BrowserTestUtils.isVisible(indicatorCloseButton)); + Assert.ok(!BrowserTestUtils.isVisible(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.isVisible(indicator)); + Assert.ok(!BrowserTestUtils.isVisible(indicatorCloseButton)); + Assert.ok(!BrowserTestUtils.isVisible(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.isVisible(indicator)); + const indicatorCloseButton = document.getElementById( + "urlbar-search-mode-indicator-close" + ); + Assert.ok(!BrowserTestUtils.isVisible(indicatorCloseButton)); + + await UrlbarTestUtils.enterSearchMode(window); + Assert.ok(BrowserTestUtils.isVisible(indicator)); + Assert.ok(BrowserTestUtils.isVisible(indicatorCloseButton)); + + info("Blur the urlbar"); + gURLBar.blur(); + Assert.ok(BrowserTestUtils.isVisible(indicator)); + Assert.ok(BrowserTestUtils.isVisible(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.isVisible(indicator)); + Assert.ok(!BrowserTestUtils.isVisible(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..2068d4c1d5 --- /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.isVisible(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.isVisible(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.getIconURL(), + "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.isVisible(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.isVisible(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.isVisible(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..6e9b3c1031 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_suggestions.js @@ -0,0 +1,581 @@ +/* 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], + ["browser.urlbar.suggest.trending", false], + ["browser.urlbar.suggest.recentsearches", 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/`, + }, + { + heuristic: false, + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + url: `http://mochi.test/redirect`, + }, + ]); + + 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..db278ad9ba --- /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.startLoadingURIString( + 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..499399db3a --- /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.isVisible( + 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_continuation.js b/browser/components/urlbar/tests/browser/browser_search_continuation.js new file mode 100644 index 0000000000..8a24d57856 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_search_continuation.js @@ -0,0 +1,113 @@ +/* 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 how trending and recent searches work together. + */ + +const CONFIG_DEFAULT = [ + { + webExtension: { id: "basic@search.mozilla.org" }, + urls: { + trending: { + fullPath: + "https://example.com/browser/browser/components/search/test/browser/trendingSuggestionEngine.sjs", + query: "", + }, + }, + appliesTo: [{ included: { everywhere: true } }], + default: "yes", + }, +]; + +add_setup(async () => { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.searches", true], + ["browser.urlbar.suggest.trending", true], + ["browser.urlbar.maxRichResults", 3], + ["browser.urlbar.trending.featureGate", true], + ["browser.urlbar.trending.requireSearchMode", false], + ["browser.urlbar.suggest.recentsearches", true], + ["browser.urlbar.recentsearches.featureGate", true], + [ + "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features", + false, + ], + ], + }); + + await UrlbarTestUtils.formHistory.clear(); + await SearchTestUtils.setupTestEngines("search-engines", CONFIG_DEFAULT); + + registerCleanupFunction(async () => { + await UrlbarTestUtils.formHistory.clear(); + }); +}); + +add_task(async function test_trending_results() { + await check_results([ + "SearchSuggestions", + "SearchSuggestions", + "SearchSuggestions", + ]); + await doSearch("Testing 1"); + await check_results([ + "RecentSearches", + "SearchSuggestions", + "SearchSuggestions", + ]); + await doSearch("Testing 2"); + await check_results([ + "RecentSearches", + "RecentSearches", + "SearchSuggestions", + ]); + await doSearch("Testing 3"); + await check_results(["RecentSearches", "RecentSearches", "RecentSearches"]); +}); + +async function check_results(results) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + waitForFocus: SimpleTest.waitForFocus, + }); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + results.length, + "We matched the expected number of results" + ); + + for (let i = 0; i < results.length; i++) { + let { result } = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + Assert.equal(result.providerName, results[i]); + } + + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Escape"); + }); +} + +async function doSearch(search) { + info("Perform a search that will be added to search history."); + let tab = await BrowserTestUtils.openNewForegroundTab( + window.gBrowser, + "data:text/html," + ); + let browserLoaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: search, + waitForFocus: SimpleTest.waitForFocus, + }); + + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Enter", {}, window); + }); + await browserLoaded; + + await BrowserTestUtils.removeTab(tab); +} 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..a61f9a6eed --- /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.isVisible( + 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.isVisible( + 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..c381478712 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_selectStaleResults.js @@ -0,0 +1,329 @@ +/* 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]], + }); + + // We'll later replace this, so ensure it's restored. + let originalRemoveStaleRowsTimeout = UrlbarView.removeStaleRowsTimeout; + 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() { + // Set the remove-stale-rows timer to a very large value, so there's no + // possibility it interferes with this test. + UrlbarView.removeStaleRowsTimeout = 10000; + + // For the test stability we need a slow provider that ensures the search + // doesn't complete too fast. + let slowProvider = new UrlbarTestUtils.TestProvider({ + results: [], + name: "emptySlowProvider", + addTimeout: 1000, + }); + UrlbarProvidersManager.registerProvider(slowProvider); + registerCleanupFunction(() => { + UrlbarProvidersManager.unregisterProvider(slowProvider); + }); + + 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, and await for it to be updated. + Assert.ok( + !UrlbarTestUtils.getRowAt(window, halfResults).hasAttribute("stale"), + "Should not be stale" + ); + + let lastMatchingResultUpdatedPromise = TestUtils.waitForCondition(() => { + let row = UrlbarTestUtils.getRowAt(window, halfResults); + console.log(row.result.title); + return row.result.title.startsWith("xx"); + }, "Wait for the result to be updated"); + + // Type another "x" so that we search for "xx", but don't wait for the search + // to finish. Instead, wait for the row to be updated. + EventUtils.synthesizeKey("x"); + await lastMatchingResultUpdatedPromise; + + // 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; + + // Check stale status of results. + Assert.ok( + !UrlbarTestUtils.getRowAt(window, halfResults).hasAttribute("stale"), + "Should not be stale" + ); + Assert.ok( + UrlbarTestUtils.getRowAt(window, halfResults + 1).hasAttribute("stale"), + "Should be stale" + ); + + // 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.isVisible(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") + ); + UrlbarProvidersManager.unregisterProvider(slowProvider); +}); + +// 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 it. + UrlbarView.removeStaleRowsTimeout = 1000; + + 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..92eebf1997 --- /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.isVisible(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_slow_heuristic.js b/browser/components/urlbar/tests/browser/browser_slow_heuristic.js new file mode 100644 index 0000000000..22b71d87b6 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_slow_heuristic.js @@ -0,0 +1,84 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that slow heuristic results are still waited for on selection. + +"use strict"; + +add_task(async function test_slow_heuristic() { + // Must be between CHUNK_RESULTS_DELAY_MS and DEFERRING_TIMEOUT_MS + let timeout = 150; + Assert.greater(timeout, UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS); + Assert.greater(UrlbarEventBufferer.DEFERRING_TIMEOUT_MS, timeout); + + // First, add a provider that adds a heuristic result on a delay. + let heuristicResult = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { url: "https://example.com/" } + ); + heuristicResult.heuristic = true; + let heuristicProvider = new UrlbarTestUtils.TestProvider({ + results: [heuristicResult], + name: "heuristicProvider", + priority: Infinity, + addTimeout: timeout, + }); + UrlbarProvidersManager.registerProvider(heuristicProvider); + registerCleanupFunction(() => { + UrlbarProvidersManager.unregisterProvider(heuristicProvider); + }); + + // Do a search without waiting for a result. + const win = await BrowserTestUtils.openNewBrowserWindow(); + let promiseLoaded = BrowserTestUtils.browserLoaded( + win.gBrowser.selectedBrowser + ); + + win.gURLBar.focus(); + EventUtils.sendString("test", win); + EventUtils.synthesizeKey("KEY_Enter", {}, win); + await promiseLoaded; + + await UrlbarTestUtils.promisePopupClose(win); + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function test_fast_heuristic() { + let longTimeoutMs = 1000000; + let originalHeuristicTimeout = UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS; + UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS = longTimeoutMs; + registerCleanupFunction(() => { + UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS = originalHeuristicTimeout; + }); + + // Add a fast heuristic provider. + let heuristicResult = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { url: "https://example.com/" } + ); + heuristicResult.heuristic = true; + let heuristicProvider = new UrlbarTestUtils.TestProvider({ + results: [heuristicResult], + name: "heuristicProvider", + priority: Infinity, + }); + UrlbarProvidersManager.registerProvider(heuristicProvider); + registerCleanupFunction(() => { + UrlbarProvidersManager.unregisterProvider(heuristicProvider); + }); + + // Do a search. + const win = await BrowserTestUtils.openNewBrowserWindow(); + + let startTime = Cu.now(); + Assert.greater( + longTimeoutMs, + Cu.now() - startTime, + "Heuristic result is returned faster than CHUNK_RESULTS_DELAY_MS" + ); + + await UrlbarTestUtils.promisePopupClose(win); + await BrowserTestUtils.closeWindow(win); +}); 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..dc1b4a4c11 --- /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 PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + 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 PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + 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, + }); + Assert.greater(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..62aec6f67a --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_speculative_connect_not_with_client_cert.js @@ -0,0 +1,230 @@ +/* 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 clientAuthDialogService = { + chooseCertificate(hostname, certArray, loadContext, callback) { + ok( + expectingChooseCertificate, + `${ + expectingChooseCertificate ? "" : "not " + }expecting chooseCertificate to be called` + ); + is( + certArray.length, + 1, + "should have only one client certificate available" + ); + ok( + !chooseCertificateCalled, + "chooseCertificate should only be called once" + ); + chooseCertificateCalled = true; + callback.certificateChosen(certArray[0], false); + }, + + QueryInterface: ChromeUtils.generateQI(["nsIClientAuthDialogService"]), +}; + +/** + * 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 clientAuthDialogServiceCID = MockRegistrar.register( + "@mozilla.org/security/ClientAuthDialogService;1", + clientAuthDialogService + ); + + 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(clientAuthDialogServiceCID); + 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..50f5dfdeec --- /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.startLoadingURIString(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..508106ccdc --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_strip_on_share.js @@ -0,0 +1,197 @@ +/* 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(); +}); + +// 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 + ); +}); + +// Menu item should be visible, the whole url is copied without a selection, url should be stripped. +add_task(async function testQueryParamIsStripped() { + let originalUrl = "https://www.example.com/?stripParam=1234"; + let shortenedUrl = "https://www.example.com/"; + await testMenuItemEnabled({ + selectWholeUrl: false, + validUrl: originalUrl, + strippedUrl: shortenedUrl, + useTestList: false, + }); +}); + +// Menu item should be visible, selecting the whole url, url should be stripped. +add_task(async function testQueryParamIsStrippedSelectURL() { + let originalUrl = "https://www.example.com/?stripParam=1234"; + let shortenedUrl = "https://www.example.com/"; + await testMenuItemEnabled({ + selectWholeUrl: true, + validUrl: originalUrl, + strippedUrl: shortenedUrl, + useTestList: false, + }); +}); + +// Menu item should be visible, selecting the whole url, url should be the same. +add_task(async function testURLIsCopiedWithNoParams() { + let originalUrl = "https://www.example.com/"; + let shortenedUrl = "https://www.example.com/"; + await testMenuItemEnabled({ + selectWholeUrl: true, + validUrl: originalUrl, + strippedUrl: shortenedUrl, + useTestList: false, + }); +}); + +// Testing site specific parameter stripping +add_task(async function testQueryParamIsStrippedForSiteSpecific() { + let originalUrl = "https://www.example.com/?test_2=1234"; + let shortenedUrl = "https://www.example.com/"; + await testMenuItemEnabled({ + selectWholeUrl: true, + validUrl: originalUrl, + strippedUrl: shortenedUrl, + useTestList: true, + }); +}); + +// Ensuring site specific parameters are not stripped for other sites +add_task(async function testQueryParamIsNotStrippedForWrongSiteSpecific() { + let originalUrl = "https://www.example.com/?test_3=1234"; + let shortenedUrl = "https://www.example.com/?test_3=1234"; + await testMenuItemEnabled({ + selectWholeUrl: true, + validUrl: originalUrl, + strippedUrl: shortenedUrl, + useTestList: true, + }); +}); + +/** + * 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.isVisible(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 {object} options - method options + * @param {boolean} options.selectWholeUrl - Whether the whole url should be selected + * @param {string} options.validUrl - The original url before the stripping occurs + * @param {string} options.strippedUrl - The expected url after stripping occurs + * @param {boolean} options.useTestList - Whether the StripOnShare or Test list should be used + */ +async function testMenuItemEnabled({ + selectWholeUrl, + validUrl, + strippedUrl, + useTestList, +}) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.query_stripping.strip_on_share.enabled", true], + ["privacy.query_stripping.strip_on_share.enableTestMode", useTestList], + ], + }); + + if (useTestList) { + let testJson = { + global: { + queryParams: ["utm_ad"], + topLevelSites: ["*"], + }, + example: { + queryParams: ["test_2", "test_1"], + topLevelSites: ["www.example.com"], + }, + exampleNet: { + queryParams: ["test_3", "test_4"], + topLevelSites: ["www.example.net"], + }, + }; + + await listService.testSetList(testJson); + } + + await BrowserTestUtils.withNewTab(validUrl, async function (browser) { + gURLBar.focus(); + if (selectWholeUrl) { + gURLBar.select(); + } + let menuitem = await promiseContextualMenuitem("strip-on-share"); + Assert.ok(BrowserTestUtils.isVisible(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; + }); + + await SpecialPowers.popPrefEnv(); +} diff --git a/browser/components/urlbar/tests/browser/browser_strip_on_share_telemetry.js b/browser/components/urlbar/tests/browser/browser_strip_on_share_telemetry.js new file mode 100644 index 0000000000..48a8b6c729 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_strip_on_share_telemetry.js @@ -0,0 +1,98 @@ +"use strict"; + +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +let listService; + +const STRIP_ON_SHARE_PARAMS_REMOVED = "STRIP_ON_SHARE_PARAMS_REMOVED"; +const STRIP_ON_SHARE_LENGTH_DECREASE = "STRIP_ON_SHARE_LENGTH_DECREASE"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.query_stripping.strip_on_share.enabled", true], + ["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(); +}); + +// Checking telemetry for single query params being stripped +add_task(async function testSingleQueryParam() { + let originalURI = "https://www.example.com/?utm_source=1"; + let strippedURI = "https://www.example.com/"; + + // Calculating length difference between URLs to check correct telemetry label + let lengthDiff = originalURI.length - strippedURI.length; + + let paramHistogram = TelemetryTestUtils.getAndClearHistogram( + STRIP_ON_SHARE_PARAMS_REMOVED + ); + let lengthHistogram = TelemetryTestUtils.getAndClearHistogram( + STRIP_ON_SHARE_LENGTH_DECREASE + ); + + await testStripOnShare(originalURI, strippedURI); + + // The "1" Label is being checked as 1 Query Param is being stripped + TelemetryTestUtils.assertHistogram(paramHistogram, 1, 1); + TelemetryTestUtils.assertHistogram(lengthHistogram, lengthDiff, 1); + + await testStripOnShare(originalURI, strippedURI); + + TelemetryTestUtils.assertHistogram(paramHistogram, 1, 2); + TelemetryTestUtils.assertHistogram(lengthHistogram, lengthDiff, 2); +}); + +// Checking telemetry for mutliple query params being stripped +add_task(async function testMultiQueryParams() { + let originalURI = "https://www.example.com/?utm_source=1&utm_ad=1&utm_id=1"; + let strippedURI = "https://www.example.com/"; + + // Calculating length difference between URLs to check correct telemetry label + let lengthDiff = originalURI.length - strippedURI.length; + + let paramHistogram = TelemetryTestUtils.getAndClearHistogram( + STRIP_ON_SHARE_PARAMS_REMOVED + ); + let lengthHistogram = TelemetryTestUtils.getAndClearHistogram( + STRIP_ON_SHARE_LENGTH_DECREASE + ); + + await testStripOnShare(originalURI, strippedURI); + + // The "3" Label is being checked as 3 Query Params are being stripped + TelemetryTestUtils.assertHistogram(paramHistogram, 3, 1); + TelemetryTestUtils.assertHistogram(lengthHistogram, lengthDiff, 1); + + await testStripOnShare(originalURI, strippedURI); + + TelemetryTestUtils.assertHistogram(paramHistogram, 3, 2); + TelemetryTestUtils.assertHistogram(lengthHistogram, lengthDiff, 2); +}); + +async function testStripOnShare(validUrl, strippedUrl) { + await BrowserTestUtils.withNewTab(validUrl, async function (browser) { + gURLBar.focus(); + gURLBar.select(); + let menuitem = await promiseContextualMenuitem("strip-on-share"); + Assert.ok(BrowserTestUtils.isVisible(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..769c1790a9 --- /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..66426a154b --- /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.isVisible(label)); + } else { + Assert.ok(BrowserTestUtils.isHidden(label)); + } + } + + info("Override switch-to-tab"); + let deferred = Promise.withResolvers(); + // 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.isHidden(label)); + } + + registerCleanupFunction(() => { + // Avoid confusing next tests by leaving a pending keydown. + EventUtils.synthesizeKey("KEY_Shift", { type: "keyup" }); + }); + + let attribute = "action-override"; + 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..ee887c6796 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_switchToTab_chiclet.js @@ -0,0 +1,122 @@ +/* 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"); + let promiseLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + TEST_URL + ); + BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, TEST_URL); + await promiseLoad; + + 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.isVisible(searchModeTitle) && + searchModeTitle.textContent === "Tabs", + "Waiting until the search mode title will be visible" + ); + await BrowserTestUtils.waitForCondition( + () => BrowserTestUtils.isHidden(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"); + let promiseLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + TEST_URL + ); + BrowserTestUtils.startLoadingURIString(gBrowser, TEST_URL); + await promiseLoad; + + 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.isHidden(searchModeTitle), + "Waiting until the search mode title will be hidden" + ); + await BrowserTestUtils.waitForCondition( + () => BrowserTestUtils.isVisible(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_closed_tab.js b/browser/components/urlbar/tests/browser/browser_switchToTab_closed_tab.js new file mode 100644 index 0000000000..85b428db61 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_switchToTab_closed_tab.js @@ -0,0 +1,90 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This tests that the "switch to tab" result in the urlbar + * will still load the relevant URL if the tab being referred + * to does not exist. + */ + +"use strict"; + +const { UrlbarProviderOpenTabs } = ChromeUtils.importESModule( + "resource:///modules/UrlbarProviderOpenTabs.sys.mjs" +); + +async function openPagesCount() { + let conn = await PlacesUtils.promiseLargeCacheDBConnection(); + let res = await conn.executeCached( + "SELECT COUNT(*) AS count FROM moz_openpages_temp;" + ); + return res[0].getResultByName("count"); +} + +add_task(async function test_switchToTab_tab_closed() { + let testURL = + "https://example.org/browser/browser/components/urlbar/tests/browser/dummy_page.html"; + + // Open a blank tab to start the test from. + let testTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "https://example.org" + ); + + // Check how many currently open pages are registered + let pagesCount = await openPagesCount(); + + // Register an open tab that does not exist, this simulates a tab being + // opened but not properly unregistered. + await UrlbarProviderOpenTabs.registerOpenTab( + testURL, + gBrowser.contentPrincipal.userContextId, + false + ); + + Assert.equal( + await openPagesCount(), + pagesCount + 1, + "We registered a new open page" + ); + + let tabOpenPromise = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabOpen", + false + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: testURL, + }); + + // 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 tabOpenPromise; + await BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + testURL + ); + + Assert.equal( + gBrowser.selectedBrowser.currentURI.spec, + testURL, + "We opened a new tab with the URL" + ); + + gBrowser.removeTab(gBrowser.selectedTab); + + Assert.equal( + await openPagesCount(), + pagesCount, + "We unregistered the orphaned open tab" + ); + + gBrowser.removeTab(testTab); + await PlacesUtils.history.clear(); +}); 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..32e842d43e --- /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.isHidden(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..354cd3a802 --- /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.startLoadingURIString(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..9aac30e6b6 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_tabMatchesInAwesomebar_perwindowpb.js @@ -0,0 +1,174 @@ +/* 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) { + BrowserTestUtils.addTab(aSourceWindow.gBrowser, TEST_URL, { + userContextId: 1, + }); + 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); + } + ); + }); + + Assert.less( + 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); + } +} + +// Ensure that if the same page is opened in a non-private and a private window, +// the address bar in the non-private window doesn't show the private tab. +add_task(async function same_url_both_windows() { + let win = await BrowserTestUtils.openNewBrowserWindow(); + let tab = await BrowserTestUtils.openNewForegroundTab(win.gBrowser, TEST_URL); + + let privateWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + await BrowserTestUtils.openNewForegroundTab(privateWin.gBrowser, TEST_URL); + + // The current tab is not suggested, so open and focus another tab. + await BrowserTestUtils.openNewForegroundTab(win.gBrowser); + + // Check the switch-tab is not shown twice (one per window). + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "dummy_page", + }); + Assert.equal(2, UrlbarTestUtils.getResultCount(win), "Check results count"); + let result = await UrlbarTestUtils.getDetailsOfResultAt(win, 0); + Assert.ok(result.heuristic, "First result is heuristic"); + result = await UrlbarTestUtils.getDetailsOfResultAt(win, 1); + Assert.equal( + UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + result.type, + "Second result is tab switch" + ); + + // Now close the non-private tab, and check there's no switch-tab entry in + // the non-private window. + BrowserTestUtils.removeTab(tab); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "dummy_page", + }); + Assert.equal(2, UrlbarTestUtils.getResultCount(win), "Check results count"); + result = await UrlbarTestUtils.getDetailsOfResultAt(win, 0); + Assert.ok(result.heuristic, "First result is heuristic"); + result = await UrlbarTestUtils.getDetailsOfResultAt(win, 1); + Assert.notEqual( + UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + result.type, + "Second result is not tab switch" + ); + + await BrowserTestUtils.closeWindow(privateWin); + await BrowserTestUtils.closeWindow(win); +}); 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..a336980583 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_tabToSearch.js @@ -0,0 +1,647 @@ +/* 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}/`]); + } + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + 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." + ); + Assert.ok( + BrowserTestUtils.isVisible( + onboardingDetails.element.row.querySelector(".urlbarView-title-separator") + ), + "The title separator should be visible." + ); + + // 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..a473216ab1 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_top_sites.js @@ -0,0 +1,478 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs", + NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs", +}); + +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..c52239a800 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_top_sites_private.js @@ -0,0 +1,171 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs", + NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs", +}); + +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..01a957b5df --- /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, + UrlbarTestUtils.trimURL("http://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..ee49f9d477 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_updateForDomainCompletion.js @@ -0,0 +1,22 @@ +"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"); + } + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_url_formatted_correctly_on_load.js b/browser/components/urlbar/tests/browser/browser_url_formatted_correctly_on_load.js new file mode 100644 index 0000000000..fe923b4ebf --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_url_formatted_correctly_on_load.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + XPCShellContentUtils: + "resource://testing-common/XPCShellContentUtils.sys.mjs", +}); + +let PUNYCODE_PAGE = "xn--31b1c3b9b.com"; +// eslint-disable-next-line @microsoft/sdl/no-insecure-url +let DECODED_PAGE = "http://योगा.com/"; + +function startServer() { + XPCShellContentUtils.ensureInitialized(this); + let server = XPCShellContentUtils.createHttpServer({ + hosts: [PUNYCODE_PAGE], + }); + server.registerPathHandler("/", (request, response) => { + response.write("A page without icon"); + }); +} + +add_task(async function test_url_formatted_correctly_on_page_load() { + SpecialPowers.pushPrefEnv({ set: [["browser.urlbar.trimURLs", false]] }); + startServer(); + + let onValueChangeCalledAtLeastOnce = false; + let onValueChanged = _ => { + is(gURLBar.value, DECODED_PAGE, "Value is decoded."); + onValueChangeCalledAtLeastOnce = true; + }; + + gURLBar.inputField.addEventListener("ValueChange", onValueChanged); + registerCleanupFunction(() => { + gURLBar.inputField.removeEventListener("ValueChange", onValueChanged); + }); + + BrowserTestUtils.startLoadingURIString(gBrowser, PUNYCODE_PAGE); + // Check that whenever the value of the urlbar is changed, the correct + // decoded punycode url is used. + await BrowserTestUtils.browserLoaded(gBrowser, false, null, true); + + ok( + onValueChangeCalledAtLeastOnce, + "OnValueChanged of UrlbarInput was called at least once." + ); + // Check that the final value is decoded punycode as well. + is(gURLBar.value, DECODED_PAGE, "Final Urlbar value is correct"); + + // Cleanup. + SpecialPowers.popPrefEnv(); +}); 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..d737fb3561 --- /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: "https://example.com/", + isSponsored: true, + }, + expected: { + source: VISIT_SOURCE_SPONSORED, + frecency: FRECENCY.SPONSORED, + }, + }, + { + description: "Bookmarked result", + input: "exa", + payload: { + url: "https://example.com/", + }, + bookmarks: [ + { + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: Services.io.newURI("https://example.com/"), + title: "test bookmark", + }, + ], + expected: { + source: VISIT_SOURCE_BOOKMARKED, + frecency: FRECENCY.BOOKMARKED, + }, + }, + { + description: "Sponsored and bookmarked result", + input: "exa", + payload: { + url: "https://example.com/", + isSponsored: true, + }, + bookmarks: [ + { + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: Services.io.newURI("https://example.com/"), + title: "test bookmark", + }, + ], + expected: { + source: VISIT_SOURCE_SPONSORED, + frecency: FRECENCY.BOOKMARKED, + }, + }, + { + description: "Organic result", + input: "exa", + payload: { + url: "https://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 = "https://example.com/"; + const payload = { + url: "https://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: "https://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_selection.js b/browser/components/urlbar/tests/browser/browser_urlbar_selection.js new file mode 100644 index 0000000000..233f61e4eb --- /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, + UrlbarTestUtils.trimURL(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, + UrlbarTestUtils.trimURL(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, + UrlbarTestUtils.trimURL(exampleUrl).length, + "The entire URL should be selected after clicking selectAll button." + ); + + gURLBar.querySelector("moz-input-box").menupopup.hidePopup(); + gURLBar.blur(); + checkPrimarySelection(gURLBar._untrimmedValue); + 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..679beb5752 --- /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/", + queryParamNames: ["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..9abd990700 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_autofill.js @@ -0,0 +1,684 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This file tests urlbar autofill telemetry. + */ + +"use strict"; + +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.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); + await PlacesTestUtils.clearInputHistory(); + + // Enable local telemetry recording for the duration of the tests. + const originalCanRecord = Services.telemetry.canRecordExtended; + Services.telemetry.canRecordExtended = true; + + // Make sure autofill is tested without upgrading pages to https + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_first_schemeless", false]], + }); + + 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); + } + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + 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 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: "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, + useOtherProvider = false, + unpickResult = false, + visitHistory, + inputHistory, + userInput, + select, + autofilled, + expected, + } of testData) { + info(description); + + UrlbarPrefs.set("autoFill.adaptiveHistory.enabled", useAdaptiveHistory); + 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); + } + } + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + 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"); + + 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/"]); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + 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..6a0f84fbd0 --- /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/, + queryParamNames: ["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.startLoadingURIString(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..629e39855c --- /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.startLoadingURIString(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..671ff9320b --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_places.js @@ -0,0 +1,321 @@ +/* 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_history_adaptive() { + const histograms = snapshotHistograms(); + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + 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_adaptive", + 1, + UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection + ); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_bookmark_without_history() { + await PlacesUtils.history.clear(); + + const histograms = snapshotHistograms(); + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + 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_bookmark_with_history() { + const histograms = snapshotHistograms(); + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + 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_adaptive", + 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_tabtosearch.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tabtosearch.js new file mode 100644 index 0000000000..318b29ad19 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tabtosearch.js @@ -0,0 +1,418 @@ +/* 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 PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + 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}/`]); + } + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + // 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..345b063441 --- /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..c4e44bf778 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_topsite.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 topsite results. + */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs", + UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs", +}); + +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..9c3e63ae12 --- /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 = Promise.withResolvers(); + 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..14749c6e82 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_userTypedValue.js @@ -0,0 +1,50 @@ +/* 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, + UrlbarTestUtils.trimURL(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, + UrlbarTestUtils.trimURL(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, + UrlbarTestUtils.trimURL(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..ba249adb3b --- /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.startLoadingURIString(deletedURLTab.linkedBrowser, testURL); + BrowserTestUtils.startLoadingURIString(fullURLTab.linkedBrowser, testURL); + BrowserTestUtils.startLoadingURIString(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_removedSelectedElement.js b/browser/components/urlbar/tests/browser/browser_view_removedSelectedElement.js new file mode 100644 index 0000000000..532f9e10a2 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_view_removedSelectedElement.js @@ -0,0 +1,87 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that if the selectedElement is removed from the DOM, the view still +// sets a selection on the next received results. + +add_task(async function () { + let view = gURLBar.view; + // We need a heuristic provider that the Muxer will prefer over other + // heuristics and that will return results after the first onQueryResults. + // Luckily TEST providers come first in the heuristic group! + let result = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { url: "https://example.com/1", title: "example" } + ); + result.heuristic = true; + // To ensure the selectedElement is removed, we use this special property that + // asks the view to generate new content for the row. + result.testForceNewContent = true; + + let receivedResults = false; + let firstSelectedElement; + let delayResultsPromise = new Promise(resolve => { + gURLBar.controller.addQueryListener({ + async onQueryResults(queryContext) { + Assert.ok(!receivedResults, "Should execute only once"); + gURLBar.controller.removeQueryListener(this); + receivedResults = true; + // Store the corrent selection. + firstSelectedElement = view.selectedElement; + Assert.ok(firstSelectedElement, "There should be a selected element"); + Assert.ok( + view.selectedResult.heuristic, + "Selected result should be a heuristic" + ); + Assert.notEqual( + result, + view.selectedResult, + "Should not immediately select our result" + ); + resolve(); + }, + }); + }); + + let delayedHeuristicProvider = new UrlbarTestUtils.TestProvider({ + delayResultsPromise, + results: [result], + type: UrlbarUtils.PROVIDER_TYPE.HEURISTIC, + }); + UrlbarProvidersManager.registerProvider(delayedHeuristicProvider); + registerCleanupFunction(async function () { + UrlbarProvidersManager.unregisterProvider(delayedHeuristicProvider); + await UrlbarTestUtils.promisePopupClose(window); + gURLBar.handleRevert(); + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exa", + }); + Assert.ok(receivedResults, "Results observer was invoked"); + Assert.ok( + UrlbarTestUtils.getResultCount(window) > 0, + `There should be some results in the view.` + ); + Assert.ok(view.isOpen, `The view should be open.`); + Assert.ok(view.selectedElement.isConnected, "selectedElement is connected"); + Assert.equal(view.selectedElementIndex, 0, "selectedElementIndex is correct"); + Assert.deepEqual( + view.getResultFromElement(view.selectedElement), + result, + "result is the expected one" + ); + Assert.notEqual( + view.selectedElement, + firstSelectedElement, + "Selected element should have changed" + ); + Assert.ok( + !firstSelectedElement.isConnected, + "Previous selected element should be disconnected" + ); +}); 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..c9bd4750f8 --- /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.substring("https://".length) + "?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..fc617220b6 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_view_selectionByMouse.js @@ -0,0 +1,567 @@ +/* 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.startLoadingURIString( + gBrowser.selectedBrowser, + "about:about" + ), + }); + UrlbarProviderQuickActions.addAction("test-downloads", { + commands: ["test-downloads"], + label: "quickactions-downloads2", + onPick: () => + BrowserTestUtils.startLoadingURIString( + 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-button[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-button[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-button[data-key=test-addons]", + mouseup: ".urlbarView-quickaction-button[data-key=test-downloads]", + expected: "about:downloads", + }, + { + description: "Quick action button to out of result", + mousedown: ".urlbarView-quickaction-button[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" + ); + + if (upElement.tagName === "html:body") { + // We intentionally turn off this a11y check, because the following + // click is sent to test the selection behavior using an alternative way + // of the urlbar dismissal, where other ways are accessible, therefore + // this test can be ignored. + AccessibilityUtils.setEnv({ + mustHaveAccessibleRule: false, + }); + } + EventUtils.synthesizeMouseAtCenter(upElement, { type: "mouseup" }); + AccessibilityUtils.resetEnv(); + 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-button[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-button[selected]", + selectedElementAfterMouseDown: + "#urlbar-results .urlbarView-quickaction-button[selected]", + actionedPage: false, + }, + }, + { + description: "Select normal result, then click on about:downloads", + mousedown: ".urlbarView-quickaction-button[data-key=test-downloads]", + mouseup: ".urlbarView-quickaction-button[data-key=test-downloads]", + expected: { + selectedElementByKey: + "#urlbar-results .urlbarView-row > .urlbarView-row-inner[selected]", + selectedElementAfterMouseDown: + ".urlbarView-quickaction-button[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" + ); + } + + if (upElement.tagName === "html:body") { + // We intentionally turn off this a11y check, because the following + // click is sent to test the selection behavior using an alternative way + // of the urlbar dismissal, where other ways are accessible, therefore + // this test can be ignored. + AccessibilityUtils.setEnv({ + mustHaveAccessibleRule: false, + }); + } + EventUtils.synthesizeMouseAtCenter(upElement, { + type: "mouseup", + }); + AccessibilityUtils.resetEnv(); + + 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: "urlbar-result-menu-learn-more-about-firefox-suggest", + }, + isBlockable: true, + blockL10n: { + id: "urlbar-result-menu-dismiss-firefox-suggest", + }, + } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + url: otherResultUrl, + } + ), + ], + }); + + UrlbarProvidersManager.registerProvider(provider); + + let assertResultMenuOpen = () => { + Assert.equal( + gURLBar.view.resultMenu.state, + "showing", + "Result menu is showing" + ); + EventUtils.synthesizeKey("KEY_Escape"); + }; + + let testData = [ + { + description: "Menu button to menu button", + mousedown: ".urlbarView-row:nth-child(1) .urlbarView-button-menu", + afterMouseupCallback: assertResultMenuOpen, + expected: { + mousedownSelected: false, + topSites: { + pageProxyState: "valid", + value: initialTabUrl, + }, + searchString: { + pageProxyState: "invalid", + value: searchString, + }, + }, + }, + { + description: "Row-inner to menu button", + mousedown: ".urlbarView-row:nth-child(1) > .urlbarView-row-inner", + mouseup: ".urlbarView-row:nth-child(1) .urlbarView-button-menu", + afterMouseupCallback: assertResultMenuOpen, + expected: { + mousedownSelected: true, + topSites: { + pageProxyState: "valid", + value: initialTabUrl, + }, + searchString: { + pageProxyState: "invalid", + value: searchString, + }, + }, + }, + { + description: "Menu button to row-inner", + mousedown: ".urlbarView-row:nth-child(1) .urlbarView-button-menu", + 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, + } of testData) { + 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, + UrlbarTestUtils.trimURL(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, + UrlbarTestUtils.trimURL(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.isVisible(e)); + }, "Waiting for elements to become visible: " + JSON.stringify(selectors)); + return elements; +} diff --git a/browser/components/urlbar/tests/browser/browser_waitForLoadStartOrTimeout.js b/browser/components/urlbar/tests/browser/browser_waitForLoadStartOrTimeout.js new file mode 100644 index 0000000000..0fc6f0739f --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_waitForLoadStartOrTimeout.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests the waitForLoadStartOrTimeout test helper function in head.js. + */ + +"use strict"; + +add_task(async function load() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + let url = "https://example.com/"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: url, + }); + + let loadPromise = waitForLoadStartOrTimeout(); + EventUtils.synthesizeKey("KEY_Enter"); + let uri = await loadPromise; + info("Page should have loaded before timeout"); + + Assert.equal(uri.spec, url, "example.com should have loaded"); + }); +}); + +add_task(async function timeout() { + await Assert.rejects( + waitForLoadStartOrTimeout(), + /timed out/, + "Should have timed out" + ); +}); 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_copying_home.html b/browser/components/urlbar/tests/browser/file_copying_home.html new file mode 100644 index 0000000000..7aaafc26af --- /dev/null +++ b/browser/components/urlbar/tests/browser/file_copying_home.html @@ -0,0 +1 @@ +wait-a-bit.sjs 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..2119d33123 --- /dev/null +++ b/browser/components/urlbar/tests/browser/head-common.js @@ -0,0 +1,153 @@ +ChromeUtils.defineESModuleGetters(this, { + HttpServer: "resource://testing-common/httpd.sys.mjs", + 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", +}); + +ChromeUtils.defineLazyGetter(this, "TEST_BASE_URL", () => + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "clipboardHelper", + "@mozilla.org/widget/clipboardhelper;1", + "nsIClipboardHelper" +); + +ChromeUtils.defineLazyGetter(this, "UrlbarTestUtils", () => { + const { UrlbarTestUtils: module } = ChromeUtils.importESModule( + "resource://testing-common/UrlbarTestUtils.sys.mjs" + ); + module.init(this); + return module; +}); + +ChromeUtils.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..a81e8e4811 --- /dev/null +++ b/browser/components/urlbar/tests/browser/head.js @@ -0,0 +1,248 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs", + ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs", + ExperimentFakes: "resource://testing-common/NimbusTestUtils.sys.mjs", + ObjectUtils: "resource://gre/modules/ObjectUtils.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", + UrlbarEventBufferer: "resource:///modules/UrlbarEventBufferer.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", +}); + +ChromeUtils.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 starting 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 {Promise} resolved to the loading uri in case of load, rejected in + * case of timeout. + */ +function waitForLoadStartOrTimeout(win = window, timeoutMs = 1000) { + let listener; + let timeout; + return Promise.race([ + new Promise(resolve => { + listener = { + onStateChange(browser, webprogress, request, flags, status) { + if (flags & Ci.nsIWebProgressListener.STATE_START) { + resolve(request.QueryInterface(Ci.nsIChannel).URI); + } + }, + }; + win.gBrowser.addTabsProgressListener(listener); + }), + new Promise((resolve, reject) => { + timeout = win.setTimeout(() => reject("timed out"), timeoutMs); + }), + ]).finally(() => { + win.gBrowser.removeTabsProgressListener(listener); + win.clearTimeout(timeout); + }); +} + +/** + * 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); +} + +/** + * 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); + + // Subtract the protocol length, when the searchString contains the https:// + // protocol and trimHttps is enabled. + let trimmedProtocolWSlashes = UrlbarTestUtils.getTrimmedProtocolWithSlashes(); + let selectionOffset = searchString.includes(trimmedProtocolWSlashes) + ? trimmedProtocolWSlashes.length + : 0; + + // Check the input value and selection immediately, before waiting on the + // search to complete. + Assert.equal( + gURLBar.value, + UrlbarTestUtils.trimURL(valueBefore), + "gURLBar.value before the search completes" + ); + Assert.equal( + gURLBar.selectionStart, + searchString.length - selectionOffset, + "gURLBar.selectionStart before the search completes" + ); + Assert.equal( + gURLBar.selectionEnd, + valueBefore.length - selectionOffset, + "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, + UrlbarTestUtils.trimURL(valueAfter), + "gURLBar.value after the search completes" + ); + Assert.equal( + gURLBar.selectionStart, + searchString.length - selectionOffset, + "gURLBar.selectionStart after the search completes" + ); + Assert.equal( + gURLBar.selectionEnd, + valueAfter.length - selectionOffset, + "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, + UrlbarTestUtils.trimURL(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" + ); +} diff --git a/browser/components/urlbar/tests/browser/mixed_active.html b/browser/components/urlbar/tests/browser/mixed_active.html new file mode 100644 index 0000000000..4ce8e78dc4 --- /dev/null +++ b/browser/components/urlbar/tests/browser/mixed_active.html @@ -0,0 +1,14 @@ + + + + + + + Mixed Active Content test + + + + + 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/search-engines/basic/manifest.json b/browser/components/urlbar/tests/browser/search-engines/basic/manifest.json new file mode 100644 index 0000000000..d66c1ed3d8 --- /dev/null +++ b/browser/components/urlbar/tests/browser/search-engines/basic/manifest.json @@ -0,0 +1,20 @@ +{ + "name": "basic", + "manifest_version": 2, + "version": "1.0", + "description": "basic", + "browser_specific_settings": { + "gecko": { + "id": "basic@search.mozilla.org" + } + }, + "hidden": true, + "chrome_settings_overrides": { + "search_provider": { + "name": "basic", + "keyword": "@basic", + "search_url": "https://example.com/?search={searchTerms}&foo=1", + "suggest_url": "https://example.com/browser/browser/components/search/test/browser/trendingSuggestionEngine.sjs?richsuggestions=true&query={searchTerms}" + } + } +} 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/browser/wait-a-bit.sjs b/browser/components/urlbar/tests/browser/wait-a-bit.sjs new file mode 100644 index 0000000000..52a6ae2c22 --- /dev/null +++ b/browser/components/urlbar/tests/browser/wait-a-bit.sjs @@ -0,0 +1,11 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +async function handleRequest(request, response) { + response.processAsync(); + + const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.init(response.finish, 3000, Ci.nsITimer.TYPE_ONE_SHOT); +} diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser.toml b/browser/components/urlbar/tests/engagementTelemetry/browser/browser.toml new file mode 100644 index 0000000000..68a7881399 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser.toml @@ -0,0 +1,87 @@ +# 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-search_engine_default_id.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", +] +prefs = ["browser.bookmarks.testing.skipDefaultBookmarksImport=true"] + +["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_engine_default_id.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_engine_default_id.js"] + +["browser_glean_telemetry_engagement_search_mode.js"] + +["browser_glean_telemetry_engagement_selected_result.js"] +support-files = ["../../../../search/test/browser/trendingSuggestionEngine.sjs"] +skip-if = ["verify"] # Bug 1852375 - MerinoTestUtils.initWeather() doesn't play well with pushPrefEnv() + +["browser_glean_telemetry_engagement_tips.js"] + +["browser_glean_telemetry_engagement_type.js"] + +["browser_glean_telemetry_exposure.js"] + +["browser_glean_telemetry_exposure_edge_cases.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_engine_default_id.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..ce69d30517 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_groups.js @@ -0,0 +1,235 @@ +/* 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 + +// This test has many subtests and can time out in verify mode. +requestLongerTimeout(5); + +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 recent_search() { + await doRecentSearchTest({ + trigger: () => doBlur(), + assert: () => + assertAbandonmentTelemetry([ + { + groups: "recent_search,suggested_index", + results: "recent_search,action", + n_results: 2, + }, + ]), + }); +}); + +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,merino_top_picks,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 clipboard() { + await doClipboardTest({ + trigger: () => doBlur(), + assert: () => + assertAbandonmentTelemetry([ + { + groups: "general,suggested_index", + results: "clipboard,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: UrlbarPrefs.get("quickSuggestRustEnabled") + ? "search_engine,rust_adm_nonsponsored" + : "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..73820be059 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_interaction.js @@ -0,0 +1,61 @@ +/* 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() { + await doTopsitesSearchTest({ + trigger: () => doBlur(), + assert: () => + 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_engine_default_id.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_search_engine_default_id.js new file mode 100644 index 0000000000..d64b540d25 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_search_engine_default_id.js @@ -0,0 +1,19 @@ +/* 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_engine_default_id + +add_setup(async function () { + await initSearchEngineDefaultIdTest(); +}); + +add_task(async function basic() { + await doSearchEngineDefaultIdTest({ + trigger: () => doBlur(), + assert: engineId => + assertAbandonmentTelemetry([{ search_engine_default_id: engineId }]), + }); +}); 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..71087d03d0 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_tips.js @@ -0,0 +1,99 @@ +/* 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 () { + 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 () { + 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, () => { + // We intentionally turn off this a11y check, because the following click + // is sent to test the telemetry behavior using an alternative way of the + // urlbar dismissal, where other ways are accessible, therefore this test + // can be ignored. + AccessibilityUtils.setEnv({ + mustHaveAccessibleRule: false, + }); + EventUtils.synthesizeMouseAtCenter(browser, {}); + AccessibilityUtils.resetEnv(); + }); + + assertAbandonmentTelemetry([{ results: "tip_persist" }]); + }); +}); + +add_task(async function mouse_down_without_tip() { + await doTest(async browser => { + // We intentionally turn off this a11y check, because the following click + // is sent to test the telemetry behavior using an alternative way of the + // urlbar dismissal, where other ways are accessible, therefore this test + // can be ignored. + AccessibilityUtils.setEnv({ + mustHaveAccessibleRule: false, + }); + EventUtils.synthesizeMouseAtCenter(browser, {}); + AccessibilityUtils.resetEnv(); + + 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..fcac924879 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_edge_cases.js @@ -0,0 +1,221 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test edge cases for engagement. + +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 = Promise.withResolvers(); + } + + 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 = Promise.withResolvers(); + } + + 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]], + }); + + // Increase chunk delays to delay the call to notifyResults. + let originalChunkTimeout = UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS; + UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS = 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.CHUNK_RESULTS_DELAY_MS = originalChunkTimeout; + }; + 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"), + () => { + // We intentionally turn off this a11y check, because the following click + // is sent to test the telemetry behavior using an alternative way of the + // urlbar dismissal, where other ways are accessible (and tested above), + // therefore this test can be ignored. + AccessibilityUtils.setEnv({ + mustHaveAccessibleRule: false, + }); + EventUtils.synthesizeMouseAtCenter( + document.getElementById("customizableui-special-spring2"), + {} + ); + AccessibilityUtils.resetEnv(); + }, + ]; + + 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..8779487960 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_groups.js @@ -0,0 +1,292 @@ +/* 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 + +// This test has many subtests and can time out in verify mode. +requestLongerTimeout(5); + +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 recent_search() { + await doRecentSearchTest({ + trigger: () => doEnter(), + assert: () => + assertEngagementTelemetry([ + { + groups: "recent_search,suggested_index", + results: "recent_search,action", + n_results: 2, + }, + ]), + }); +}); + +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,merino_top_picks,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 clipboard() { + await doClipboardTest({ + trigger: () => doEnter(), + assert: () => + assertEngagementTelemetry([ + { + groups: "general,suggested_index", + results: "clipboard,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: UrlbarPrefs.get("quickSuggestRustEnabled") + ? "search_engine,rust_adm_nonsponsored" + : "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..f4880d2205 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_interaction.js @@ -0,0 +1,90 @@ +/* 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() { + await doTopsitesSearchTest({ + trigger: () => doEnter(), + assert: () => + 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_engine_default_id.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_search_engine_default_id.js new file mode 100644 index 0000000000..60331ff53b --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_search_engine_default_id.js @@ -0,0 +1,19 @@ +/* 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_engine_default_id + +add_setup(async function () { + await initSearchEngineDefaultIdTest(); +}); + +add_task(async function basic() { + await doSearchEngineDefaultIdTest({ + trigger: () => doEnter(), + assert: engineId => + assertEngagementTelemetry([{ search_engine_default_id: engineId }]), + }); +}); 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..013bef1904 --- /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-button[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..6a3422d939 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_selected_result.js @@ -0,0 +1,974 @@ +/* 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 +// - selected_position +// - provider +// - results + +ChromeUtils.defineESModuleGetters(this, { + UrlbarProviderClipboard: + "resource:///modules/UrlbarProviderClipboard.sys.mjs", +}); + +// This test has many subtests and can time out in verify mode. +requestLongerTimeout(5); + +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: "", + selected_position: 1, + 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: "", + selected_position: 1, + 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 PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + await openPopup("exa"); + await doEnter(); + + assertEngagementTelemetry([ + { + selected_result: "autofill_origin", + selected_result_subtype: "", + selected_position: 1, + 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 PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + await openPopup("https://example.com/test"); + await doEnter(); + + assertEngagementTelemetry([ + { + selected_result: "autofill_url", + selected_result_subtype: "", + selected_position: 1, + 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: "", + selected_position: 3, + 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: "", + selected_position: 2, + 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: "", + selected_position: 1, + 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: "", + selected_position: 1, + 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: "", + selected_position: 2, + 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: "", + selected_position: 3, + 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: "", + selected_position: 1, + 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-button[data-key=addons]"); + await onLoad; + + assertEngagementTelemetry([ + { + selected_result: "action", + selected_result_subtype: "addons", + selected_position: 1, + 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: "", + selected_position: 4, + 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: "", + selected_position: 2, + 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: "", + selected_position: 1, + 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: "", + selected_position: 2, + 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: "", + selected_position: 1, + 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: "", + selected_position: 2, + provider: "calculator", + results: "search_engine,calc", + }, + ]); + }); + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function selected_result_clipboard() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.clipboard.featureGate", true], + ["browser.urlbar.suggest.clipboard", true], + ], + }); + SpecialPowers.clipboardCopyString( + "https://example.com/selected_result_clipboard" + ); + + await doTest(async browser => { + await openPopup(""); + await selectRowByProvider("UrlbarProviderClipboard"); + await doEnter(); + + assertEngagementTelemetry([ + { + selected_result: "clipboard", + selected_result_subtype: "", + selected_position: 1, + provider: "UrlbarProviderClipboard", + results: "clipboard,action", + }, + ]); + }); + + SpecialPowers.clipboardCopyString(""); + UrlbarProviderClipboard.setPreviousClipboardValue(""); + 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: "", + selected_position: 2, + 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.startLoadingURIString( + 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: "", + selected_position: 2, + provider: "UrlbarProviderContextualSearch", + results: "search_engine,site_specific_contextual_search", + }, + ]); + + await extension.unload(); + }); + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function selected_result_rs_adm_sponsored() { + const cleanupQuickSuggest = await ensureQuickSuggestInit({ + prefs: [["quicksuggest.rustEnabled", false]], + }); + + await doTest(async browser => { + await openPopup("sponsored"); + await selectRowByURL("https://example.com/sponsored"); + await doEnter(); + + assertEngagementTelemetry([ + { + selected_result: "rs_adm_sponsored", + selected_result_subtype: "", + selected_position: 2, + provider: "UrlbarProviderQuickSuggest", + results: "search_engine,rs_adm_sponsored", + }, + ]); + }); + + await cleanupQuickSuggest(); +}); + +add_task(async function selected_result_rs_adm_nonsponsored() { + const cleanupQuickSuggest = await ensureQuickSuggestInit({ + prefs: [["quicksuggest.rustEnabled", false]], + }); + + await doTest(async browser => { + await openPopup("nonsponsored"); + await selectRowByURL("https://example.com/nonsponsored"); + await doEnter(); + + assertEngagementTelemetry([ + { + selected_result: "rs_adm_nonsponsored", + selected_result_subtype: "", + selected_position: 2, + provider: "UrlbarProviderQuickSuggest", + results: "search_engine,rs_adm_nonsponsored", + }, + ]); + }); + + await cleanupQuickSuggest(); +}); + +add_task(async function selected_result_input_field() { + const expected = [ + { + selected_result: "input_field", + selected_result_subtype: "", + selected_position: 0, + 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(); + + let provider = UrlbarPrefs.get("quickSuggestRustEnabled") + ? "UrlbarProviderQuickSuggest" + : "Weather"; + await doTest(async browser => { + await openPopup(MerinoTestUtils.WEATHER_KEYWORD); + await selectRowByProvider(provider); + await doEnter(); + + assertEngagementTelemetry([ + { + selected_result: "weather", + selected_result_subtype: "", + selected_position: 2, + provider, + results: "search_engine,weather", + }, + ]); + }); + + await 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: "", + selected_position: 2, + provider: "UrlbarProviderQuickSuggest", + results: "search_engine,merino_top_picks", + }, + ]); + }); + + await 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: "", + selected_position: 2, + provider: "UrlbarProviderQuickSuggest", + results: "search_engine,merino_wikipedia", + }, + ]); + }); + + await 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: "", + selected_position: 0, + 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: "", + selected_position: 1, + 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: "", + selected_position: 1, + 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: "", + selected_position: 2, + provider: "UrlbarProviderQuickSuggest", + results: "search_engine,merino_amo", + }, + ]); + }); + + await cleanupQuickSuggest(); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function selected_result_rust_adm_sponsored() { + const cleanupQuickSuggest = await ensureQuickSuggestInit({ + prefs: [["quicksuggest.rustEnabled", true]], + }); + + await doTest(async browser => { + await openPopup("sponsored"); + await selectRowByURL("https://example.com/sponsored"); + await doEnter(); + + assertEngagementTelemetry([ + { + selected_result: "rust_adm_sponsored", + selected_result_subtype: "", + selected_position: 2, + provider: "UrlbarProviderQuickSuggest", + results: "search_engine,rust_adm_sponsored", + }, + ]); + }); + + await cleanupQuickSuggest(); +}); + +add_task(async function selected_result_rust_adm_nonsponsored() { + const cleanupQuickSuggest = await ensureQuickSuggestInit({ + prefs: [["quicksuggest.rustEnabled", true]], + }); + + await doTest(async browser => { + await openPopup("nonsponsored"); + await selectRowByURL("https://example.com/nonsponsored"); + await doEnter(); + + assertEngagementTelemetry([ + { + selected_result: "rust_adm_nonsponsored", + selected_result_subtype: "", + selected_position: 2, + provider: "UrlbarProviderQuickSuggest", + results: "search_engine,rust_adm_nonsponsored", + }, + ]); + }); + + await cleanupQuickSuggest(); +}); 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..2b38631747 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_tips.js @@ -0,0 +1,173 @@ +/* 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 +); + +add_setup(async function () { + makeProfileResettable(); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.quickactions.enabled", false]], + }); + + registerCleanupFunction(async function () { + 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 = Promise.withResolvers(); + 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() { + let useOldClearHistoryDialog = Services.prefs.getBoolPref( + "privacy.sanitize.useOldClearHistoryDialog" + ); + let dialogURL = useOldClearHistoryDialog + ? "chrome://browser/content/sanitize.xhtml" + : "chrome://browser/content/sanitize_v2.xhtml"; + await doInterventionTest( + SEARCH_STRINGS.CLEAR, + "intervention_clear", + dialogURL, + [ + { + 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..5972dd331d --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_type.js @@ -0,0 +1,118 @@ +/* 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 + +// This test has many subtests and can time out in verify mode. +requestLongerTimeout(5); + +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(); + + await doTest(async browser => { + await openPopup("sponsored"); + + const originalResultCount = UrlbarTestUtils.getResultCount(window); + await selectRowByURL("https://example.com/sponsored"); + UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "D"); + 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 cleanupQuickSuggest(); +}); + +add_task(async function engagement_type_help() { + const cleanupQuickSuggest = await ensureQuickSuggestInit(); + + await doTest(async browser => { + await openPopup("sponsored"); + await selectRowByURL("https://example.com/sponsored"); + const onTabOpened = BrowserTestUtils.waitForNewTab(gBrowser); + UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "L"); + const tab = await onTabOpened; + BrowserTestUtils.removeTab(tab); + + assertEngagementTelemetry([{ engagement_type: "help" }]); + }); + + await 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..07e8b9b360 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_exposure.js @@ -0,0 +1,136 @@ +/* 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", suggestResultType("adm_sponsored")], + ["browser.urlbar.showExposureResults", true], + ], + query: SPONSORED_QUERY, + trigger: () => doClick(), + assert: () => + assertExposureTelemetry([ + { results: suggestResultType("adm_sponsored") }, + ]), + }); +}); + +add_task(async function exposureSponsoredOnAbandonment() { + await doExposureTest({ + prefs: [ + ["browser.urlbar.exposureResults", suggestResultType("adm_sponsored")], + ["browser.urlbar.showExposureResults", true], + ], + query: SPONSORED_QUERY, + trigger: () => doBlur(), + assert: () => + assertExposureTelemetry([ + { results: suggestResultType("adm_sponsored") }, + ]), + }); +}); + +add_task(async function exposureFilter() { + await doExposureTest({ + prefs: [ + ["browser.urlbar.exposureResults", suggestResultType("adm_sponsored")], + ["browser.urlbar.showExposureResults", false], + ], + query: SPONSORED_QUERY, + select: async () => { + // assert that the urlbar has no results + Assert.equal( + await getResultByType(suggestResultType("adm_sponsored")), + null + ); + }, + trigger: () => doBlur(), + assert: () => + assertExposureTelemetry([ + { results: suggestResultType("adm_sponsored") }, + ]), + }); +}); + +add_task(async function innerQueryExposure() { + await doExposureTest({ + prefs: [ + ["browser.urlbar.exposureResults", suggestResultType("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: suggestResultType("adm_sponsored") }, + ]), + }); +}); + +add_task(async function innerQueryInvertedExposure() { + await doExposureTest({ + prefs: [ + ["browser.urlbar.exposureResults", suggestResultType("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: suggestResultType("adm_sponsored") }, + ]), + }); +}); + +add_task(async function multipleProviders() { + await doExposureTest({ + prefs: [ + [ + "browser.urlbar.exposureResults", + [ + suggestResultType("adm_sponsored"), + suggestResultType("adm_nonsponsored"), + ].join(","), + ], + ["browser.urlbar.showExposureResults", true], + ], + query: NONSPONSORED_QUERY, + trigger: () => doClick(), + assert: () => + assertExposureTelemetry([ + { results: suggestResultType("adm_nonsponsored") }, + ]), + }); +}); + +function suggestResultType(typeWithoutSource) { + let source = UrlbarPrefs.get("quickSuggestRustEnabled") ? "rust" : "rs"; + return `${source}_${typeWithoutSource}`; +} diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_exposure_edge_cases.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_exposure_edge_cases.js new file mode 100644 index 0000000000..d28352b417 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_exposure_edge_cases.js @@ -0,0 +1,539 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests edge cases related to the exposure event and view updates. + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs", + UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs", + UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs", + UrlbarView: "resource:///modules/UrlbarView.sys.mjs", +}); + +const MAX_RESULT_COUNT = 10; + +let gProvider; + +add_setup(async function () { + await initExposureTest(); + + 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], + + // Set maxRichResults for sanity. + ["browser.urlbar.maxRichResults", MAX_RESULT_COUNT], + ], + }); + + await Services.fog.testFlushAllChildren(); + Services.fog.testResetFOG(); + + gProvider = new TestProvider(); + UrlbarProvidersManager.registerProvider(gProvider); + + // Increase the timeout of the stale-rows timer so it doesn't interfere with + // this test, which specifically tests what happens before the timer fires. + let originalRemoveStaleRowsTimeout = UrlbarView.removeStaleRowsTimeout; + UrlbarView.removeStaleRowsTimeout = 30000; + + registerCleanupFunction(() => { + UrlbarView.removeStaleRowsTimeout = originalRemoveStaleRowsTimeout; + UrlbarProvidersManager.unregisterProvider(gProvider); + }); +}); + +// Does one query that fills up the view with search suggestions, starts a +// second query that returns a history result, and cancels it before it can +// finish but after the view is updated. Regardless of `showExposureResults`, +// the history result should not trigger an exposure since it never had a chance +// to be visible in the view. +add_task(async function noExposure() { + for (let showExposureResults of [true, false]) { + await do_noExposure(showExposureResults); + } +}); + +async function do_noExposure(showExposureResults) { + info("Starting do_noExposure: " + JSON.stringify({ showExposureResults })); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.exposureResults", "history"], + ["browser.urlbar.showExposureResults", showExposureResults], + ], + }); + + // Make the provider return enough search suggestions to fill the view. + gProvider.results = []; + for (let i = 0; i < MAX_RESULT_COUNT; i++) { + gProvider.results.push( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.SEARCH, + UrlbarUtils.RESULT_SOURCE.SEARCH, + { + suggestion: "suggestion " + i, + engine: Services.search.defaultEngine.name, + } + ) + ); + } + + // Do the first query to fill the view with search suggestions. + info("Doing first query"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test 1", + }); + + // Now make the provider return a history result and bookmark. If + // `showExposureResults` is true, the history result will be added to the view + // but it should be hidden since the view is already full. If it's false, it + // shouldn't be added at all. The bookmark will always be added, which will + // tell us when the view has been updated either way. (It also will be hidden + // since the view is already full.) + let historyUrl = "https://example.com/history"; + let bookmarkUrl = "https://example.com/bookmark"; + gProvider.results = [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: historyUrl } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + { url: bookmarkUrl } + ), + ]; + + // When the provider's `startQuery()` is called, let it add its results but + // don't let it return. That will cause the view to be updated with the new + // results but prevent it from showing hidden rows since the query won't + // finish. + let queryResolver = Promise.withResolvers(); + gProvider.finishQueryPromise = queryResolver.promise; + + // Observe when the view appends the bookmark row. This will tell us when the + // view has been updated with the provider's new results. The bookmark row + // will be hidden since the view is already full with search suggestions. + let lastRowPromise = promiseLastRowAppended( + row => row.result.payload.url == bookmarkUrl + ); + + // Now start the second query but don't await it. + info("Starting second query"); + let queryPromise = UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test 2", + reopenOnBlur: false, + }); + + // Wait for the view to be updated. + info("Waiting for last row"); + let lastRow = await lastRowPromise; + info("Done waiting for last row"); + + Assert.ok( + BrowserTestUtils.isHidden(lastRow), + "The new bookmark row should be hidden since the view is full" + ); + + // Make sure the view is full of visible rows as expected, plus the one or two + // hidden rows for the history and/or bookmark results. + let rows = UrlbarTestUtils.getResultsContainer(window); + let expectedCount = MAX_RESULT_COUNT + 1; + if (showExposureResults) { + expectedCount++; + } + Assert.equal( + rows.children.length, + expectedCount, + "The view has the expected number of rows" + ); + + // Check the visible rows. + for (let i = 0; i < MAX_RESULT_COUNT; i++) { + let row = rows.children[i]; + Assert.ok(BrowserTestUtils.isVisible(row), `rows[${i}] should be visible`); + Assert.ok( + row.result.type == UrlbarUtils.RESULT_TYPE.SEARCH, + `rows[${i}].result.type should be SEARCH` + ); + // The heuristic won't have a suggestion so skip it. + if (i > 0) { + Assert.ok( + row.result.payload.suggestion, + `rows[${i}] should have a suggestion` + ); + } + } + + // Check the hidden history and/or bookmark rows. + let expected = [ + { source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, url: bookmarkUrl }, + ]; + if (showExposureResults) { + expected.unshift({ + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + url: historyUrl, + }); + } + for (let i = 0; i < expected.length; i++) { + let { source, url } = expected[i]; + let row = rows.children[MAX_RESULT_COUNT + i]; + Assert.ok(row, `rows[${i}] should exist`); + Assert.ok(BrowserTestUtils.isHidden(row), `rows[${i}] should be hidden`); + Assert.equal( + row.result.type, + UrlbarUtils.RESULT_TYPE.URL, + `rows[${i}].result.type should be URL` + ); + Assert.equal( + row.result.source, + source, + `rows[${i}].result.source should be as expected` + ); + Assert.equal( + row.result.payload.url, + url, + `rows[${i}] URL should be as expected` + ); + } + + // Close the view. Blur the urlbar to end the session. + info("Closing view and blurring"); + await UrlbarTestUtils.promisePopupClose(window); + gURLBar.blur(); + + // No exposure should have been recorded since the history result was never + // visible. + assertExposureTelemetry([]); + + // Clean up. + queryResolver.resolve(); + await queryPromise; + await SpecialPowers.popPrefEnv(); + Services.fog.testResetFOG(); +} + +// Does one query that underfills the view and then a second query that returns +// a search suggestion. The search suggestion should be appended and trigger an +// exposure. When `showExposureResults` is true, it should also be shown. After +// the view is updated, it shouldn't matter whether or not the second query is +// canceled. +add_task(async function exposure_append() { + for (let showExposureResults of [true, false]) { + for (let cancelSecondQuery of [true, false]) { + await do_exposure_append({ + showExposureResults, + cancelSecondQuery, + }); + } + } +}); + +async function do_exposure_append({ showExposureResults, cancelSecondQuery }) { + info( + "Starting do_exposure_append: " + + JSON.stringify({ showExposureResults, cancelSecondQuery }) + ); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.exposureResults", "search_suggest"], + ["browser.urlbar.showExposureResults", showExposureResults], + ], + }); + + // Make the provider return no results at first. + gProvider.results = []; + + // Do the first query to open the view. + info("Doing first query"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test 1", + }); + + // Now make the provider return a search suggestion and a bookmark. If + // `showExposureResults` is true, the suggestion should be added to the view + // and be visible immediately. If it's false, it shouldn't be added at + // all. The bookmark will always be added, which will tell us when the view + // has been updated either way. + let newSuggestion = "new suggestion"; + let bookmarkUrl = "https://example.com/bookmark"; + gProvider.results = [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.SEARCH, + UrlbarUtils.RESULT_SOURCE.SEARCH, + { + suggestion: newSuggestion, + engine: Services.search.defaultEngine.name, + } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + { url: bookmarkUrl } + ), + ]; + + // When the provider's `startQuery()` is called, let it add its results but + // don't let it return. That will cause the view to be updated with the new + // results but let us test the specific case where the query doesn't finish. + let queryResolver = Promise.withResolvers(); + gProvider.finishQueryPromise = queryResolver.promise; + + // Observe when the view appends the bookmark row. This will tell us when the + // view has been updated with the provider's new results. + let lastRowPromise = promiseLastRowAppended( + row => row.result.payload.url == bookmarkUrl + ); + + // Now start the second query but don't await it. + info("Starting second query"); + let queryPromise = UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test 2", + reopenOnBlur: false, + }); + + // Wait for the view to be updated. + info("Waiting for last row"); + let lastRow = await lastRowPromise; + info("Done waiting for last row"); + + Assert.ok( + BrowserTestUtils.isVisible(lastRow), + "The new bookmark row should be visible since the view is not full" + ); + + // Check the new suggestion row. + let rows = UrlbarTestUtils.getResultsContainer(window); + let newSuggestionRow = [...rows.children].find( + r => r.result.payload.suggestion == newSuggestion + ); + if (showExposureResults) { + Assert.ok( + newSuggestionRow, + "The new suggestion row should have been added" + ); + Assert.ok( + BrowserTestUtils.isVisible(newSuggestionRow), + "The new suggestion row should be visible" + ); + } else { + Assert.ok( + !newSuggestionRow, + "The new suggestion row should not have been added" + ); + } + + if (!cancelSecondQuery) { + // Finish the query. + queryResolver.resolve(); + await queryPromise; + } + + // Close the view. Blur the urlbar to end the session. + info("Closing view and blurring"); + await UrlbarTestUtils.promisePopupClose(window); + gURLBar.blur(); + + // If `showExposureResults` is true, the new search suggestion should have + // been shown; if it's false, it would have been shown. Either way, it should + // have triggered an exposure. + assertExposureTelemetry([{ results: "search_suggest" }]); + + // Clean up. + queryResolver.resolve(); + await queryPromise; + await SpecialPowers.popPrefEnv(); + Services.fog.testResetFOG(); +} + +// Does one query that returns a search suggestion and then a second query that +// returns a new search suggestion. The new search suggestion can replace the +// old one, so it should trigger an exposure. When `showExposureResults` is +// true, it should actually replace it. After the view is updated, it shouldn't +// matter whether or not the second query is canceled. +add_task(async function exposure_replace() { + for (let showExposureResults of [true, false]) { + for (let cancelSecondQuery of [true, false]) { + await do_exposure_replace({ showExposureResults, cancelSecondQuery }); + } + } +}); + +async function do_exposure_replace({ showExposureResults, cancelSecondQuery }) { + info( + "Starting do_exposure_replace: " + + JSON.stringify({ showExposureResults, cancelSecondQuery }) + ); + + // Make the provider return a search suggestion. + gProvider.results = [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.SEARCH, + UrlbarUtils.RESULT_SOURCE.SEARCH, + { + suggestion: "suggestion", + engine: Services.search.defaultEngine.name, + } + ), + ]; + + // Do the first query to show the suggestion. + info("Doing first query"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test 1", + }); + + // Set exposure results to search suggestions and hide them. We can't do this + // before now because that would hide the search suggestions in the first + // query, and here we're specifically testing the case where a new row + // replaces an old row, which is only allowed for rows of the same type. + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.exposureResults", "search_suggest"], + ["browser.urlbar.showExposureResults", showExposureResults], + ], + }); + + // Now make the provider return another search suggestion and a bookmark. If + // `showExposureResults` is true, the new suggestion should replace the old + // one in the view and be visible immediately. If it's false, it shouldn't be + // added at all. The bookmark will always be added, which will tell us when + // the view has been updated either way. + let newSuggestion = "new suggestion"; + let bookmarkUrl = "https://example.com/bookmark"; + gProvider.results = [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.SEARCH, + UrlbarUtils.RESULT_SOURCE.SEARCH, + { + suggestion: newSuggestion, + engine: Services.search.defaultEngine.name, + } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + { url: bookmarkUrl } + ), + ]; + + // When the provider's `startQuery()` is called, let it add its results but + // don't let it return. That will cause the view to be updated with the new + // results but let us test the specific case where the query doesn't finish. + let queryResolver = Promise.withResolvers(); + gProvider.finishQueryPromise = queryResolver.promise; + + // Observe when the view appends the bookmark row. This will tell us when the + // view has been updated with the provider's new results. + let lastRowPromise = promiseLastRowAppended( + row => row.result.payload.url == bookmarkUrl + ); + + // Now start the second query but don't await it. + info("Starting second query"); + let queryPromise = UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test 2", + reopenOnBlur: false, + }); + + // Wait for the view to be updated. + info("Waiting for last row"); + let lastRow = await lastRowPromise; + info("Done waiting for last row"); + + Assert.ok( + BrowserTestUtils.isVisible(lastRow), + "The new bookmark row should be visible since the view is not full" + ); + + // Check the new suggestion row. + let rows = UrlbarTestUtils.getResultsContainer(window); + let newSuggestionRow = [...rows.children].find( + r => r.result.payload.suggestion == newSuggestion + ); + if (showExposureResults) { + Assert.ok( + newSuggestionRow, + "The new suggestion row should have replaced the old one" + ); + Assert.ok( + BrowserTestUtils.isVisible(newSuggestionRow), + "The new suggestion row should be visible" + ); + } else { + Assert.ok( + !newSuggestionRow, + "The new suggestion row should not have been added" + ); + } + + if (!cancelSecondQuery) { + // Finish the query. + queryResolver.resolve(); + await queryPromise; + } + + // Close the view. Blur the urlbar to end the session. + info("Closing view and blurring"); + await UrlbarTestUtils.promisePopupClose(window); + gURLBar.blur(); + + // If `showExposureResults` is true, the new search suggestion should have + // been shown; if it's false, it would have been shown. Either way, it should + // have triggered an exposure. + assertExposureTelemetry([{ results: "search_suggest" }]); + + // Clean up. + queryResolver.resolve(); + await queryPromise; + await SpecialPowers.popPrefEnv(); + Services.fog.testResetFOG(); +} + +/** + * A test provider that doesn't finish startQuery() until `finishQueryPromise` + * is resolved. + */ +class TestProvider extends UrlbarTestUtils.TestProvider { + finishQueryPromise = null; + + async startQuery(context, addCallback) { + for (let result of this.results) { + addCallback(this, result); + } + await this.finishQueryPromise; + } +} + +function promiseLastRowAppended(predicate) { + return new Promise(resolve => { + let rows = UrlbarTestUtils.getResultsContainer(window); + let observer = new MutationObserver(mutations => { + let lastRow = rows.children[rows.children.length - 1]; + info( + "Observed mutation, lastRow.result is: " + + JSON.stringify(lastRow.result) + ); + if (predicate(lastRow)) { + observer.disconnect(); + resolve(lastRow); + } + }); + observer.observe(rows, { childList: true }); + }); +} 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..354876e512 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_groups.js @@ -0,0 +1,258 @@ +/* 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 + +// This test has many subtests and can time out in verify mode. +requestLongerTimeout(5); + +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 recent_search() { + await doRecentSearchTest({ + trigger: () => waitForPauseImpression(), + assert: () => + assertImpressionTelemetry([ + { + reason: "pause", + groups: "recent_search,suggested_index", + results: "recent_search,action", + n_results: 2, + }, + ]), + }); +}); + +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,merino_top_picks,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 clipboard() { + await doClipboardTest({ + trigger: () => waitForPauseImpression(), + assert: () => + assertImpressionTelemetry([ + { + reason: "pause", + groups: "general,suggested_index", + results: "clipboard,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: UrlbarPrefs.get("quickSuggestRustEnabled") + ? "search_engine,rust_adm_nonsponsored" + : "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..a16b55cac6 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_interaction.js @@ -0,0 +1,68 @@ +/* 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() { + await doTopsitesSearchTest({ + trigger: () => waitForPauseImpression(), + assert: () => + assertImpressionTelemetry([ + { reason: "pause", 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_engine_default_id.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_search_engine_default_id.js new file mode 100644 index 0000000000..c5bd983d7f --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_search_engine_default_id.js @@ -0,0 +1,28 @@ +/* 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_engine_default_id + +add_setup(async function () { + await initSearchEngineDefaultIdTest(); + // Increase the pausing time to ensure to ready for all suggestions. + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "browser.urlbar.searchEngagementTelemetry.pauseImpressionIntervalMs", + 500, + ], + ], + }); +}); + +add_task(async function basic() { + await doSearchEngineDefaultIdTest({ + trigger: () => waitForPauseImpression(), + assert: engineId => + assertImpressionTelemetry([{ search_engine_default_id: engineId }]), + }); +}); 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..88adc2fc11 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_record_preferences.js @@ -0,0 +1,74 @@ +/* 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 boolPref() { + const testData = [ + { + green: "prefSuggestDataCollection", + pref: "quicksuggest.dataCollection.enabled", + }, + { + green: "prefSuggestNonsponsored", + pref: "suggest.quicksuggest.nonsponsored", + }, + { + green: "prefSuggestSponsored", + pref: "suggest.quicksuggest.sponsored", + }, + { + green: "prefSuggestTopsites", + pref: "suggest.topsites", + }, + ]; + + for (const { green, pref } of testData) { + Assert.equal( + Glean.urlbar[green].testGetValue(), + UrlbarPrefs.get(pref), + `Record ${green} when UrlbarController is initialized` + ); + + await SpecialPowers.pushPrefEnv({ + set: [[`browser.urlbar.${pref}`, !UrlbarPrefs.get(pref)]], + }); + + Assert.equal( + Glean.urlbar[green].testGetValue(), + UrlbarPrefs.get(pref), + `Record ${green} when the ${pref} 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..f0723be701 --- /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(); + await 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..e86c664b46 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/head-groups.js @@ -0,0 +1,339 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from head.js */ + +ChromeUtils.defineESModuleGetters(this, { + UrlbarProviderClipboard: + "resource:///modules/UrlbarProviderClipboard.sys.mjs", +}); + +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 doRecentSearchTest({ trigger, assert }) { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.recentsearches.featureGate", true]], + }); + + await doTest(async browser => { + await UrlbarTestUtils.formHistory.add([ + { value: "foofoo", source: Services.search.defaultEngine.name }, + ]); + + await openPopup(""); + 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({ + 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("navigational"); + await selectRowByURL("https://example.com/navigational-suggestion"); + + await trigger(); + await assert(); + }); + + await 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 doClipboardTest({ trigger, assert }) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.clipboard.featureGate", true], + ["browser.urlbar.suggest.clipboard", true], + ], + }); + SpecialPowers.clipboardCopyString("https://example.com/clipboard"); + await doTest(async browser => { + await showResultByArrowDown(); + await selectRowByURL("https://example.com/clipboard"); + + await trigger(); + await assert(); + }); + SpecialPowers.clipboardCopyString(""); + UrlbarProviderClipboard.setPreviousClipboardValue(""); + await SpecialPowers.popPrefEnv(); +} + +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 doTest(async browser => { + await openPopup("nonsponsored"); + await selectRowByURL("https://example.com/nonsponsored"); + + await trigger(); + await assert(); + }); + + await 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..244e27d272 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/head-interaction.js @@ -0,0 +1,340 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from head.js */ + +ChromeUtils.defineESModuleGetters(this, { + CUSTOM_SEARCH_SHORTCUTS: + "resource://activity-stream/lib/SearchShortcuts.sys.mjs", + NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs", + SEARCH_SHORTCUTS: "resource://activity-stream/lib/SearchShortcuts.sys.mjs", + SearchService: "resource://gre/modules/SearchService.sys.mjs", +}); + +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 doTopsitesSearchTest({ trigger, assert }) { + await doTest(async browser => { + let extension = await SearchTestUtils.installSearchExtension( + { + name: "MozSearch", + keyword: "@test", + search_url: "https://example.com/", + search_url_get_params: "q={searchTerms}", + }, + { skipUnload: true } + ); + + // Fresh profiles come with an empty set of pinned websites (pref doesn't + // exist). Search shortcut topsites make this test more complicated because + // the feature pins a new website on startup. Behaviour can vary when running + // with --verify so it's more predictable to clear pins entirely. + Services.prefs.clearUserPref("browser.newtabpage.pinned"); + NewTabUtils.pinnedLinks.resetCache(); + + let entry = { + keyword: "@test", + shortURL: "example", + url: "https://example.com/", + }; + + // The array is used to identify sites that should be converted to + // a Top Site. + let searchShortcuts = JSON.parse(JSON.stringify(SEARCH_SHORTCUTS)); + SEARCH_SHORTCUTS.push(entry); + + // TopSitesFeed takes a list of app provided engines and determine if the + // engine containing an alias that matches a keyword inside of this array. + // If so, the list of search shortcuts in the store will be updated. + let customSearchShortcuts = JSON.parse( + JSON.stringify(CUSTOM_SEARCH_SHORTCUTS) + ); + CUSTOM_SEARCH_SHORTCUTS.push(entry); + + // TopSitesFeed only allows app provided engines to be included as + // search shortcuts. + // eslint-disable-next-line mozilla/valid-lazy + let sandbox = lazy.sinon.createSandbox(); + sandbox + .stub(SearchService.prototype, "getAppProvidedEngines") + .resolves([{ aliases: ["@test"] }]); + + let siteToPin = { + url: "https://example.com", + label: "@test", + searchTopSite: true, + }; + NewTabUtils.pinnedLinks.pin(siteToPin, 0); + + await updateTopSites(sites => { + return sites && sites[0] && sites[0].url == "https://example.com"; + }, true); + + BrowserTestUtils.startLoadingURIString(browser, "about:newtab"); + await BrowserTestUtils.browserStopped(browser, "about:newtab"); + + await BrowserTestUtils.synthesizeMouseAtCenter( + ".search-shortcut .top-site-button", + {}, + gBrowser.selectedBrowser + ); + + EventUtils.synthesizeKey("x"); + await UrlbarTestUtils.promiseSearchComplete(window); + + await trigger(); + await assert(); + + // Clean up. + NewTabUtils.pinnedLinks.unpin(siteToPin); + SEARCH_SHORTCUTS.pop(); + CUSTOM_SEARCH_SHORTCUTS.pop(); + // Sanity check to ensure we're leaving the shortcuts in their default state. + Assert.deepEqual( + searchShortcuts, + SEARCH_SHORTCUTS, + "SEARCH_SHORTCUTS values" + ); + Assert.deepEqual( + customSearchShortcuts, + CUSTOM_SEARCH_SHORTCUTS, + "CUSTOM_SEARCH_SHORTCUTS values" + ); + sandbox.restore(); + Services.prefs.clearUserPref("browser.newtabpage.pinned"); + NewTabUtils.pinnedLinks.resetCache(); + await extension.unload(); + }); +} + +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..ef95873813 --- /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.startLoadingURIString(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.startLoadingURIString(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_engine_default_id.js b/browser/components/urlbar/tests/engagementTelemetry/browser/head-search_engine_default_id.js new file mode 100644 index 0000000000..c0af764e7f --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/head-search_engine_default_id.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from head.js */ + +async function doSearchEngineDefaultIdTest({ trigger, assert }) { + await doTest(async browser => { + info("Test with current engine"); + const defaultEngine = await Services.search.getDefault(); + + await openPopup("x"); + await trigger(); + await assert(defaultEngine.telemetryId); + }); + + await doTest(async browser => { + info("Test with new engine"); + const defaultEngine = await Services.search.getDefault(); + const newEngineName = "NewDummyEngine"; + await SearchTestUtils.installSearchExtension({ + name: newEngineName, + search_url: "https://example.com/", + search_url_get_params: "q={searchTerms}", + }); + const newEngine = await Services.search.getEngineByName(newEngineName); + Assert.notEqual(defaultEngine.telemetryId, newEngine.telemetryId); + await Services.search.setDefault( + newEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + await openPopup("x"); + await trigger(); + await assert(newEngine.telemetryId); + + await Services.search.setDefault( + defaultEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + }); +} 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..367387b0e8 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/head.js @@ -0,0 +1,473 @@ +/* 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 = {}; + +ChromeUtils.defineLazyGetter(lazy, "QuickSuggestTestUtils", () => { + const { QuickSuggestTestUtils: module } = ChromeUtils.importESModule( + "resource://testing-common/QuickSuggestTestUtils.sys.mjs" + ); + module.init(this); + return module; +}); + +ChromeUtils.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", +}); + +ChromeUtils.defineLazyGetter(this, "PlacesFrecencyRecalculator", () => { + return Cc["@mozilla.org/places/frecency-recalculator;1"].getService( + Ci.nsIObserver + ).wrappedJSObject; +}); + +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() ?? []; + info( + "Asserting Glean telemetry is correct, actual events are: " + + JSON.stringify(telemetries) + ); + 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({ ...args } = {}) { + return lazy.QuickSuggestTestUtils.ensureQuickSuggestInit({ + ...args, + remoteSettingsRecords: [ + { + 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", + icon: "1234", + }, + { + 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: "Wikipedia", + iab_category: "5 - Education", + icon: "1234", + }, + ], + }, + { + 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 impression, as it is disabled by default. + Services.fog.setMetricsFeatureConfig( + JSON.stringify({ + "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 initSearchEngineDefaultIdTest() { + /* import-globals-from head-search_engine_default_id.js */ + Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/components/urlbar/tests/engagementTelemetry/browser/head-search_engine_default_id.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/quicksuggest/MerinoTestUtils.sys.mjs b/browser/components/urlbar/tests/quicksuggest/MerinoTestUtils.sys.mjs new file mode 100644 index 0000000000..6cda9bb9a7 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/MerinoTestUtils.sys.mjs @@ -0,0 +1,809 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs", + TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs", + UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs", +}); + +import { HttpServer } from "resource://testing-common/httpd.sys.mjs"; + +// 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], + min_keyword_length: 3, + score: "0.29", +}; + +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 + +const GEOLOCATION_DATA = { + provider: "geolocation", + title: "", + url: "https://merino.services.mozilla.com/", + is_sponsored: false, + score: 0, + custom_details: { + geolocation: { + country: "Japan", + region: "Kanagawa", + city: "Yokohama", + }, + }, +}; + +/** + * 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 {object} + * Mock geolocation data. + */ + get GEOLOCATION() { + return { ...GEOLOCATION_DATA.custom_details.geolocation }; + } + + /** + * @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. + if (!client) { + this.Assert.ok( + !latencyStopwatchRunning, + "Client is null, latency stopwatch should not be expected to be running" + ); + } else { + 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() { + this.info("MockMerinoServer initializing weather, starting server"); + await this.server.start(); + this.info("MockMerinoServer initializing weather, server now started"); + 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. + this.info("MockMerinoServer initializing weather, waiting for fetch"); + let fetchPromise = lazy.QuickSuggest.weather.waitForFetches(); + lazy.UrlbarPrefs.set("weather.featureGate", true); + lazy.UrlbarPrefs.set("suggest.weather", true); + await fetchPromise; + this.info("MockMerinoServer initializing weather, got fetch"); + + 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; + }); + } + + /** + * Initializes the mock Merino geolocation server. + */ + async initGeolocation() { + await this.server.start(); + this.server.response.body.suggestions = [GEOLOCATION_DATA]; + } + + #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: "amp", + title: "Amp Suggestion", + url: "http://example.com/amp", + icon: null, + impression_url: "http://example.com/amp-impression", + click_url: "http://example.com/amp-click", + block_id: 1, + advertiser: "Amp", + iab_category: "22 - Shopping", + 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 = Promise.withResolvers(); + } + 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 = Promise.withResolvers(); + 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..2ba9dce8be --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/QuickSuggestTestUtils.sys.mjs @@ -0,0 +1,915 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-disable mozilla/valid-lazy */ +/* eslint-disable jsdoc/require-param */ + +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", + RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", + RemoteSettingsConfig: "resource://gre/modules/RustRemoteSettings.sys.mjs", + RemoteSettingsServer: + "resource://testing-common/RemoteSettingsServer.sys.mjs", + SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs", + SuggestBackendRust: + "resource:///modules/urlbar/private/SuggestBackendRust.sys.mjs", + Suggestion: "resource://gre/modules/RustSuggest.sys.mjs", + SuggestionProvider: "resource://gre/modules/RustSuggest.sys.mjs", + SuggestStore: "resource://gre/modules/RustSuggest.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; + }, +}); + +// TODO bug 1881409: Previously this was an empty object, but the Rust backend +// seems to persist old config after ingesting an empty config object. +const DEFAULT_CONFIG = { + // Zero means there is no cap, the same as if this wasn't specified at all. + show_less_frequently_cap: 0, +}; + +// 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", +]; + +/** + * 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); + } + + /** + * Sets up local remote settings and Merino servers, registers test + * suggestions, and initializes Suggest. + * + * @param {object} options + * Options object + * @param {Array} options.remoteSettingsRecords + * Array of remote settings records. Each item in this array should be a + * realistic remote settings record with some exceptions, e.g., + * `record.attachment`, if defined, should be the attachment itself and not + * its metadata. For details see `RemoteSettingsServer.addRecords()`. + * @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 Suggest configuration object. This should not be the full remote + * settings record; only pass the object that should be set to the nested + * `configuration` object inside the record. + * @param {Array} options.prefs + * An array of Suggest-related prefs to set. This is useful because setting + * some prefs, like feature gates, can cause Suggest to sync from remote + * settings; this function will set them, wait for sync to finish, and clear + * them when the cleanup function is called. Each item in this array should + * itself be a two-element array `[prefName, prefValue]` similar to the + * `set` array passed to `SpecialPowers.pushPrefEnv()`, except here pref + * names are relative to `browser.urlbar`. + * @returns {Function} + * An async cleanup function. This function is automatically registered as a + * cleanup function, so you only need to call it if your test needs to clean + * up Suggest before it ends, for example if you have a small number of + * tasks that need Suggest and it's not enabled throughout your test. The + * cleanup function is idempotent so there's no harm in calling it more than + * once. Be sure to `await` it. + */ + async ensureQuickSuggestInit({ + remoteSettingsRecords = [], + merinoSuggestions = null, + config = DEFAULT_CONFIG, + prefs = [], + } = {}) { + prefs.push(["quicksuggest.enabled", true]); + + // Set up the local remote settings server. + this.#log( + "ensureQuickSuggestInit", + "Started, preparing remote settings server" + ); + if (!this.#remoteSettingsServer) { + this.#remoteSettingsServer = new lazy.RemoteSettingsServer(); + } + await this.#remoteSettingsServer.setRecords({ + collection: "quicksuggest", + records: [ + ...remoteSettingsRecords, + { type: "configuration", configuration: config }, + ], + }); + this.#log("ensureQuickSuggestInit", "Starting remote settings server"); + await this.#remoteSettingsServer.start(); + this.#log("ensureQuickSuggestInit", "Remote settings server started"); + + // Get the cached `RemoteSettings` client used by the JS backend and tell it + // to ignore signatures and to always force sync. Otherwise it won't sync if + // the previous sync was recent enough, which is incompatible with testing. + let rs = lazy.RemoteSettings("quicksuggest"); + let { get, verifySignature } = rs; + rs.verifySignature = false; + rs.get = opts => get.call(rs, { forceSync: true, ...opts }); + this.#restoreRemoteSettings = () => { + rs.verifySignature = verifySignature; + rs.get = get; + }; + + // Finally, init Suggest and set prefs. Do this after setting up remote + // settings because the current backend will immediately try to sync. + this.#log( + "ensureQuickSuggestInit", + "Calling QuickSuggest.init() and setting prefs" + ); + lazy.QuickSuggest.init(); + for (let [name, value] of prefs) { + lazy.UrlbarPrefs.set(name, value); + } + + // Tell the Rust backend to use the local remote setting server. + await lazy.QuickSuggest.rustBackend._test_setRemoteSettingsConfig( + new lazy.RemoteSettingsConfig({ + collectionName: "quicksuggest", + bucketName: "main", + serverUrl: this.#remoteSettingsServer.url.toString(), + }) + ); + + // Wait for the current backend to finish syncing. + await this.forceSync(); + + // Set up Merino. This can happen any time relative to Suggest init. + if (merinoSuggestions) { + this.#log("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.#log("ensureQuickSuggestInit", "Done setting up Merino server"); + } + + let cleanupCalled = false; + let cleanup = async () => { + if (!cleanupCalled) { + cleanupCalled = true; + await this.#uninitQuickSuggest(prefs, !!merinoSuggestions); + } + }; + this.registerCleanupFunction?.(cleanup); + + this.#log("ensureQuickSuggestInit", "Done"); + return cleanup; + } + + async #uninitQuickSuggest(prefs, clearDataCollectionEnabled) { + this.#log("#uninitQuickSuggest", "Started"); + + // Reset prefs, which can cause the current backend to start syncing. Wait + // for it to finish. + for (let [name] of prefs) { + lazy.UrlbarPrefs.clear(name); + } + await this.forceSync(); + + this.#log("#uninitQuickSuggest", "Stopping remote settings server"); + await this.#remoteSettingsServer.stop(); + this.#restoreRemoteSettings(); + + if (clearDataCollectionEnabled) { + lazy.UrlbarPrefs.clear("quicksuggest.dataCollection.enabled"); + } + + this.#log("#uninitQuickSuggest", "Done"); + } + + /** + * Removes all records from the local remote settings server and adds a new + * batch of records. + * + * @param {Array} records + * Array of remote settings records. See `ensureQuickSuggestInit()`. + * @param {object} options + * Options object. + * @param {boolean} options.forceSync + * Whether to force Suggest to sync after updating the records. + */ + async setRemoteSettingsRecords(records, { forceSync = true } = {}) { + this.#log("setRemoteSettingsRecords", "Started"); + await this.#remoteSettingsServer.setRecords({ + collection: "quicksuggest", + records, + }); + if (forceSync) { + this.#log("setRemoteSettingsRecords", "Forcing sync"); + await this.forceSync(); + } + this.#log("setRemoteSettingsRecords", "Done"); + } + + /** + * Sets the quick suggest configuration. You should call this again with + * `DEFAULT_CONFIG` before your test finishes. See also `withConfig()`. + * + * @param {object} config + * The quick suggest configuration object. This should not be the full + * remote settings record; only pass the object that should be set to the + * `configuration` nested object inside the record. + */ + async setConfig(config) { + this.#log("setConfig", "Started"); + let type = "configuration"; + this.#remoteSettingsServer.removeRecords({ type }); + await this.#remoteSettingsServer.addRecords({ + collection: "quicksuggest", + records: [{ type, configuration: config }], + }); + this.#log("setConfig", "Forcing sync"); + await this.forceSync(); + this.#log("setConfig", "Done"); + } + + /** + * Forces Suggest to sync with remote settings. This can be used to ensure + * Suggest has finished all sync activity. + */ + async forceSync() { + this.#log("forceSync", "Started"); + if (lazy.QuickSuggest.rustBackend.isEnabled) { + this.#log("forceSync", "Syncing Rust backend"); + await lazy.QuickSuggest.rustBackend._test_ingest(); + this.#log("forceSync", "Done syncing Rust backend"); + } + if (lazy.QuickSuggest.jsBackend.isEnabled) { + this.#log("forceSync", "Syncing JS backend"); + await lazy.QuickSuggest.jsBackend._test_syncAll(); + this.#log("forceSync", "Done syncing JS backend"); + } + this.#log("forceSync", "Done"); + } + + /** + * 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.QuickSuggest.jsBackend.config; + await this.setConfig(config); + await callback(); + await this.setConfig(original); + } + + /** + * Returns an AMP (sponsored) suggestion suitable for storing in a remote + * settings attachment. + * + * @returns {object} + * An AMP suggestion for storing in remote settings. + */ + ampRemoteSettings({ + keywords = ["amp"], + url = "http://example.com/amp", + title = "Amp Suggestion", + score = 0.3, + }) { + return { + keywords, + url, + title, + score, + id: 1, + click_url: "http://example.com/amp-click", + impression_url: "http://example.com/amp-impression", + advertiser: "Amp", + iab_category: "22 - Shopping", + icon: "1234", + }; + } + + /** + * Returns a Wikipedia (non-sponsored) suggestion suitable for storing in a + * remote settings attachment. + * + * @returns {object} + * A Wikipedia suggestion for storing in remote settings. + */ + wikipediaRemoteSettings({ + keywords = ["wikipedia"], + url = "http://example.com/wikipedia", + title = "Wikipedia Suggestion", + score = 0.2, + }) { + return { + keywords, + url, + title, + score, + id: 2, + click_url: "http://example.com/wikipedia-click", + impression_url: "http://example.com/wikipedia-impression", + advertiser: "Wikipedia", + iab_category: "5 - Education", + icon: "1234", + }; + } + + /** + * Returns an AMO (addons) suggestion suitable for storing in a remote + * settings attachment. + * + * @returns {object} + * An AMO suggestion for storing in remote settings. + */ + amoRemoteSettings({ + keywords = ["amo"], + url = "http://example.com/amo", + title = "Amo Suggestion", + score = 0.2, + }) { + return { + keywords, + url, + title, + score, + guid: "amo-suggestion@example.com", + icon: "https://example.com/addon.svg", + rating: "4.7", + description: "Addon with score", + number_of_ratings: 1256, + }; + } + + /** + * 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.#log( + "assertIsQuickSuggest", + `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 = row._elements.get("description"); + if (isSponsored || isBestMatch) { + this.Assert.ok(sponsoredElement, "Result sponsored label element exists"); + this.Assert.equal( + sponsoredElement.textContent, + isSponsored ? "Sponsored" : "", + "Result sponsored label" + ); + } else { + this.Assert.ok( + !sponsoredElement, + "Result sponsored label element should not exist" + ); + } + + this.Assert.equal( + result.payload.helpUrl, + lazy.QuickSuggest.HELP_URL, + "Result helpURL" + ); + + this.Assert.ok( + row._buttons.get("menu"), + "The menu button should be present" + ); + + 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 + ); + } + + /** + * 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.#log( + "assertTimestampsReplaced", + "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.#log("enrollExperiment", "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.#log( + "enrollExperiment", + "Awaiting update after enrolling in experiment" + ); + await this.waitForScenarioUpdated(); + + return async () => { + this.#log("enrollExperiment.cleanup", "Awaiting experiment cleanup"); + await doExperimentCleanup(); + + // The same pref updates will be triggered by unenrollment, so wait for + // them again. + this.#log( + "enrollExperiment.cleanup", + "Awaiting update after unenrolling in experiment" + ); + await this.waitForScenarioUpdated(); + }; + } + + /** + * 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.#log( + "withLocales", + "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.#log("withLocales", "Waiting for intl:requested-locales-changed"); + await lazy.TestUtils.topicObserved("intl:requested-locales-changed"); + this.#log("withLocales", "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.#log("withLocales", "Waiting for TOPIC_SEARCH_SERVICE"); + await Promise.race([ + lazy.TestUtils.topicObserved( + lazy.SearchUtils.TOPIC_SEARCH_SERVICE, + (subject, data) => { + this.#log( + "withLocales", + "Observed TOPIC_SEARCH_SERVICE with data: " + data + ); + return data == "engines-reloaded"; + } + ), + new Promise(resolve => { + lazy.setTimeout(() => { + this.#log( + "withLocales", + "Timed out waiting for TOPIC_SEARCH_SERVICE" + ); + resolve(); + }, 2000); + }), + ]); + + this.#log("withLocales", "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; + } + + #log(fnName, msg) { + this.info?.(`QuickSuggestTestUtils.${fnName} ${msg}`); + } + + #remoteSettingsServer; + #restoreRemoteSettings; +} + +export var QuickSuggestTestUtils = new _QuickSuggestTestUtils(); diff --git a/browser/components/urlbar/tests/quicksuggest/RemoteSettingsServer.sys.mjs b/browser/components/urlbar/tests/quicksuggest/RemoteSettingsServer.sys.mjs new file mode 100644 index 0000000000..32b42198c3 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/RemoteSettingsServer.sys.mjs @@ -0,0 +1,619 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-disable jsdoc/require-param-description */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + HttpError: "resource://testing-common/httpd.sys.mjs", + HttpServer: "resource://testing-common/httpd.sys.mjs", + HTTP_404: "resource://testing-common/httpd.sys.mjs", + Log: "resource://gre/modules/Log.sys.mjs", +}); + +const SERVER_PREF = "services.settings.server"; + +/** + * A remote settings server. Tested with the desktop and Rust remote settings + * clients. + */ +export class RemoteSettingsServer { + /** + * The server must be started by calling `start()`. + * + * @param {object} options + * @param {number} options.logLevel + * A `Log.Level` value from `Log.sys.mjs`. `Log.Level.Info` logs basic info + * on requests and responses like paths and status codes. Pass + * `Log.Level.Debug` to log more info like headers, response bodies, and + * added and removed records. + */ + constructor({ logLevel = lazy.Log.Level.Info } = {}) { + this.#log = lazy.Log.repository.getLogger("RemoteSettingsServer"); + this.#log.level = logLevel; + + // Use `DumpAppender` instead of `ConsoleAppender`. The xpcshell and browser + // test harnesses buffer console messages and log them later, which makes it + // really hard to debug problems. `DumpAppender` logs to stdout, which the + // harnesses log immediately. + this.#log.addAppender( + new lazy.Log.DumpAppender(new lazy.Log.BasicFormatter()) + ); + } + + /** + * @returns {URL} + * The server's URL. Null when the server is stopped. + */ + get url() { + return this.#url; + } + + /** + * Starts the server and sets the `services.settings.server` pref to its + * URL. The server's `url` property will be non-null on return. + */ + async start() { + this.#log.info("Starting"); + + if (this.#url) { + this.#log.info("Already started at " + this.#url); + return; + } + + if (!this.#server) { + this.#server = new lazy.HttpServer(); + this.#server.registerPrefixHandler("/", this); + } + this.#server.start(-1); + + this.#url = new URL("http://localhost/v1"); + this.#url.port = this.#server.identity.primaryPort; + + this.#originalServerPrefValue = Services.prefs.getCharPref( + SERVER_PREF, + null + ); + Services.prefs.setCharPref(SERVER_PREF, this.#url.toString()); + + this.#log.info("Server is now started at " + this.#url); + } + + /** + * Stops the server and clears the `services.settings.server` pref. The + * server's `url` property will be null on return. + */ + async stop() { + this.#log.info("Stopping"); + + if (!this.#url) { + this.#log.info("Already stopped"); + return; + } + + await this.#server.stop(); + this.#url = null; + + if (this.#originalServerPrefValue === null) { + Services.prefs.clearUserPref(SERVER_PREF); + } else { + Services.prefs.setCharPref(SERVER_PREF, this.#originalServerPrefValue); + } + + this.#log.info("Server is now stopped"); + } + + /** + * Adds remote settings records to the server. Records may have attachments; + * see the param doc below. + * + * @param {object} options + * @param {string} options.bucket + * @param {string} options.collection + * @param {Array} options.records + * Each object in this array should be a realistic remote settings record + * with the following exceptions: + * + * - `record.id` will be generated if it's undefined. + * - `record.last_modified` will be set to the `#lastModified` property of + * the server if it's undefined. + * - `record.attachment`, if defined, should be the attachment itself and + * not its metadata. The server will automatically create some dummy + * metadata. Currently the only supported attachment type is plain + * JSON'able objects that the server will convert to JSON in responses. + */ + async addRecords({ bucket = "main", collection = "test", records }) { + this.#log.debug( + "Adding records: " + + JSON.stringify({ bucket, collection, records }, null, 2) + ); + + this.#lastModified++; + + let key = this.#recordsKey(bucket, collection); + let allRecords = this.#records.get(key); + if (!allRecords) { + allRecords = []; + this.#records.set(key, allRecords); + } + + for (let record of records) { + let copy = { ...record }; + if (!copy.hasOwnProperty("id")) { + copy.id = String(this.#nextRecordId++); + } + if (!copy.hasOwnProperty("last_modified")) { + copy.last_modified = this.#lastModified; + } + if (copy.attachment) { + await this.#addAttachment({ bucket, collection, record: copy }); + } + allRecords.push(copy); + } + + this.#log.debug( + "Done adding records. All records are now: " + + JSON.stringify([...this.#records.entries()], null, 2) + ); + } + + /** + * Marks records as deleted. Deleted records will still be returned in + * responses, but they'll have a `deleted = true` property. Their attachments + * will be deleted immediately, however. + * + * @param {object} filter + * If null, all records will be marked as deleted. Otherwise only records + * that match the filter will be marked as deleted. For a given record, each + * value in the filter object will be compared to the value with the same + * key in the record. If all values are the same, the record will be + * removed. Examples: + * + * To remove remove records whose `type` key has the value "data": + * `{ type: "data" } + * + * To remove remove records whose `type` key has the value "data" and whose + * `last_modified` key has the value 1234: + * `{ type: "data", last_modified: 1234 } + */ + removeRecords(filter = null) { + this.#log.debug("Removing records: " + JSON.stringify({ filter })); + + this.#lastModified++; + + for (let [recordsKey, records] of this.#records.entries()) { + for (let record of records) { + if ( + !filter || + Object.entries(filter).every( + ([filterKey, filterValue]) => + record.hasOwnProperty(filterKey) && + record[filterKey] == filterValue + ) + ) { + if (record.attachment) { + let attachmentKey = `${recordsKey}/${record.attachment.filename}`; + this.#attachments.delete(attachmentKey); + } + record.deleted = true; + record.last_modified = this.#lastModified; + } + } + } + + this.#log.debug( + "Done removing records. All records are now: " + + JSON.stringify([...this.#records.entries()], null, 2) + ); + } + + /** + * Removes all existing records and adds the given records to the server. + * + * @param {object} options + * @param {string} options.bucket + * @param {string} options.collection + * @param {Array} options.records + * See `addRecords()`. + */ + async setRecords({ bucket = "main", collection = "test", records }) { + this.#log.debug("Setting records"); + + this.removeRecords(); + await this.addRecords({ bucket, collection, records }); + + this.#log.debug("Done setting records"); + } + + /** + * `nsIHttpRequestHandler` callback from the backing server. Handles a + * request. + * + * @param {nsIHttpRequest} request + * @param {nsIHttpResponse} response + */ + handle(request, response) { + this.#logRequest(request); + + // Get the route that matches the request path. + let { match, route } = this.#getRoute(request.path) || {}; + if (!route) { + this.#prepareError({ request, response, error: lazy.HTTP_404 }); + return; + } + + let respInfo = route.response(match, request, response); + if (respInfo instanceof lazy.HttpError) { + this.#prepareError({ request, response, error: respInfo }); + } else { + this.#prepareResponse({ ...respInfo, request, response }); + } + } + + /** + * @returns {Array} + * The routes handled by the server. Each item in this array is an object + * with the following properties that describes one or more paths and the + * response that should be sent when a request is made on those paths: + * + * {string} spec + * A path spec. This is required unless `specs` is defined. To determine + * which route should be used for a given request, the server will check + * each route's spec(s) until it finds the first that matches the + * request's path. A spec is just a path whose components can be variables + * that start with "$". When a spec with variables matches a request path, + * the `match` object passed to the route's `response` function will map + * from variable names to the corresponding components in the path. + * {Array} specs + * An array of path spec strings. Use this instead of `spec` if the route + * handles more than one. + * {function} response + * A function that will be called when the route matches a request. It is + * called as: `response(match, request, response)` + * + * {object} match + * An object mapping variable names in the spec to their matched + * components in the path. See `#match()` for details. + * {nsIHttpRequest} request + * {nsIHttpResponse} response + * + * The function must return one of the following: + * + * {object} + * An object that describes the response with the following properties: + * {object} body + * A plain JSON'able object. The server will convert this to JSON and + * set it to the response body. + * {HttpError} + * An `HttpError` instance defined in `httpd.sys.mjs`. + */ + get #routes() { + return [ + { + spec: "/v1", + response: () => ({ + body: { + capabilities: { + attachments: { + base_url: this.#url.toString(), + }, + }, + }, + }), + }, + + { + spec: "/v1/buckets/monitor/collections/changes/changeset", + response: () => ({ + body: { + timestamp: this.#lastModified, + changes: [ + { + last_modified: this.#lastModified, + }, + ], + }, + }), + }, + + { + spec: "/v1/buckets/$bucket/collections/$collection/changeset", + response: ({ bucket, collection }) => { + let records = this.#getRecords(bucket, collection); + return !records + ? lazy.HTTP_404 + : { + body: { + metadata: null, + timestamp: this.#lastModified, + changes: records, + }, + }; + }, + }, + + { + spec: "/v1/buckets/$bucket/collections/$collection/records", + response: ({ bucket, collection }) => { + let records = this.#getRecords(bucket, collection); + return !records + ? lazy.HTTP_404 + : { + body: { + data: records, + }, + }; + }, + }, + + { + specs: [ + // The Rust remote settings client doesn't include "v1" in attachment + // URLs, but the JS client does. + "/attachments/$bucket/$collection/$filename", + "/v1/attachments/$bucket/$collection/$filename", + ], + response: ({ bucket, collection, filename }) => { + return { + body: this.#getAttachment(bucket, collection, filename), + }; + }, + }, + ]; + } + + /** + * @returns {object} + * Default response headers. + */ + get #responseHeaders() { + return { + "Access-Control-Allow-Origin": "*", + "Access-Control-Expose-Headers": + "Retry-After, Content-Length, Alert, Backoff", + Server: "waitress", + Etag: `"${this.#lastModified}"`, + }; + } + + /** + * Returns the route that matches a request path. + * + * @param {string} path + * A request path. + * @returns {object} + * If no route matches the path, returns an empty object. Otherwise returns + * an object with the following properties: + * + * {object} match + * An object describing the matched variables in the route spec. See + * `#match()` for details. + * {object} route + * The matched route. See `#routes` for details. + */ + #getRoute(path) { + for (let route of this.#routes) { + let specs = route.specs || [route.spec]; + for (let spec of specs) { + let match = this.#match(path, spec); + if (match) { + return { match, route }; + } + } + } + return {}; + } + + /** + * Matches a request path to a route spec. + * + * @param {string} path + * A request path. + * @param {string} spec + * A route spec. See `#routes` for details. + * @returns {object|null} + * If the spec doesn't match the path, returns null. Otherwise returns an + * object mapping variable names in the spec to their matched components in + * the path. Example: + * + * path : "/main/myfeature/foo" + * spec : "/$bucket/$collection/foo" + * returns: `{ bucket: "main", collection: "myfeature" }` + */ + #match(path, spec) { + let pathParts = path.split("/"); + let specParts = spec.split("/"); + + if (pathParts.length != specParts.length) { + // If the path has only one more part than the spec and its last part is + // empty, then the path ends in a trailing slash but the spec does not. + // Consider that a match. Otherwise return null for no match. + if ( + pathParts[pathParts.length - 1] || + pathParts.length != specParts.length + 1 + ) { + return null; + } + pathParts.pop(); + } + + let match = {}; + for (let i = 0; i < pathParts.length; i++) { + let pathPart = pathParts[i]; + let specPart = specParts[i]; + if (specPart.startsWith("$")) { + match[specPart.substring(1)] = pathPart; + } else if (pathPart != specPart) { + return null; + } + } + + return match; + } + + #getRecords(bucket, collection) { + return this.#records.get(this.#recordsKey(bucket, collection)); + } + + #recordsKey(bucket, collection) { + return `${bucket}/${collection}`; + } + + /** + * Registers an attachment for a record. + * + * @param {object} options + * @param {string} options.bucket + * @param {string} options.collection + * @param {object} options.record + * The record should have an `attachment` property as described in + * `addRecords()`. + */ + async #addAttachment({ bucket, collection, record }) { + let { attachment } = record; + let filename = record.id; + + this.#attachments.set( + this.#attachmentsKey(bucket, collection, filename), + attachment + ); + + let encoder = new TextEncoder(); + let bytes = encoder.encode(JSON.stringify(attachment)); + + let hashBuffer = await crypto.subtle.digest("SHA-256", bytes); + let hashBytes = new Uint8Array(hashBuffer); + let toHex = b => b.toString(16).padStart(2, "0"); + let hash = Array.from(hashBytes, toHex).join(""); + + // Replace `record.attachment` with appropriate metadata in order to conform + // with the remote settings API. + record.attachment = { + hash, + filename, + mimetype: "application/json; charset=UTF-8", + size: bytes.length, + location: `attachments/${bucket}/${collection}/${filename}`, + }; + } + + #attachmentsKey(bucket, collection, filename) { + return `${bucket}/${collection}/${filename}`; + } + + #getAttachment(bucket, collection, filename) { + return this.#attachments.get( + this.#attachmentsKey(bucket, collection, filename) + ); + } + + /** + * Prepares an HTTP response. + * + * @param {object} options + * @param {nsIHttpRequest} options.request + * @param {nsIHttpResponse} options.response + * @param {object|null} options.body + * Currently only JSON'able objects are supported. They will be converted to + * JSON in the response. + * @param {integer} options.status + * @param {string} options.statusText + */ + #prepareResponse({ + request, + response, + body = null, + status = 200, + statusText = "OK", + }) { + let headers = { ...this.#responseHeaders }; + if (body) { + headers["Content-Type"] = "application/json; charset=UTF-8"; + } + + this.#logResponse({ request, status, statusText, headers, body }); + + for (let [name, value] of Object.entries(headers)) { + response.setHeader(name, value, false); + } + if (body) { + response.write(JSON.stringify(body)); + } + response.setStatusLine(request.httpVersion, status, statusText); + } + + /** + * Prepares an HTTP error response. + * + * @param {object} options + * @param {nsIHttpRequest} options.request + * @param {nsIHttpResponse} options.response + * @param {HttpError} options.error + * An `HttpError` instance defined in `httpd.sys.mjs`. + */ + #prepareError({ request, response, error }) { + this.#prepareResponse({ + request, + response, + status: error.code, + statusText: error.description, + }); + } + + /** + * Logs a request. + * + * @param {nsIHttpRequest} request + */ + #logRequest(request) { + let pathAndQuery = request.path; + if (request.queryString) { + pathAndQuery += "?" + request.queryString; + } + this.#log.info( + `< HTTP ${request.httpVersion} ${request.method} ${pathAndQuery}` + ); + for (let name of request.headers) { + this.#log.debug(`${name}: ${request.getHeader(name.toString())}`); + } + } + + /** + * Logs a response. + * + * @param {object} options + * @param {nsIHttpRequest} options.request + * The associated request. + * @param {integer} options.status + * The HTTP status code of the response. + * @param {string} options.statusText + * The description of the status code. + * @param {object} options.headers + * An object mapping from response header names to values. + * @param {object} options.body + * The response body, if any. + */ + #logResponse({ request, status, statusText, headers, body }) { + this.#log.info(`> ${status} ${request.path}`); + for (let [name, value] of Object.entries(headers)) { + this.#log.debug(`${name}: ${value}`); + } + if (body) { + this.#log.debug("Response body: " + JSON.stringify(body, null, 2)); + } + } + + // records key (see `#recordsKey()`) -> array of record objects + #records = new Map(); + + // attachments key (see `#attachmentsKey()`) -> attachment object + #attachments = new Map(); + + #log; + #server; + #originalServerPrefValue; + #url = null; + #lastModified = 1368273600000; + #nextRecordId = 1; +} diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser.toml b/browser/components/urlbar/tests/quicksuggest/browser/browser.toml new file mode 100644 index 0000000000..a77d26c2a6 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser.toml @@ -0,0 +1,68 @@ +# 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", +] +prefs = ["browser.bookmarks.testing.skipDefaultBookmarksImport=true"] + +["browser_quicksuggest.js"] + +["browser_quicksuggest_addons.js"] + +["browser_quicksuggest_block.js"] + +["browser_quicksuggest_configuration.js"] + +["browser_quicksuggest_indexes.js"] + +["browser_quicksuggest_mdn.js"] + +["browser_quicksuggest_merinoSessions.js"] + +["browser_quicksuggest_onboardingDialog.js"] +skip-if = ["os == 'linux' && bits == 64"] # Bug 1773830 + +["browser_quicksuggest_pocket.js"] +tags = "search-telemetry" + +["browser_quicksuggest_yelp.js"] + +["browser_telemetry_dynamicWikipedia.js"] +tags = "search-telemetry" +skip-if = ["true"] # Bug 1880214 + +["browser_telemetry_gleanEmptyStrings.js"] +tags = "search-telemetry" +skip-if = ["true"] # Bug 1880214 + +["browser_telemetry_impressionEdgeCases.js"] +tags = "search-telemetry" +skip-if = ["true"] # Bug 1880214 + +["browser_telemetry_navigationalSuggestions.js"] +tags = "search-telemetry" +skip-if = ["true"] # Bug 1880214 + +["browser_telemetry_nonsponsored.js"] +tags = "search-telemetry" +skip-if = ["true"] # Bug 1880214 + +["browser_telemetry_other.js"] +tags = "search-telemetry" +skip-if = ["true"] # Bug 1880214 + +["browser_telemetry_sponsored.js"] +tags = "search-telemetry" +skip-if = ["true"] # Bug 1880214 + +["browser_telemetry_weather.js"] +tags = "search-telemetry" +skip-if = ["true"] # Bug 1880214 + +["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..130afe8c53 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest.js @@ -0,0 +1,166 @@ +/* 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", + iab_category: "22 - Shopping", + icon: "1234", + }, + { + 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: "Wikipedia", + iab_category: "5 - Education", + icon: "1234", + }, +]; + +add_setup(async function () { + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + await UrlbarTestUtils.formHistory.clear(); + + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + remoteSettingsRecords: [ + { + type: "data", + attachment: REMOTE_SETTINGS_RESULTS, + }, + ], + }); +}); + +// Tests a sponsored result and keyword highlighting. +add_tasks_with_rust(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_tasks_with_rust(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); +}); + +// Tests sponsored priority feature. +add_tasks_with_rust(async function sponsoredPriority() { + const cleanUpNimbus = await UrlbarTestUtils.initNimbusFeature({ + quickSuggestSponsoredPriority: true, + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "fra", + }); + await QuickSuggestTestUtils.assertIsQuickSuggest({ + window, + index: 1, + isSponsored: true, + isBestMatch: 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." + ); + + // Group label. + let before = window.getComputedStyle(row, "::before"); + Assert.equal(before.content, "attr(label)", "::before.content is enabled"); + Assert.equal( + row.getAttribute("label"), + "Top pick", + "Row has 'Top pick' group label" + ); + + await UrlbarTestUtils.promisePopupClose(window); + await cleanUpNimbus(); +}); + +// Tests sponsored priority feature does not affect to non-sponsored suggestion. +add_tasks_with_rust( + async function sponsoredPriorityButNotSponsoredSuggestion() { + const cleanUpNimbus = await UrlbarTestUtils.initNimbusFeature({ + quickSuggestSponsoredPriority: true, + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "nonspon", + }); + await QuickSuggestTestUtils.assertIsQuickSuggest({ + window, + index: 1, + isSponsored: false, + url: `${TEST_URL}?q=nonsponsored`, + }); + + let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1); + let before = window.getComputedStyle(row, "::before"); + Assert.equal(before.content, "attr(label)", "::before.content is enabled"); + Assert.equal( + row.getAttribute("label"), + "Firefox Suggest", + "Row has general group label for quick suggest" + ); + + await UrlbarTestUtils.promisePopupClose(window); + await cleanUpNimbus(); + } +); 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..b09345aa54 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_addons.js @@ -0,0 +1,443 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for addon suggestions. + +// The expected index of the addon suggestion. +const EXPECTED_RESULT_INDEX = 1; + +// Allow more time for TV runs. +requestLongerTimeout(5); + +// TODO: Firefox no longer uses `rating` and `number_of_ratings` but they are +// still present in Merino and RS suggestions, so they are included here for +// greater accuracy. We should remove them from Merino, RS, and tests. +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: [ + // Disable search suggestions so we don't hit the network. + ["browser.search.suggest.enabled", false], + ], + }); + + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + merinoSuggestions: TEST_MERINO_SUGGESTIONS, + }); +}); + +add_task(async function basic() { + 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-favicon"); + Assert.equal(icon.src, merinoSuggestion.icon); + const url = row.querySelector(".urlbarView-url"); + const expectedUrl = makeExpectedUrl(merinoSuggestion.url); + const displayUrl = expectedUrl.replace(/^https:\/\//, ""); + Assert.equal(url.textContent, displayUrl); + const title = row.querySelector(".urlbarView-title"); + Assert.equal(title.textContent, merinoSuggestion.title); + const description = row.querySelector(".urlbarView-row-body-description"); + Assert.equal(description.textContent, merinoSuggestion.description); + const bottom = row.querySelector(".urlbarView-row-body-bottom"); + Assert.equal(bottom.textContent, "Recommended"); + Assert.ok( + BrowserTestUtils.isVisible( + row.querySelector(".urlbarView-title-separator") + ), + "The title separator should be visible" + ); + + Assert.equal(result.suggestedIndex, 1); + + const onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + expectedUrl + ); + EventUtils.synthesizeMouseAtCenter(row, {}); + await onLoad; + Assert.ok(true, "Expected page is loaded"); + + await PlacesUtils.history.clear(); + } +}); + +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.showLessFrequentlyCount", 0]], + }); + + await QuickSuggestTestUtils.setConfig({ + show_less_frequently_cap: 3, + }); + + 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); + + // The cap will be reached this time. Keep the view open so we can make sure + // the command has been removed from the menu before it closes. + await doShowLessFrequently({ + keepViewOpen: true, + input: "aaa b", + expected: { + isSuggestionShown: true, + isMenuItemShown: true, + }, + }); + Assert.equal(UrlbarPrefs.get("addons.showLessFrequentlyCount"), 3); + + // Make sure the command has been removed. + let menuitem = await UrlbarTestUtils.openResultMenuAndGetItem({ + window, + command: "show_less_frequently", + resultIndex: EXPECTED_RESULT_INDEX, + openByMouse: true, + }); + Assert.ok(!menuitem, "Menuitem should be absent before closing the view"); + gURLBar.view.resultMenu.hidePopup(true); + await UrlbarTestUtils.promisePopupClose(window); + + 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 QuickSuggestTestUtils.setConfig(QuickSuggestTestUtils.DEFAULT_CONFIG); + await SpecialPowers.popPrefEnv(); +}); + +// Tests the "Not interested" result menu dismissal command. +add_task(async function resultMenu_notInterested() { + await doDismissTest("not_interested", true); +}); + +// Tests the "Not relevant" result menu dismissal command. +add_task(async function notRelevant() { + await doDismissTest("not_relevant", false); +}); + +// Tests the row/group label. +add_task(async function rowLabel() { + 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"), "Firefox extension"); + + await UrlbarTestUtils.promisePopupClose(window); +}); + +async function doShowLessFrequently({ input, expected, keepViewOpen = false }) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: input, + }); + + if (!expected.isSuggestionShown) { + Assert.ok( + !(await getAddonResultDetails()), + "Addons suggestion should be absent" + ); + return; + } + + const details = await getAddonResultDetails(); + Assert.ok( + details, + `Addons suggestion should be present at expected index after ${input} search` + ); + + // Click the command. + try { + await UrlbarTestUtils.openResultMenuAndClickItem( + window, + "show_less_frequently", + { + resultIndex: EXPECTED_RESULT_INDEX, + } + ); + 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" + ); + } + + if (!keepViewOpen) { + await UrlbarTestUtils.promisePopupClose(window); + } +} + +async function doDismissTest(command, allDismissed) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "123", + }); + + const resultCount = UrlbarTestUtils.getResultCount(window); + let details = await getAddonResultDetails(); + Assert.ok(details, "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: EXPECTED_RESULT_INDEX, openByMouse: true } + ); + + Assert.equal( + UrlbarPrefs.get("suggest.addons"), + !allDismissed, + "suggest.addons should be true iff all suggestions weren't dismissed" + ); + Assert.equal( + await QuickSuggest.blockedSuggestions.has( + details.result.payload.originalUrl + ), + !allDismissed, + "Suggestion URL should be blocked iff all suggestions weren't dismissed" + ); + + // 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, + EXPECTED_RESULT_INDEX + ); + 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" + ); + + // Check tip title. + let title = details.element.row.querySelector(".urlbarView-title"); + let titleL10nId = title.dataset.l10nId; + if (allDismissed) { + Assert.equal(titleL10nId, "firefox-suggest-dismissal-acknowledgment-all"); + } else { + Assert.equal(titleL10nId, "firefox-suggest-dismissal-acknowledgment-one"); + } + + // Get the dismissal acknowledgment's "Got it" button and click it. + let gotItButton = UrlbarTestUtils.getButtonForResultIndex( + window, + 0, + EXPECTED_RESULT_INDEX + ); + 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 && + !isAddonResult(details.result), + "Tip result and addon result should not be present" + ); + } + + await UrlbarTestUtils.promisePopupClose(window); + + UrlbarPrefs.clear("suggest.addons"); + await QuickSuggest.blockedSuggestions.clear(); +} + +function makeExpectedUrl(originalUrl) { + let url = new URL(originalUrl); + url.searchParams.set("utm_medium", "firefox-desktop"); + url.searchParams.set("utm_source", "firefox-suggest"); + return url.href; +} + +async function getAddonResultDetails() { + for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) { + const details = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + if (isAddonResult(details.result)) { + return details; + } + } + return null; +} + +function isAddonResult(result) { + return ["AddonSuggestions", "amo"].includes(result.payload.provider); +} 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..c400cf72f6 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_block.js @@ -0,0 +1,252 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests quick suggest dismissals ("blocks"). + +"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", + icon: "1234", + }, + { + 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: "Wikipedia", + iab_category: "5 - Education", + icon: "1234", + }, +]; + +add_setup(async function () { + 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({ + remoteSettingsRecords: [ + { + type: "data", + attachment: REMOTE_SETTINGS_RESULTS, + }, + ], + }); +}); + +// Picks the dismiss command in the result menu. +add_tasks_with_rust(async function basic() { + await doBasicBlockTest({ + block: async () => { + await UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "D", { + resultIndex: 1, + }); + }, + }); +}); + +// Uses the key shortcut to block a suggestion. +add_tasks_with_rust(async function basic_keyShortcut() { + await doBasicBlockTest({ + block: () => { + // Arrow down once to select the row. + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_Delete", { shiftKey: true }); + }, + }); +}); + +async function doBasicBlockTest({ block }) { + for (let result of REMOTE_SETTINGS_RESULTS) { + info("Doing basic block test with result: " + JSON.stringify({ result })); + await doOneBasicBlockTest({ result, block }); + } +} + +async function doOneBasicBlockTest({ result, block }) { + let index = 2; + let suggested_index = -1; + let suggested_index_relative_to_group = true; + let match_type = "firefox-suggest"; + let isSponsored = result.iab_category != "5 - Education"; + let expectedBlockId = + UrlbarPrefs.get("quicksuggest.rustEnabled") && !isSponsored + ? null + : result.id; + + let pingsSubmitted = 0; + GleanPings.quickSuggest.testBeforeNextSubmit(() => { + pingsSubmitted++; + // First ping's an impression. + Assert.equal( + Glean.quickSuggest.pingType.testGetValue(), + CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION + ); + Assert.equal(Glean.quickSuggest.matchType.testGetValue(), match_type); + Assert.equal(Glean.quickSuggest.blockId.testGetValue(), expectedBlockId); + Assert.equal(Glean.quickSuggest.isClicked.testGetValue(), false); + Assert.equal(Glean.quickSuggest.position.testGetValue(), index); + Assert.equal( + Glean.quickSuggest.suggestedIndex.testGetValue(), + suggested_index + ); + Assert.equal( + Glean.quickSuggest.suggestedIndexRelativeToGroup.testGetValue(), + suggested_index_relative_to_group + ); + Assert.equal(Glean.quickSuggest.position.testGetValue(), index); + GleanPings.quickSuggest.testBeforeNextSubmit(() => { + pingsSubmitted++; + // Second ping's a block. + Assert.equal( + Glean.quickSuggest.pingType.testGetValue(), + CONTEXTUAL_SERVICES_PING_TYPES.QS_BLOCK + ); + Assert.equal(Glean.quickSuggest.matchType.testGetValue(), match_type); + Assert.equal(Glean.quickSuggest.blockId.testGetValue(), expectedBlockId); + Assert.equal( + Glean.quickSuggest.iabCategory.testGetValue(), + result.iab_category + ); + Assert.equal(Glean.quickSuggest.position.testGetValue(), index); + }); + }); + + // 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)" + ); + + await QuickSuggestTestUtils.assertIsQuickSuggest({ + window, + 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 Glean. + Assert.equal(pingsSubmitted, 2, "Both Glean pings submitted."); + + // Check telemetry scalars. + 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; + } + QuickSuggestTestUtils.assertScalars(scalars); + + // Check the engagement event. + QuickSuggestTestUtils.assertEvents([ + { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "engagement", + object: "block", + extra: { + match_type, + position: String(index), + suggestion_type: isSponsored ? "sponsored" : "nonsponsored", + }, + }, + ]); + + await UrlbarTestUtils.promisePopupClose(window); + await QuickSuggest.blockedSuggestions.clear(); +} + +// Blocks multiple suggestions one after the other. +add_tasks_with_rust(async function blockMultiple() { + 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, + originalUrl: url, + isSponsored: keywords[0] == "sponsored", + }); + + // Block it. + await UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "D", { + resultIndex: 1, + }); + 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(); +}); 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..d9a4345898 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_configuration.js @@ -0,0 +1,2099 @@ +/* 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: { + merinoEndpointURL: "http://example.com/test_merino_config", + merinoClientVariants: "test-client-variants", + merinoProviders: "test-providers", + }, + callback: () => { + 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..713df1ec02 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_indexes.js @@ -0,0 +1,410 @@ +/* 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 = [ + QuickSuggestTestUtils.ampRemoteSettings({ + keywords: [SPONSORED_SEARCH_STRING], + }), + QuickSuggestTestUtils.wikipediaRemoteSettings({ + keywords: [NON_SPONSORED_SEARCH_STRING], + }), +]; + +// Trying to avoid timeouts. +requestLongerTimeout(3); + +add_setup(async function () { + 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({ + remoteSettingsRecords: [ + { + 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 + ? REMOTE_SETTINGS_RESULTS[0].url + : REMOTE_SETTINGS_RESULTS[1].url, + }); + + 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_mdn.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_mdn.js new file mode 100644 index 0000000000..b7da7533c4 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_mdn.js @@ -0,0 +1,230 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for mdn suggestions. + +const REMOTE_SETTINGS_DATA = [ + { + type: "mdn-suggestions", + attachment: [ + { + url: "https://example.com/array-filter", + title: "Array.prototype.filter()", + description: + "The filter() method creates a shallow copy of a portion of a given array, filtered down to just the elements from the given array that pass the test implemented by the provided function.", + keywords: ["array"], + score: 0.24, + }, + ], + }, +]; + +add_setup(async function () { + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + remoteSettingsRecords: REMOTE_SETTINGS_DATA, + }); +}); + +add_tasks_with_rust(async function basic() { + const suggestion = REMOTE_SETTINGS_DATA[0].attachment[0]; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: suggestion.keywords[0], + }); + Assert.equal(UrlbarTestUtils.getResultCount(window), 2); + + const { element, result } = await UrlbarTestUtils.getDetailsOfResultAt( + window, + 1 + ); + Assert.equal( + result.providerName, + UrlbarProviderQuickSuggest.name, + "The result should be from the expected provider" + ); + Assert.equal( + result.payload.provider, + UrlbarPrefs.get("quickSuggestRustEnabled") ? "Mdn" : "MDNSuggestions" + ); + + const onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + result.payload.url + ); + EventUtils.synthesizeMouseAtCenter(element.row, {}); + await onLoad; + Assert.ok(true, "Expected page is loaded"); + + await PlacesUtils.history.clear(); +}); + +// Tests the row/group label. +add_tasks_with_rust(async function rowLabel() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: REMOTE_SETTINGS_DATA[0].attachment[0].keywords[0], + }); + Assert.equal(UrlbarTestUtils.getResultCount(window), 2); + + const { element } = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + const row = element.row; + Assert.equal(row.getAttribute("label"), "Recommended resource"); + + await UrlbarTestUtils.promisePopupClose(window); +}); + +add_tasks_with_rust(async function disable() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.mdn.featureGate", false]], + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "array", + }); + Assert.equal(UrlbarTestUtils.getResultCount(window), 1); + + const { result } = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal(result.providerName, "HeuristicFallback"); + + await SpecialPowers.popPrefEnv(); + await QuickSuggestTestUtils.forceSync(); +}); + +// Tests the "Not interested" result menu dismissal command. +add_tasks_with_rust(async function resultMenu_notInterested() { + await doDismissTest("not_interested"); + + Assert.equal(UrlbarPrefs.get("suggest.mdn"), false); + const exists = await QuickSuggest.blockedSuggestions.has( + REMOTE_SETTINGS_DATA[0].attachment[0].url + ); + Assert.ok(!exists); + + // Re-enable suggestions and wait until MDNSuggestions syncs them from + // remote settings again. + UrlbarPrefs.set("suggest.mdn", true); + await QuickSuggestTestUtils.forceSync(); +}); + +// Tests the "Not relevant" result menu dismissal command. +add_tasks_with_rust(async function notRelevant() { + await doDismissTest("not_relevant"); + + Assert.equal(UrlbarPrefs.get("suggest.mdn"), true); + const exists = await QuickSuggest.blockedSuggestions.has( + REMOTE_SETTINGS_DATA[0].attachment[0].url + ); + Assert.ok(exists); + + await QuickSuggest.blockedSuggestions.clear(); +}); + +async function doDismissTest(command) { + const keyword = REMOTE_SETTINGS_DATA[0].attachment[0].keywords[0]; + // Do a search. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: keyword, + }); + + // Check the result. + const resultCount = 2; + Assert.equal( + UrlbarTestUtils.getResultCount(window), + resultCount, + "There should be two results" + ); + + const resultIndex = 1; + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex); + Assert.equal( + details.result.providerName, + UrlbarProviderQuickSuggest.name, + "The result should be from the expected provider" + ); + Assert.equal( + details.result.payload.telemetryType, + "mdn", + "The result should be a MDN result" + ); + + // Click the command. + await UrlbarTestUtils.openResultMenuAndClickItem( + window, + ["[data-l10n-id=firefox-suggest-command-dont-show-mdn]", command], + { resultIndex, openByMouse: true } + ); + + // 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. + const 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.telemetryType !== "mdn", + "Tip result and suggestion should not be present" + ); + } + + gURLBar.handleRevert(); + + // Do the search again. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: keyword, + }); + 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.telemetryType !== "mdn", + "Tip result and suggestion should not be present" + ); + } + + await UrlbarTestUtils.promisePopupClose(window); +} 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..eab63f4c9e --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_merinoSessions.js @@ -0,0 +1,138 @@ +/* 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.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..6256a5aec2 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_onboardingDialog.js @@ -0,0 +1,1569 @@ +/* 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(); + + // Ensure the test remote settings server is set up. This test doesn't trigger + // any suggestions but it enables Suggest, which will attempt to sync from + // remote settings. + await QuickSuggestTestUtils.ensureQuickSuggestInit(); +}); + +// 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" + ); + }, + }); +}); + +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", + "onboardingDialog", + "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", + "onboardingDialog", + "onboardingAccept", + ], + acceptFocusOrder: [ + "onboardingAccept", + "onboardingLearnMore", + "onboardingSubmit", + "onboardingSkipLink", + "onboardingDialog", + "onboardingAccept", + ], + rejectFocusOrder: [ + "onboardingReject", + "onboardingSubmit", + "onboardingSkipLink", + "onboardingDialog", + "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", + "onboardingDialog", + "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", + "onboardingDialog", + "onboardingLearnMore", + ], + acceptFocusOrder: [ + "onboardingAccept", + "onboardingSubmit", + "onboardingSkipLink", + "onboardingDialog", + "onboardingLearnMore", + "onboardingAccept", + ], + rejectFocusOrder: [ + "onboardingReject", + "onboardingSubmit", + "onboardingSkipLink", + "onboardingDialog", + "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", + "onboardingDialog", + "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.isVisible(introductionSection)); + Assert.ok(BrowserTestUtils.isHidden(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.isHidden(introductionSection) && + BrowserTestUtils.isVisible(mainSection) + ); + } else { + info("Check the section visibility"); + Assert.ok(BrowserTestUtils.isHidden(introductionSection)); + Assert.ok(BrowserTestUtils.isVisible(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.isVisible(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.isVisible(element)); + } else { + if (!element) { + Assert.ok(true); + return; + } + Assert.ok(BrowserTestUtils.isHidden(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.isVisible(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.isVisible(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.isHidden(introductionSection) && + BrowserTestUtils.isVisible(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.isVisible(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.isVisible(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.isHidden(introductionSection) && + BrowserTestUtils.isVisible(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_quicksuggest_pocket.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_pocket.js new file mode 100644 index 0000000000..0064b6a297 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_pocket.js @@ -0,0 +1,435 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Browser tests for Pocket suggestions. +// +// TODO: Make this work with Rust enabled. Right now, running this test with +// Rust hits the following error on ingest, which prevents ingest from finishing +// successfully: +// +// 0:03.17 INFO Console message: [JavaScript Error: "1698289045697 urlbar ERROR QuickSuggest.SuggestBackendRust :: Ingest error: Error executing SQL: FOREIGN KEY constraint failed" {file: "resource://gre/modules/Log.sys.mjs" line: 722}] + +// The expected index of the Pocket suggestion. +const EXPECTED_RESULT_INDEX = 1; + +const REMOTE_SETTINGS_DATA = [ + { + type: "pocket-suggestions", + attachment: [ + { + url: "https://example.com/pocket-suggestion", + title: "Pocket Suggestion", + description: "Pocket description", + lowConfidenceKeywords: ["pocket suggestion"], + highConfidenceKeywords: ["high"], + score: 0.25, + }, + ], + }, +]; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + // Disable search suggestions so we don't hit the network. + ["browser.search.suggest.enabled", false], + ], + }); + + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + remoteSettingsRecords: REMOTE_SETTINGS_DATA, + prefs: [ + ["suggest.quicksuggest.nonsponsored", true], + ["pocket.featureGate", true], + ], + }); +}); + +add_task(async function basic() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + // Do a search. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "pocket suggestion", + }); + + // Check the result. + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "There should be two results" + ); + + let { element, result } = await UrlbarTestUtils.getDetailsOfResultAt( + window, + 1 + ); + Assert.equal( + result.providerName, + UrlbarProviderQuickSuggest.name, + "The result should be from the expected provider" + ); + Assert.equal( + result.payload.telemetryType, + "pocket", + "The result should be a Pocket result" + ); + + // Click it. + const onLoad = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + EventUtils.synthesizeMouseAtCenter(element.row, {}); + await onLoad; + // Append utm parameters. + let url = new URL(REMOTE_SETTINGS_DATA[0].attachment[0].url); + url.searchParams.set("utm_medium", "firefox-desktop"); + url.searchParams.set("utm_source", "firefox-suggest"); + url.searchParams.set( + "utm_campaign", + "pocket-collections-in-the-address-bar" + ); + url.searchParams.set("utm_content", "treatment"); + + Assert.equal(gBrowser.currentURI.spec, url.href, "Expected page loaded"); + }); +}); + +// Tests the "Show less frequently" command. +add_task(async function resultMenu_showLessFrequently() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.pocket.featureGate", true], + ["browser.urlbar.pocket.showLessFrequentlyCount", 0], + ], + }); + await QuickSuggestTestUtils.forceSync(); + + await QuickSuggestTestUtils.setConfig({ + show_less_frequently_cap: 3, + }); + + await doShowLessFrequently({ + input: "pocket s", + expected: { + isSuggestionShown: true, + isMenuItemShown: true, + }, + }); + Assert.equal(UrlbarPrefs.get("pocket.showLessFrequentlyCount"), 1); + + await doShowLessFrequently({ + input: "pocket s", + expected: { + isSuggestionShown: true, + isMenuItemShown: true, + }, + }); + Assert.equal(UrlbarPrefs.get("pocket.showLessFrequentlyCount"), 2); + + // The cap will be reached this time. Keep the view open so we can make sure + // the command has been removed from the menu before it closes. + await doShowLessFrequently({ + keepViewOpen: true, + input: "pocket s", + expected: { + isSuggestionShown: true, + isMenuItemShown: true, + }, + }); + Assert.equal(UrlbarPrefs.get("pocket.showLessFrequentlyCount"), 3); + + // Make sure the command has been removed. + let menuitem = await UrlbarTestUtils.openResultMenuAndGetItem({ + window, + command: "show_less_frequently", + resultIndex: EXPECTED_RESULT_INDEX, + openByMouse: true, + }); + Assert.ok(!menuitem, "Menuitem should be absent before closing the view"); + gURLBar.view.resultMenu.hidePopup(true); + await UrlbarTestUtils.promisePopupClose(window); + + await doShowLessFrequently({ + input: "pocket s", + expected: { + isSuggestionShown: false, + }, + }); + + await doShowLessFrequently({ + input: "pocket su", + expected: { + isSuggestionShown: true, + isMenuItemShown: false, + }, + }); + + await QuickSuggestTestUtils.setConfig(QuickSuggestTestUtils.DEFAULT_CONFIG); + await SpecialPowers.popPrefEnv(); + await QuickSuggestTestUtils.forceSync(); +}); + +async function doShowLessFrequently({ input, expected, keepViewOpen = false }) { + 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.telemetryType, + "pocket", + `Pocket suggestion should be absent (checking index ${i})` + ); + } + + return; + } + + const details = await UrlbarTestUtils.getDetailsOfResultAt( + window, + EXPECTED_RESULT_INDEX + ); + Assert.equal( + details.result.payload.telemetryType, + "pocket", + `Pocket suggestion should be present at expected index after ${input} search` + ); + + // Click the command. + try { + await UrlbarTestUtils.openResultMenuAndClickItem( + window, + "show_less_frequently", + { + resultIndex: EXPECTED_RESULT_INDEX, + } + ); + 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" + ); + } + + if (!keepViewOpen) { + await UrlbarTestUtils.promisePopupClose(window); + } +} + +// Tests the "Not interested" result menu dismissal command. +add_task(async function resultMenu_notInterested() { + await doDismissTest("not_interested"); + + // Re-enable suggestions and wait until PocketSuggestions syncs them from + // remote settings again. + UrlbarPrefs.set("suggest.pocket", true); + await QuickSuggestTestUtils.forceSync(); +}); + +// Tests the "Not relevant" result menu dismissal command. +add_task(async function notRelevant() { + await doDismissTest("not_relevant"); +}); + +async function doDismissTest(command) { + // Do a search. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "pocket suggestion", + }); + + // Check the result. + let resultCount = 2; + Assert.equal( + UrlbarTestUtils.getResultCount(window), + resultCount, + "There should be two results" + ); + + let { result } = await UrlbarTestUtils.getDetailsOfResultAt( + window, + EXPECTED_RESULT_INDEX + ); + Assert.equal( + result.providerName, + UrlbarProviderQuickSuggest.name, + "The result should be from the expected provider" + ); + Assert.equal( + result.payload.telemetryType, + "pocket", + "The result should be a Pocket result" + ); + + // Click the command. + await UrlbarTestUtils.openResultMenuAndClickItem( + window, + ["[data-l10n-id=firefox-suggest-command-dont-show-this]", command], + { resultIndex: EXPECTED_RESULT_INDEX, openByMouse: true } + ); + + // 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, + EXPECTED_RESULT_INDEX + ); + 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, + EXPECTED_RESULT_INDEX + ); + 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.telemetryType !== "pocket", + "Tip result and suggestion should not be present" + ); + } + + gURLBar.handleRevert(); + + // Do the search again. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "pocket suggestion", + }); + 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.telemetryType !== "pocket", + "Tip result and suggestion should not be present" + ); + } + + await UrlbarTestUtils.promisePopupClose(window); + await QuickSuggest.blockedSuggestions.clear(); +} + +// Tests row labels. +add_task(async function rowLabel() { + const testCases = [ + // high confidence keyword best match + { + searchString: "high", + expected: "Recommended reads", + }, + // low confidence keyword non-best match + { + searchString: "pocket suggestion", + expected: "Firefox Suggest", + }, + ]; + + for (const { searchString, expected } of testCases) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: searchString, + }); + Assert.equal(UrlbarTestUtils.getResultCount(window), 2); + + const { element } = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + const row = element.row; + Assert.equal(row.getAttribute("label"), expected); + } +}); + +// Tests visibility of "Show less frequently" menu. +add_task(async function showLessFrequentlyMenuVisibility() { + const testCases = [ + // high confidence keyword best match + { + searchString: "high", + expected: false, + }, + // low confidence keyword non-best match + { + searchString: "pocket suggestion", + expected: true, + }, + ]; + + for (const { searchString, expected } of testCases) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: searchString, + }); + Assert.equal(UrlbarTestUtils.getResultCount(window), 2); + + const details = await UrlbarTestUtils.getDetailsOfResultAt( + window, + EXPECTED_RESULT_INDEX + ); + Assert.equal( + details.result.payload.telemetryType, + "pocket", + "Pocket suggestion should be present at expected index" + ); + + const menuitem = await UrlbarTestUtils.openResultMenuAndGetItem({ + resultIndex: EXPECTED_RESULT_INDEX, + openByMouse: true, + command: "show_less_frequently", + window, + }); + Assert.equal(!!menuitem, expected); + + gURLBar.view.resultMenu.hidePopup(true); + } + + await UrlbarTestUtils.promisePopupClose(window); +}); diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_yelp.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_yelp.js new file mode 100644 index 0000000000..b7c2bdc25c --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_yelp.js @@ -0,0 +1,429 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for Yelp suggestions. + +const REMOTE_SETTINGS_RECORDS = [ + { + type: "yelp-suggestions", + attachment: { + subjects: ["ramen"], + preModifiers: ["best"], + postModifiers: ["delivery"], + locationSigns: [{ keyword: "in", needLocation: true }], + yelpModifiers: [], + icon: "1234", + score: 0.5, + }, + }, +]; + +add_setup(async function () { + Services.prefs.setBoolPref("browser.search.suggest.enabled", false); + + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + remoteSettingsRecords: REMOTE_SETTINGS_RECORDS, + prefs: [ + ["quicksuggest.rustEnabled", true], + ["suggest.quicksuggest.sponsored", true], + ["suggest.yelp", true], + ["yelp.featureGate", true], + ], + }); +}); + +add_task(async function basic() { + for (let topPick of [true, false]) { + info("Setting yelpPriority: " + topPick); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.quicksuggest.yelpPriority", topPick]], + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "RaMeN iN tOkYo", + }); + + Assert.equal(UrlbarTestUtils.getResultCount(window), 2); + + const details = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + const { result } = details; + Assert.equal( + result.providerName, + UrlbarProviderQuickSuggest.name, + "The result should be from the expected provider" + ); + Assert.equal(result.payload.provider, "Yelp"); + Assert.equal( + result.payload.url, + "https://www.yelp.com/search?find_desc=RaMeN&find_loc=tOkYo&utm_medium=partner&utm_source=mozilla" + ); + Assert.equal(result.payload.title, "RaMeN iN tOkYo"); + + const { row } = details.element; + const bottom = row.querySelector(".urlbarView-row-body-bottom"); + Assert.ok(bottom, "Bottom text element should exist"); + Assert.ok( + BrowserTestUtils.isVisible(bottom), + "Bottom text element should be visible" + ); + Assert.equal( + bottom.textContent, + "Yelp · Sponsored", + "Bottom text is correct" + ); + + await UrlbarTestUtils.promisePopupClose(window); + await SpecialPowers.popPrefEnv(); + } +}); + +// Tests the "Show less frequently" result menu command. +add_task(async function resultMenu_show_less_frequently() { + info("Test for no yelpMinKeywordLength and no yelpShowLessFrequentlyCap"); + await doShowLessFrequently({ + minKeywordLength: 0, + frequentlyCap: 0, + testData: [ + { + input: "best ra", + expected: { + hasSuggestion: true, + hasShowLessItem: true, + }, + }, + { + input: "best ra", + expected: { + hasSuggestion: false, + }, + }, + { + input: "best ram", + expected: { + hasSuggestion: true, + hasShowLessItem: true, + }, + }, + { + input: "best ram", + expected: { + hasSuggestion: false, + }, + }, + { + input: "best rame", + expected: { + hasSuggestion: true, + hasShowLessItem: true, + }, + }, + { + input: "best rame", + expected: { + hasSuggestion: false, + }, + }, + ], + }); + + info("Test whether yelpShowLessFrequentlyCap can work"); + await doShowLessFrequently({ + minKeywordLength: 0, + frequentlyCap: 2, + testData: [ + { + input: "best ra", + expected: { + hasSuggestion: true, + hasShowLessItem: true, + }, + }, + { + input: "best ram", + expected: { + hasSuggestion: true, + hasShowLessItem: true, + }, + }, + { + input: "best ram", + expected: { + hasSuggestion: false, + }, + }, + { + input: "best rame", + expected: { + hasSuggestion: true, + hasShowLessItem: false, + }, + }, + { + input: "best ramen", + expected: { + hasSuggestion: true, + hasShowLessItem: false, + }, + }, + ], + }); + + info( + "Test whether local yelp.minKeywordLength pref can override nimbus variable yelpMinKeywordLength" + ); + await doShowLessFrequently({ + minKeywordLength: 8, + frequentlyCap: 0, + testData: [ + { + input: "best ra", + expected: { + hasSuggestion: false, + }, + }, + { + input: "best ram", + expected: { + hasSuggestion: true, + hasShowLessItem: true, + }, + }, + { + input: "best rame", + expected: { + hasSuggestion: true, + hasShowLessItem: true, + }, + }, + { + input: "best rame", + expected: { + hasSuggestion: false, + }, + }, + ], + }); +}); + +async function doShowLessFrequently({ + minKeywordLength, + frequentlyCap, + testData, +}) { + UrlbarPrefs.clear("yelp.showLessFrequentlyCount"); + UrlbarPrefs.clear("yelp.minKeywordLength"); + + let cleanUpNimbus = await UrlbarTestUtils.initNimbusFeature({ + yelpMinKeywordLength: minKeywordLength, + yelpShowLessFrequentlyCap: frequentlyCap, + }); + + for (let { input, expected } of testData) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: input, + }); + + if (expected.hasSuggestion) { + let resultIndex = 1; + let details = await UrlbarTestUtils.getDetailsOfResultAt( + window, + resultIndex + ); + Assert.equal(details.result.payload.provider, "Yelp"); + + if (expected.hasShowLessItem) { + // Click the command. + let previousShowLessFrequentlyCount = UrlbarPrefs.get( + "yelp.showLessFrequentlyCount" + ); + await UrlbarTestUtils.openResultMenuAndClickItem( + window, + "show_less_frequently", + { resultIndex, openByMouse: true } + ); + + Assert.equal( + UrlbarPrefs.get("yelp.showLessFrequentlyCount"), + previousShowLessFrequentlyCount + 1 + ); + Assert.equal( + UrlbarPrefs.get("yelp.minKeywordLength"), + input.length + 1 + ); + } else { + let menuitem = await UrlbarTestUtils.openResultMenuAndGetItem({ + window, + command: "show_less_frequently", + resultIndex: 1, + openByMouse: true, + }); + Assert.ok(!menuitem); + } + } else { + // Yelp suggestion should not be shown. + for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) { + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + Assert.notEqual(details.result.payload.provider, "Yelp"); + } + } + + await UrlbarTestUtils.promisePopupClose(window); + } + + await cleanUpNimbus(); + UrlbarPrefs.clear("yelp.showLessFrequentlyCount"); + UrlbarPrefs.clear("yelp.minKeywordLength"); +} + +// Tests the "Not relevant" result menu dismissal command. +add_task(async function resultMenu_not_relevant() { + await doDismiss({ + menu: "not_relevant", + assert: resuilt => { + Assert.ok( + QuickSuggest.blockedSuggestions.has(resuilt.payload.url), + "The URL should be register as blocked" + ); + }, + }); + + await QuickSuggest.blockedSuggestions.clear(); +}); + +// Tests the "Not interested" result menu dismissal command. +add_task(async function resultMenu_not_interested() { + await doDismiss({ + menu: "not_interested", + assert: () => { + Assert.ok(!UrlbarPrefs.get("suggest.yelp")); + }, + }); + + UrlbarPrefs.clear("suggest.yelp"); +}); + +async function doDismiss({ menu, assert }) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "ramen", + }); + + let resultCount = UrlbarTestUtils.getResultCount(window); + let resultIndex = 1; + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex); + Assert.equal(details.result.payload.provider, "Yelp"); + let result = details.result; + + // Click the command. + await UrlbarTestUtils.openResultMenuAndClickItem( + window, + ["[data-l10n-id=firefox-suggest-command-dont-show-this]", menu], + { + resultIndex, + openByMouse: true, + } + ); + + // 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.provider !== "Yelp", + "Tip result and Yelp result should not be present" + ); + } + + assert(result); + + await UrlbarTestUtils.promisePopupClose(window); + + // Check that the result should not be shown anymore. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "ramen", + }); + + for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) { + details = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + Assert.ok( + details.result.payload.provider !== "Yelp", + "Yelp result should not be present" + ); + } + + await UrlbarTestUtils.promisePopupClose(window); +} + +// Tests the row/group label. +add_task(async function rowLabel() { + let tests = [ + { topPick: true, label: "Local recommendations" }, + { topPick: false, label: "Firefox Suggest" }, + ]; + + for (let { topPick, label } of tests) { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.yelp.priority", topPick]], + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "ramen", + }); + Assert.equal(UrlbarTestUtils.getResultCount(window), 2); + + const { element } = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + const row = element.row; + Assert.equal(row.getAttribute("label"), label); + + await UrlbarTestUtils.promisePopupClose(window); + await SpecialPowers.popPrefEnv(); + } +}); 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..001c54458c --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_dynamicWikipedia.js @@ -0,0 +1,116 @@ +/* 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(), + }, + }, + }, + // click + click: { + 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(), + }, + }, + }, + commands: [ + // dismiss + { + command: "dismiss", + 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 + { + command: "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_gleanEmptyStrings.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_gleanEmptyStrings.js new file mode 100644 index 0000000000..00cbe6c4e1 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_gleanEmptyStrings.js @@ -0,0 +1,221 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This file tests that Glean handles empty request IDs properly. + */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + CONTEXTUAL_SERVICES_PING_TYPES: + "resource:///modules/PartnerLinkAttribution.sys.mjs", +}); + +const { TELEMETRY_SCALARS } = UrlbarProviderQuickSuggest; + +const MERINO_RESULT = { + block_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", + provider: "adm", + is_sponsored: true, +}; + +const suggestion_type = "sponsored"; +const index = 1; +const position = index + 1; + +// Trying to avoid timeouts in TV mode. +requestLongerTimeout(3); + +add_setup(async function () { + await setUpTelemetryTest({ + merinoSuggestions: [MERINO_RESULT], + }); + MerinoTestUtils.server.response.body.request_id = ""; +}); + +// sponsored +add_task(async function sponsored() { + let match_type = "firefox-suggest"; + let source = "merino"; + + let improve_suggest_experience_checked = true; + + await doTelemetryTest({ + index, + suggestion: MERINO_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: { + source, + match_type, + position, + suggested_index: -1, + suggested_index_relative_to_group: true, + improve_suggest_experience_checked, + is_clicked: false, + block_id: MERINO_RESULT.block_id, + advertiser: MERINO_RESULT.advertiser, + request_id: "", + }, + }, + }, + // click + click: { + 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: { + source, + match_type, + position, + suggested_index: -1, + suggested_index_relative_to_group: true, + improve_suggest_experience_checked, + is_clicked: true, + block_id: MERINO_RESULT.block_id, + advertiser: MERINO_RESULT.advertiser, + request_id: "", + }, + }, + { + type: CONTEXTUAL_SERVICES_PING_TYPES.QS_SELECTION, + payload: { + source, + match_type, + position, + suggested_index: -1, + suggested_index_relative_to_group: true, + improve_suggest_experience_checked, + block_id: MERINO_RESULT.block_id, + advertiser: MERINO_RESULT.advertiser, + request_id: "", + }, + }, + ], + }, + commands: [ + // dismiss + { + command: "dismiss", + 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: { + source, + match_type, + position, + suggested_index: -1, + suggested_index_relative_to_group: true, + improve_suggest_experience_checked, + is_clicked: false, + block_id: MERINO_RESULT.block_id, + advertiser: MERINO_RESULT.advertiser, + request_id: "", + }, + }, + { + type: CONTEXTUAL_SERVICES_PING_TYPES.QS_BLOCK, + payload: { + source, + match_type, + position, + suggested_index: -1, + suggested_index_relative_to_group: true, + improve_suggest_experience_checked, + block_id: MERINO_RESULT.block_id, + advertiser: MERINO_RESULT.advertiser, + iab_category: MERINO_RESULT.iab_category, + request_id: "", + }, + }, + ], + }, + // help + { + command: "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: { + source, + match_type, + position, + suggested_index: -1, + suggested_index_relative_to_group: true, + improve_suggest_experience_checked, + is_clicked: false, + block_id: MERINO_RESULT.block_id, + advertiser: MERINO_RESULT.advertiser, + request_id: "", + }, + }, + ], + }, + ], + }); +}); 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..8682f1f53a --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_impressionEdgeCases.js @@ -0,0 +1,482 @@ +/* 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]; + +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({ + remoteSettingsRecords: [ + { + 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([]); +}); + +// 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([]); + }); + 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.isHidden(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([]); + + 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([]); + }); +}); + +// 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]; + let index = 1; + + let pingSubmitted = false; + GleanPings.quickSuggest.testBeforeNextSubmit(() => { + pingSubmitted = true; + Assert.equal( + Glean.quickSuggest.pingType.testGetValue(), + CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION + ); + Assert.equal( + Glean.quickSuggest.improveSuggestExperience.testGetValue(), + false + ); + Assert.equal( + Glean.quickSuggest.blockId.testGetValue(), + firstSuggestion.id + ); + Assert.equal(Glean.quickSuggest.isClicked.testGetValue(), false); + Assert.equal( + Glean.quickSuggest.matchType.testGetValue(), + "firefox-suggest" + ); + Assert.equal(Glean.quickSuggest.position.testGetValue(), index + 1); + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: firstSuggestion.keywords[0], + fireInputEvent: true, + }); + + 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", + }, + }, + ]); + Assert.ok(pingSubmitted, "Glean ping was submitted"); + }); +}); + +/** + * 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 originalChunkTimeout = UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS; + UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS = 30000; + const cleanup = () => { + UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS = originalChunkTimeout; + }; + registerCleanupFunction(cleanup); + + // 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[0]; + return state == "engagement"; + }); + Assert.equal(engagementCalls.length, 1, "One engagement occurred"); + + // Clean up. + resolveQuery(); + UrlbarProvidersManager.unregisterProvider(provider); + cleanup(); + 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..4762095795 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_navigationalSuggestions.js @@ -0,0 +1,346 @@ +/* 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: [ + // 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, + }, +}) { + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + MerinoTestUtils.server.response.body.suggestions = suggestion + ? [suggestion] + : []; + + Services.telemetry.clearEvents(); + + 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); + + 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..9a1aa06c02 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_nonsponsored.js @@ -0,0 +1,236 @@ +/* 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"], + advertiser: "Wikipedia", + iab_category: "5 - Education", + icon: "1234", +}; + +const suggestion_type = "nonsponsored"; +const index = 1; +const position = index + 1; + +// Trying to avoid timeouts in TV mode. +requestLongerTimeout(3); + +add_setup(async function () { + await setUpTelemetryTest({ + remoteSettingsRecords: [ + { + type: "data", + attachment: [REMOTE_SETTINGS_RESULT], + }, + ], + }); +}); + +add_tasks_with_rust(async function nonsponsored() { + let match_type = "firefox-suggest"; + let advertiser = REMOTE_SETTINGS_RESULT.advertiser.toLowerCase(); + let reporting_url = undefined; + let source = UrlbarPrefs.get("quicksuggest.rustEnabled") + ? "rust" + : "remote-settings"; + let block_id = source == "rust" ? undefined : REMOTE_SETTINGS_RESULT.id; + + // 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: { + source, + match_type, + position, + block_id, + advertiser, + reporting_url, + suggested_index: -1, + suggested_index_relative_to_group: true, + improve_suggest_experience_checked, + is_clicked: false, + }, + }, + }, + // click + click: { + 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: { + source, + match_type, + position, + block_id, + advertiser, + reporting_url, + suggested_index: -1, + suggested_index_relative_to_group: true, + improve_suggest_experience_checked, + is_clicked: true, + }, + }, + { + type: CONTEXTUAL_SERVICES_PING_TYPES.QS_SELECTION, + payload: { + source, + match_type, + position, + block_id, + advertiser, + reporting_url, + suggested_index: -1, + suggested_index_relative_to_group: true, + improve_suggest_experience_checked, + }, + }, + ], + }, + commands: [ + // dismiss + { + command: "dismiss", + 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: { + source, + match_type, + position, + block_id, + advertiser, + reporting_url, + suggested_index: -1, + suggested_index_relative_to_group: true, + improve_suggest_experience_checked, + is_clicked: false, + }, + }, + { + type: CONTEXTUAL_SERVICES_PING_TYPES.QS_BLOCK, + payload: { + source, + match_type, + position, + block_id, + advertiser, + suggested_index: -1, + suggested_index_relative_to_group: true, + improve_suggest_experience_checked, + iab_category: REMOTE_SETTINGS_RESULT.iab_category, + }, + }, + ], + }, + // help + { + command: "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: { + source, + match_type, + position, + block_id, + advertiser, + reporting_url, + suggested_index: -1, + suggested_index_relative_to_group: true, + improve_suggest_experience_checked, + is_clicked: false, + }, + }, + ], + }, + ], + }); + 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..d40c70107e --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_other.js @@ -0,0 +1,298 @@ +/* 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({ + remoteSettingsRecords: [ + { + 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); +}); + +// 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..7c477e8af7 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_sponsored.js @@ -0,0 +1,408 @@ +/* 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", + iab_category: "22 - Shopping", + icon: "1234", +}; + +const suggestion_type = "sponsored"; +const index = 1; +const position = index + 1; + +// Trying to avoid timeouts in TV mode. +requestLongerTimeout(3); + +add_setup(async function () { + await setUpTelemetryTest({ + remoteSettingsRecords: [ + { + type: "data", + attachment: [REMOTE_SETTINGS_RESULT], + }, + ], + }); +}); + +// sponsored +add_tasks_with_rust(async function sponsored() { + let match_type = "firefox-suggest"; + let source = UrlbarPrefs.get("quicksuggest.rustEnabled") + ? "rust" + : "remote-settings"; + + // 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: { + source, + match_type, + position, + suggested_index: -1, + suggested_index_relative_to_group: true, + improve_suggest_experience_checked, + is_clicked: false, + block_id: REMOTE_SETTINGS_RESULT.id, + advertiser: REMOTE_SETTINGS_RESULT.advertiser, + }, + }, + }, + // click + click: { + 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: { + source, + match_type, + position, + suggested_index: -1, + suggested_index_relative_to_group: true, + 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: { + source, + match_type, + position, + suggested_index: -1, + suggested_index_relative_to_group: true, + improve_suggest_experience_checked, + block_id: REMOTE_SETTINGS_RESULT.id, + advertiser: REMOTE_SETTINGS_RESULT.advertiser, + }, + }, + ], + }, + commands: [ + // dismiss + { + command: "dismiss", + 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: { + source, + match_type, + position, + suggested_index: -1, + suggested_index_relative_to_group: true, + 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: { + source, + match_type, + position, + suggested_index: -1, + suggested_index_relative_to_group: true, + improve_suggest_experience_checked, + block_id: REMOTE_SETTINGS_RESULT.id, + advertiser: REMOTE_SETTINGS_RESULT.advertiser, + iab_category: REMOTE_SETTINGS_RESULT.iab_category, + }, + }, + ], + }, + // help + { + command: "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: { + source, + match_type, + position, + suggested_index: -1, + suggested_index_relative_to_group: true, + improve_suggest_experience_checked, + is_clicked: false, + block_id: REMOTE_SETTINGS_RESULT.id, + advertiser: REMOTE_SETTINGS_RESULT.advertiser, + }, + }, + ], + }, + ], + }); + await SpecialPowers.popPrefEnv(); + } +}); + +// higher-placement sponsored, a.k.a sponsored priority, sponsored best match +add_tasks_with_rust(async function sponsoredBestMatch() { + let match_type = "best-match"; + let source = UrlbarPrefs.get("quicksuggest.rustEnabled") + ? "rust" + : "remote-settings"; + + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.quicksuggest.sponsoredPriority", true]], + }); + 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: { + source, + match_type, + position, + suggested_index: 1, + suggested_index_relative_to_group: false, + is_clicked: false, + improve_suggest_experience_checked: false, + block_id: REMOTE_SETTINGS_RESULT.id, + advertiser: REMOTE_SETTINGS_RESULT.advertiser, + }, + }, + }, + // click + click: { + 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: { + source, + match_type, + position, + suggested_index: 1, + suggested_index_relative_to_group: false, + 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: { + source, + match_type, + position, + suggested_index: 1, + suggested_index_relative_to_group: false, + improve_suggest_experience_checked: false, + block_id: REMOTE_SETTINGS_RESULT.id, + advertiser: REMOTE_SETTINGS_RESULT.advertiser, + }, + }, + ], + }, + commands: [ + // dismiss + { + command: "dismiss", + 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: { + source, + match_type, + position, + suggested_index: 1, + suggested_index_relative_to_group: false, + 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: { + source, + match_type, + position, + suggested_index: 1, + suggested_index_relative_to_group: false, + improve_suggest_experience_checked: false, + block_id: REMOTE_SETTINGS_RESULT.id, + advertiser: REMOTE_SETTINGS_RESULT.advertiser, + iab_category: REMOTE_SETTINGS_RESULT.iab_category, + }, + }, + ], + }, + // help + { + command: "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: { + source, + match_type, + position, + suggested_index: 1, + suggested_index_relative_to_group: false, + is_clicked: false, + improve_suggest_experience_checked: false, + block_id: REMOTE_SETTINGS_RESULT.id, + advertiser: REMOTE_SETTINGS_RESULT.advertiser, + }, + }, + ], + }, + ], + }); + 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..e87c64740f --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_weather.js @@ -0,0 +1,158 @@ +/* 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({ + remoteSettingsRecords: [ + { + 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; + + // Wait for keywords to be re-synced from remote settings. + await QuickSuggestTestUtils.forceSync(); + } + }, + // 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(), + }, + }, + }, + // click + click: { + 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(), + }, + }, + }, + commands: [ + // not relevant + { + command: [ + "[data-l10n-id=firefox-suggest-command-dont-show-this]", + "not_relevant", + ], + scalars: { + [WEATHER_SCALARS.IMPRESSION]: position, + }, + event: { + category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, + method: "engagement", + object: "other", + extra: { + suggestion_type, + match_type, + position: position.toString(), + }, + }, + }, + // help + { + command: "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..1c3f0e62e7 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_weather.js @@ -0,0 +1,426 @@ +/* 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({ + remoteSettingsRecords: [ + { + type: "weather", + weather: MerinoTestUtils.WEATHER_RS_DATA, + }, + ], + }); + await MerinoTestUtils.initWeather(); +}); + +// Basic checks of the row DOM. +add_tasks_with_rust(async function dom() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: MerinoTestUtils.WEATHER_KEYWORD, + }); + + let resultIndex = 1; + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex); + assertIsWeatherResult(details.result, true); + let { row } = details.element; + + Assert.ok( + BrowserTestUtils.isVisible( + row.querySelector(".urlbarView-title-separator") + ), + "The title separator should be visible" + ); + + await UrlbarTestUtils.promisePopupClose(window); +}); + +// This test ensures the browser navigates to the weather webpage after +// the weather result is selected. +add_tasks_with_rust(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_tasks_with_rust(async function showLessFrequentlyCapReached_manySearches() { + // Set up a min keyword length and cap. + await QuickSuggestTestUtils.setRemoteSettingsRecords([ + { + type: "weather", + weather: { + keywords: ["weather"], + min_keyword_length: 3, + }, + }, + { + type: "configuration", + configuration: { + show_less_frequently_cap: 1, + }, + }, + ]); + + // Trigger the suggestion. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "wea", + }); + + let resultIndex = 1; + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex); + info("Weather suggestion should be present after 'wea' search"); + assertIsWeatherResult(details.result, true); + + // 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); + info(`Weather suggestion should be absent (checking index ${i})`); + assertIsWeatherResult(details.result, false); + } + + // Do a search using one more character. The suggestion should appear. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "weat", + }); + + details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex); + info("Weather suggestion should be present after 'weat' search"); + assertIsWeatherResult(details.result, true); + 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.setRemoteSettingsRecords([ + { + 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_tasks_with_rust(async function showLessFrequentlyCapReached_oneSearch() { + // Set up a min keyword length and cap. + await QuickSuggestTestUtils.setRemoteSettingsRecords([ + { + type: "weather", + weather: { + keywords: ["weather"], + min_keyword_length: 3, + }, + }, + { + type: "configuration", + configuration: { + show_less_frequently_cap: 3, + }, + }, + ]); + + // Trigger the suggestion. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "wea", + }); + + let resultIndex = 1; + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex); + info("Weather suggestion should be present after 'wea' search"); + assertIsWeatherResult(details.result, true); + + 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.setRemoteSettingsRecords([ + { + type: "weather", + weather: MerinoTestUtils.WEATHER_RS_DATA, + }, + ]); + UrlbarPrefs.clear("weather.minKeywordLength"); +}); + +// Tests the "Not interested" result menu dismissal command. +add_tasks_with_rust(async function notInterested() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: MerinoTestUtils.WEATHER_KEYWORD, + }); + await doDismissTest("not_interested"); +}); + +// Tests the "Not relevant" result menu dismissal command. +add_tasks_with_rust(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); + assertIsWeatherResult(details.result, true); + + // 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.notEqual( + details.type, + UrlbarUtils.RESULT_TYPE.TIP, + "Tip result should not be present" + ); + info("Weather result should not be present"); + assertIsWeatherResult(details.result, false); + } + + 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"); + + // Wait for keywords to be re-synced from remote settings. + await QuickSuggestTestUtils.forceSync(); +} + +// 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_tasks_with_rust(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_tasks_with_rust(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); + info("Weather suggestion should be present after search"); + assertIsWeatherResult(details.result, true); + + // 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"); +} + +function assertIsWeatherResult(result, isWeatherResult) { + let provider = UrlbarPrefs.get("quickSuggestRustEnabled") + ? UrlbarProviderQuickSuggest + : UrlbarProviderWeather; + if (isWeatherResult) { + Assert.equal( + result.providerName, + provider.name, + "Result should be from a weather provider" + ); + Assert.equal( + UrlbarUtils.searchEngagementTelemetryType(result), + "weather", + "Result telemetry type should be 'weather'" + ); + } else { + Assert.notEqual( + result.providerName, + provider.name, + "Result should not be from a weather provider" + ); + Assert.notEqual( + UrlbarUtils.searchEngagementTelemetryType(result), + "weather", + "Result telemetry type should not be 'weather'" + ); + } +} 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..7d62a44d45 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/browser/head.js @@ -0,0 +1,693 @@ +/* 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", +}); + +ChromeUtils.defineLazyGetter(this, "QuickSuggestTestUtils", () => { + const { QuickSuggestTestUtils: module } = ChromeUtils.importESModule( + "resource://testing-common/QuickSuggestTestUtils.sys.mjs" + ); + module.init(this); + return module; +}); + +ChromeUtils.defineLazyGetter(this, "MerinoTestUtils", () => { + const { MerinoTestUtils: module } = ChromeUtils.importESModule( + "resource://testing-common/MerinoTestUtils.sys.mjs" + ); + module.init(this); + return module; +}); + +ChromeUtils.defineLazyGetter(this, "PlacesFrecencyRecalculator", () => { + return Cc["@mozilla.org/places/frecency-recalculator;1"].getService( + Ci.nsIObserver + ).wrappedJSObject; +}); + +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.remoteSettingsRecords + * See `QuickSuggestTestUtils.ensureQuickSuggestInit()`. + * @param {Array} options.merinoSuggestions + * See `QuickSuggestTestUtils.ensureQuickSuggestInit()`. + * @param {Array} options.config + * See `QuickSuggestTestUtils.ensureQuickSuggestInit()`. + */ +async function setUpTelemetryTest({ + remoteSettingsRecords, + merinoSuggestions = null, + config = QuickSuggestTestUtils.DEFAULT_CONFIG, +}) { + await SpecialPowers.pushPrefEnv({ + set: [ + // 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(); + + // Add a mock engine so we don't hit the network. + await SearchTestUtils.installSearchExtension({}, { setAsDefault: true }); + + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + remoteSettingsRecords, + 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.click + * An object describing the expected click telemetry. It must have the same + * properties as `impressionOnly` except `ping` must be `pings` (plural), an + * array of expected pings. + * @param {Array} options.commands + * Each element in this array is an object that describes the expected + * telemetry for a result menu command. Each object must have the following + * properties: + * {string|Array} command + * A command name or array; this is passed directly to + * `UrlbarTestUtils.openResultMenuAndClickItem()` as the `commandOrArray` + * arg, so see its documentation for details. + * {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, + click, + commands, + 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, + }), +}) { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + + await doImpressionOnlyTest({ + index, + suggestion, + providerName, + showSuggestion, + expected: impressionOnly, + }); + + await doClickTest({ + suggestion, + providerName, + showSuggestion, + index, + expected: click, + }); + + for (let command of commands) { + await doCommandTest({ + suggestion, + providerName, + showSuggestion, + index, + commandOrArray: command.command, + expected: command, + }); + + if (teardown) { + info("Calling teardown"); + await teardown(); + info("Finished teardown"); + } + } +} + +/** + * 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. + */ +async function doImpressionOnlyTest({ + index, + suggestion, + providerName, + expected, + showSuggestion, +}) { + info("Starting impression-only test"); + + Services.telemetry.clearEvents(); + + let expectedPings = expected.ping ? [expected.ping] : []; + let gleanPingCount = watchGleanPings(expectedPings); + + 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" + ); + return; + } + + // 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" + ); + return; + } + + // 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]); + + Assert.equal( + expectedPings.length, + gleanPingCount.value, + "Submitted one Glean ping per expected ping" + ); + + // Clean up. + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(); + + info("Finished impression-only test"); +} + +/** + * Helper for `doTelemetryTest()` that clicks 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 {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 doClickTest({ + index, + suggestion, + providerName, + expected, + showSuggestion, +}) { + info("Starting click test"); + + Services.telemetry.clearEvents(); + + let expectedPings = expected.pings ?? []; + let gleanPingCount = watchGleanPings(expectedPings); + + info("Showing suggestion"); + await showSuggestion(); + + let row = await validateSuggestionRow(index, suggestion, providerName); + if (!row) { + Assert.ok(false, "Couldn't get suggestion row, stopping click test"); + return; + } + + // We assume clicking the row will load a page in the current browser. + let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + info("Clicking row"); + EventUtils.synthesizeMouseAtCenter(row, {}); + + info("Waiting for load"); + await loadPromise; + await TestUtils.waitForTick(); + + info("Checking scalars. Expected: " + JSON.stringify(expected.scalars)); + QuickSuggestTestUtils.assertScalars(expected.scalars); + + info("Checking events. Expected: " + JSON.stringify([expected.event])); + QuickSuggestTestUtils.assertEvents([expected.event]); + + Assert.equal( + expectedPings.length, + gleanPingCount.value, + "Submitted one Glean ping per expected ping" + ); + + await PlacesUtils.history.clear(); + + info("Finished click test"); +} + +/** + * Helper for `doTelemetryTest()` that clicks a result menu command for a + * suggestion 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|Array} options.commandOrArray + * A command name or array; this is passed directly to + * `UrlbarTestUtils.openResultMenuAndClickItem()` as the `commandOrArray` arg, + * so see its documentation for details. + * @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 doCommandTest({ + index, + suggestion, + providerName, + commandOrArray, + expected, + showSuggestion, +}) { + info("Starting command test: " + JSON.stringify({ commandOrArray })); + + Services.telemetry.clearEvents(); + + let expectedPings = expected.pings ?? []; + let gleanPingCount = watchGleanPings(expectedPings); + + info("Showing suggestion"); + await showSuggestion(); + + let row = await validateSuggestionRow(index, suggestion, providerName); + if (!row) { + Assert.ok(false, "Couldn't get suggestion row, stopping click test"); + return; + } + + let command = + typeof commandOrArray == "string" + ? commandOrArray + : commandOrArray[commandOrArray.length - 1]; + + let loadPromise; + if (command == "help") { + // We assume clicking "help" will load a page in a new tab. + loadPromise = BrowserTestUtils.waitForNewTab(gBrowser); + } + + info("Clicking command"); + await UrlbarTestUtils.openResultMenuAndClickItem(window, commandOrArray, { + resultIndex: index, + openByMouse: true, + }); + + if (loadPromise) { + info("Waiting for load"); + await loadPromise; + await TestUtils.waitForTick(); + if (command == "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]); + + Assert.equal( + expectedPings.length, + gleanPingCount.value, + "Submitted one Glean ping per expected ping" + ); + + if (command == "dismiss") { + await QuickSuggest.blockedSuggestions.clear(); + } + await PlacesUtils.history.clear(); + + info("Finished command test: " + JSON.stringify({ commandOrArray })); +} + +/** + * 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; +} + +function watchGleanPings(pings) { + let countObject = { value: 0 }; + + let checkPing = (ping, next) => { + countObject.value++; + _assertGleanPing(ping); + if (next) { + GleanPings.quickSuggest.testBeforeNextSubmit(next); + } + }; + + // Build the chain of `testBeforeNextSubmit`s backwards. + let next = undefined; + pings + .slice() + .reverse() + .forEach(ping => { + next = checkPing.bind(null, ping, next); + }); + if (next) { + GleanPings.quickSuggest.testBeforeNextSubmit(next); + } + + return countObject; +} + +function _assertGleanPing(ping) { + Assert.equal(Glean.quickSuggest.pingType.testGetValue(), ping.type); + const keymap = { + // present in all pings + source: Glean.quickSuggest.source, + match_type: Glean.quickSuggest.matchType, + position: Glean.quickSuggest.position, + suggested_index: Glean.quickSuggest.suggestedIndex, + suggested_index_relative_to_group: + Glean.quickSuggest.suggestedIndexRelativeToGroup, + improve_suggest_experience_checked: + Glean.quickSuggest.improveSuggestExperience, + block_id: Glean.quickSuggest.blockId, + advertiser: Glean.quickSuggest.advertiser, + request_id: Glean.quickSuggest.requestId, + context_id: Glean.quickSuggest.contextId, + // impression and click pings + reporting_url: Glean.quickSuggest.reportingUrl, + // impression ping + is_clicked: Glean.quickSuggest.isClicked, + // block/dismiss ping + iab_category: Glean.quickSuggest.iabCategory, + }; + for (let [key, value] of Object.entries(ping.payload)) { + Assert.ok(key in keymap, `A Glean metric exists for field ${key}`); + + // Merino results may contain empty strings, but Glean will represent these + // as nulls. + if (value === "") { + value = null; + } + + Assert.equal( + keymap[key].testGetValue(), + value ?? null, + `Glean metric field ${key} should be the expected value` + ); + } +} + +/** + * Adds two tasks: One with the Rust backend disabled and one with it enabled. + * The names of the task functions will be the name of the passed-in task + * function appended with "_rustDisabled" and "_rustEnabled" respectively. Call + * with the usual `add_task()` arguments. + * + * @param {...any} args + * The usual `add_task()` arguments. + */ +function add_tasks_with_rust(...args) { + let taskFnIndex = args.findIndex(a => typeof a == "function"); + let taskFn = args[taskFnIndex]; + + for (let rustEnabled of [false, true]) { + let newTaskFn = async (...taskFnArgs) => { + info("add_tasks_with_rust: Setting rustEnabled: " + rustEnabled); + UrlbarPrefs.set("quicksuggest.rustEnabled", rustEnabled); + info("add_tasks_with_rust: Done setting rustEnabled: " + rustEnabled); + + // The current backend may now start syncing, so wait for it to finish. + info("add_tasks_with_rust: Forcing sync"); + await QuickSuggestTestUtils.forceSync(); + info("add_tasks_with_rust: Done forcing sync"); + + let rv; + try { + info( + "add_tasks_with_rust: Calling original task function: " + taskFn.name + ); + rv = await taskFn(...taskFnArgs); + } finally { + info( + "add_tasks_with_rust: Done calling original task function: " + + taskFn.name + ); + info("add_tasks_with_rust: Clearing rustEnabled"); + UrlbarPrefs.clear("quicksuggest.rustEnabled"); + info("add_tasks_with_rust: Done clearing rustEnabled"); + + // The current backend may now start syncing, so wait for it to finish. + info("add_tasks_with_rust: Forcing sync"); + await QuickSuggestTestUtils.forceSync(); + info("add_tasks_with_rust: Done forcing sync"); + } + return rv; + }; + + Object.defineProperty(newTaskFn, "name", { + value: taskFn.name + (rustEnabled ? "_rustEnabled" : "_rustDisabled"), + }); + let addTaskArgs = [...args]; + addTaskArgs[taskFnIndex] = newTaskFn; + add_task(...addTaskArgs); + } +} 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..c468e4526f --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/head.js @@ -0,0 +1,911 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* import-globals-from ../../unit/head.js */ +/* eslint-disable jsdoc/require-param */ + +ChromeUtils.defineESModuleGetters(this, { + QuickSuggest: "resource:///modules/QuickSuggest.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; +}); + +/** + * Adds two tasks: One with the Rust backend disabled and one with it enabled. + * The names of the task functions will be the name of the passed-in task + * function appended with "_rustDisabled" and "_rustEnabled". If the passed-in + * task doesn't have a name, "anonymousTask" will be used. Call this with the + * usual `add_task()` arguments. + */ +function add_tasks_with_rust(...args) { + let taskFnIndex = args.findIndex(a => typeof a == "function"); + let taskFn = args[taskFnIndex]; + + for (let rustEnabled of [false, true]) { + let newTaskFn = async (...taskFnArgs) => { + info("add_tasks_with_rust: Setting rustEnabled: " + rustEnabled); + UrlbarPrefs.set("quicksuggest.rustEnabled", rustEnabled); + info("add_tasks_with_rust: Done setting rustEnabled: " + rustEnabled); + + // The current backend may now start syncing, so wait for it to finish. + info("add_tasks_with_rust: Forcing sync"); + await QuickSuggestTestUtils.forceSync(); + info("add_tasks_with_rust: Done forcing sync"); + + let rv; + try { + info( + "add_tasks_with_rust: Calling original task function: " + taskFn.name + ); + rv = await taskFn(...taskFnArgs); + } catch (e) { + // Clearly report any unusual errors to make them easier to spot and to + // make the flow of the test clearer. The harness throws NS_ERROR_ABORT + // when a normal assertion fails, so don't report that. + if (e.result != Cr.NS_ERROR_ABORT) { + Assert.ok( + false, + "add_tasks_with_rust: The original task function threw an error: " + + e + ); + } + throw e; + } finally { + info( + "add_tasks_with_rust: Done calling original task function: " + + taskFn.name + ); + info("add_tasks_with_rust: Clearing rustEnabled"); + UrlbarPrefs.clear("quicksuggest.rustEnabled"); + info("add_tasks_with_rust: Done clearing rustEnabled"); + + // The current backend may now start syncing, so wait for it to finish. + info("add_tasks_with_rust: Forcing sync"); + await QuickSuggestTestUtils.forceSync(); + info("add_tasks_with_rust: Done forcing sync"); + } + return rv; + }; + + Object.defineProperty(newTaskFn, "name", { + value: + (taskFn.name || "anonymousTask") + + (rustEnabled ? "_rustEnabled" : "_rustDisabled"), + }); + let addTaskArgs = [...args]; + addTaskArgs[taskFnIndex] = newTaskFn; + add_task(...addTaskArgs); + } +} + +/** + * Returns an expected Wikipedia (non-sponsored) result that can be passed to + * `check_results()` regardless of whether the Rust backend is enabled. + * + * @returns {object} + * An object that can be passed to `check_results()`. + */ +function makeWikipediaResult({ + source, + provider, + keyword = "wikipedia", + title = "Wikipedia Suggestion", + url = "http://example.com/wikipedia", + originalUrl = "http://example.com/wikipedia", + icon = null, + iconBlob = new Blob([new Uint8Array([])]), + impressionUrl = "http://example.com/wikipedia-impression", + clickUrl = "http://example.com/wikipedia-click", + blockId = 1, + advertiser = "Wikipedia", + iabCategory = "5 - Education", + suggestedIndex = -1, + isSuggestedIndexRelativeToGroup = true, +}) { + let result = { + suggestedIndex, + isSuggestedIndexRelativeToGroup, + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + heuristic: false, + payload: { + title, + url, + originalUrl, + displayUrl: url.replace(/^https:\/\//, ""), + isSponsored: false, + qsSuggestion: keyword, + sponsoredAdvertiser: "Wikipedia", + sponsoredIabCategory: "5 - Education", + helpUrl: QuickSuggest.HELP_URL, + helpL10n: { + id: "urlbar-result-menu-learn-more-about-firefox-suggest", + }, + isBlockable: true, + blockL10n: { + id: "urlbar-result-menu-dismiss-firefox-suggest", + }, + telemetryType: "adm_nonsponsored", + }, + }; + + if (UrlbarPrefs.get("quickSuggestRustEnabled")) { + result.payload.source = source || "rust"; + result.payload.provider = provider || "Wikipedia"; + result.payload.iconBlob = iconBlob; + } else { + result.payload.source = source || "remote-settings"; + result.payload.provider = provider || "AdmWikipedia"; + result.payload.icon = icon; + result.payload.sponsoredImpressionUrl = impressionUrl; + result.payload.sponsoredClickUrl = clickUrl; + result.payload.sponsoredBlockId = blockId; + result.payload.sponsoredAdvertiser = advertiser; + result.payload.sponsoredIabCategory = iabCategory; + } + + return result; +} + +/** + * Returns an expected AMP (sponsored) result that can be passed to + * `check_results()` regardless of whether the Rust backend is enabled. + * + * @returns {object} + * An object that can be passed to `check_results()`. + */ +function makeAmpResult({ + source, + provider, + keyword = "amp", + title = "Amp Suggestion", + url = "http://example.com/amp", + originalUrl = "http://example.com/amp", + icon = null, + iconBlob = new Blob([new Uint8Array([])]), + impressionUrl = "http://example.com/amp-impression", + clickUrl = "http://example.com/amp-click", + blockId = 1, + advertiser = "Amp", + iabCategory = "22 - Shopping", + suggestedIndex = -1, + isSuggestedIndexRelativeToGroup = true, + requestId = undefined, +} = {}) { + let result = { + suggestedIndex, + isSuggestedIndexRelativeToGroup, + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + heuristic: false, + payload: { + title, + url, + originalUrl, + requestId, + displayUrl: url.replace(/^https:\/\//, ""), + isSponsored: true, + qsSuggestion: keyword, + sponsoredImpressionUrl: impressionUrl, + sponsoredClickUrl: clickUrl, + sponsoredBlockId: blockId, + sponsoredAdvertiser: advertiser, + sponsoredIabCategory: iabCategory, + helpUrl: QuickSuggest.HELP_URL, + helpL10n: { + id: "urlbar-result-menu-learn-more-about-firefox-suggest", + }, + isBlockable: true, + blockL10n: { + id: "urlbar-result-menu-dismiss-firefox-suggest", + }, + telemetryType: "adm_sponsored", + descriptionL10n: { id: "urlbar-result-action-sponsored" }, + }, + }; + + if (UrlbarPrefs.get("quickSuggestRustEnabled")) { + result.payload.source = source || "rust"; + result.payload.provider = provider || "Amp"; + if (result.payload.source == "rust") { + result.payload.iconBlob = iconBlob; + } else { + result.payload.icon = icon; + } + } else { + result.payload.source = source || "remote-settings"; + result.payload.provider = provider || "AdmWikipedia"; + result.payload.icon = icon; + } + + return result; +} + +/** + * Returns an expected MDN result that can be passed to `check_results()` + * regardless of whether the Rust backend is enabled. + * + * @returns {object} + * An object that can be passed to `check_results()`. + */ +function makeMdnResult({ url, title, description }) { + let finalUrl = new URL(url); + finalUrl.searchParams.set("utm_medium", "firefox-desktop"); + finalUrl.searchParams.set("utm_source", "firefox-suggest"); + finalUrl.searchParams.set( + "utm_campaign", + "firefox-mdn-web-docs-suggestion-experiment" + ); + finalUrl.searchParams.set("utm_content", "treatment"); + + let result = { + isBestMatch: true, + suggestedIndex: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.OTHER_NETWORK, + heuristic: false, + payload: { + telemetryType: "mdn", + title, + url: finalUrl.href, + originalUrl: url, + displayUrl: finalUrl.href.replace(/^https:\/\//, ""), + description, + icon: "chrome://global/skin/icons/mdn.svg", + shouldShowUrl: true, + bottomTextL10n: { id: "firefox-suggest-mdn-bottom-text" }, + }, + }; + + if (UrlbarPrefs.get("quickSuggestRustEnabled")) { + result.payload.source = "rust"; + result.payload.provider = "Mdn"; + } else { + result.payload.source = "remote-settings"; + result.payload.provider = "MDNSuggestions"; + } + + return result; +} + +/** + * Returns an expected AMO (addons) result that can be passed to + * `check_results()` regardless of whether the Rust backend is enabled. + * + * @returns {object} + * An object that can be passed to `check_results()`. + */ +function makeAmoResult({ + source, + provider, + title = "Amo Suggestion", + description = "Amo description", + url = "http://example.com/amo", + originalUrl = "http://example.com/amo", + icon = null, + setUtmParams = true, +}) { + if (setUtmParams) { + url = new URL(url); + url.searchParams.set("utm_medium", "firefox-desktop"); + url.searchParams.set("utm_source", "firefox-suggest"); + url = url.href; + } + + let result = { + isBestMatch: true, + suggestedIndex: 1, + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + heuristic: false, + payload: { + source, + provider, + title, + description, + url, + originalUrl, + icon, + displayUrl: url.replace(/^https:\/\//, ""), + shouldShowUrl: true, + bottomTextL10n: { id: "firefox-suggest-addons-recommended" }, + helpUrl: QuickSuggest.HELP_URL, + telemetryType: "amo", + }, + }; + + if (UrlbarPrefs.get("quickSuggestRustEnabled")) { + result.payload.source = source || "rust"; + result.payload.provider = provider || "Amo"; + } else { + result.payload.source = source || "remote-settings"; + result.payload.provider = provider || "AddonSuggestions"; + } + + return result; +} + +/** + * Returns an expected weather result that can be passed to `check_results()` + * regardless of whether the Rust backend is enabled. + * + * @returns {object} + * An object that can be passed to `check_results()`. + */ +function makeWeatherResult({ + source, + provider, + telemetryType = undefined, + temperatureUnit = undefined, +} = {}) { + if (!temperatureUnit) { + temperatureUnit = + Services.locale.regionalPrefsLocales[0] == "en-US" ? "f" : "c"; + } + + let result = { + type: UrlbarUtils.RESULT_TYPE.DYNAMIC, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + heuristic: false, + suggestedIndex: 1, + payload: { + temperatureUnit, + url: MerinoTestUtils.WEATHER_SUGGESTION.url, + iconId: "6", + helpUrl: QuickSuggest.HELP_URL, + requestId: MerinoTestUtils.server.response.body.request_id, + source: "merino", + provider: "accuweather", + dynamicType: "weather", + city: MerinoTestUtils.WEATHER_SUGGESTION.city_name, + temperature: + MerinoTestUtils.WEATHER_SUGGESTION.current_conditions.temperature[ + temperatureUnit + ], + currentConditions: + MerinoTestUtils.WEATHER_SUGGESTION.current_conditions.summary, + forecast: MerinoTestUtils.WEATHER_SUGGESTION.forecast.summary, + high: MerinoTestUtils.WEATHER_SUGGESTION.forecast.high[temperatureUnit], + low: MerinoTestUtils.WEATHER_SUGGESTION.forecast.low[temperatureUnit], + shouldNavigate: true, + }, + }; + + if (UrlbarPrefs.get("quickSuggestRustEnabled")) { + result.payload.source = source || "rust"; + result.payload.provider = provider || "Weather"; + if (telemetryType !== null) { + result.payload.telemetryType = telemetryType || "weather"; + } + } else { + result.payload.source = source || "merino"; + result.payload.provider = provider || "accuweather"; + } + + return result; +} + +/** + * 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; +} + +/** + * Does some "show less frequently" tests where the cap is set in remote + * settings and Nimbus. See `doOneShowLessFrequentlyTest()`. This function + * assumes the matching behavior implemented by the given `BaseFeature` is based + * on matching prefixes of the given keyword starting at the first word. It + * also assumes the `BaseFeature` provides suggestions in remote settings. + * + * @param {object} options + * Options object. + * @param {BaseFeature} options.feature + * The feature that provides the suggestion matched by the searches. + * @param {*} options.expectedResult + * The expected result that should be matched, for searches that are expected + * to match a result. Can also be a function; it's passed the current search + * string and it should return the expected result. + * @param {string} options.showLessFrequentlyCountPref + * The name of the pref that stores the "show less frequently" count being + * tested. + * @param {string} options.nimbusCapVariable + * The name of the Nimbus variable that stores the "show less frequently" cap + * being tested. + * @param {object} options.keyword + * The primary keyword to use during the test. + * @param {number} options.keywordBaseIndex + * The index in `keyword` to base substring checks around. This function will + * test substrings starting at the beginning of keyword and ending at the + * following indexes: one index before `keywordBaseIndex`, + * `keywordBaseIndex`, `keywordBaseIndex` + 1, `keywordBaseIndex` + 2, and + * `keywordBaseIndex` + 3. + */ +async function doShowLessFrequentlyTests({ + feature, + expectedResult, + showLessFrequentlyCountPref, + nimbusCapVariable, + keyword, + keywordBaseIndex = keyword.indexOf(" "), +}) { + // Do some sanity checks on the keyword. Any checks that fail are errors in + // the test. + if (keywordBaseIndex <= 0) { + throw new Error( + "keywordBaseIndex must be > 0, but it's " + keywordBaseIndex + ); + } + if (keyword.length < keywordBaseIndex + 3) { + throw new Error( + "keyword must have at least two chars after keywordBaseIndex" + ); + } + + let tests = [ + { + showLessFrequentlyCount: 0, + canShowLessFrequently: true, + newSearches: { + [keyword.substring(0, keywordBaseIndex - 1)]: false, + [keyword.substring(0, keywordBaseIndex)]: true, + [keyword.substring(0, keywordBaseIndex + 1)]: true, + [keyword.substring(0, keywordBaseIndex + 2)]: true, + [keyword.substring(0, keywordBaseIndex + 3)]: true, + }, + }, + { + showLessFrequentlyCount: 1, + canShowLessFrequently: true, + newSearches: { + [keyword.substring(0, keywordBaseIndex)]: false, + }, + }, + { + showLessFrequentlyCount: 2, + canShowLessFrequently: true, + newSearches: { + [keyword.substring(0, keywordBaseIndex + 1)]: false, + }, + }, + { + showLessFrequentlyCount: 3, + canShowLessFrequently: false, + newSearches: { + [keyword.substring(0, keywordBaseIndex + 2)]: false, + }, + }, + { + showLessFrequentlyCount: 3, + canShowLessFrequently: false, + newSearches: {}, + }, + ]; + + info("Testing 'show less frequently' with cap in remote settings"); + await doOneShowLessFrequentlyTest({ + tests, + feature, + expectedResult, + showLessFrequentlyCountPref, + rs: { + show_less_frequently_cap: 3, + }, + }); + + // Nimbus should override remote settings. + info("Testing 'show less frequently' with cap in Nimbus and remote settings"); + await doOneShowLessFrequentlyTest({ + tests, + feature, + expectedResult, + showLessFrequentlyCountPref, + rs: { + show_less_frequently_cap: 10, + }, + nimbus: { + [nimbusCapVariable]: 3, + }, + }); +} + +/** + * Does a group of searches, increments the "show less frequently" count, 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 {BaseFeature} options.feature + * The feature that provides the suggestion matched by the searches. + * @param {*} options.expectedResult + * The expected result that should be matched, for searches that are expected + * to match a result. Can also be a function; it's passed the current search + * string and it should return the expected result. + * @param {string} options.showLessFrequentlyCountPref + * The name of the pref that stores the "show less frequently" count being + * tested. + * @param {object} options.tests + * An array where each item describes a group of new searches to perform and + * expected state. Each item should look like this: + * `{ showLessFrequentlyCount, canShowLessFrequently, newSearches }` + * + * {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} newSearches + * 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 are cumulative: The intended use is to pass a + * large initial group of searches in the first search group, and then each + * following `newSearches` 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 doOneShowLessFrequentlyTest({ + feature, + expectedResult, + showLessFrequentlyCountPref, + tests, + rs = {}, + nimbus = {}, +}) { + // Disable Merino so we trigger only remote settings suggestions. The + // `BaseFeature` is expected to add remote settings suggestions using keywords + // start starting with the first word in each full keyword, but the mock + // Merino server will always return whatever suggestion it's told to return + // regardless of the search string. That means Merino will return a suggestion + // for a keyword that's smaller than the first full word. + UrlbarPrefs.set("quicksuggest.dataCollection.enabled", false); + + // 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, + newSearches, + } of tests) { + info( + "Starting subtest: " + + JSON.stringify({ + showLessFrequentlyCount, + canShowLessFrequently, + newSearches, + }) + ); + + Assert.equal( + feature.showLessFrequentlyCount, + showLessFrequentlyCount, + "showLessFrequentlyCount should be correct initially" + ); + Assert.equal( + UrlbarPrefs.get(showLessFrequentlyCountPref), + showLessFrequentlyCount, + "Pref should be correct initially" + ); + Assert.equal( + feature.canShowLessFrequently, + canShowLessFrequently, + "canShowLessFrequently should be correct initially" + ); + + // Merge the current `newSearches` object into the cumulative object. + cumulativeSearches = { + ...cumulativeSearches, + ...newSearches, + }; + + for (let [searchString, isExpected] of Object.entries( + cumulativeSearches + )) { + info("Doing search: " + JSON.stringify({ searchString, isExpected })); + + let results = []; + if (isExpected) { + results.push( + typeof expectedResult == "function" + ? expectedResult(searchString) + : expectedResult + ); + } + + await check_results({ + context: createContext(searchString, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: results, + }); + } + + feature.incrementShowLessFrequentlyCount(); + } + }, + }); + + await cleanUpNimbus(); + UrlbarPrefs.clear(showLessFrequentlyCountPref); + UrlbarPrefs.set("quicksuggest.dataCollection.enabled", true); +} + +/** + * Queries the Rust component directly and checks the returned suggestions. The + * point is to make sure the Rust backend passes the correct providers to the + * Rust component depending on the types of enabled suggestions. Assuming the + * Rust component isn't buggy, it should return suggestions only for the + * passed-in providers. + * + * @param {object} options + * Options object + * @param {string} options.searchString + * The search string. + * @param {Array} options.tests + * Array of test objects: `{ prefs, expectedUrls }` + * + * For each object, the given prefs are set, the Rust component is queried + * using the given search string, and the URLs of the returned suggestions are + * compared to the given expected URLs (order doesn't matter). + * + * {object} prefs + * An object mapping pref names (relative to `browser.urlbar`) to values. + * These prefs will be set before querying and should be used to enable or + * disable particular types of suggestions. + * {Array} expectedUrls + * An array of the URLs of the suggestions that are expected to be returned. + * The order doesn't matter. + */ +async function doRustProvidersTests({ searchString, tests }) { + UrlbarPrefs.set("quicksuggest.rustEnabled", true); + + for (let { prefs, expectedUrls } of tests) { + info( + "Starting Rust providers test: " + JSON.stringify({ prefs, expectedUrls }) + ); + + info("Setting prefs and forcing sync"); + for (let [name, value] of Object.entries(prefs)) { + UrlbarPrefs.set(name, value); + } + await QuickSuggestTestUtils.forceSync(); + + info("Querying with search string: " + JSON.stringify(searchString)); + let suggestions = await QuickSuggest.rustBackend.query(searchString); + info("Got suggestions: " + JSON.stringify(suggestions)); + + Assert.deepEqual( + suggestions.map(s => s.url).sort(), + expectedUrls.sort(), + "query() should return the expected suggestions (by URL)" + ); + + info("Clearing prefs and forcing sync"); + for (let name of Object.keys(prefs)) { + UrlbarPrefs.clear(name); + } + await QuickSuggestTestUtils.forceSync(); + } + + info("Clearing rustEnabled pref and forcing sync"); + UrlbarPrefs.clear("quicksuggest.rustEnabled"); + await QuickSuggestTestUtils.forceSync(); +} 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..b8d62062c0 --- /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_setup(async () => { + 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..e4c145aabb --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest.js @@ -0,0 +1,1661 @@ +/* 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 = "amp"; +const NONSPONSORED_SEARCH_STRING = "wikipedia"; +const SPONSORED_AND_NONSPONSORED_SEARCH_STRING = "sponsored and non-sponsored"; + +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 = [ + QuickSuggestTestUtils.ampRemoteSettings({ + keywords: [ + SPONSORED_SEARCH_STRING, + SPONSORED_AND_NONSPONSORED_SEARCH_STRING, + ], + }), + QuickSuggestTestUtils.wikipediaRemoteSettings({ + keywords: [ + NONSPONSORED_SEARCH_STRING, + SPONSORED_AND_NONSPONSORED_SEARCH_STRING, + ], + }), + { + id: 3, + url: "http://" + PREFIX_SUGGESTIONS_STRIPPED_URL, + title: "HTTP Suggestion", + keywords: [HTTP_SEARCH_STRING], + click_url: "http://example.com/http-click", + impression_url: "http://example.com/http-impression", + advertiser: "HttpAdvertiser", + iab_category: "22 - Shopping", + icon: "1234", + }, + { + 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", + icon: "1234", + }, + { + 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", + icon: "1234", + }, +]; + +function expectedNonSponsoredResult() { + return makeWikipediaResult({ + blockId: 2, + }); +} + +function expectedSponsoredResult() { + return makeAmpResult(); +} + +function expectedSponsoredPriorityResult() { + return { + ...expectedSponsoredResult(), + isBestMatch: true, + suggestedIndex: 1, + isSuggestedIndexRelativeToGroup: false, + }; +} + +function expectedHttpResult() { + let suggestion = REMOTE_SETTINGS_RESULTS[2]; + return makeAmpResult({ + keyword: HTTP_SEARCH_STRING, + title: suggestion.title, + url: suggestion.url, + originalUrl: suggestion.url, + impressionUrl: suggestion.impression_url, + clickUrl: suggestion.click_url, + blockId: suggestion.id, + advertiser: suggestion.advertiser, + }); +} + +function expectedHttpsResult() { + let suggestion = REMOTE_SETTINGS_RESULTS[3]; + return makeAmpResult({ + keyword: HTTPS_SEARCH_STRING, + title: suggestion.title, + url: suggestion.url, + originalUrl: suggestion.url, + impressionUrl: suggestion.impression_url, + clickUrl: suggestion.click_url, + blockId: suggestion.id, + advertiser: suggestion.advertiser, + }); +} + +add_setup(async function init() { + // 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({ + remoteSettingsRecords: [ + { + type: "data", + attachment: REMOTE_SETTINGS_RESULTS, + }, + { + type: "test-data-type", + attachment: testDataTypeResults, + }, + ], + }); +}); + +add_task(async function telemetryType_sponsored() { + Assert.equal( + QuickSuggest.getFeature("AdmWikipedia").getSuggestionTelemetryType({ + is_sponsored: true, + }), + "adm_sponsored", + "Telemetry type should be 'adm_sponsored'" + ); +}); + +add_task(async function telemetryType_nonsponsored() { + Assert.equal( + QuickSuggest.getFeature("AdmWikipedia").getSuggestionTelemetryType({ + is_sponsored: false, + }), + "adm_nonsponsored", + "Telemetry type should be 'adm_nonsponsored'" + ); + Assert.equal( + QuickSuggest.getFeature("AdmWikipedia").getSuggestionTelemetryType({}), + "adm_nonsponsored", + "Telemetry type should be 'adm_nonsponsored' if `is_sponsored` not defined" + ); +}); + +// Tests with only non-sponsored suggestions enabled with a matching search +// string. +add_tasks_with_rust(async function nonsponsoredOnly_match() { + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", false); + await QuickSuggestTestUtils.forceSync(); + + let context = createContext(NONSPONSORED_SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [expectedNonSponsoredResult()], + }); + + // 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. + let result = context.results[0]; + Assert.equal( + result.title, + `${NONSPONSORED_SEARCH_STRING} — Wikipedia Suggestion`, + "result.title should be correct" + ); + Assert.deepEqual( + result.titleHighlights, + [], + "result.titleHighlights should be correct" + ); +}); + +// Tests with only non-sponsored suggestions enabled with a non-matching search +// string. +add_tasks_with_rust(async function nonsponsoredOnly_noMatch() { + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", false); + await QuickSuggestTestUtils.forceSync(); + + 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_tasks_with_rust(async function sponsoredOnly_sponsored() { + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + await QuickSuggestTestUtils.forceSync(); + + let context = createContext(SPONSORED_SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [expectedSponsoredResult()], + }); + + // 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. + let result = context.results[0]; + Assert.equal( + result.title, + `${SPONSORED_SEARCH_STRING} — Amp Suggestion`, + "result.title should be correct" + ); + Assert.deepEqual( + result.titleHighlights, + [], + "result.titleHighlights should be correct" + ); +}); + +// Tests with only sponsored suggestions enabled with a non-matching search +// string. +add_tasks_with_rust(async function sponsoredOnly_nonsponsored() { + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + await QuickSuggestTestUtils.forceSync(); + + 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_tasks_with_rust(async function both_sponsored() { + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + await QuickSuggestTestUtils.forceSync(); + + let context = createContext(SPONSORED_SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [expectedSponsoredResult()], + }); +}); + +// Tests with both sponsored and non-sponsored suggestions enabled with a +// search string that matches the non-sponsored suggestion. +add_tasks_with_rust(async function both_nonsponsored() { + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + await QuickSuggestTestUtils.forceSync(); + + let context = createContext(NONSPONSORED_SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [expectedNonSponsoredResult()], + }); +}); + +// Tests with both sponsored and non-sponsored suggestions enabled with a +// search string that doesn't match either suggestion. +add_tasks_with_rust(async function both_noMatch() { + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + await QuickSuggestTestUtils.forceSync(); + + 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_tasks_with_rust(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_tasks_with_rust(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_tasks_with_rust(async function caseInsensitiveAndLeadingSpaces() { + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + await QuickSuggestTestUtils.forceSync(); + + let context = createContext(" " + SPONSORED_SEARCH_STRING.toUpperCase(), { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [expectedSponsoredResult()], + }); +}); + +// The provider should not be active for search strings that are empty or +// contain only spaces. +add_tasks_with_rust(async function emptySearchStringsAndSpaces() { + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + await QuickSuggestTestUtils.forceSync(); + + 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_tasks_with_rust(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); + await QuickSuggestTestUtils.forceSync(); + + let context = createContext(SPONSORED_SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [expectedSponsoredResult()], + }); + + UrlbarPrefs.clear("browser.search.suggest.enabled"); +}); + +// Results should be returned even when `browser.urlbar.suggest.searches` is +// false. +add_tasks_with_rust(async function browser_search_suggest_enabled() { + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + UrlbarPrefs.set("suggest.searches", false); + await QuickSuggestTestUtils.forceSync(); + + let context = createContext(SPONSORED_SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [expectedSponsoredResult()], + }); + + UrlbarPrefs.clear("suggest.searches"); +}); + +// Neither sponsored nor non-sponsored results should appear in private contexts +// even when suggestions in private windows are enabled. +add_tasks_with_rust(async function privateContext() { + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + await QuickSuggestTestUtils.forceSync(); + + 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_tasks_with_rust(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); + await QuickSuggestTestUtils.forceSync(); + + 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, + }), + expectedSponsoredResult(), + ], + }); + + 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_tasks_with_rust(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); + await QuickSuggestTestUtils.forceSync(); + + 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, + expectedSponsoredResult(), + ], + }); + + 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_tasks_with_rust(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); + await QuickSuggestTestUtils.forceSync(); + + 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, + }), + expectedSponsoredResult(), + 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_tasks_with_rust(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); + await QuickSuggestTestUtils.forceSync(); + + 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, + expectedSponsoredResult(), + 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_tasks_with_rust(async function dedupeAgainstURL_samePrefix() { + await doDedupeAgainstURLTest({ + searchString: HTTP_SEARCH_STRING, + expectedQuickSuggestResult: expectedHttpResult(), + otherPrefix: "http://", + expectOther: false, + }); +}); + +add_tasks_with_rust(async function dedupeAgainstURL_higherPrefix() { + await doDedupeAgainstURLTest({ + searchString: HTTPS_SEARCH_STRING, + expectedQuickSuggestResult: expectedHttpsResult(), + otherPrefix: "http://", + expectOther: false, + }); +}); + +add_tasks_with_rust(async function dedupeAgainstURL_lowerPrefix() { + await doDedupeAgainstURLTest({ + searchString: HTTP_SEARCH_STRING, + expectedQuickSuggestResult: expectedHttpResult(), + 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); + await QuickSuggestTestUtils.forceSync(); + + 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"); + await QuickSuggestTestUtils.forceSync(); + + UrlbarPrefs.clear("suggest.searches"); + await PlacesUtils.history.clear(); +} + +// Tests the remote settings latency histogram. +add_task( + { + // Not supported by the Rust backend. + skip_if: () => UrlbarPrefs.get("quickSuggestRustEnabled"), + }, + async function latencyTelemetry() { + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + await QuickSuggestTestUtils.forceSync(); + + 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: [expectedSponsoredResult()], + }); + + // 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( + { + // Not supported by the Rust backend. + skip_if: () => UrlbarPrefs.get("quickSuggestRustEnabled"), + }, + async function setupAndTeardown() { + Assert.ok( + QuickSuggest.jsBackend.isEnabled, + "Remote settings backend is enabled initially" + ); + + // 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( + !QuickSuggest.jsBackend.rs, + "Settings client is null after disabling suggest prefs" + ); + Assert.ok( + QuickSuggest.jsBackend.isEnabled, + "Remote settings backend remains enabled" + ); + + // 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( + QuickSuggest.jsBackend.rs, + "Settings client is non-null after enabling suggest.quicksuggest.nonsponsored" + ); + + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false); + Assert.ok( + !QuickSuggest.jsBackend.rs, + "Settings client is null after disabling suggest.quicksuggest.nonsponsored" + ); + + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + Assert.ok( + QuickSuggest.jsBackend.rs, + "Settings client is non-null after enabling suggest.quicksuggest.sponsored" + ); + + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + Assert.ok( + QuickSuggest.jsBackend.rs, + "Settings client remains non-null after enabling suggest.quicksuggest.nonsponsored" + ); + + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false); + Assert.ok( + QuickSuggest.jsBackend.rs, + "Settings client remains non-null after disabling suggest.quicksuggest.nonsponsored" + ); + + UrlbarPrefs.set("suggest.quicksuggest.sponsored", false); + Assert.ok( + !QuickSuggest.jsBackend.rs, + "Settings client is null after disabling suggest.quicksuggest.sponsored" + ); + + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + Assert.ok( + QuickSuggest.jsBackend.rs, + "Settings client is non-null after enabling suggest.quicksuggest.nonsponsored" + ); + + UrlbarPrefs.set("quicksuggest.enabled", false); + Assert.ok( + !QuickSuggest.jsBackend.rs, + "Settings client is null after disabling quicksuggest.enabled" + ); + + UrlbarPrefs.set("quicksuggest.enabled", true); + Assert.ok( + QuickSuggest.jsBackend.rs, + "Settings client is non-null after re-enabling quicksuggest.enabled" + ); + Assert.ok( + QuickSuggest.jsBackend.isEnabled, + "Remote settings backend is enabled after re-enabling quicksuggest.enabled" + ); + + UrlbarPrefs.set("quicksuggest.rustEnabled", true); + Assert.ok( + !QuickSuggest.jsBackend.rs, + "Settings client is null after enabling the Rust backend" + ); + Assert.ok( + !QuickSuggest.jsBackend.isEnabled, + "Remote settings backend is disabled after enabling the Rust backend" + ); + + UrlbarPrefs.clear("quicksuggest.rustEnabled"); + Assert.ok( + QuickSuggest.jsBackend.rs, + "Settings client is non-null after disabling the Rust backend" + ); + Assert.ok( + QuickSuggest.jsBackend.isEnabled, + "Remote settings backend is enabled after disabling the Rust backend" + ); + + // 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( + !QuickSuggest.jsBackend.rs, + "Settings client remains null at end of task" + ); + } +); + +// Timestamp templates in URLs should be replaced with real timestamps. +add_tasks_with_rust(async function timestamps() { + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + await QuickSuggestTestUtils.forceSync(); + + // Do a search. + let context = createContext(TIMESTAMP_SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + let controller = UrlbarTestUtils.newMockController(); + 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_tasks_with_rust(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); + await QuickSuggestTestUtils.forceSync(); + context = createContext(TIMESTAMP_SEARCH_STRING, { isPrivate: false }); + + let expectedQuickSuggest = makeAmpResult({ + originalUrl: TIMESTAMP_SUGGESTION_URL, + keyword: TIMESTAMP_SEARCH_STRING, + title: "Timestamp suggestion", + impressionUrl: "http://impression.reporting.test.com/timestamp", + blockId: 5, + advertiser: "TestAdvertiserTimestamp", + iabCategory: "22 - Shopping", + }); + + let expectedResults = [ + expectedHeuristic, + ...expectedBadTimestampResults, + expectedQuickSuggest, + ]; + + let controller = UrlbarTestUtils.newMockController(); + 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"); + await QuickSuggestTestUtils.forceSync(); + + 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" + ); +}); + +// Tests blocking real `UrlbarResult`s. +add_tasks_with_rust(async function block() { + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + await QuickSuggestTestUtils.forceSync(); + + let tests = [ + // [suggestion, expected result] + [REMOTE_SETTINGS_RESULTS[0], expectedSponsoredResult()], + [REMOTE_SETTINGS_RESULTS[1], expectedNonSponsoredResult()], + [REMOTE_SETTINGS_RESULTS[2], expectedHttpResult()], + [REMOTE_SETTINGS_RESULTS[3], expectedHttpsResult()], + ]; + + for (let [suggestion, expectedResult] of tests) { + info("Testing suggestion: " + JSON.stringify(suggestion)); + + // Do a search to get a real `UrlbarResult` created for the suggestion. + let context = createContext(suggestion.keywords[0], { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [expectedResult], + }); + + // Block it. + await QuickSuggest.blockedSuggestions.add(context.results[0].payload.url); + + // Do another search. The result shouldn't be added. + await check_results({ + context: createContext(suggestion.keywords[0], { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [], + }); + + await QuickSuggest.blockedSuggestions.clear(); + } +}); + +// Tests blocking a real `UrlbarResult` whose URL has a timestamp template. +add_tasks_with_rust(async function block_timestamp() { + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + await QuickSuggestTestUtils.forceSync(); + + // Do a search. + let context = createContext(TIMESTAMP_SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + let controller = UrlbarTestUtils.newMockController(); + 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, + }); + + Assert.ok(result.payload.originalUrl, "The actual result has an originalUrl"); + Assert.equal( + result.payload.originalUrl, + REMOTE_SETTINGS_RESULTS[4].url, + "The actual result's originalUrl should be the raw suggestion URL with a timestamp template" + ); + + // Block the result. + await QuickSuggest.blockedSuggestions.add(result.payload.originalUrl); + + // Do another search. The result shouldn't be added. + await check_results({ + context: createContext(TIMESTAMP_SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + 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( + { + // Not supported by the Rust backend. + skip_if: () => UrlbarPrefs.get("quickSuggestRustEnabled"), + }, + async function remoteSettingsDataType() { + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false); + await QuickSuggestTestUtils.forceSync(); + + 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 = expectedSponsoredResult(); + if (dataType) { + expected = JSON.parse(JSON.stringify(expected)); + expected.payload.title = dataType; + } + + // Re-sync. + await QuickSuggestTestUtils.forceSync(); + + let context = createContext(SPONSORED_SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [expected], + }); + + await cleanUpNimbus(); + } + } +); + +add_tasks_with_rust(async function sponsoredPriority_normal() { + await doSponsoredPriorityTest({ + searchWord: SPONSORED_SEARCH_STRING, + remoteSettingsData: [REMOTE_SETTINGS_RESULTS[0]], + expectedMatches: [expectedSponsoredPriorityResult()], + }); +}); + +add_tasks_with_rust(async function sponsoredPriority_nonsponsoredSuggestion() { + // Not affect to except sponsored suggestion. + await doSponsoredPriorityTest({ + searchWord: NONSPONSORED_SEARCH_STRING, + remoteSettingsData: [REMOTE_SETTINGS_RESULTS[1]], + expectedMatches: [expectedNonSponsoredResult()], + }); +}); + +add_tasks_with_rust(async function sponsoredPriority_sponsoredIndex() { + await doSponsoredPriorityTest({ + nimbusSettings: { quickSuggestSponsoredIndex: 2 }, + searchWord: SPONSORED_SEARCH_STRING, + remoteSettingsData: [REMOTE_SETTINGS_RESULTS[0]], + expectedMatches: [expectedSponsoredPriorityResult()], + }); +}); + +add_tasks_with_rust(async function sponsoredPriority_position() { + await doSponsoredPriorityTest({ + nimbusSettings: { quickSuggestAllowPositionInSuggestions: true }, + searchWord: SPONSORED_SEARCH_STRING, + remoteSettingsData: [ + Object.assign({}, REMOTE_SETTINGS_RESULTS[0], { position: 2 }), + ], + expectedMatches: [expectedSponsoredPriorityResult()], + }); +}); + +async function doSponsoredPriorityTest({ + remoteSettingsConfig = {}, + nimbusSettings = {}, + searchWord, + remoteSettingsData, + expectedMatches, +}) { + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + await QuickSuggestTestUtils.forceSync(); + + const cleanUpNimbusEnable = await UrlbarTestUtils.initNimbusFeature({ + ...nimbusSettings, + quickSuggestSponsoredPriority: true, + }); + + await QuickSuggestTestUtils.setRemoteSettingsRecords([ + { + type: "data", + attachment: remoteSettingsData, + }, + ]); + await QuickSuggestTestUtils.setConfig(remoteSettingsConfig); + + await check_results({ + context: createContext(searchWord, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: expectedMatches, + }); + + await cleanUpNimbusEnable(); +} + +// When a Suggest best match and a tab-to-search (TTS) are shown in the same +// search, both will have a `suggestedIndex` value of 1. The TTS should appear +// first. +add_tasks_with_rust(async function tabToSearch() { + // We'll use a sponsored priority result as the best match result. Different + // types of Suggest results can appear as best matches, and they all should + // have the same behavior. + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + await QuickSuggestTestUtils.forceSync(); + + Services.prefs.setBoolPref( + "browser.urlbar.quicksuggest.sponsoredPriority", + true + ); + + // 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); + + // Disable search suggestions so we don't need to expect them below. + Services.prefs.setBoolPref("browser.search.suggest.enabled", false); + + // 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.${SPONSORED_SEARCH_STRING}.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(SPONSORED_SEARCH_STRING, { + isPrivate: false, + }); + await check_results({ + context, + matches: [ + // search heuristic + makeSearchResult(context, { + engineName: Services.search.defaultEngine.name, + engineIconUri: Services.search.defaultEngine.getIconURL(), + 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, + }), + // Suggest best match + expectedSponsoredPriorityResult(), + // visit + makeVisitResult(context, { + uri: engineURL, + title: `test visit for ${engineURL}`, + }), + ], + }); + + await cleanupPlaces(); + await extension.unload(); + + UrlbarPrefs.clear("tabToSearch.onboard.interactionsLeft"); + Services.prefs.clearUserPref("browser.search.suggest.enabled"); + Services.prefs.clearUserPref("browser.urlbar.quicksuggest.sponsoredPriority"); +}); + +// `suggestion.position` should be ignored when the suggestion is a best match. +add_tasks_with_rust(async function position() { + // We'll use a sponsored priority result as the best match result. Different + // types of Suggest results can appear as best matches, and they all should + // have the same behavior. + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + await QuickSuggestTestUtils.forceSync(); + + Services.prefs.setBoolPref( + "browser.urlbar.quicksuggest.sponsoredPriority", + true + ); + + // Disable search suggestions so we don't hit the network. + Services.prefs.setBoolPref("browser.search.suggest.enabled", false); + + // Set the remote settings data with a suggestion containing a position. + UrlbarPrefs.set("quicksuggest.allowPositionInSuggestions", true); + await QuickSuggestTestUtils.setRemoteSettingsRecords([ + { + type: "data", + attachment: [ + { + ...REMOTE_SETTINGS_RESULTS[0], + position: 9, + }, + ], + }, + ]); + + let context = createContext(SPONSORED_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/${SPONSORED_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.getIconURL(), + heuristic: true, + }), + // best match whose backing suggestion has a `position` + expectedSponsoredPriorityResult(), + // visits + ...visitResults.slice(0, maxResultCount - 2), + ], + }); + + await cleanupPlaces(); + await QuickSuggestTestUtils.setRemoteSettingsRecords([ + { + type: "data", + attachment: REMOTE_SETTINGS_RESULTS, + }, + ]); + + UrlbarPrefs.clear("quicksuggest.allowPositionInSuggestions"); + Services.prefs.clearUserPref("browser.search.suggest.enabled"); + Services.prefs.clearUserPref("browser.urlbar.quicksuggest.sponsoredPriority"); +}); + +// The `Amp` and `Wikipedia` Rust providers should be passed to the Rust +// component when querying depending on whether sponsored and non-sponsored +// suggestions are enabled. +add_task(async function rustProviders() { + await doRustProvidersTests({ + searchString: SPONSORED_AND_NONSPONSORED_SEARCH_STRING, + tests: [ + { + prefs: { + "suggest.quicksuggest.nonsponsored": true, + "suggest.quicksuggest.sponsored": true, + }, + expectedUrls: [ + "http://example.com/amp", + "http://example.com/wikipedia", + ], + }, + { + prefs: { + "suggest.quicksuggest.nonsponsored": true, + "suggest.quicksuggest.sponsored": false, + }, + expectedUrls: ["http://example.com/wikipedia"], + }, + { + prefs: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": true, + }, + expectedUrls: ["http://example.com/amp"], + }, + { + prefs: { + "suggest.quicksuggest.nonsponsored": false, + "suggest.quicksuggest.sponsored": false, + }, + expectedUrls: [], + }, + ], + }); +}); 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..c17f3f1655 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_addons.js @@ -0,0 +1,558 @@ +/* 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", +}); + +// TODO: Firefox no longer uses `rating` and `number_of_ratings` but they are +// still present in Merino and RS suggestions, so they are included here for +// greater accuracy. We should remove them from Merino, RS, and tests. +const MERINO_SUGGESTIONS = [ + { + provider: "amo", + icon: "icon", + url: "https://example.com/merino-addon", + 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, + score: 0.25, + }, + { + 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, + score: 0.25, + }, + { + 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, + score: 0.25, + }, + { + url: "https://example.com/fourth-addon?utm_medium=aaa&utm_source=bbb", + guid: "fourth@addon", + icon: "https://example.com/fourth-addon.svg", + title: "Fourth Addon", + rating: "4.7", + keywords: ["fourth", "4th"], + description: "Description for the Fourth Addon", + number_of_ratings: 4, + score: 0.25, + }, + ], + }, +]; + +add_setup(async function init() { + // Disable search suggestions so we don't hit the network. + Services.prefs.setBoolPref("browser.search.suggest.enabled", false); + + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + remoteSettingsRecords: REMOTE_SETTINGS_RESULTS, + merinoSuggestions: MERINO_SUGGESTIONS, + prefs: [["suggest.quicksuggest.nonsponsored", true]], + }); +}); + +add_task(async function telemetryType() { + Assert.equal( + QuickSuggest.getFeature("AddonSuggestions").getSuggestionTelemetryType({}), + "amo", + "Telemetry type should be 'amo'" + ); +}); + +// When quick suggest prefs are disabled, addon suggestions should be disabled. +add_tasks_with_rust(async function quickSuggestPrefsDisabled() { + let prefs = ["quicksuggest.enabled", "suggest.quicksuggest.nonsponsored"]; + for (let pref of prefs) { + // Before disabling the pref, 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", + }), + ], + }); + + // Now disable the pref. + UrlbarPrefs.set(pref, false); + await check_results({ + context: createContext("test", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [], + }); + + UrlbarPrefs.set(pref, true); + await QuickSuggestTestUtils.forceSync(); + } +}); + +// When addon suggestions specific preference is disabled, addon suggestions +// should not be added. +add_tasks_with_rust(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", + }), + ], + }); + + // Now disable the pref. + UrlbarPrefs.set(pref, false); + await check_results({ + context: createContext("test", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [], + }); + + // Revert. + UrlbarPrefs.clear(pref); + await QuickSuggestTestUtils.forceSync(); + } +}); + +// Check wheather the addon suggestions will be shown by the setup of Nimbus +// variable. +add_tasks_with_rust(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 QuickSuggestTestUtils.forceSync(); + await check_results({ + context: createContext("test", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [ + makeExpectedResult({ + suggestion: MERINO_SUGGESTIONS[0], + source: "merino", + }), + ], + }); + await cleanUpNimbusEnable(); + + // Enable locally. + UrlbarPrefs.set("addons.featureGate", true); + await QuickSuggestTestUtils.forceSync(); + + // 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.clear("addons.featureGate"); + await QuickSuggestTestUtils.forceSync(); +}); + +add_tasks_with_rust(async function hideIfAlreadyInstalled() { + // Show suggestion. + await check_results({ + context: createContext("test", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [ + makeExpectedResult({ + suggestion: MERINO_SUGGESTIONS[0], + source: "merino", + }), + ], + }); + + // 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_tasks_with_rust(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", + }), + }, + { + input: "1st", + expected: makeExpectedResult({ + suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0], + source: "remote-settings", + }), + }, + { + input: "t", + expected: null, + }, + { + input: "tw", + expected: null, + }, + { + input: "two", + expected: makeExpectedResult({ + suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0], + source: "remote-settings", + }), + }, + { + input: "two ", + expected: makeExpectedResult({ + suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0], + source: "remote-settings", + }), + }, + { + input: "two w", + expected: makeExpectedResult({ + suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0], + source: "remote-settings", + }), + }, + { + input: "two wo", + expected: makeExpectedResult({ + suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0], + source: "remote-settings", + }), + }, + { + input: "two wor", + expected: makeExpectedResult({ + suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0], + source: "remote-settings", + }), + }, + { + input: "two word", + expected: makeExpectedResult({ + suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0], + source: "remote-settings", + }), + }, + { + input: "two words", + expected: makeExpectedResult({ + suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0], + source: "remote-settings", + }), + }, + { + input: "a", + expected: makeExpectedResult({ + suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0], + source: "remote-settings", + }), + }, + { + input: "a ", + expected: makeExpectedResult({ + suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0], + source: "remote-settings", + }), + }, + { + input: "a b", + expected: makeExpectedResult({ + suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0], + source: "remote-settings", + }), + }, + { + input: "a b ", + expected: makeExpectedResult({ + suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0], + source: "remote-settings", + }), + }, + { + input: "a b c", + expected: makeExpectedResult({ + suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0], + source: "remote-settings", + }), + }, + { + input: "second", + expected: makeExpectedResult({ + suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[1], + source: "remote-settings", + }), + }, + { + input: "2nd", + expected: makeExpectedResult({ + suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[1], + source: "remote-settings", + }), + }, + { + input: "third", + expected: makeExpectedResult({ + suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[2], + source: "remote-settings", + }), + }, + { + input: "3rd", + expected: makeExpectedResult({ + suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[2], + source: "remote-settings", + }), + }, + { + input: "fourth", + expected: makeExpectedResult({ + suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[3], + source: "remote-settings", + setUtmParams: false, + }), + }, + { + input: "FoUrTh", + expected: makeExpectedResult({ + suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[3], + source: "remote-settings", + setUtmParams: false, + }), + }, + ]; + + // Disable Merino so we trigger only remote settings suggestions. + UrlbarPrefs.set("quicksuggest.dataCollection.enabled", false); + + for (let { 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", + }), + ], + }); + + // 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", + }), + ], + }); +}); + +// Tests the "show less frequently" behavior. +add_tasks_with_rust(async function showLessFrequently() { + await doShowLessFrequentlyTests({ + feature: QuickSuggest.getFeature("AddonSuggestions"), + showLessFrequentlyCountPref: "addons.showLessFrequentlyCount", + nimbusCapVariable: "addonsShowLessFrequentlyCap", + expectedResult: makeExpectedResult({ + suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0], + source: "remote-settings", + }), + keyword: "two words", + }); +}); + +// The `Amo` Rust provider should be passed to the Rust component when querying +// depending on whether addon suggestions are enabled. +add_task(async function rustProviders() { + await doRustProvidersTests({ + searchString: "first", + tests: [ + { + prefs: { + "suggest.addons": true, + }, + expectedUrls: ["https://example.com/first-addon"], + }, + { + prefs: { + "suggest.addons": false, + }, + expectedUrls: [], + }, + ], + }); + + UrlbarPrefs.clear("suggest.addons"); + await QuickSuggestTestUtils.forceSync(); +}); + +function makeExpectedResult({ suggestion, source, setUtmParams = true }) { + if ( + source == "remote-settings" && + UrlbarPrefs.get("quicksuggest.rustEnabled") + ) { + source = "rust"; + } + + let provider; + switch (source) { + case "remote-settings": + provider = "AddonSuggestions"; + break; + case "rust": + provider = "Amo"; + break; + case "merino": + provider = "amo"; + break; + } + + return makeAmoResult({ + source, + provider, + setUtmParams, + title: suggestion.title, + description: suggestion.description, + url: suggestion.url, + originalUrl: suggestion.url, + icon: suggestion.icon, + }); +} 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..a9f339c324 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_dynamicWikipedia.js @@ -0,0 +1,103 @@ +/* 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() { + // Disable search suggestions so we don't hit the network. + Services.prefs.setBoolPref("browser.search.suggest.enabled", false); + + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + merinoSuggestions: MERINO_SUGGESTIONS, + prefs: [["suggest.quicksuggest.nonsponsored", true]], + }); +}); + +// 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"); +}); + +add_task(async function mixedCaseQuery() { + await check_results({ + context: createContext("TeSt", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [makeExpectedResult()], + }); +}); + +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", + provider: "wikipedia", + 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..1c00cb5320 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_impressionCaps.js @@ -0,0 +1,3907 @@ +/* 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", + descriptionL10n: { id: "urlbar-result-action-sponsored" }, + helpUrl: QuickSuggest.HELP_URL, + helpL10n: { + id: "urlbar-result-menu-learn-more-about-firefox-suggest", + }, + isBlockable: true, + blockL10n: { + id: "urlbar-result-menu-dismiss-firefox-suggest", + }, + source: "remote-settings", + provider: "AdmWikipedia", + }, +}; + +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: "urlbar-result-menu-learn-more-about-firefox-suggest", + }, + isBlockable: true, + blockL10n: { + id: "urlbar-result-menu-dismiss-firefox-suggest", + }, + source: "remote-settings", + provider: "AdmWikipedia", + }, +}; + +let gSandbox; +let gDateNowStub; +let gStartupDateMsStub; + +add_setup(async () => { + // Disable search suggestions so we don't hit the network. + Services.prefs.setBoolPref("browser.search.suggest.enabled", false); + + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + remoteSettingsRecords: [ + { + type: "data", + attachment: REMOTE_SETTINGS_RESULTS, + }, + ], + prefs: [ + ["quicksuggest.impressionCaps.sponsoredEnabled", true], + ["quicksuggest.impressionCaps.nonSponsoredEnabled", true], + ["suggest.quicksuggest.nonsponsored", true], + ["suggest.quicksuggest.sponsored", true], + ], + }); + + // 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. + if (UrlbarProviderQuickSuggest._resultFromLastQuery) { + UrlbarProviderQuickSuggest._resultFromLastQuery.isVisible = true; + } + const controller = UrlbarTestUtils.newMockController({ + input: { + isPrivate: true, + onFirstResult() { + return false; + }, + getSearchSource() { + return "dummy-search-source"; + }, + window: { + location: { + href: AppConstants.BROWSER_CHROME_URL, + }, + }, + }, + }); + controller.setView({ + get visibleResults() { + return context.results; + }, + controller: { + removeResult() {}, + }, + }); + UrlbarProviderQuickSuggest.onEngagement( + "engagement", + context, + { + selIndex: -1, + }, + controller + ); +} + +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_mdn.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_mdn.js new file mode 100644 index 0000000000..e9bccba649 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_mdn.js @@ -0,0 +1,190 @@ +/* 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 MDN quick suggest results. + +"use strict"; + +const REMOTE_SETTINGS_DATA = [ + { + type: "mdn-suggestions", + attachment: [ + { + url: "https://example.com/array-filter", + title: "Array.prototype.filter()", + description: + "The filter() method creates a shallow copy of a portion of a given array, filtered down to just the elements from the given array that pass the test implemented by the provided function.", + keywords: ["array filter"], + score: 0.24, + }, + { + url: "https://example.com/input", + title: ": The Input (Form Input) element", + description: + "The HTML element is used to create interactive controls for web-based forms in order to accept data from the user; a wide variety of types of input data and control widgets are available, depending on the device and user agent. The element is one of the most powerful and complex in all of HTML due to the sheer number of combinations of input types and attributes.", + keywords: ["input"], + score: 0.24, + }, + { + url: "https://example.com/grid", + title: "CSS Grid Layout", + description: + "CSS Grid Layout excels at dividing a page into major regions or defining the relationship in terms of size, position, and layer, between parts of a control built from HTML primitives.", + keywords: ["grid"], + score: 0.24, + }, + ], + }, +]; + +add_setup(async function init() { + // Disable search suggestions so we don't hit the network. + Services.prefs.setBoolPref("browser.search.suggest.enabled", false); + + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + remoteSettingsRecords: REMOTE_SETTINGS_DATA, + prefs: [ + ["suggest.quicksuggest.nonsponsored", true], + ["suggest.quicksuggest.sponsored", false], + ], + }); +}); + +add_tasks_with_rust(async function basic() { + for (const suggestion of REMOTE_SETTINGS_DATA[0].attachment) { + const fullKeyword = suggestion.keywords[0]; + const firstWord = fullKeyword.split(" ")[0]; + for (let i = 1; i < fullKeyword.length; i++) { + const keyword = fullKeyword.substring(0, i); + const shouldMatch = i >= firstWord.length; + const matches = shouldMatch ? [makeMdnResult(suggestion)] : []; + await check_results({ + context: createContext(keyword, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches, + }); + } + + await check_results({ + context: createContext(fullKeyword + " ", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: + UrlbarPrefs.get("quickSuggestRustEnabled") && !fullKeyword.includes(" ") + ? [makeMdnResult(suggestion)] + : [], + }); + } +}); + +// Check wheather the MDN suggestions will be hidden by the pref. +add_tasks_with_rust(async function disableByLocalPref() { + const suggestion = REMOTE_SETTINGS_DATA[0].attachment[0]; + const keyword = suggestion.keywords[0]; + + const prefs = [ + "suggest.mdn", + "quicksuggest.enabled", + "suggest.quicksuggest.nonsponsored", + ]; + + for (const pref of prefs) { + // First make sure the suggestion is added. + await check_results({ + context: createContext(keyword, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [makeMdnResult(suggestion)], + }); + + // Now disable them. + UrlbarPrefs.set(pref, false); + await check_results({ + context: createContext(keyword, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [], + }); + + // Revert. + UrlbarPrefs.set(pref, true); + await QuickSuggestTestUtils.forceSync(); + } +}); + +// Check wheather the MDN suggestions will be shown by the setup of Nimbus +// variable. +add_tasks_with_rust(async function nimbus() { + const defaultPrefs = Services.prefs.getDefaultBranch("browser.urlbar."); + + const suggestion = REMOTE_SETTINGS_DATA[0].attachment[0]; + const keyword = suggestion.keywords[0]; + + // Disable the fature gate. + defaultPrefs.setBoolPref("mdn.featureGate", false); + await check_results({ + context: createContext(keyword, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [], + }); + + // Enable by Nimbus. + const cleanUpNimbusEnable = await UrlbarTestUtils.initNimbusFeature( + { mdnFeatureGate: true }, + "urlbar", + "config" + ); + await QuickSuggestTestUtils.forceSync(); + await check_results({ + context: createContext(keyword, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [makeMdnResult(suggestion)], + }); + await cleanUpNimbusEnable(); + + // Enable locally. + defaultPrefs.setBoolPref("mdn.featureGate", true); + await QuickSuggestTestUtils.forceSync(); + + // Disable by Nimbus. + const cleanUpNimbusDisable = await UrlbarTestUtils.initNimbusFeature( + { mdnFeatureGate: false }, + "urlbar", + "config" + ); + await check_results({ + context: createContext(keyword, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [], + }); + await cleanUpNimbusDisable(); + + // Revert. + defaultPrefs.setBoolPref("mdn.featureGate", true); + await QuickSuggestTestUtils.forceSync(); +}); + +add_tasks_with_rust(async function mixedCaseQuery() { + const suggestion = REMOTE_SETTINGS_DATA[0].attachment[1]; + const keyword = "InPuT"; + + await check_results({ + context: createContext(keyword, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [makeMdnResult(suggestion)], + }); +}); 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..64f4991236 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_merino.js @@ -0,0 +1,574 @@ +/* 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 SEARCH_STRING = "frab"; + +const { DEFAULT_SUGGESTION_SCORE } = UrlbarProviderQuickSuggest; + +const REMOTE_SETTINGS_RESULTS = [ + QuickSuggestTestUtils.ampRemoteSettings({ + keywords: [SEARCH_STRING], + }), +]; + +const EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT = makeAmpResult({ + keyword: SEARCH_STRING, +}); + +const EXPECTED_MERINO_URLBAR_RESULT = makeAmpResult({ + source: "merino", + provider: "adm", + requestId: "request_id", +}); + +// `UrlbarProviderQuickSuggest.#merino` is lazily created on the first Merino +// fetch, so it's easiest to create `gClient` lazily too. +ChromeUtils.defineLazyGetter( + this, + "gClient", + () => UrlbarProviderQuickSuggest._test_merino +); + +add_setup(async () => { + await MerinoTestUtils.server.start(); + + // Set up the remote settings client with the test data. + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + remoteSettingsRecords: [ + { + type: "data", + attachment: REMOTE_SETTINGS_RESULTS, + }, + ], + prefs: [ + ["suggest.quicksuggest.nonsponsored", true], + ["suggest.quicksuggest.sponsored", true], + ], + }); + + Assert.equal( + typeof DEFAULT_SUGGESTION_SCORE, + "number", + "Sanity check: DEFAULT_SUGGESTION_SCORE is defined" + ); +}); + +// Tests with the Merino endpoint URL set to an empty string, which disables +// fetching from Merino. +add_task(async function merinoDisabled() { + let mockEndpointUrl = UrlbarPrefs.get("merino.endpointURL"); + UrlbarPrefs.set("merino.endpointURL", ""); + UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true); + + // Clear the remote settings suggestions so that if Merino is actually queried + // -- which would be a bug -- we don't accidentally mask the Merino suggestion + // by also matching an RS suggestion with the same or higher score. + await QuickSuggestTestUtils.setRemoteSettingsRecords([]); + + 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: UrlbarProviderQuickSuggest._test_merino, + }); + + UrlbarPrefs.set("merino.endpointURL", mockEndpointUrl); + + await QuickSuggestTestUtils.setRemoteSettingsRecords([ + { + type: "data", + attachment: REMOTE_SETTINGS_RESULTS, + }, + ]); +}); + +// Tests with Merino enabled but with data collection disabled. Results should +// not be fetched from Merino in that case. +add_task(async function dataCollectionDisabled() { + UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, false); + + // Clear the remote settings suggestions so that if Merino is actually queried + // -- which would be a bug -- we don't accidentally mask the Merino suggestion + // by also matching an RS suggestion with the same or higher score. + await QuickSuggestTestUtils.setRemoteSettingsRecords([]); + + let context = createContext(SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [], + }); + + await QuickSuggestTestUtils.setRemoteSettingsRecords([ + { + type: "data", + attachment: REMOTE_SETTINGS_RESULTS, + }, + ]); +}); + +// 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_DATA_COLLECTION_ENABLED, true); + + let histograms = MerinoTestUtils.getAndClearHistograms(); + + MerinoTestUtils.server.response.body.suggestions[0].score = + 2 * 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_DATA_COLLECTION_ENABLED, true); + + let histograms = MerinoTestUtils.getAndClearHistograms(); + + MerinoTestUtils.server.response.body.suggestions[0].score = + 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_DATA_COLLECTION_ENABLED, true); + + let histograms = MerinoTestUtils.getAndClearHistograms(); + + MerinoTestUtils.server.response.body.suggestions[0].score = + 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_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_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_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(); +}); + +// When Merino returns multiple suggestions, the one with the largest score +// should be used. +add_task(async function multipleMerinoSuggestions() { + 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", + iab_category: "22 - Shopping", + 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", + iab_category: "22 - Shopping", + 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", + iab_category: "22 - Shopping", + is_sponsored: true, + score: 0.2, + }, + ]; + + let context = createContext("test", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [ + makeAmpResult({ + keyword: "multipleMerinoSuggestions 1 full_keyword", + title: "multipleMerinoSuggestions 1 title", + url: "multipleMerinoSuggestions 1 url", + originalUrl: "multipleMerinoSuggestions 1 url", + icon: "multipleMerinoSuggestions 1 icon", + impressionUrl: "multipleMerinoSuggestions 1 impression_url", + clickUrl: "multipleMerinoSuggestions 1 click_url", + blockId: 1, + advertiser: "multipleMerinoSuggestions 1 advertiser", + requestId: "request_id", + source: "merino", + provider: "adm", + }), + ], + }); + + 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_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_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); + await QuickSuggestTestUtils.forceSync(); + gClient.resetSession(); +}); + +// Test whether the blocking for Merino results works. +add_task(async function block() { + UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true); + + // Make sure the Merino suggestions have different URLs from the remote + // settings suggestion. + let { suggestions } = MerinoTestUtils.server.response.body; + for (let i = 0; i < suggestions.length; i++) { + let suggestion = suggestions[i]; + suggestion.url = "https://example.com/merino-url-" + i; + 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 top pick/best match. +add_task(async function bestMatch() { + UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true); + + // Set up a suggestion with `is_top_pick` and an unknown provider so that + // UrlbarProviderQuickSuggest will make a default result for it. + MerinoTestUtils.server.response.body.suggestions = [ + { + is_top_pick: true, + provider: "some_top_pick_provider", + full_keyword: "full_keyword", + title: "title", + url: "url", + icon: null, + score: 1, + }, + ]; + + let context = createContext(SEARCH_STRING, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [ + { + isBestMatch: true, + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + heuristic: false, + payload: { + telemetryType: "some_top_pick_provider", + title: "title", + url: "url", + icon: null, + qsSuggestion: "full_keyword", + helpUrl: QuickSuggest.HELP_URL, + helpL10n: { + id: "urlbar-result-menu-learn-more-about-firefox-suggest", + }, + isBlockable: true, + blockL10n: { + id: "urlbar-result-menu-dismiss-firefox-suggest", + }, + displayUrl: "url", + source: "merino", + provider: "some_top_pick_provider", + }, + }, + ], + }); + + // 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"); + + 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..61b1b9186f --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_merinoSessions.js @@ -0,0 +1,173 @@ +/* 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. +ChromeUtils.defineLazyGetter( + this, + "gClient", + () => UrlbarProviderQuickSuggest._test_merino +); + +add_setup(async () => { + await MerinoTestUtils.server.start(); + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + prefs: [ + ["suggest.quicksuggest.sponsored", true], + ["quicksuggest.dataCollection.enabled", true], + ], + }); +}); + +// 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({ controller }); +}); + +// 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, controller }); + } +} + +// 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({ controller }); +}); + +function endEngagement({ controller, context = null, state = "engagement" }) { + UrlbarProviderQuickSuggest.onEngagement( + state, + context || + createContext("endEngagement", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + { selIndex: -1 }, + controller + ); + + 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..851757b11b --- /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_setup(async () => { + 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..991e8c66f9 --- /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_setup(async () => { + 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..8ac7b85ba2 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_nonUniqueKeywords.js @@ -0,0 +1,285 @@ +/* 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"; + +const { DEFAULT_SUGGESTION_SCORE } = UrlbarProviderQuickSuggest; + +// 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, + score: DEFAULT_SUGGESTION_SCORE, + }, + { + keywords: ["aaa", "bbb"], + isSponsored: false, + score: 2 * DEFAULT_SUGGESTION_SCORE, + }, + { + keywords: ["bbb"], + isSponsored: true, + score: 4 * DEFAULT_SUGGESTION_SCORE, + }, + { + keywords: ["bbb"], + isSponsored: false, + score: 3 * DEFAULT_SUGGESTION_SCORE, + }, + { + keywords: ["ccc"], + isSponsored: true, + score: DEFAULT_SUGGESTION_SCORE, + }, +]; + +// 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 () { + // 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, + score, + block_id: qsResult.id, + is_sponsored: isSponsored, + 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, + descriptionL10n: isSponsored + ? { id: "urlbar-result-action-sponsored" } + : undefined, + helpUrl: QuickSuggest.HELP_URL, + helpL10n: { + id: "urlbar-result-menu-learn-more-about-firefox-suggest", + }, + isBlockable: true, + blockL10n: { + id: "urlbar-result-menu-dismiss-firefox-suggest", + }, + source: "remote-settings", + provider: "AdmWikipedia", + }, + }); + } + + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + remoteSettingsRecords: [ + { + type: "data", + attachment: qsResults, + }, + ], + prefs: [ + ["suggest.quicksuggest.sponsored", true], + ["suggest.quicksuggest.nonsponsored", true], + ], + }); + + // 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 QuickSuggest.jsBackend.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); + await QuickSuggestTestUtils.forceSync(); + + // 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); + await QuickSuggestTestUtils.forceSync(); + } +}); 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..c01792e321 --- /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_setup(async () => { + 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_pocket.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_pocket.js new file mode 100644 index 0000000000..29133a8579 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_pocket.js @@ -0,0 +1,531 @@ +/* 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 Pocket quick suggest results. + +"use strict"; + +const LOW_KEYWORD = "low one two"; +const HIGH_KEYWORD = "high three"; + +const REMOTE_SETTINGS_DATA = [ + { + type: "pocket-suggestions", + attachment: [ + { + url: "https://example.com/pocket-0", + title: "Pocket Suggestion 0", + description: "Pocket description 0", + lowConfidenceKeywords: [LOW_KEYWORD, "how to low"], + highConfidenceKeywords: [HIGH_KEYWORD], + score: 0.25, + }, + { + url: "https://example.com/pocket-1", + title: "Pocket Suggestion 1", + description: "Pocket description 1", + lowConfidenceKeywords: ["other low"], + highConfidenceKeywords: ["another high"], + score: 0.25, + }, + ], + }, +]; + +add_setup(async () => { + // Disable search suggestions so we don't hit the network. + Services.prefs.setBoolPref("browser.search.suggest.enabled", false); + + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + remoteSettingsRecords: REMOTE_SETTINGS_DATA, + prefs: [ + ["suggest.quicksuggest.nonsponsored", true], + ["pocket.featureGate", true], + ], + }); +}); + +add_task(async function telemetryType() { + Assert.equal( + QuickSuggest.getFeature("PocketSuggestions").getSuggestionTelemetryType({}), + "pocket", + "Telemetry type should be 'pocket'" + ); +}); + +// When non-sponsored suggestions are disabled, Pocket suggestions should be +// disabled. +add_tasks_with_rust(async function nonsponsoredDisabled() { + // Disable sponsored suggestions. Pocket 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(LOW_KEYWORD, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [makeExpectedResult({ searchString: LOW_KEYWORD })], + }); + + // Now disable them. + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false); + await check_results({ + context: createContext(LOW_KEYWORD, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [], + }); + + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.clear("suggest.quicksuggest.sponsored"); + await QuickSuggestTestUtils.forceSync(); +}); + +// When Pocket-specific preferences are disabled, suggestions should not be +// added. +add_tasks_with_rust(async function pocketSpecificPrefsDisabled() { + const prefs = ["suggest.pocket", "pocket.featureGate"]; + for (const pref of prefs) { + // First make sure the suggestion is added. + await check_results({ + context: createContext(LOW_KEYWORD, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [makeExpectedResult({ searchString: LOW_KEYWORD })], + }); + + // Now disable the pref. + UrlbarPrefs.set(pref, false); + await check_results({ + context: createContext(LOW_KEYWORD, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [], + }); + + // Revert. + UrlbarPrefs.set(pref, true); + await QuickSuggestTestUtils.forceSync(); + } +}); + +// Check wheather the Pocket suggestions will be shown by the setup of Nimbus +// variable. +add_tasks_with_rust(async function nimbus() { + // Disable the fature gate. + UrlbarPrefs.set("pocket.featureGate", false); + await check_results({ + context: createContext(LOW_KEYWORD, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [], + }); + + // Enable by Nimbus. + const cleanUpNimbusEnable = await UrlbarTestUtils.initNimbusFeature({ + pocketFeatureGate: true, + }); + await QuickSuggestTestUtils.forceSync(); + await check_results({ + context: createContext(LOW_KEYWORD, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [makeExpectedResult({ searchString: LOW_KEYWORD })], + }); + await cleanUpNimbusEnable(); + + // Enable locally. + UrlbarPrefs.set("pocket.featureGate", true); + await QuickSuggestTestUtils.forceSync(); + + // Disable by Nimbus. + const cleanUpNimbusDisable = await UrlbarTestUtils.initNimbusFeature({ + pocketFeatureGate: false, + }); + await check_results({ + context: createContext(LOW_KEYWORD, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [], + }); + await cleanUpNimbusDisable(); + + // Revert. + UrlbarPrefs.set("pocket.featureGate", true); + await QuickSuggestTestUtils.forceSync(); +}); + +// The suggestion should be shown as a top pick when a high-confidence keyword +// is matched. +add_tasks_with_rust(async function topPick() { + await check_results({ + context: createContext(HIGH_KEYWORD, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [ + makeExpectedResult({ searchString: HIGH_KEYWORD, isTopPick: true }), + ], + }); +}); + +// Low-confidence keywords should do prefix matching starting at the first word. +add_tasks_with_rust(async function lowPrefixes() { + // search string -> should match + let tests = { + l: false, + lo: false, + low: true, + "low ": true, + "low o": true, + "low on": true, + "low one": true, + "low one ": true, + "low one t": true, + "low one tw": true, + "low one two": true, + "low one two ": false, + }; + for (let [searchString, shouldMatch] of Object.entries(tests)) { + info("Doing search: " + JSON.stringify({ searchString, shouldMatch })); + await check_results({ + context: createContext(searchString, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: shouldMatch + ? [makeExpectedResult({ searchString, fullKeyword: LOW_KEYWORD })] + : [], + }); + } +}); + +// Low-confidence keywords that start with "how to" should do prefix matching +// starting at "how to" instead of the first word. +// +// Note: The Rust implementation doesn't support this. +add_task( + { + skip_if: () => UrlbarPrefs.get("quickSuggestRustEnabled"), + }, + async function lowPrefixes_howTo() { + // search string -> should match + let tests = { + h: false, + ho: false, + how: false, + "how ": false, + "how t": false, + "how to": true, + "how to ": true, + "how to l": true, + "how to lo": true, + "how to low": true, + }; + for (let [searchString, shouldMatch] of Object.entries(tests)) { + info("Doing search: " + JSON.stringify({ searchString, shouldMatch })); + await check_results({ + context: createContext(searchString, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: shouldMatch + ? [makeExpectedResult({ searchString, fullKeyword: "how to low" })] + : [], + }); + } + } +); + +// High-confidence keywords should not do prefix matching at all. +add_tasks_with_rust(async function highPrefixes() { + // search string -> should match + let tests = { + h: false, + hi: false, + hig: false, + high: false, + "high ": false, + "high t": false, + "high th": false, + "high thr": false, + "high thre": false, + "high three": true, + "high three ": false, + }; + for (let [searchString, shouldMatch] of Object.entries(tests)) { + info("Doing search: " + JSON.stringify({ searchString, shouldMatch })); + await check_results({ + context: createContext(searchString, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: shouldMatch + ? [ + makeExpectedResult({ + searchString, + fullKeyword: HIGH_KEYWORD, + isTopPick: true, + }), + ] + : [], + }); + } +}); + +// Keyword matching should be case insenstive. +add_tasks_with_rust(async function uppercase() { + await check_results({ + context: createContext(LOW_KEYWORD.toUpperCase(), { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [ + makeExpectedResult({ + searchString: LOW_KEYWORD.toUpperCase(), + fullKeyword: LOW_KEYWORD, + }), + ], + }); + await check_results({ + context: createContext(HIGH_KEYWORD.toUpperCase(), { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [ + makeExpectedResult({ + searchString: HIGH_KEYWORD.toUpperCase(), + fullKeyword: HIGH_KEYWORD, + isTopPick: true, + }), + ], + }); +}); + +// Tests the "Not relevant" command: a dismissed suggestion shouldn't be added. +add_tasks_with_rust(async function notRelevant() { + let result = makeExpectedResult({ searchString: LOW_KEYWORD }); + + info("Triggering the 'Not relevant' command"); + QuickSuggest.getFeature("PocketSuggestions").handleCommand( + { + controller: { removeResult() {} }, + }, + result, + "not_relevant" + ); + await QuickSuggest.blockedSuggestions._test_readyPromise; + + Assert.ok( + await QuickSuggest.blockedSuggestions.has(result.payload.originalUrl), + "The result's URL should be blocked" + ); + + info("Doing search for blocked suggestion"); + await check_results({ + context: createContext(LOW_KEYWORD, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [], + }); + + info("Doing search for blocked suggestion using high-confidence keyword"); + await check_results({ + context: createContext(HIGH_KEYWORD, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [], + }); + + info("Doing search for a suggestion that wasn't blocked"); + await check_results({ + context: createContext("other low", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [ + makeExpectedResult({ + searchString: "other low", + suggestion: REMOTE_SETTINGS_DATA[0].attachment[1], + }), + ], + }); + + info("Clearing blocked suggestions"); + await QuickSuggest.blockedSuggestions.clear(); + + info("Doing search for unblocked suggestion"); + await check_results({ + context: createContext(LOW_KEYWORD, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [result], + }); +}); + +// Tests the "Not interested" command: all Pocket suggestions should be disabled +// and not added anymore. +add_tasks_with_rust(async function notInterested() { + let result = makeExpectedResult({ searchString: LOW_KEYWORD }); + + info("Triggering the 'Not interested' command"); + QuickSuggest.getFeature("PocketSuggestions").handleCommand( + { + controller: { removeResult() {} }, + }, + result, + "not_interested" + ); + + Assert.ok( + !UrlbarPrefs.get("suggest.pocket"), + "Pocket suggestions should be disabled" + ); + + info("Doing search for the suggestion the command was used on"); + await check_results({ + context: createContext(LOW_KEYWORD, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [], + }); + + info("Doing search for another Pocket suggestion"); + await check_results({ + context: createContext("other low", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [], + }); + + UrlbarPrefs.clear("suggest.pocket"); + await QuickSuggestTestUtils.forceSync(); +}); + +// Tests the "show less frequently" behavior. +add_tasks_with_rust(async function showLessFrequently() { + await doShowLessFrequentlyTests({ + feature: QuickSuggest.getFeature("PocketSuggestions"), + showLessFrequentlyCountPref: "pocket.showLessFrequentlyCount", + nimbusCapVariable: "pocketShowLessFrequentlyCap", + expectedResult: searchString => + makeExpectedResult({ searchString, fullKeyword: LOW_KEYWORD }), + keyword: LOW_KEYWORD, + }); +}); + +// The `Pocket` Rust provider should be passed to the Rust component when +// querying depending on whether Pocket suggestions are enabled. +add_task(async function rustProviders() { + // TODO bug 1874074: The Rust component fetches Pocket suggestions when the + // AMO provider is specified regardless of whether the Pocket provider is + // specified. AMO suggestions are enabled by default, so disable them first so + // that the Rust backend does not pass in the AMO provider. + UrlbarPrefs.set("suggest.addons", false); + + await doRustProvidersTests({ + searchString: LOW_KEYWORD, + tests: [ + { + prefs: { + "suggest.pocket": true, + }, + expectedUrls: ["https://example.com/pocket-0"], + }, + { + prefs: { + "suggest.pocket": false, + }, + expectedUrls: [], + }, + ], + }); + + UrlbarPrefs.clear("suggest.addons"); + UrlbarPrefs.clear("suggest.pocket"); + await QuickSuggestTestUtils.forceSync(); +}); + +function makeExpectedResult({ + searchString, + fullKeyword = searchString, + suggestion = REMOTE_SETTINGS_DATA[0].attachment[0], + source = "remote-settings", + isTopPick = false, +} = {}) { + if ( + source == "remote-settings" && + UrlbarPrefs.get("quicksuggest.rustEnabled") + ) { + source = "rust"; + } + + let provider; + let keywordSubstringNotTyped = fullKeyword.substring(searchString.length); + let description = suggestion.description; + switch (source) { + case "remote-settings": + provider = "PocketSuggestions"; + break; + case "rust": + provider = "Pocket"; + // Rust suggestions currently do not include full keyword or description. + keywordSubstringNotTyped = ""; + description = suggestion.title; + break; + case "merino": + provider = "pocket"; + break; + } + + let url = new URL(suggestion.url); + url.searchParams.set("utm_medium", "firefox-desktop"); + url.searchParams.set("utm_source", "firefox-suggest"); + url.searchParams.set("utm_campaign", "pocket-collections-in-the-address-bar"); + url.searchParams.set("utm_content", "treatment"); + + return { + isBestMatch: isTopPick, + suggestedIndex: isTopPick ? 1 : -1, + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.OTHER_NETWORK, + heuristic: false, + payload: { + source, + provider, + telemetryType: "pocket", + title: suggestion.title, + url: url.href, + displayUrl: url.href.replace(/^https:\/\//, ""), + originalUrl: suggestion.url, + description: isTopPick ? description : "", + icon: isTopPick + ? "chrome://global/skin/icons/pocket.svg" + : "chrome://global/skin/icons/pocket-favicon.ico", + helpUrl: QuickSuggest.HELP_URL, + shouldShowUrl: true, + bottomTextL10n: { + id: "firefox-suggest-pocket-bottom-text", + args: { + keywordSubstringTyped: searchString, + keywordSubstringNotTyped, + }, + }, + }, + }; +} 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..d1845a9b22 --- /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, + descriptionL10n: isSponsored + ? { id: "urlbar-result-action-sponsored" } + : undefined, + helpUrl: QuickSuggest.HELP_URL, + helpL10n: { + id: "urlbar-result-menu-learn-more-about-firefox-suggest", + }, + isBlockable: true, + blockL10n: { + id: "urlbar-result-menu-dismiss-firefox-suggest", + }, + displayUrl: suggest.url, + source: "remote-settings", + provider: "AdmWikipedia", + }, + }; +} + +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_setup(async function () { + // Setup for quick suggest result. + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + remoteSettingsRecords: [ + { + 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, + ], + }, + ], + prefs: [ + ["suggest.quicksuggest.sponsored", true], + ["suggest.quicksuggest.nonsponsored", true], + ], + }); + + // 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_scoreMap.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_scoreMap.js new file mode 100644 index 0000000000..224dd6cb22 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_scoreMap.js @@ -0,0 +1,670 @@ +/* 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 `quickSuggestScoreMap` Nimbus variable that assigns scores to +// specified types of quick suggest suggestions. The scores in the map should +// override the scores in the individual suggestion objects so that experiments +// can fully control the relative ranking of suggestions. + +"use strict"; + +const { DEFAULT_SUGGESTION_SCORE } = UrlbarProviderQuickSuggest; + +const REMOTE_SETTINGS_RECORDS = [ + { + type: "data", + attachment: [ + // sponsored without score + QuickSuggestTestUtils.ampRemoteSettings({ + score: undefined, + keywords: [ + "sponsored without score", + "sponsored without score, nonsponsored without score", + "sponsored without score, nonsponsored with score", + "sponsored without score, addon without score", + ], + url: "https://example.com/sponsored-without-score", + title: "Sponsored without score", + }), + // sponsored with score + QuickSuggestTestUtils.ampRemoteSettings({ + score: 2 * DEFAULT_SUGGESTION_SCORE, + keywords: [ + "sponsored with score", + "sponsored with score, nonsponsored without score", + "sponsored with score, nonsponsored with score", + "sponsored with score, addon with score", + ], + url: "https://example.com/sponsored-with-score", + title: "Sponsored with score", + }), + // nonsponsored without score + QuickSuggestTestUtils.wikipediaRemoteSettings({ + score: undefined, + keywords: [ + "nonsponsored without score", + "sponsored without score, nonsponsored without score", + "sponsored with score, nonsponsored without score", + ], + url: "https://example.com/nonsponsored-without-score", + title: "Nonsponsored without score", + }), + // nonsponsored with score + QuickSuggestTestUtils.wikipediaRemoteSettings({ + score: 2 * DEFAULT_SUGGESTION_SCORE, + keywords: [ + "nonsponsored with score", + "sponsored without score, nonsponsored with score", + "sponsored with score, nonsponsored with score", + ], + url: "https://example.com/nonsponsored-with-score", + title: "Nonsponsored with score", + }), + ], + }, + { + type: "amo-suggestions", + attachment: [ + // addon with score + QuickSuggestTestUtils.amoRemoteSettings({ + score: 2 * DEFAULT_SUGGESTION_SCORE, + keywords: [ + "addon with score", + "sponsored with score, addon with score", + ], + url: "https://example.com/addon-with-score", + title: "Addon with score", + }), + ], + }, +]; + +const ADM_RECORD = REMOTE_SETTINGS_RECORDS[0]; +const SPONSORED_WITHOUT_SCORE = ADM_RECORD.attachment[0]; +const SPONSORED_WITH_SCORE = ADM_RECORD.attachment[1]; +const NONSPONSORED_WITHOUT_SCORE = ADM_RECORD.attachment[2]; +const NONSPONSORED_WITH_SCORE = ADM_RECORD.attachment[3]; + +const ADDON_RECORD = REMOTE_SETTINGS_RECORDS[1]; +const ADDON_WITH_SCORE = ADDON_RECORD.attachment[0]; + +const MERINO_SPONSORED_SUGGESTION = { + provider: "adm", + score: DEFAULT_SUGGESTION_SCORE, + iab_category: "22 - Shopping", + is_sponsored: true, + keywords: ["test"], + full_keyword: "test", + block_id: 1, + url: "https://example.com/merino-sponsored", + title: "Merino sponsored", + click_url: "https://example.com/click", + impression_url: "https://example.com/impression", + advertiser: "TestAdvertiser", + icon: "1234", +}; + +const MERINO_ADDON_SUGGESTION = { + provider: "amo", + score: DEFAULT_SUGGESTION_SCORE, + keywords: ["test"], + icon: "https://example.com/addon.svg", + url: "https://example.com/merino-addon", + title: "Merino addon", + description: "Merino addon", + custom_details: { + amo: { + guid: "merino-addon@example.com", + rating: "4.7", + number_of_ratings: "1256", + }, + }, +}; + +const MERINO_UNKNOWN_SUGGESTION = { + provider: "some_unknown_provider", + score: DEFAULT_SUGGESTION_SCORE, + keywords: ["test"], + url: "https://example.com/merino-unknown", + title: "Merino unknown", +}; + +add_setup(async function init() { + // Disable search suggestions so we don't hit the network. + Services.prefs.setBoolPref("browser.search.suggest.enabled", false); + + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + remoteSettingsRecords: REMOTE_SETTINGS_RECORDS, + merinoSuggestions: [], + prefs: [ + ["suggest.quicksuggest.sponsored", true], + ["suggest.quicksuggest.nonsponsored", true], + ], + }); +}); + +add_task(async function sponsoredWithout_nonsponsoredWithout_sponsoredWins() { + let keyword = "sponsored without score, nonsponsored without score"; + let score = 10 * DEFAULT_SUGGESTION_SCORE; + await doTest({ + keyword, + scoreMap: { + adm_sponsored: score, + }, + expectedFeatureName: "AdmWikipedia", + expectedScore: score, + expectedResult: makeExpectedAdmResult({ + keyword, + suggestion: SPONSORED_WITHOUT_SCORE, + }), + }); +}); + +add_task( + async function sponsoredWithout_nonsponsoredWithout_nonsponsoredWins() { + let keyword = "sponsored without score, nonsponsored without score"; + let score = 10 * DEFAULT_SUGGESTION_SCORE; + await doTest({ + keyword, + scoreMap: { + adm_nonsponsored: score, + }, + expectedFeatureName: "AdmWikipedia", + expectedScore: score, + expectedResult: makeExpectedWikipediaResult({ + keyword, + suggestion: NONSPONSORED_WITHOUT_SCORE, + }), + }); + } +); + +add_task( + async function sponsoredWithout_nonsponsoredWithout_sponsoredWins_both() { + let keyword = "sponsored without score, nonsponsored without score"; + let score = 10 * DEFAULT_SUGGESTION_SCORE; + await doTest({ + keyword, + scoreMap: { + adm_sponsored: score, + adm_nonsponsored: score / 2, + }, + expectedFeatureName: "AdmWikipedia", + expectedScore: score, + expectedResult: makeExpectedAdmResult({ + keyword, + suggestion: SPONSORED_WITHOUT_SCORE, + }), + }); + } +); + +add_task( + async function sponsoredWithout_nonsponsoredWithout_nonsponsoredWins_both() { + let keyword = "sponsored without score, nonsponsored without score"; + let score = 10 * DEFAULT_SUGGESTION_SCORE; + await doTest({ + keyword, + scoreMap: { + adm_nonsponsored: score, + adm_sponsored: score / 2, + }, + expectedFeatureName: "AdmWikipedia", + expectedScore: score, + expectedResult: makeExpectedWikipediaResult({ + keyword, + suggestion: NONSPONSORED_WITHOUT_SCORE, + }), + }); + } +); + +add_task(async function sponsoredWith_nonsponsoredWith_sponsoredWins() { + let keyword = "sponsored with score, nonsponsored with score"; + let score = 10 * DEFAULT_SUGGESTION_SCORE; + await doTest({ + keyword, + scoreMap: { + adm_sponsored: score, + }, + expectedFeatureName: "AdmWikipedia", + expectedScore: score, + expectedResult: makeExpectedAdmResult({ + keyword, + suggestion: SPONSORED_WITH_SCORE, + }), + }); +}); + +add_task(async function sponsoredWith_nonsponsoredWith_nonsponsoredWins() { + let keyword = "sponsored with score, nonsponsored with score"; + let score = 10 * DEFAULT_SUGGESTION_SCORE; + await doTest({ + keyword, + scoreMap: { + adm_nonsponsored: score, + }, + expectedFeatureName: "AdmWikipedia", + expectedScore: score, + expectedResult: makeExpectedWikipediaResult({ + keyword, + suggestion: NONSPONSORED_WITH_SCORE, + }), + }); +}); + +add_task(async function sponsoredWith_nonsponsoredWith_sponsoredWins_both() { + let keyword = "sponsored with score, nonsponsored with score"; + let score = 10 * DEFAULT_SUGGESTION_SCORE; + await doTest({ + keyword, + scoreMap: { + adm_sponsored: score, + adm_nonsponsored: score / 2, + }, + expectedFeatureName: "AdmWikipedia", + expectedScore: score, + expectedResult: makeExpectedAdmResult({ + keyword, + suggestion: SPONSORED_WITH_SCORE, + }), + }); +}); + +add_task(async function sponsoredWith_nonsponsoredWith_nonsponsoredWins_both() { + let keyword = "sponsored with score, nonsponsored with score"; + let score = 10 * DEFAULT_SUGGESTION_SCORE; + await doTest({ + keyword, + scoreMap: { + adm_nonsponsored: score, + adm_sponsored: score / 2, + }, + expectedFeatureName: "AdmWikipedia", + expectedScore: score, + expectedResult: makeExpectedWikipediaResult({ + keyword, + suggestion: NONSPONSORED_WITH_SCORE, + }), + }); +}); + +add_task(async function sponsoredWithout_addonWithout_sponsoredWins() { + let keyword = "sponsored without score, addon without score"; + let score = 10 * DEFAULT_SUGGESTION_SCORE; + await doTest({ + keyword, + scoreMap: { + adm_sponsored: score, + }, + expectedFeatureName: "AdmWikipedia", + expectedScore: score, + expectedResult: makeExpectedAdmResult({ + keyword, + suggestion: SPONSORED_WITHOUT_SCORE, + }), + }); +}); + +add_task(async function sponsoredWithout_addonWithout_sponsoredWins_both() { + let keyword = "sponsored without score, addon without score"; + let score = 10 * DEFAULT_SUGGESTION_SCORE; + await doTest({ + keyword, + scoreMap: { + adm_sponsored: score, + amo: score / 2, + }, + expectedFeatureName: "AdmWikipedia", + expectedScore: score, + expectedResult: makeExpectedAdmResult({ + keyword, + suggestion: SPONSORED_WITHOUT_SCORE, + }), + }); +}); + +add_task(async function sponsoredWith_addonWith_sponsoredWins() { + let keyword = "sponsored with score, addon with score"; + let score = 10 * DEFAULT_SUGGESTION_SCORE; + await doTest({ + keyword, + scoreMap: { + adm_sponsored: score, + }, + expectedFeatureName: "AdmWikipedia", + expectedScore: score, + expectedResult: makeExpectedAdmResult({ + keyword, + suggestion: SPONSORED_WITH_SCORE, + }), + }); +}); + +add_task(async function sponsoredWith_addonWith_addonWins() { + let keyword = "sponsored with score, addon with score"; + let score = 10 * DEFAULT_SUGGESTION_SCORE; + await doTest({ + keyword, + scoreMap: { + amo: score, + }, + expectedFeatureName: "AddonSuggestions", + expectedScore: score, + expectedResult: makeExpectedAddonResult({ + suggestion: ADDON_WITH_SCORE, + }), + }); +}); + +add_task(async function sponsoredWith_addonWith_sponsoredWins_both() { + let keyword = "sponsored with score, addon with score"; + let score = 10 * DEFAULT_SUGGESTION_SCORE; + await doTest({ + keyword, + scoreMap: { + adm_sponsored: score, + amo: score / 2, + }, + expectedFeatureName: "AdmWikipedia", + expectedScore: score, + expectedResult: makeExpectedAdmResult({ + keyword, + suggestion: SPONSORED_WITH_SCORE, + }), + }); +}); + +add_task(async function sponsoredWith_addonWith_addonWins_both() { + let keyword = "sponsored with score, addon with score"; + let score = 10 * DEFAULT_SUGGESTION_SCORE; + await doTest({ + keyword, + scoreMap: { + amo: score, + adm_sponsored: score / 2, + }, + expectedFeatureName: "AddonSuggestions", + expectedScore: score, + expectedResult: makeExpectedAddonResult({ + suggestion: ADDON_WITH_SCORE, + }), + }); +}); + +add_task(async function merino_sponsored_addon_sponsoredWins() { + await QuickSuggestTestUtils.setRemoteSettingsRecords([]); + + MerinoTestUtils.server.response.body.suggestions = [ + MERINO_SPONSORED_SUGGESTION, + MERINO_ADDON_SUGGESTION, + ]; + + let score = 10 * DEFAULT_SUGGESTION_SCORE; + await doTest({ + keyword: "test", + scoreMap: { + adm_sponsored: score, + }, + expectedFeatureName: "AdmWikipedia", + expectedScore: score, + expectedResult: makeExpectedAdmResult({ + keyword: "test", + suggestion: MERINO_SPONSORED_SUGGESTION, + source: "merino", + provider: "adm", + requestId: MerinoTestUtils.server.response.body.request_id, + }), + }); + + await QuickSuggestTestUtils.setRemoteSettingsRecords(REMOTE_SETTINGS_RECORDS); +}); + +add_task(async function merino_sponsored_addon_addonWins() { + await QuickSuggestTestUtils.setRemoteSettingsRecords([]); + + MerinoTestUtils.server.response.body.suggestions = [ + MERINO_SPONSORED_SUGGESTION, + MERINO_ADDON_SUGGESTION, + ]; + + let score = 10 * DEFAULT_SUGGESTION_SCORE; + await doTest({ + keyword: "test", + scoreMap: { + amo: score, + }, + expectedFeatureName: "AddonSuggestions", + expectedScore: score, + expectedResult: makeExpectedAddonResult({ + suggestion: MERINO_ADDON_SUGGESTION, + source: "merino", + provider: "amo", + requestId: MerinoTestUtils.server.response.body.request_id, + }), + }); + + await QuickSuggestTestUtils.setRemoteSettingsRecords(REMOTE_SETTINGS_RECORDS); +}); + +add_task(async function merino_sponsored_unknown_sponsoredWins() { + await QuickSuggestTestUtils.setRemoteSettingsRecords([]); + + MerinoTestUtils.server.response.body.suggestions = [ + MERINO_SPONSORED_SUGGESTION, + MERINO_UNKNOWN_SUGGESTION, + ]; + + let score = 10 * DEFAULT_SUGGESTION_SCORE; + await doTest({ + keyword: "test", + scoreMap: { + adm_sponsored: score, + }, + expectedFeatureName: "AdmWikipedia", + expectedScore: score, + expectedResult: makeExpectedAdmResult({ + keyword: "test", + suggestion: MERINO_SPONSORED_SUGGESTION, + source: "merino", + provider: "adm", + requestId: MerinoTestUtils.server.response.body.request_id, + }), + }); + + await QuickSuggestTestUtils.setRemoteSettingsRecords(REMOTE_SETTINGS_RECORDS); +}); + +add_task(async function merino_sponsored_unknown_unknownWins() { + await QuickSuggestTestUtils.setRemoteSettingsRecords([]); + + MerinoTestUtils.server.response.body.suggestions = [ + MERINO_SPONSORED_SUGGESTION, + MERINO_UNKNOWN_SUGGESTION, + ]; + + let score = 10 * DEFAULT_SUGGESTION_SCORE; + await doTest({ + keyword: "test", + scoreMap: { + [MERINO_UNKNOWN_SUGGESTION.provider]: score, + }, + expectedFeatureName: null, + expectedScore: score, + expectedResult: makeExpectedDefaultResult({ + suggestion: MERINO_UNKNOWN_SUGGESTION, + }), + }); + + await QuickSuggestTestUtils.setRemoteSettingsRecords(REMOTE_SETTINGS_RECORDS); +}); + +add_task(async function stringValue() { + let keyword = "sponsored with score, nonsponsored with score"; + await doTest({ + keyword, + scoreMap: { + adm_sponsored: "123.456", + }, + expectedFeatureName: "AdmWikipedia", + expectedScore: 123.456, + expectedResult: makeExpectedAdmResult({ + keyword, + suggestion: SPONSORED_WITH_SCORE, + }), + }); +}); + +/** + * Sets up Nimbus with a `quickSuggestScoreMap` variable value, does a search, + * and makes sure the expected result is shown and the expected score is set on + * the suggestion. + * + * @param {object} options + * Options object. + * @param {string} options.keyword + * The search string. This should be equal to a keyword from one or more + * suggestions. + * @param {object} options.scoreMap + * The value to set for the `quickSuggestScoreMap` variable. + * @param {string} options.expectedFeatureName + * The name of the `BaseFeature` instance that is expected to create the + * `UrlbarResult` that's shown. If the suggestion is intentionally from an + * unknown Merino provider and therefore the quick suggest provider is + * expected to create a default result for it, set this to null. + * @param {UrlbarResultstring} options.expectedResult + * The `UrlbarResult` that's expected to be shown. + * @param {number} options.expectedScore + * The final `score` value that's expected to be defined on the suggestion + * object. + */ +async function doTest({ + keyword, + scoreMap, + expectedFeatureName, + expectedResult, + expectedScore, +}) { + let cleanUpNimbus = await UrlbarTestUtils.initNimbusFeature({ + quickSuggestScoreMap: scoreMap, + }); + + // Stub the expected feature's `makeResult()` so we can see the value of the + // passed-in suggestion's score. If the suggestion's type is in the score map, + // the provider will set its score before calling `makeResult()`. + let actualScore; + let sandbox; + if (expectedFeatureName) { + sandbox = sinon.createSandbox(); + let feature = QuickSuggest.getFeature(expectedFeatureName); + let stub = sandbox + .stub(feature, "makeResult") + .callsFake((queryContext, suggestion, searchString) => { + actualScore = suggestion.score; + return stub.wrappedMethod.call( + feature, + queryContext, + suggestion, + searchString + ); + }); + } + + await check_results({ + context: createContext(keyword, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [expectedResult], + }); + + if (expectedFeatureName) { + Assert.equal( + actualScore, + expectedScore, + "Suggestion score should be set correctly" + ); + sandbox.restore(); + } + + await cleanUpNimbus(); +} + +function makeExpectedAdmResult({ + suggestion, + keyword, + source, + provider, + requestId, +}) { + return makeAmpResult({ + keyword, + source, + provider, + requestId, + title: suggestion.title, + url: suggestion.url, + originalUrl: suggestion.url, + impressionUrl: suggestion.impression_url, + clickUrl: suggestion.click_url, + blockId: suggestion.id, + advertiser: suggestion.advertiser, + icon: suggestion.icon, + }); +} + +function makeExpectedWikipediaResult({ suggestion, keyword, source }) { + return makeWikipediaResult({ + keyword, + source, + title: suggestion.title, + url: suggestion.url, + originalUrl: suggestion.url, + impressionUrl: suggestion.impression_url, + clickUrl: suggestion.click_url, + blockId: suggestion.id, + }); +} + +function makeExpectedAddonResult({ suggestion, source, provider }) { + return makeAmoResult({ + source, + provider, + title: suggestion.title, + description: suggestion.description, + url: suggestion.url, + originalUrl: suggestion.url, + icon: suggestion.icon, + }); +} + +function makeExpectedDefaultResult({ suggestion }) { + return { + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + heuristic: false, + payload: { + source: "merino", + provider: suggestion.provider, + telemetryType: suggestion.provider, + isSponsored: suggestion.is_sponsored, + title: suggestion.title, + url: suggestion.url, + displayUrl: suggestion.url.replace(/^https:\/\//, ""), + icon: suggestion.icon, + descriptionL10n: suggestion.is_sponsored + ? { id: "urlbar-result-action-sponsored" } + : undefined, + shouldShowUrl: true, + 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_topPicks.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_topPicks.js new file mode 100644 index 0000000000..1b8da54920 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_topPicks.js @@ -0,0 +1,192 @@ +/* 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. +// (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() { + // Disable search suggestions so we don't hit the network. + Services.prefs.setBoolPref("browser.search.suggest.enabled", false); + + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + merinoSuggestions: MERINO_SUGGESTIONS, + prefs: [["suggest.quicksuggest.nonsponsored", true]], + }); +}); + +// 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(); +}); + +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, + shouldShowUrl: true, + source: "merino", + provider: telemetryType, + 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_quicksuggest_yelp.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_yelp.js new file mode 100644 index 0000000000..aa9c700f1c --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_yelp.js @@ -0,0 +1,842 @@ +/* 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 Yelp suggestions. + +"use strict"; + +const { GEOLOCATION } = MerinoTestUtils; + +const REMOTE_SETTINGS_RECORDS = [ + { + type: "yelp-suggestions", + attachment: { + subjects: ["ramen", "ab", "alongerkeyword"], + preModifiers: ["best"], + postModifiers: ["delivery"], + locationSigns: [ + { keyword: "in", needLocation: true }, + { keyword: "nearby", needLocation: false }, + ], + yelpModifiers: [], + icon: "1234", + score: 0.5, + }, + }, +]; + +add_setup(async function () { + Services.prefs.setBoolPref("browser.search.suggest.enabled", false); + + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + remoteSettingsRecords: REMOTE_SETTINGS_RECORDS, + prefs: [ + ["quicksuggest.rustEnabled", true], + ["suggest.quicksuggest.sponsored", true], + ["suggest.yelp", true], + ["yelp.featureGate", true], + ["yelp.minKeywordLength", 5], + ], + }); + + await MerinoTestUtils.initGeolocation(); +}); + +add_task(async function basic() { + const TEST_DATA = [ + { + description: "Basic", + query: "best ramen delivery in tokyo", + expected: { + url: "https://www.yelp.com/search?find_desc=best+ramen+delivery&find_loc=tokyo", + title: "best ramen delivery in tokyo", + }, + }, + { + description: "With upper case", + query: "BeSt RaMeN dElIvErY iN tOkYo", + expected: { + url: "https://www.yelp.com/search?find_desc=BeSt+RaMeN+dElIvErY&find_loc=tOkYo", + title: "BeSt RaMeN dElIvErY iN tOkYo", + }, + }, + { + description: "No specific location with location-sign", + query: "ramen in", + expected: { + url: "https://www.yelp.com/search?find_desc=ramen&find_loc=Yokohama%2C+Kanagawa", + originalUrl: "https://www.yelp.com/search?find_desc=ramen", + displayUrl: + "yelp.com/search?find_desc=ramen&find_loc=Yokohama,+Kanagawa", + title: "ramen in Yokohama, Kanagawa", + }, + }, + { + description: "No specific location with location-modifier", + query: "ramen nearby", + expected: { + url: "https://www.yelp.com/search?find_desc=ramen+nearby&find_loc=Yokohama%2C+Kanagawa", + originalUrl: "https://www.yelp.com/search?find_desc=ramen+nearby", + displayUrl: + "yelp.com/search?find_desc=ramen+nearby&find_loc=Yokohama,+Kanagawa", + title: "ramen nearby in Yokohama, Kanagawa", + }, + }, + { + description: "Query too short, no subject exact match: ra", + query: "ra", + expected: null, + }, + { + description: "Query too short, no subject not exact match: ram", + query: "ram", + expected: null, + }, + { + description: "Query too short, no subject exact match: rame", + query: "rame", + expected: null, + }, + { + description: + "Query length == minKeywordLength, subject exact match: ramen", + query: "ramen", + expected: { + url: "https://www.yelp.com/search?find_desc=ramen&find_loc=Yokohama%2C+Kanagawa", + originalUrl: "https://www.yelp.com/search?find_desc=ramen", + displayUrl: + "yelp.com/search?find_desc=ramen&find_loc=Yokohama,+Kanagawa", + title: "ramen in Yokohama, Kanagawa", + }, + }, + { + description: "Pre-modifier only", + query: "best", + expected: null, + }, + { + description: "Pre-modifier only with trailing space", + query: "best ", + expected: null, + }, + { + description: "Pre-modifier, subject too short", + query: "best r", + expected: null, + }, + { + description: "Pre-modifier, query long enough, subject long enough", + query: "best ra", + expected: { + url: "https://www.yelp.com/search?find_desc=best+ramen&find_loc=Yokohama%2C+Kanagawa", + originalUrl: "https://www.yelp.com/search?find_desc=best+ramen", + displayUrl: + "yelp.com/search?find_desc=best+ramen&find_loc=Yokohama,+Kanagawa", + title: "best ramen in Yokohama, Kanagawa", + }, + }, + { + description: "Subject exact match with length < minKeywordLength", + query: "ab", + expected: { + url: "https://www.yelp.com/search?find_desc=ab&find_loc=Yokohama%2C+Kanagawa", + originalUrl: "https://www.yelp.com/search?find_desc=ab", + displayUrl: "yelp.com/search?find_desc=ab&find_loc=Yokohama,+Kanagawa", + title: "ab in Yokohama, Kanagawa", + }, + }, + { + description: + "Subject exact match with length < minKeywordLength, showLessFrequentlyCount non-zero", + query: "ab", + showLessFrequentlyCount: 1, + expected: null, + }, + { + description: + "Subject exact match with length == minKeywordLength, showLessFrequentlyCount non-zero", + query: "ramen", + showLessFrequentlyCount: 1, + expected: { + url: "https://www.yelp.com/search?find_desc=ramen&find_loc=Yokohama%2C+Kanagawa", + originalUrl: "https://www.yelp.com/search?find_desc=ramen", + displayUrl: + "yelp.com/search?find_desc=ramen&find_loc=Yokohama,+Kanagawa", + title: "ramen in Yokohama, Kanagawa", + }, + }, + { + description: "Query too short: alon", + query: "alon", + expected: null, + }, + { + description: "Query length == minKeywordLength, subject not exact match", + query: "along", + expected: { + url: "https://www.yelp.com/search?find_desc=alongerkeyword&find_loc=Yokohama%2C+Kanagawa", + originalUrl: "https://www.yelp.com/search?find_desc=alongerkeyword", + displayUrl: + "yelp.com/search?find_desc=alongerkeyword&find_loc=Yokohama,+Kanagawa", + title: "alongerkeyword in Yokohama, Kanagawa", + }, + }, + { + description: + "Query length == minKeywordLength, subject not exact match, showLessFrequentlyCount non-zero", + query: "along", + showLessFrequentlyCount: 1, + expected: { + url: "https://www.yelp.com/search?find_desc=alongerkeyword&find_loc=Yokohama%2C+Kanagawa", + originalUrl: "https://www.yelp.com/search?find_desc=alongerkeyword", + displayUrl: + "yelp.com/search?find_desc=alongerkeyword&find_loc=Yokohama,+Kanagawa", + title: "alongerkeyword in Yokohama, Kanagawa", + }, + }, + { + description: + "Query length == minKeywordLength + showLessFrequentlyCount, subject not exact match", + query: "alonge", + showLessFrequentlyCount: 1, + expected: { + url: "https://www.yelp.com/search?find_desc=alongerkeyword&find_loc=Yokohama%2C+Kanagawa", + originalUrl: "https://www.yelp.com/search?find_desc=alongerkeyword", + displayUrl: + "yelp.com/search?find_desc=alongerkeyword&find_loc=Yokohama,+Kanagawa", + title: "alongerkeyword in Yokohama, Kanagawa", + }, + }, + { + description: + "Query length < minKeywordLength + showLessFrequentlyCount, subject not exact match", + query: "alonge", + showLessFrequentlyCount: 2, + expected: { + url: "https://www.yelp.com/search?find_desc=alongerkeyword&find_loc=Yokohama%2C+Kanagawa", + originalUrl: "https://www.yelp.com/search?find_desc=alongerkeyword", + displayUrl: + "yelp.com/search?find_desc=alongerkeyword&find_loc=Yokohama,+Kanagawa", + title: "alongerkeyword in Yokohama, Kanagawa", + }, + }, + { + description: + "Query length == minKeywordLength + showLessFrequentlyCount, subject not exact match", + query: "alonger", + showLessFrequentlyCount: 2, + expected: { + url: "https://www.yelp.com/search?find_desc=alongerkeyword&find_loc=Yokohama%2C+Kanagawa", + originalUrl: "https://www.yelp.com/search?find_desc=alongerkeyword", + displayUrl: + "yelp.com/search?find_desc=alongerkeyword&find_loc=Yokohama,+Kanagawa", + title: "alongerkeyword in Yokohama, Kanagawa", + }, + }, + ]; + + for (let { + description, + query, + showLessFrequentlyCount, + expected, + } of TEST_DATA) { + info( + "Doing basic subtest: " + + JSON.stringify({ + description, + query, + showLessFrequentlyCount, + expected, + }) + ); + + if (typeof showLessFrequentlyCount == "number") { + UrlbarPrefs.set("yelp.showLessFrequentlyCount", showLessFrequentlyCount); + } + + await check_results({ + context: createContext(query, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: expected ? [makeExpectedResult(expected)] : [], + }); + + UrlbarPrefs.clear("yelp.showLessFrequentlyCount"); + } +}); + +add_task(async function telemetryType() { + Assert.equal( + QuickSuggest.getFeature("YelpSuggestions").getSuggestionTelemetryType({}), + "yelp", + "Telemetry type should be 'yelp'" + ); +}); + +// When sponsored suggestions are disabled, Yelp suggestions should be +// disabled. +add_task(async function sponsoredDisabled() { + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false); + + // First make sure the suggestion is added when non-sponsored + // suggestions are enabled, if the rust is enabled. + await check_results({ + context: createContext("ramen in tokyo", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [ + makeExpectedResult({ + url: "https://www.yelp.com/search?find_desc=ramen&find_loc=tokyo", + title: "ramen in tokyo", + }), + ], + }); + + // Now disable the pref. + UrlbarPrefs.set("suggest.quicksuggest.sponsored", false); + Assert.ok( + !QuickSuggest.getFeature("YelpSuggestions").isEnabled, + "Yelp should be disabled" + ); + await check_results({ + context: createContext("ramen in tokyo", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [], + }); + + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + UrlbarPrefs.clear("suggest.quicksuggest.nonsponsored"); + await QuickSuggestTestUtils.forceSync(); + + // Make sure Yelp is enabled again. + Assert.ok( + QuickSuggest.getFeature("YelpSuggestions").isEnabled, + "Yelp should be re-enabled" + ); + await check_results({ + context: createContext("ramen in tokyo", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [ + makeExpectedResult({ + url: "https://www.yelp.com/search?find_desc=ramen&find_loc=tokyo", + title: "ramen in tokyo", + }), + ], + }); +}); + +// When Yelp-specific preferences are disabled, suggestions should not be +// added. +add_task(async function yelpSpecificPrefsDisabled() { + const prefs = ["suggest.yelp", "yelp.featureGate"]; + for (const pref of prefs) { + // First make sure the suggestion is added, if the rust is enabled. + await check_results({ + context: createContext("ramen in tokyo", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [ + makeExpectedResult({ + url: "https://www.yelp.com/search?find_desc=ramen&find_loc=tokyo", + title: "ramen in tokyo", + }), + ], + }); + + // Now disable the pref. + UrlbarPrefs.set(pref, false); + Assert.ok( + !QuickSuggest.getFeature("YelpSuggestions").isEnabled, + "Yelp should be disabled" + ); + await check_results({ + context: createContext("ramen in tokyo", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [], + }); + + // Revert. + UrlbarPrefs.set(pref, true); + await QuickSuggestTestUtils.forceSync(); + + // Make sure Yelp is enabled again. + Assert.ok( + QuickSuggest.getFeature("YelpSuggestions").isEnabled, + "Yelp should be re-enabled" + ); + await check_results({ + context: createContext("ramen in tokyo", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [ + makeExpectedResult({ + url: "https://www.yelp.com/search?find_desc=ramen&find_loc=tokyo", + title: "ramen in tokyo", + }), + ], + }); + } +}); + +// Check wheather the Yelp suggestions will be shown by the setup of Nimbus +// variable. +add_task(async function featureGate() { + // Disable the fature gate. + UrlbarPrefs.set("yelp.featureGate", false); + await check_results({ + context: createContext("ramem in tokyo", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [], + }); + + // Enable by Nimbus. + const cleanUpNimbusEnable = await UrlbarTestUtils.initNimbusFeature({ + yelpFeatureGate: true, + }); + await QuickSuggestTestUtils.forceSync(); + await check_results({ + context: createContext("ramen in tokyo", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [ + makeExpectedResult({ + url: "https://www.yelp.com/search?find_desc=ramen&find_loc=tokyo", + title: "ramen in tokyo", + }), + ], + }); + await cleanUpNimbusEnable(); + + // Enable locally. + UrlbarPrefs.set("yelp.featureGate", true); + await QuickSuggestTestUtils.forceSync(); + + // Disable by Nimbus. + const cleanUpNimbusDisable = await UrlbarTestUtils.initNimbusFeature({ + yelpFeatureGate: false, + }); + await check_results({ + context: createContext("ramen in tokyo", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [], + }); + await cleanUpNimbusDisable(); + + // Revert. + UrlbarPrefs.set("yelp.featureGate", true); + await QuickSuggestTestUtils.forceSync(); +}); + +// Check wheather the Yelp suggestions will be shown as top_pick by the Nimbus +// variable. +add_task(async function yelpSuggestPriority() { + // Enable by Nimbus. + const cleanUpNimbusEnable = await UrlbarTestUtils.initNimbusFeature({ + yelpSuggestPriority: true, + }); + await QuickSuggestTestUtils.forceSync(); + + await check_results({ + context: createContext("ramen in tokyo", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [ + makeExpectedResult({ + url: "https://www.yelp.com/search?find_desc=ramen&find_loc=tokyo", + title: "ramen in tokyo", + isTopPick: true, + }), + ], + }); + + await cleanUpNimbusEnable(); + await QuickSuggestTestUtils.forceSync(); + + await check_results({ + context: createContext("ramen in tokyo", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [ + makeExpectedResult({ + url: "https://www.yelp.com/search?find_desc=ramen&find_loc=tokyo", + title: "ramen in tokyo", + isTopPick: false, + }), + ], + }); +}); + +// Tests the `yelpSuggestNonPriorityIndex` Nimbus variable, which controls the +// group-relative suggestedIndex. The default Yelp suggestedIndex is 0, unlike +// most other Suggest suggestion types, which use -1. +add_task(async function nimbusSuggestedIndex() { + const cleanUpNimbusEnable = await UrlbarTestUtils.initNimbusFeature({ + yelpSuggestNonPriorityIndex: -1, + }); + await QuickSuggestTestUtils.forceSync(); + + await check_results({ + context: createContext("ramen in tokyo", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [ + makeExpectedResult({ + url: "https://www.yelp.com/search?find_desc=ramen&find_loc=tokyo", + title: "ramen in tokyo", + isTopPick: false, + suggestedIndex: -1, + }), + ], + }); + + await cleanUpNimbusEnable(); + await QuickSuggestTestUtils.forceSync(); + + await check_results({ + context: createContext("ramen in tokyo", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [ + makeExpectedResult({ + url: "https://www.yelp.com/search?find_desc=ramen&find_loc=tokyo", + title: "ramen in tokyo", + isTopPick: false, + suggestedIndex: 0, + }), + ], + }); +}); + +// Tests the "Not relevant" command: a dismissed suggestion shouldn't be added. +add_task(async function notRelevant() { + let result = makeExpectedResult({ + url: "https://www.yelp.com/search?find_desc=ramen&find_loc=tokyo", + title: "ramen in tokyo", + }); + + info("Triggering the 'Not relevant' command"); + QuickSuggest.getFeature("YelpSuggestions").handleCommand( + { + controller: { removeResult() {} }, + }, + result, + "not_relevant" + ); + await QuickSuggest.blockedSuggestions._test_readyPromise; + + Assert.ok( + await QuickSuggest.blockedSuggestions.has(result.payload.originalUrl), + "The result's URL should be blocked" + ); + + info("Doing search for blocked suggestion"); + await check_results({ + context: createContext("ramen in tokyo", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [], + }); + + info("Doing search for a suggestion that wasn't blocked"); + await check_results({ + context: createContext("alongerkeyword in tokyo", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [ + makeExpectedResult({ + url: "https://www.yelp.com/search?find_desc=alongerkeyword&find_loc=tokyo", + title: "alongerkeyword in tokyo", + }), + ], + }); + + info("Clearing blocked suggestions"); + await QuickSuggest.blockedSuggestions.clear(); + + info("Doing search for unblocked suggestion"); + await check_results({ + context: createContext("ramen in tokyo", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [result], + }); +}); + +// Tests the "Not interested" command: all Yelp suggestions should be disabled +// and not added anymore. +add_task(async function notInterested() { + let result = makeExpectedResult({ + url: "https://www.yelp.com/search?find_desc=ramen&find_loc=tokyo", + title: "ramen in tokyo", + }); + + info("Triggering the 'Not interested' command"); + QuickSuggest.getFeature("YelpSuggestions").handleCommand( + { + controller: { removeResult() {} }, + }, + result, + "not_interested" + ); + + Assert.ok( + !UrlbarPrefs.get("suggest.yelp"), + "Yelp suggestions should be disabled" + ); + + info("Doing search for the suggestion the command was used on"); + await check_results({ + context: createContext("ramen in tokyo", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [], + }); + + info("Doing search for another Yelp suggestion"); + await check_results({ + context: createContext("alongerkeyword in tokyo", { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [], + }); + + UrlbarPrefs.clear("suggest.yelp"); + await QuickSuggestTestUtils.forceSync(); +}); + +// Tests the "show less frequently" behavior. +add_task(async function showLessFrequently() { + UrlbarPrefs.set("yelp.showLessFrequentlyCount", 0); + UrlbarPrefs.set("yelp.minKeywordLength", 0); + let cleanUpNimbus = await UrlbarTestUtils.initNimbusFeature({ + yelpMinKeywordLength: 0, + yelpShowLessFrequentlyCap: 3, + }); + + let location = `${GEOLOCATION.city}, ${GEOLOCATION.region}`; + + let originalUrl = new URL("https://www.yelp.com/search"); + originalUrl.searchParams.set("find_desc", "best ramen"); + + let url = new URL(originalUrl); + url.searchParams.set("find_loc", location); + + let result = makeExpectedResult({ + url: url.toString(), + originalUrl: originalUrl.toString(), + title: `best ramen in ${location}`, + }); + + const testData = [ + { + input: "best ra", + before: { + canShowLessFrequently: true, + showLessFrequentlyCount: 0, + minKeywordLength: 0, + }, + after: { + canShowLessFrequently: true, + showLessFrequentlyCount: 1, + minKeywordLength: 8, + }, + }, + { + input: "best ram", + before: { + canShowLessFrequently: true, + showLessFrequentlyCount: 1, + minKeywordLength: 8, + }, + after: { + canShowLessFrequently: true, + showLessFrequentlyCount: 2, + minKeywordLength: 9, + }, + }, + { + input: "best rame", + before: { + canShowLessFrequently: true, + showLessFrequentlyCount: 2, + minKeywordLength: 9, + }, + after: { + canShowLessFrequently: false, + showLessFrequentlyCount: 3, + minKeywordLength: 10, + }, + }, + { + input: "best ramen", + before: { + canShowLessFrequently: false, + showLessFrequentlyCount: 3, + minKeywordLength: 10, + }, + after: { + canShowLessFrequently: false, + showLessFrequentlyCount: 3, + minKeywordLength: 11, + }, + }, + ]; + + for (let { input, before, after } of testData) { + let feature = QuickSuggest.getFeature("YelpSuggestions"); + + await check_results({ + context: createContext(input, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [result], + }); + + Assert.equal( + UrlbarPrefs.get("yelp.minKeywordLength"), + before.minKeywordLength + ); + Assert.equal(feature.canShowLessFrequently, before.canShowLessFrequently); + Assert.equal( + feature.showLessFrequentlyCount, + before.showLessFrequentlyCount + ); + + feature.handleCommand( + { + acknowledgeFeedback: () => {}, + invalidateResultMenuCommands: () => {}, + }, + result, + "show_less_frequently", + input + ); + + Assert.equal( + UrlbarPrefs.get("yelp.minKeywordLength"), + after.minKeywordLength + ); + Assert.equal(feature.canShowLessFrequently, after.canShowLessFrequently); + Assert.equal( + feature.showLessFrequentlyCount, + after.showLessFrequentlyCount + ); + + await check_results({ + context: createContext(input, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }), + matches: [], + }); + } + + await cleanUpNimbus(); + UrlbarPrefs.clear("yelp.showLessFrequentlyCount"); + UrlbarPrefs.clear("yelp.minKeywordLength"); +}); + +// The `Yelp` Rust provider should be passed to the Rust component when +// querying depending on whether Yelp suggestions are enabled. +add_task(async function rustProviders() { + await doRustProvidersTests({ + searchString: "ramen in tokyo", + tests: [ + { + prefs: { + "suggest.yelp": true, + }, + expectedUrls: [ + "https://www.yelp.com/search?find_desc=ramen&find_loc=tokyo", + ], + }, + { + prefs: { + "suggest.yelp": false, + }, + expectedUrls: [], + }, + ], + }); + + UrlbarPrefs.clear("suggest.yelp"); + await QuickSuggestTestUtils.forceSync(); +}); + +function makeExpectedResult({ + url, + title, + isTopPick = false, + // The default Yelp suggestedIndex is 0, unlike most other Suggest suggestion + // types, which use -1. + suggestedIndex = 0, + isSuggestedIndexRelativeToGroup = true, + originalUrl = undefined, + displayUrl = undefined, +}) { + const utmParameters = "&utm_medium=partner&utm_source=mozilla"; + + originalUrl ??= url; + + displayUrl = + (displayUrl ?? + url + .replace(/^https:\/\/www[.]/, "") + .replace("%20", " ") + .replace("%2C", ",")) + utmParameters; + + url += utmParameters; + + if (isTopPick) { + suggestedIndex = 1; + isSuggestedIndexRelativeToGroup = false; + } + + return { + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + isBestMatch: !!isTopPick, + suggestedIndex, + isSuggestedIndexRelativeToGroup, + heuristic: false, + payload: { + source: "rust", + provider: "Yelp", + telemetryType: "yelp", + shouldShowUrl: true, + bottomTextL10n: { id: "firefox-suggest-yelp-bottom-text" }, + url, + originalUrl, + title, + displayUrl, + icon: null, + }, + }; +} diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_rust_ingest.js b/browser/components/urlbar/tests/quicksuggest/unit/test_rust_ingest.js new file mode 100644 index 0000000000..e6ec61bcd4 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_rust_ingest.js @@ -0,0 +1,244 @@ +/* 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 ingest in the Rust backend. + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + setTimeout: "resource://gre/modules/Timer.sys.mjs", +}); + +// These consts are copied from the update timer manager test. See +// `initUpdateTimerManager()`. +const PREF_APP_UPDATE_TIMERMINIMUMDELAY = "app.update.timerMinimumDelay"; +const PREF_APP_UPDATE_TIMERFIRSTINTERVAL = "app.update.timerFirstInterval"; +const MAIN_TIMER_INTERVAL = 1000; // milliseconds +const CATEGORY_UPDATE_TIMER = "update-timer"; + +const REMOTE_SETTINGS_SUGGESTION = { + id: 1, + url: "http://example.com/amp", + title: "AMP Suggestion", + keywords: ["amp"], + click_url: "http://example.com/amp-click", + impression_url: "http://example.com/amp-impression", + advertiser: "Amp", + iab_category: "22 - Shopping", + icon: "1234", +}; + +add_setup(async function () { + initUpdateTimerManager(); + + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + remoteSettingsRecords: [ + { + type: "data", + attachment: [REMOTE_SETTINGS_SUGGESTION], + }, + ], + prefs: [ + ["suggest.quicksuggest.sponsored", true], + ["quicksuggest.rustEnabled", false], + ], + }); +}); + +// IMPORTANT: This task must run first! +// +// This simulates the first time the Rust backend is enabled in a profile. The +// backend should perform ingestion immediately. +add_task(async function firstRun() { + Assert.ok( + !UrlbarPrefs.get("quicksuggest.rustEnabled"), + "rustEnabled pref is initially false (this task must run first!)" + ); + Assert.strictEqual( + QuickSuggest.rustBackend.isEnabled, + false, + "Rust backend is initially disabled (this task must run first!)" + ); + Assert.ok( + !QuickSuggest.rustBackend.ingestPromise, + "No ingest has been performed yet (this task must run first!)" + ); + + info("Enabling the Rust backend"); + UrlbarPrefs.set("quicksuggest.rustEnabled", true); + Assert.ok(QuickSuggest.rustBackend.isEnabled, "Rust backend is now enabled"); + + // An ingest should start. + let { ingestPromise } = await waitForIngestStart(null); + + info("Awaiting ingest promise"); + await ingestPromise; + info("Done awaiting ingest promise"); + + await checkSuggestions(); + + // Disable and re-enable the backend. No new ingestion should start + // immediately since this isn't the first time the backend has been enabled. + UrlbarPrefs.set("quicksuggest.rustEnabled", false); + UrlbarPrefs.set("quicksuggest.rustEnabled", true); + await assertNoNewIngestStarted(ingestPromise); + + await checkSuggestions(); + + UrlbarPrefs.set("quicksuggest.rustEnabled", false); +}); + +// Ingestion should be performed according to the defined interval. +add_task(async function interval() { + let { ingestPromise } = QuickSuggest.rustBackend; + Assert.ok( + ingestPromise, + "Sanity check: An ingest has already been performed" + ); + Assert.ok( + !UrlbarPrefs.get("quicksuggest.rustEnabled"), + "Sanity check: Rust backend is initially disabled" + ); + + // Set a small interval and enable the backend. No new ingestion should start + // immediately since this isn't the first time the backend has been enabled. + let intervalSecs = 1; + UrlbarPrefs.set("quicksuggest.rustIngestIntervalSeconds", intervalSecs); + UrlbarPrefs.set("quicksuggest.rustEnabled", true); + await assertNoNewIngestStarted(ingestPromise); + + // Wait for a few ingests to happen. + for (let i = 0; i < 3; i++) { + info("Preparing for ingest at index " + i); + + // Set a new suggestion so we can make sure ingest really happened. + let suggestion = { + ...REMOTE_SETTINGS_SUGGESTION, + url: REMOTE_SETTINGS_SUGGESTION.url + "/" + i, + }; + await QuickSuggestTestUtils.setRemoteSettingsRecords( + [ + { + type: "data", + attachment: [suggestion], + }, + ], + // Don't force sync since the whole point here is to make sure the backend + // ingests on its own! + { forceSync: false } + ); + + // Wait for ingest to start and finish. + info("Waiting for ingest to start at index " + i); + ({ ingestPromise } = await waitForIngestStart(ingestPromise)); + info("Waiting for ingest to finish at index " + i); + await ingestPromise; + await checkSuggestions([suggestion]); + } + + // In the loop above, there was one additional async call after awaiting the + // ingest promise, to `checkSuggestions()`. It's possible, though unlikely, + // that call took so long that another ingest has started. To be sure, wait + // for one final ingest to start before continuing. + ({ ingestPromise } = await waitForIngestStart(ingestPromise)); + + // Now immediately disable the backend. New ingests should not start, but the + // final one will still be ongoing. + info("Disabling the backend"); + UrlbarPrefs.set("quicksuggest.rustEnabled", false); + + info("Awaiting final ingest promise"); + await ingestPromise; + + // Wait a few seconds. + let waitSecs = 3 * intervalSecs; + info(`Waiting ${waitSecs}s...`); + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(r => setTimeout(r, 1000 * waitSecs)); + + // No new ingests should have started. + Assert.equal( + QuickSuggest.rustBackend.ingestPromise, + ingestPromise, + "No new ingest started after disabling the backend" + ); + + UrlbarPrefs.clear("quicksuggest.rustIngestIntervalSeconds"); +}); + +async function waitForIngestStart(oldIngestPromise) { + let newIngestPromise; + await TestUtils.waitForCondition(() => { + let { ingestPromise } = QuickSuggest.rustBackend; + if ( + (oldIngestPromise && ingestPromise != oldIngestPromise) || + (!oldIngestPromise && ingestPromise) + ) { + newIngestPromise = ingestPromise; + return true; + } + return false; + }, "Waiting for a new ingest to start"); + + Assert.equal( + QuickSuggest.rustBackend.ingestPromise, + newIngestPromise, + "Sanity check: ingestPromise hasn't changed since waitForCondition returned" + ); + + // A bare promise can't be returned because it will cause the awaiting caller + // to await that promise! We're simply trying to return the promise, which the + // caller can later await. + return { ingestPromise: newIngestPromise }; +} + +async function assertNoNewIngestStarted(oldIngestPromise) { + for (let i = 0; i < 3; i++) { + await TestUtils.waitForTick(); + } + Assert.equal( + QuickSuggest.rustBackend.ingestPromise, + oldIngestPromise, + "No new ingest started" + ); +} + +async function checkSuggestions(expected = [REMOTE_SETTINGS_SUGGESTION]) { + let actual = await QuickSuggest.rustBackend.query("amp"); + Assert.deepEqual( + actual.map(s => s.url), + expected.map(s => s.url), + "Backend should be serving the expected suggestions" + ); +} + +/** + * Sets up the update timer manager for testing: makes it fire more often, + * removes all existing timers, and initializes it for testing. The body of this + * function is copied from: + * https://searchfox.org/mozilla-central/source/toolkit/components/timermanager/tests/unit/consumerNotifications.js + */ +function initUpdateTimerManager() { + // Set the timer to fire every second + Services.prefs.setIntPref( + PREF_APP_UPDATE_TIMERMINIMUMDELAY, + MAIN_TIMER_INTERVAL / 1000 + ); + Services.prefs.setIntPref( + PREF_APP_UPDATE_TIMERFIRSTINTERVAL, + MAIN_TIMER_INTERVAL + ); + + // Remove existing update timers to prevent them from being notified + for (let { data: entry } of Services.catMan.enumerateCategory( + CATEGORY_UPDATE_TIMER + )) { + Services.catMan.deleteCategoryEntry(CATEGORY_UPDATE_TIMER, entry, false); + } + + Cc["@mozilla.org/updates/timer-manager;1"] + .getService(Ci.nsIUpdateTimerManager) + .QueryInterface(Ci.nsIObserver) + .observe(null, "utm-test-init", ""); +} 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..f50fe32dd3 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_suggestionsMap.js @@ -0,0 +1,293 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Tests `SuggestionsMap`. + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs", + SuggestionsMap: "resource:///modules/urlbar/private/SuggestBackendJs.sys.mjs", +}); + +// 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_setup(async () => { + // 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, { + mapKeyword: 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 + ); + } +}); + +// Tests `keywordsProperty`. +add_task(async function keywordsProperty() { + let suggestion = { + title: "suggestion", + keywords: ["should be ignored"], + foo: ["hello"], + }; + + let map = new SuggestionsMap(); + await map.add([suggestion], { + keywordsProperty: "foo", + }); + + Assert.deepEqual( + map.get("hello"), + [suggestion], + "Keyword in `foo` should match" + ); + Assert.deepEqual( + map.get("should be ignored"), + [], + "Keyword in `keywords` should not match" + ); +}); + +// Tests `MAP_KEYWORD_PREFIXES_STARTING_AT_FIRST_WORD`. +add_task(async function prefixesStartingAtFirstWord() { + let suggestion = { + title: "suggestion", + keywords: ["one two three", "four five six"], + }; + + // keyword passed to `get()` -> should match + let tests = { + o: false, + on: false, + one: true, + "one ": true, + "one t": true, + "one tw": true, + "one two": true, + "one two ": true, + "one two t": true, + "one two th": true, + "one two thr": true, + "one two thre": true, + "one two three": true, + "one two three ": false, + f: false, + fo: false, + fou: false, + four: true, + "four ": true, + "four f": true, + "four fi": true, + "four fiv": true, + "four five": true, + "four five ": true, + "four five s": true, + "four five si": true, + "four five six": true, + "four five six ": false, + }; + + let map = new SuggestionsMap(); + await map.add([suggestion], { + mapKeyword: SuggestionsMap.MAP_KEYWORD_PREFIXES_STARTING_AT_FIRST_WORD, + }); + + for (let [keyword, shouldMatch] of Object.entries(tests)) { + Assert.deepEqual( + map.get(keyword), + shouldMatch ? [suggestion] : [], + "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..28801904a1 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_weather.js @@ -0,0 +1,1402 @@ +/* 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_setup(async () => { + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + prefs: [["suggest.quicksuggest.nonsponsored", true]], + remoteSettingsRecords: [ + { + type: "weather", + weather: WEATHER_RS_DATA, + }, + ], + }); + + 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_tasks_with_rust(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_tasks_with_rust(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: [UrlbarProviderQuickSuggest.name, 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, + }); + + // Wait for keywords to be re-synced from remote settings. + await QuickSuggestTestUtils.forceSync(); + + // The suggestion should be returned for a search. + context = createContext(MerinoTestUtils.WEATHER_KEYWORD, { + providers: [UrlbarProviderQuickSuggest.name, UrlbarProviderWeather.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [makeWeatherResult()], + }); +} + +// This task is only appropriate for the JS backend, not Rust, since fetching is +// always active with Rust. +add_task( + { + skip_if: () => UrlbarPrefs.get("quickSuggestRustEnabled"), + }, + 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.setRemoteSettingsRecords([ + { + 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.setRemoteSettingsRecords([ + { + 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: [makeWeatherResult()], + }); + } +); + +// 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_tasks_with_rust(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; + + // Wait for keywords to be re-synced from remote settings. + await QuickSuggestTestUtils.forceSync(); + + 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_tasks_with_rust(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, + }); + + // Wait for keywords to be re-synced from remote settings. + await QuickSuggestTestUtils.forceSync(); +}); + +// A fetch that doesn't return a suggestion should cause the last-fetched +// suggestion to be discarded. +add_tasks_with_rust(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: [UrlbarProviderQuickSuggest.name, 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_tasks_with_rust(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: [UrlbarProviderQuickSuggest.name, 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_tasks_with_rust(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: [UrlbarProviderQuickSuggest.name, 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_tasks_with_rust(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: [UrlbarProviderQuickSuggest.name, 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_tasks_with_rust(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_tasks_with_rust(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_tasks_with_rust(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: [ + UrlbarProviderQuickSuggest.name, + UrlbarProviderWeather.name, + ], + isPrivate: false, + }), + matches: [makeWeatherResult({ 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: [ + UrlbarProviderQuickSuggest.name, + UrlbarProviderWeather.name, + ], + isPrivate: false, + }), + matches: [makeWeatherResult({ temperatureUnit: osUnit })], + }); + Services.prefs.clearUserPref("intl.regional_prefs.use_os_locales"); + }); + } +} + +// Blocks a result and makes sure the weather pref is disabled. +add_tasks_with_rust(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: [UrlbarProviderQuickSuggest.name, UrlbarProviderWeather.name], + isPrivate: false, + }); + await check_results({ + context, + matches: [makeWeatherResult()], + }); + + // Block the result. + const controller = UrlbarTestUtils.newMockController(); + controller.setView({ + get visibleResults() { + return context.results; + }, + controller: { + removeResult() {}, + }, + }); + let result = context.results[0]; + let provider = UrlbarProvidersManager.getProvider(result.providerName); + Assert.ok(provider, "Sanity check: Result provider found"); + provider.onEngagement( + "engagement", + context, + { + result, + selType: "dismiss", + selIndex: context.results[0].rowIndex, + }, + controller + ); + 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: [UrlbarProviderQuickSuggest.name, 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; + + // Wait for keywords to be re-synced from remote settings. + await QuickSuggestTestUtils.forceSync(); + + 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_tasks_with_rust(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_tasks_with_rust(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_tasks_with_rust(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_tasks_with_rust(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_tasks_with_rust(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_tasks_with_rust(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_tasks_with_rust(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_tasks_with_rust(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_tasks_with_rust(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_tasks_with_rust(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_tasks_with_rust(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_tasks_with_rust(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 +// weather record. +add_tasks_with_rust(async function nimbusOverride() { + // Sanity check initial state. + assertEnabled({ + message: "Sanity check initial state", + hasSuggestion: true, + pendingFetchCount: 0, + }); + + let defaultResult = makeWeatherResult(); + + // Verify a search works as expected with the default remote settings weather + // record (which was added in the init task). + await check_results({ + context: createContext(MerinoTestUtils.WEATHER_KEYWORD, { + providers: [UrlbarProviderQuickSuggest.name, UrlbarProviderWeather.name], + isPrivate: false, + }), + matches: [defaultResult], + }); + + // 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: [UrlbarProviderQuickSuggest.name, UrlbarProviderWeather.name], + isPrivate: false, + }), + matches: [], + }); + + // The new keyword from Nimbus should match. Since keywords are defined in + // Nimbus, the result will be served from UrlbarProviderWeather and its source + // will be "merino", not "rust", even when Rust is enabled. + let merinoResult = makeWeatherResult({ + source: "merino", + provider: "accuweather", + telemetryType: null, + }); + await check_results({ + context: createContext("nimbusoverride", { + providers: [UrlbarProviderQuickSuggest.name, UrlbarProviderWeather.name], + isPrivate: false, + }), + matches: [merinoResult], + }); + await check_results({ + context: createContext("nimbus", { + providers: [UrlbarProviderQuickSuggest.name, UrlbarProviderWeather.name], + isPrivate: false, + }), + matches: [merinoResult], + }); + + // Uninstall the experiment. + await nimbusCleanup(); + + // The usual default keyword should match again. + await check_results({ + context: createContext(MerinoTestUtils.WEATHER_KEYWORD, { + providers: [UrlbarProviderQuickSuggest.name, UrlbarProviderWeather.name], + isPrivate: false, + }), + matches: [defaultResult], + }); + + // The keywords from Nimbus shouldn't match anymore. + await check_results({ + context: createContext("nimbusoverride", { + providers: [UrlbarProviderQuickSuggest.name, UrlbarProviderWeather.name], + isPrivate: false, + }), + matches: [], + }); + await check_results({ + context: createContext("nimbus", { + providers: [UrlbarProviderQuickSuggest.name, 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" + ); +} 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..efa5922c3e --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_weather_keywords.js @@ -0,0 +1,1503 @@ +/* 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 behavior of quick suggest weather. + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarProviderWeather: "resource:///modules/UrlbarProviderWeather.sys.mjs", +}); + +const { WEATHER_RS_DATA } = MerinoTestUtils; + +add_setup(async () => { + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + remoteSettingsRecords: [ + { + type: "weather", + weather: WEATHER_RS_DATA, + }, + ], + prefs: [["suggest.quicksuggest.nonsponsored", true]], + }); + await MerinoTestUtils.initWeather(); +}); + +// * Settings data: none +// * Nimbus values: none +// * Min keyword length pref: none +// * Expected: no suggestion +add_tasks_with_rust(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_tasks_with_rust(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 +// +// JS backend only. The Rust component expects settings data to contain +// min_keyword_length. +add_task( + { + skip_if: () => UrlbarPrefs.get("quickSuggestRustEnabled"), + }, + 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 +// +// JS backend only. The Rust component doesn't treat minKeywordLength == 0 as a +// special case. +add_task( + { + skip_if: () => UrlbarPrefs.get("quickSuggestRustEnabled"), + }, + 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_tasks_with_rust(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_tasks_with_rust(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_tasks_with_rust(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_tasks_with_rust(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 +// +// JS backend only. The Rust component expects settings data to contain +// min_keyword_length. +add_task( + { + skip_if: () => UrlbarPrefs.get("quickSuggestRustEnabled"), + }, + 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 +// +// JS backend only. The Rust component doesn't treat minKeywordLength == 0 as a +// special case. +add_task( + { + skip_if: () => UrlbarPrefs.get("quickSuggestRustEnabled"), + }, + 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. +// Even when Rust is enabled, UrlbarProviderWeather should serve the +// suggestion since the keywords come from Nimbus. +add_tasks_with_rust(async function () { + await doKeywordsTest({ + desc: "Settings: keywords, min keyword length > 0; Nimbus: keywords", + settingsData: { + keywords: ["weather"], + min_keyword_length: 3, + }, + nimbusValues: { + weatherKeywords: ["forecast"], + }, + alwaysExpectMerinoResult: true, + 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. +// Even when Rust is enabled, UrlbarProviderWeather should serve the +// suggestion since the keywords come from Nimbus. +add_tasks_with_rust(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, + }, + alwaysExpectMerinoResult: true, + 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. +// Even when Rust is enabled, UrlbarProviderWeather should serve the +// suggestion since the keywords come from Nimbus. +add_tasks_with_rust(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, + }, + alwaysExpectMerinoResult: true, + 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. +// Even when Rust is enabled, UrlbarProviderWeather should serve the +// suggestion since the keywords come from Nimbus. +add_tasks_with_rust(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, + alwaysExpectMerinoResult: true, + 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 +// Even when Rust is enabled, UrlbarProviderWeather should serve the +// suggestion since the keywords come from Nimbus. +add_tasks_with_rust(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, + alwaysExpectMerinoResult: true, + 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 +// +// TODO bug 1879209: This doesn't work with the Rust backend because if +// min_keyword_length isn't specified on ingest, the Rust database will retain +// the last known good min_keyword_length, which interferes with this task. +add_task( + { + skip_if: () => UrlbarPrefs.get("quickSuggestRustEnabled"), + }, + 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 +// +// TODO bug 1879209: This doesn't work with the Rust backend because if +// min_keyword_length isn't specified on ingest, the Rust database will retain +// the last known good min_keyword_length, which interferes with this task. +add_task( + { + skip_if: () => UrlbarPrefs.get("quickSuggestRustEnabled"), + }, + 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 +// Even when Rust is enabled, UrlbarProviderWeather should serve the +// suggestion since the keywords come from Nimbus. +add_tasks_with_rust(async function () { + await doKeywordsTest({ + desc: "Settings: none; Nimbus: keywords, min keyword length > 0", + nimbusValues: { + weatherKeywords: ["weather", "forecast"], + weatherKeywordsMinimumLength: 3, + }, + alwaysExpectMerinoResult: true, + 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 +// Even when Rust is enabled, UrlbarProviderWeather should serve the +// suggestion since the keywords come from Nimbus. +add_tasks_with_rust(async function () { + await doKeywordsTest({ + desc: "Settings: none; Nimbus: keywords, min keyword length > 0; pref exists", + nimbusValues: { + weatherKeywords: ["weather", "forecast"], + weatherKeywordsMinimumLength: 3, + }, + minKeywordLength: 6, + alwaysExpectMerinoResult: true, + 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. +// Even when Rust is enabled, UrlbarProviderWeather should serve the +// suggestion since the keywords come from Nimbus. +add_tasks_with_rust(async function minLength_large() { + await doKeywordsTest({ + desc: "Large min length", + nimbusValues: { + weatherKeywords: ["weather", "forecast"], + weatherKeywordsMinimumLength: 999, + }, + alwaysExpectMerinoResult: true, + 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_tasks_with_rust(async function leadingAndTrailingSpaces() { + await doKeywordsTest({ + settingsData: { + keywords: ["weather"], + min_keyword_length: 3, + }, + tests: { + " wea": true, + " wea": true, + "wea ": true, + "wea ": true, + " wea ": true, + " weat": true, + " weat": true, + "weat ": true, + "weat ": true, + " weat ": true, + }, + }); +}); + +add_tasks_with_rust(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, + alwaysExpectMerinoResult = false, +}) { + info("Doing keywords test: " + desc); + info(JSON.stringify({ nimbusValues, settingsData, minKeywordLength })); + + // If the JS backend is enabled, a suggestion hasn't already been fetched, and + // the data contains keywords, a fetch will start. Wait for it to finish later + // below. + let fetchPromise; + if ( + !QuickSuggest.weather.suggestion && + !UrlbarPrefs.get("quickSuggestRustEnabled") && + (nimbusValues?.weatherKeywords || settingsData?.keywords) + ) { + fetchPromise = QuickSuggest.weather.waitForFetches(); + } + + let nimbusCleanup; + if (nimbusValues) { + nimbusCleanup = await UrlbarTestUtils.initNimbusFeature(nimbusValues); + } + + await QuickSuggestTestUtils.setRemoteSettingsRecords([ + { + 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"); + } + + let expectedResult = makeWeatherResult( + !alwaysExpectMerinoResult + ? undefined + : { source: "merino", provider: "accuweather", telemetryType: null } + ); + + for (let [searchString, expected] of Object.entries(tests)) { + info( + "Doing keywords test search: " + + JSON.stringify({ + searchString, + expected, + }) + ); + + await check_results({ + context: createContext(searchString, { + providers: [ + UrlbarProviderQuickSuggest.name, + UrlbarProviderWeather.name, + ], + isPrivate: false, + }), + matches: expected ? [expectedResult] : [], + }); + } + + await nimbusCleanup?.(); + + fetchPromise = null; + if (!QuickSuggest.weather.suggestion) { + fetchPromise = QuickSuggest.weather.waitForFetches(); + } + await QuickSuggestTestUtils.setRemoteSettingsRecords([ + { + 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_tasks_with_rust(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_tasks_with_rust(async function matchingQuickSuggest_nonsponsored() { + await doMatchingQuickSuggestTest("suggest.quicksuggest.nonsponsored", false); +}); + +async function doMatchingQuickSuggestTest(pref, isSponsored) { + let keyword = "test"; + + let attachment = isSponsored + ? { + id: 1, + url: "http://example.com/amp", + title: "AMP Suggestion", + keywords: [keyword], + click_url: "http://example.com/amp-click", + impression_url: "http://example.com/amp-impression", + advertiser: "Amp", + iab_category: "22 - Shopping", + icon: "1234", + } + : { + id: 2, + url: "http://example.com/wikipedia", + title: "Wikipedia Suggestion", + keywords: [keyword], + click_url: "http://example.com/wikipedia-click", + impression_url: "http://example.com/wikipedia-impression", + advertiser: "Wikipedia", + iab_category: "5 - Education", + icon: "1234", + }; + + // Add a remote settings result to quick suggest. + let oldPrefValue = UrlbarPrefs.get(pref); + UrlbarPrefs.set(pref, true); + await QuickSuggestTestUtils.setRemoteSettingsRecords([ + { + type: "data", + attachment: [attachment], + }, + { + type: "weather", + weather: MerinoTestUtils.WEATHER_RS_DATA, + }, + ]); + + // First do a search to verify the quick suggest result matches the keyword. + let payload; + if (!UrlbarPrefs.get("quickSuggestRustEnabled")) { + payload = { + source: "remote-settings", + provider: "AdmWikipedia", + sponsoredImpressionUrl: attachment.impression_url, + sponsoredClickUrl: attachment.click_url, + sponsoredBlockId: attachment.id, + }; + } else { + payload = { + source: "rust", + provider: isSponsored ? "Amp" : "Wikipedia", + }; + if (isSponsored) { + payload.sponsoredImpressionUrl = attachment.impression_url; + payload.sponsoredClickUrl = attachment.click_url; + payload.sponsoredBlockId = attachment.id; + } + } + + 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: { + ...payload, + telemetryType: isSponsored ? "adm_sponsored" : "adm_nonsponsored", + qsSuggestion: keyword, + title: attachment.title, + url: attachment.url, + displayUrl: attachment.url.replace(/[/]$/, ""), + originalUrl: attachment.url, + icon: null, + sponsoredAdvertiser: attachment.advertiser, + sponsoredIabCategory: attachment.iab_category, + isSponsored, + descriptionL10n: isSponsored + ? { id: "urlbar-result-action-sponsored" } + : undefined, + helpUrl: QuickSuggest.HELP_URL, + helpL10n: { + id: "urlbar-result-menu-learn-more-about-firefox-suggest", + }, + isBlockable: true, + blockL10n: { + id: "urlbar-result-menu-dismiss-firefox-suggest", + }, + }, + }, + ], + }); + + // 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, + }), + // The result should always come from Merino. + matches: [ + makeWeatherResult({ + source: "merino", + provider: "accuweather", + telemetryType: null, + }), + ], + }); + await cleanup(); + + UrlbarPrefs.set(pref, oldPrefValue); +} + +add_tasks_with_rust(async function () { + await doIncrementTest({ + desc: "Settings only without cap", + setup: { + settingsData: { + weather: { + 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_tasks_with_rust(async function () { + await doIncrementTest({ + desc: "Settings only with cap", + setup: { + settingsData: { + weather: { + keywords: ["forecast", "wind"], + min_keyword_length: 3, + }, + configuration: { + show_less_frequently_cap: 3, + }, + }, + }, + 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_tasks_with_rust(async function () { + await doIncrementTest({ + desc: "Settings and Nimbus without cap", + setup: { + settingsData: { + weather: { + keywords: ["weather"], + min_keyword_length: 5, + }, + }, + nimbusValues: { + weatherKeywords: ["forecast", "wind"], + weatherKeywordsMinimumLength: 3, + }, + }, + // The suggestion should be served by UrlbarProviderWeather and therefore + // be from Merino. + alwaysExpectMerinoResult: true, + 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: { + weather: { + keywords: ["weather"], + min_keyword_length: 5, + }, + }, + nimbusValues: { + weatherKeywords: ["forecast", "wind"], + weatherKeywordsMinimumLength: 3, + weatherKeywordsMinimumLengthCap: 6, + }, + }, + // The suggestion should be served by UrlbarProviderWeather and therefore + // be from Merino. + alwaysExpectMerinoResult: true, + 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, + alwaysExpectMerinoResult = false, +}) { + info("Doing increment test: " + desc); + info(JSON.stringify({ setup })); + + let { nimbusValues, settingsData } = setup; + + // If the JS backend is enabled, a suggestion hasn't already been fetched, and + // the data contains keywords, a fetch will start. Wait for it to finish later + // below. + let fetchPromise; + if ( + !QuickSuggest.weather.suggestion && + !UrlbarPrefs.get("quickSuggestRustEnabled") && + (nimbusValues?.weatherKeywords || settingsData?.weather?.keywords) + ) { + fetchPromise = QuickSuggest.weather.waitForFetches(); + } + + let nimbusCleanup; + if (nimbusValues) { + nimbusCleanup = await UrlbarTestUtils.initNimbusFeature(nimbusValues); + } + + await QuickSuggestTestUtils.setRemoteSettingsRecords([ + { + type: "weather", + weather: settingsData?.weather, + }, + { + type: "configuration", + configuration: settingsData?.configuration, + }, + ]); + + if (fetchPromise) { + info("Waiting for fetch"); + assertFetchingStarted({ pendingFetchCount: 1 }); + await fetchPromise; + info("Got fetch"); + } + + let expectedResult = makeWeatherResult( + !alwaysExpectMerinoResult + ? undefined + : { source: "merino", provider: "accuweather", telemetryType: null } + ); + + 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)) { + await check_results({ + context: createContext(searchString, { + providers: [ + UrlbarProviderQuickSuggest.name, + UrlbarProviderWeather.name, + ], + isPrivate: false, + }), + matches: expected ? [expectedResult] : [], + }); + } + + 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.setRemoteSettingsRecords([ + { + type: "weather", + weather: MerinoTestUtils.WEATHER_RS_DATA, + }, + ]); + UrlbarPrefs.clear("weather.minKeywordLength"); + await fetchPromise; +} + +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.toml b/browser/components/urlbar/tests/quicksuggest/unit/xpcshell.toml new file mode 100644 index 0000000000..ceab478795 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/xpcshell.toml @@ -0,0 +1,51 @@ +[DEFAULT] +skip-if = ["os == '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_dynamicWikipedia.js"] + +["test_quicksuggest_impressionCaps.js"] +skip-if = ["true"] # Bug 1880214 + +["test_quicksuggest_mdn.js"] + +["test_quicksuggest_merino.js"] + +["test_quicksuggest_merinoSessions.js"] + +["test_quicksuggest_migrate_v1.js"] + +["test_quicksuggest_migrate_v2.js"] + +["test_quicksuggest_nonUniqueKeywords.js"] +skip-if = ["true"] # Bug 1880214 + +["test_quicksuggest_offlineDefault.js"] + +["test_quicksuggest_pocket.js"] + +["test_quicksuggest_positionInSuggestions.js"] +skip-if = ["true"] # Bug 1880214 + +["test_quicksuggest_scoreMap.js"] + +["test_quicksuggest_topPicks.js"] + +["test_quicksuggest_yelp.js"] + +["test_rust_ingest.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..6f78608c94 --- /dev/null +++ b/browser/components/urlbar/tests/unit/head.js @@ -0,0 +1,1173 @@ +/* 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", + HttpServer: "resource://testing-common/httpd.sys.mjs", + PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs", + PlacesUtils: "resource://gre/modules/PlacesUtils.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", +}); + +ChromeUtils.defineLazyGetter(this, "QuickSuggestTestUtils", () => { + const { QuickSuggestTestUtils: module } = ChromeUtils.importESModule( + "resource://testing-common/QuickSuggestTestUtils.sys.mjs" + ); + module.init(this); + return module; +}); + +ChromeUtils.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; +}); + +ChromeUtils.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 + ) + ); + 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"); + } + 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. + * @param {object} options + * Options for the check. + * @param {string} [options.name] + * The name of the engine to install. + * @returns {nsISearchEngine} The new engine. + */ +async function addTestSuggestionsEngine( + suggestionsFn = null, + { name = SUGGESTIONS_ENGINE_NAME } = {} +) { + // 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, + 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(name); + 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; +} + +/** + * Creates a function that can be provided to the new engine + * utility function to mimic a search engine that returns + * rich suggestions. + * + * @param {string} searchStr + * The string being searched for. + * + * @returns {object} + * A JSON object mimicing the data format returned by + * a search engine. + */ +function defaultRichSuggestionsFn(searchStr) { + let suffixes = ["toronto", "tunisia", "tacoma", "taipei"]; + return [ + "what time is it in t", + suffixes.map(s => searchStr + s.slice(1)), + [], + { + "google:irrelevantparameter": [], + "google:suggestdetail": suffixes.map((suffix, i) => { + // Set every other suggestion as a rich suggestion so we can + // test how they are handled and ordered when interleaved. + if (i % 2) { + return {}; + } + return { + a: "description", + dc: "#FFFFFF", + i: "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==", + t: "Title", + }; + }), + }, + ]; +} + +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_setup(async () => { + 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"); + + 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], + isBlockable: + source == UrlbarUtils.RESULT_SOURCE.HISTORY ? true : undefined, + blockL10n: + source == UrlbarUtils.RESULT_SOURCE.HISTORY + ? { id: "urlbar-result-menu-remove-from-history" } + : undefined, + helpUrl: + source == UrlbarUtils.RESULT_SOURCE.HISTORY + ? Services.urlFormatter.formatURLPref("app.support.baseURL") + + "awesome-bar-result-menu" + : undefined, + }) + ); + + 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(), + isBlockable: true, + blockL10n: { id: "urlbar-result-menu-remove-from-history" }, + helpUrl: + Services.urlFormatter.formatURLPref("app.support.baseURL") + + "awesome-bar-result-menu", + }) + ); +} + +/** + * 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. + * @param {number} [options.userContextId] + * A id of the userContext in which the tab is located. + * @returns {UrlbarResult} + */ +function makeTabSwitchResult( + queryContext, + { uri, title, iconUri, userContextId } +) { + 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}`, + userContextId: [userContextId || 0], + }) + ); +} + +/** + * 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 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 : `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 && !isRichSuggestion) { + 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.isRichSuggestion = isRichSuggestion; + } + + if (isRichSuggestion) { + result.payload.icon = + "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; + result.payload.description = "description"; + } + + 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 ( + !heuristic && + providerName != "AboutPages" && + providerName != "PreloadedSites" && + source == UrlbarUtils.RESULT_SOURCE.HISTORY + ) { + payload.isBlockable = true; + payload.blockL10n = { id: "urlbar-result-menu-remove-from-history" }; + payload.helpUrl = + Services.urlFormatter.formatURLPref("app.support.baseURL") + + "awesome-bar-result-menu"; + } + + 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 PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + const controller = UrlbarTestUtils.newMockController({ + input: { + isPrivate: context.isPrivate, + onFirstResult() { + return false; + }, + getSearchSource() { + return "dummy-search-source"; + }, + window: { + location: { + href: AppConstants.BROWSER_CHROME_URL, + }, + }, + }, + }); + controller.setView({ + get visibleResults() { + return context.results; + }, + controller: { + removeResult() {}, + }, + }); + + 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.hasOwnProperty("isSuggestedIndexRelativeToGroup")) { + Assert.equal( + !!actual.isSuggestedIndexRelativeToGroup, + expected.isSuggestedIndexRelativeToGroup, + `result.isSuggestedIndexRelativeToGroup 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..220af80e06 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_UrlbarController_integration.js @@ -0,0 +1,106 @@ +/* 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 TEST_URL = "http://example.com"; +const match = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + UrlbarUtils.RESULT_SOURCE.TABS, + { url: TEST_URL } +); +let controller; + +add_setup(async function () { + 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 = Promise.withResolvers(); + let provider = registerBasicTestProvider( + [match], + providerCanceledDeferred.resolve + ); + const context = createContext(TEST_URL, { providers: [provider.name] }); + + let startedPromise = promiseControllerNotification( + controller, + "onQueryStarted" + ); + let cancelPromise = promiseControllerNotification( + controller, + "onQueryCancelled" + ); + + let delayResultsPromise = new Promise(resolve => { + controller.addQueryListener({ + async onQueryResults(queryContext) { + controller.removeQueryListener(this); + controller.cancelQuery(queryContext); + resolve(); + }, + }); + }); + + let result = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { url: "https://example.com/1", title: "example" } + ); + + // We are awaiting for asynchronous work on initialization. + // For this test, we need the query objects to be created. We ensure this by + // using a delayed Provider. We wait for onQueryResults, then cancel the + // query. By that time the query objects are created. Then we unblock the + // delayed provider. + let delayedProvider = new UrlbarTestUtils.TestProvider({ + delayResultsPromise, + results: [result], + type: UrlbarUtils.PROVIDER_TYPE.PROFILE, + }); + + UrlbarProvidersManager.registerProvider(delayedProvider); + + controller.startQuery(context); + + let params = await startedPromise; + Assert.equal(params[0], context); + + info("Should have notified the provider the query is canceled"); + await providerCanceledDeferred.promise; + + params = await cancelPromise; + UrlbarProvidersManager.unregisterProvider(delayedProvider); +}); 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..d344c4f8e1 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_UrlbarController_telemetry.js @@ -0,0 +1,253 @@ +/* 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_setup(function () { + 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 provider = new TestProvider({ + results: [], + }); + 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" + ); + + let startQueryPromise = 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 startQueryPromise; + + 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..31a0b48227 --- /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_setup(function () { + 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..d30739f03e --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_UrlbarPrefs.js @@ -0,0 +1,447 @@ +/* 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_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: 99, + group: UrlbarUtils.RESULT_GROUP.RECENT_SEARCH, + }, + { + 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, + }, + ], + }, + { + 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_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, + }, + ], + }, + { + group: UrlbarUtils.RESULT_GROUP.INPUT_HISTORY, + }, + ], + }, + // suggestions + { + flex: 1, + children: [ + { + flexChildren: true, + children: [ + { + flex: 2, + group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY, + }, + { + flex: 99, + group: UrlbarUtils.RESULT_GROUP.RECENT_SEARCH, + }, + { + 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 = Promise.withResolvers(); + 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..3867668c1a --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_UrlbarQueryContext_restrictSource.js @@ -0,0 +1,113 @@ +/* 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 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..fe33228007 --- /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); + engine.hideOneOffButton = true; + let matchedEngines = await UrlbarSearchUtils.enginesForDomainPrefix(token, { + onlyEnabled: true, + }); + Assert.notEqual(matchedEngines[0].searchUrlDomain, domain); + engine.hideOneOffButton = false; + 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.getIconURL(), 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.getIconURL(), 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.getIconURL(), 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.getIconURL(), 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.getIconURL(), 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.getIconURL(), 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.getIconURL(), 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.getIconURL(), 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..dc668e69ea --- /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_setup(function () { + 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_copySnakeKeysToCamel.js b/browser/components/urlbar/tests/unit/test_UrlbarUtils_copySnakeKeysToCamel.js new file mode 100644 index 0000000000..4b5352bc2a --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_UrlbarUtils_copySnakeKeysToCamel.js @@ -0,0 +1,226 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests `UrlbarUtils.copySnakeKeysToCamel()`. + +"use strict"; + +add_task(async function noSnakes() { + Assert.deepEqual( + UrlbarUtils.copySnakeKeysToCamel({ + foo: "foo key", + bar: "bar key", + }), + { + foo: "foo key", + bar: "bar key", + } + ); +}); + +add_task(async function oneSnake() { + Assert.deepEqual( + UrlbarUtils.copySnakeKeysToCamel({ + foo: "foo key", + snake_key: "snake key", + bar: "bar key", + }), + { + foo: "foo key", + snake_key: "snake key", + bar: "bar key", + snakeKey: "snake key", + } + ); +}); + +add_task(async function manySnakeKeys() { + Assert.deepEqual( + UrlbarUtils.copySnakeKeysToCamel({ + foo: "foo key", + snake_one: "snake key 1", + bar: "bar key", + and_snake_two_also: "snake key 2", + snake_key_3: "snake key 3", + snake_key_4_too: "snake key 4", + }), + { + foo: "foo key", + snake_one: "snake key 1", + bar: "bar key", + and_snake_two_also: "snake key 2", + snake_key_3: "snake key 3", + snake_key_4_too: "snake key 4", + snakeOne: "snake key 1", + andSnakeTwoAlso: "snake key 2", + snakeKey3: "snake key 3", + snakeKey4Too: "snake key 4", + } + ); +}); + +add_task(async function singleChars() { + Assert.deepEqual( + UrlbarUtils.copySnakeKeysToCamel({ + a: "a key", + b_c: "b_c key", + d_e_f: "d_e_f key", + g_h_i_j: "g_h_i_j key", + }), + { + a: "a key", + b_c: "b_c key", + d_e_f: "d_e_f key", + g_h_i_j: "g_h_i_j key", + bC: "b_c key", + dEF: "d_e_f key", + gHIJ: "g_h_i_j key", + } + ); +}); + +add_task(async function numbers() { + Assert.deepEqual( + UrlbarUtils.copySnakeKeysToCamel({ + snake_1: "snake 1 key", + snake_2_too: "snake 2 key", + "3_snakes": "snake 3 key", + }), + { + snake_1: "snake 1 key", + snake_2_too: "snake 2 key", + "3_snakes": "snake 3 key", + snake1: "snake 1 key", + snake2Too: "snake 2 key", + "3Snakes": "snake 3 key", + } + ); +}); + +add_task(async function leadingUnderscores() { + Assert.deepEqual( + UrlbarUtils.copySnakeKeysToCamel({ + _foo: "foo key", + __bar: "bar key", + _snake_with_leading: "snake key 1", + __snake_with_two_leading: "snake key 2", + }), + { + _foo: "foo key", + __bar: "bar key", + _snake_with_leading: "snake key 1", + __snake_with_two_leading: "snake key 2", + _snakeWithLeading: "snake key 1", + __snakeWithTwoLeading: "snake key 2", + } + ); +}); + +add_task(async function trailingUnderscores() { + Assert.deepEqual( + UrlbarUtils.copySnakeKeysToCamel({ + foo_: "foo key", + bar__: "bar key", + snake_with_trailing_: "snake key 1", + snake_with_two_trailing__: "snake key 2", + }), + { + foo_: "foo key", + bar__: "bar key", + snake_with_trailing_: "snake key 1", + snake_with_two_trailing__: "snake key 2", + snakeWithTrailing_: "snake key 1", + snakeWithTwoTrailing__: "snake key 2", + } + ); +}); + +add_task(async function leadingAndTrailingUnderscores() { + Assert.deepEqual( + UrlbarUtils.copySnakeKeysToCamel({ + _foo_: "foo key", + _extra_long_snake_: "snake key", + }), + { + _foo_: "foo key", + _extra_long_snake_: "snake key", + _extraLongSnake_: "snake key", + } + ); +}); + +add_task(async function consecutiveUnderscores() { + Assert.deepEqual( + UrlbarUtils.copySnakeKeysToCamel({ weird__snake: "snake key" }), + { + weird__snake: "snake key", + weird_Snake: "snake key", + } + ); +}); + +add_task(async function nested() { + let obj = UrlbarUtils.copySnakeKeysToCamel({ + foo: "foo key", + nested: { + bar: "bar key", + baz: { + snake_in_baz: "snake_in_baz key", + }, + snake_in_nested: { + snake_in_snake_in_nested: "snake_in_snake_in_nested key", + }, + }, + snake_key: { + snake_in_snake_key: "snake_in_snake_key key", + }, + }); + + Assert.equal(obj.foo, "foo key"); + Assert.equal(obj.nested.bar, "bar key"); + Assert.deepEqual(obj.nested.baz, { + snake_in_baz: "snake_in_baz key", + snakeInBaz: "snake_in_baz key", + }); + Assert.deepEqual(obj.nested.snake_in_nested, { + snake_in_snake_in_nested: "snake_in_snake_in_nested key", + snakeInSnakeInNested: "snake_in_snake_in_nested key", + }); + Assert.equal(obj.nested.snake_in_nested, obj.nested.snakeInNested); + Assert.deepEqual(obj.snake_key, { + snake_in_snake_key: "snake_in_snake_key key", + snakeInSnakeKey: "snake_in_snake_key key", + }); + Assert.equal(obj.snake_key, obj.snakeKey); +}); + +add_task(async function noOverwrite_ok() { + Assert.deepEqual( + UrlbarUtils.copySnakeKeysToCamel( + { + foo: "foo key", + snake_key: "snake key", + }, + false + ), + { + foo: "foo key", + snake_key: "snake key", + snakeKey: "snake key", + } + ); +}); + +add_task(async function noOverwrite_throws() { + Assert.throws( + () => + UrlbarUtils.copySnakeKeysToCamel( + { + snake_key: "snake key", + snakeKey: "snake key", + }, + false + ), + /Can't copy snake_case key/ + ); +}); 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_skippableTimer.js b/browser/components/urlbar/tests/unit/test_UrlbarUtils_skippableTimer.js new file mode 100644 index 0000000000..7400d507af --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_UrlbarUtils_skippableTimer.js @@ -0,0 +1,89 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests UrlbarUtils.SkippableTimer + */ + +"use strict"; + +let { SkippableTimer } = ChromeUtils.importESModule( + "resource:///modules/UrlbarUtils.sys.mjs" +); +const { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +add_task(async function test_basic() { + let invoked = 0; + let deferred = Promise.withResolvers(); + let timer = new SkippableTimer({ + name: "test 1", + callback: () => { + invoked++; + deferred.resolve(); + }, + time: 50, + }); + Assert.equal(timer.name, "test 1", "Timer should have the correct name"); + Assert.ok(!timer.done, "Should not be done"); + Assert.equal(invoked, 0, "Should not have invoked the callback yet"); + await deferred.promise; + Assert.ok(timer.done, "Should be done"); + Assert.equal(invoked, 1, "Should have invoked the callback"); +}); + +add_task(async function test_fire() { + let longTimeMs = 1000; + let invoked = 0; + let deferred = Promise.withResolvers(); + let timer = new SkippableTimer({ + name: "test 1", + callback: () => { + invoked++; + deferred.resolve(); + }, + time: longTimeMs, + }); + let start = Cu.now(); + Assert.equal(timer.name, "test 1", "Timer should have the correct name"); + Assert.ok(!timer.done, "Should not be done"); + Assert.equal(invoked, 0, "Should not have invoked the callback yet"); + // Call fire() many times to also verify the callback is invoked just once. + timer.fire(); + timer.fire(); + timer.fire(); + Assert.ok(timer.done, "Should be done"); + await deferred.promise; + Assert.greater(longTimeMs, Cu.now() - start, "Should have resolved earlier"); + Assert.equal(invoked, 1, "Should have invoked the callback"); +}); + +add_task(async function test_cancel() { + let timeMs = 50; + let invoked = 0; + let deferred = Promise.withResolvers(); + let timer = new SkippableTimer({ + name: "test 1", + callback: () => { + invoked++; + deferred.resolve(); + }, + time: timeMs, + }); + let start = Cu.now(); + Assert.equal(timer.name, "test 1", "Timer should have the correct name"); + Assert.ok(!timer.done, "Should not be done"); + Assert.equal(invoked, 0, "Should not have invoked the callback yet"); + // Calling cancel many times shouldn't rise any error. + timer.cancel(); + timer.cancel(); + Assert.ok(timer.done, "Should be done"); + await Promise.race([ + deferred.promise, + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + new Promise(r => setTimeout(r, timeMs * 4)), + ]); + Assert.greater(Cu.now() - start, timeMs, "Should not have resolved earlier"); + Assert.equal(invoked, 0, "Should not have invoked the callback"); +}); 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..5b0c496aa9 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_autofill_adaptiveHistory.js @@ -0,0 +1,1443 @@ +/* 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: UrlbarTestUtils.trimURL("http://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: UrlbarTestUtils.trimURL("http://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: UrlbarTestUtils.trimURL("http://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: UrlbarTestUtils.trimURL("http://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: UrlbarTestUtils.trimURL("http://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: UrlbarTestUtils.trimURL("http://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: UrlbarTestUtils.trimURL("http://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: UrlbarTestUtils.trimURL("http://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: UrlbarTestUtils.trimURL("http://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: UrlbarTestUtils.trimURL("http://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: UrlbarTestUtils.trimURL("http://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: UrlbarTestUtils.trimURL("http://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: UrlbarTestUtils.trimURL( + "http://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: UrlbarTestUtils.trimURL("http://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..2c6b874dbb --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_autofill_bookmarked.js @@ -0,0 +1,151 @@ +/* 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() { + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + 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: UrlbarTestUtils.trimURL(`http://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: UrlbarTestUtils.trimURL(`http://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: UrlbarTestUtils.trimURL(`http://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..37e2a8bbcb --- /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_setup(async () => { + 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..ad8d567a30 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_autofill_functional.js @@ -0,0 +1,147 @@ +/* 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_setup(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); +}); + +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(); +}); + +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 still autofill after a search is cancelled 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: UrlbarTestUtils.trimURL("http://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_autofill_origins.js b/browser/components/urlbar/tests/unit/test_autofill_origins.js new file mode 100644 index 0000000000..33e462a8af --- /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: UrlbarTestUtils.trimURL(baseURL), + 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: UrlbarTestUtils.trimURL(baseURL), + 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 PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + }); + } + + 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..05e3a230f1 --- /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 PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + }); + + // 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. + await TestUtils.waitForCondition(async () => { + // Add a visit to another origin to boost the threshold. + await PlacesTestUtils.addVisits("http://foo-" + url); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + let originFrecency = await getOriginFrecency("http://", host); + let threshold = await getOriginAutofillThreshold(); + return threshold > originFrecency; + }, "Make the bookmark fall below the frecency threshold"); + + // 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. + await TestUtils.waitForCondition(async () => { + // Add a visit to another origin to boost the threshold. + await PlacesTestUtils.addVisits("http://foo-" + url); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + let originFrecency = await getOriginFrecency("http://", host); + let threshold = await getOriginAutofillThreshold(); + return threshold > originFrecency; + }, "Make the bookmark fall below the frecency threshold"); + + // 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..41ff69acf2 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_autofill_origins_alt_frecency.js @@ -0,0 +1,272 @@ +/* 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. + +ChromeUtils.defineLazyGetter(this, "PlacesFrecencyRecalculator", () => { + return Cc["@mozilla.org/places/frecency-recalculator;1"].getService( + Ci.nsIObserver + ).wrappedJSObject; +}); + +testEngine_setup(); + +add_task(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(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.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(async function test_autofill_threshold() { + 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 PlacesTestUtils.getDatabaseValue("moz_origins", "alt_frecency", { + host: "somethingelse.org", + }), + "Check mozilla.org has a lower frecency than the threshold" + ); + Assert.equal( + threshold, + await PlacesTestUtils.getDatabaseValue("moz_origins", "avg(alt_frecency)"), + "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(async function test_autofill_cutoff() { + // 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 PlacesTestUtils.getDatabaseValue("moz_origins", "alt_frecency", { + host: "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(); +}); + +add_task(async function test_autofill_threshold_www() { + // Only one visit to the non-www origin, many to the www. version. We expect + // example.com to autofill even if its frecency is small, because the overall + // frecency for both origins should be considered. + await PlacesTestUtils.addVisits("https://example.com/"); + await PlacesTestUtils.addVisits( + new Array(10).fill("https://www.example.com/") + ); + await PlacesTestUtils.addVisits( + new Array(10).fill("https://www.somethingelse.org/") + ); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + let threshold = await PlacesUtils.metadata.get( + "origin_alt_frecency_threshold", + 0 + ); + let frecencyOfExampleCom = await PlacesTestUtils.getDatabaseValue( + "moz_origins", + "alt_frecency", + { + host: "example.com", + } + ); + let frecencyOfWwwExampleCom = await PlacesTestUtils.getDatabaseValue( + "moz_origins", + "alt_frecency", + { + host: "www.example.com", + } + ); + Assert.greater( + threshold, + frecencyOfExampleCom, + "example.com frecency is lower than the threshold" + ); + Assert.greater( + frecencyOfWwwExampleCom, + threshold, + "www.example.com frecency is higher than the threshold" + ); + + // We used to wrongly use the average between the 2 domains, so check also + // the average would not autofill. + Assert.greater( + threshold, + [frecencyOfExampleCom, frecencyOfWwwExampleCom].reduce( + (acc, v, i, arr) => acc + v / arr.length, + 0 + ), + "Check frecency average is lower than the threshold" + ); + + let context = createContext("ex", { isPrivate: false }); + await check_results({ + context, + autofilled: "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, + }), + 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..9ebee29cc2 --- /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: UrlbarTestUtils.trimURL(`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: UrlbarTestUtils.trimURL(`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..40df51ecf3 --- /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_setup(async () => { + // 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_urls.js b/browser/components/urlbar/tests/unit/test_autofill_urls.js new file mode 100644 index 0000000000..9805dc9ffc --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_autofill_urls.js @@ -0,0 +1,916 @@ +/* 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: UrlbarTestUtils.trimURL( + "http://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 uriFragmentCaseSensitiveNoMatch() { + 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: UrlbarTestUtils.trimURL("http://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: UrlbarTestUtils.trimURL("http://example.com/", { + removeSingleTrailingSlash: false, + }), + 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: UrlbarTestUtils.trimURL("http://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: UrlbarTestUtils.trimURL("http://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: UrlbarTestUtils.trimURL("http://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: UrlbarTestUtils.trimURL("http://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: UrlbarTestUtils.trimURL( + "http://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: UrlbarTestUtils.trimURL("http://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: UrlbarTestUtils.trimURL("http://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: UrlbarTestUtils.trimURL( + "http://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: UrlbarTestUtils.trimURL( + "http://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: UrlbarTestUtils.trimURL("http://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: UrlbarTestUtils.trimURL( + "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(); + } +}); + +add_task(async function formatPunycodeResultCorrectly() { + await PlacesTestUtils.addVisits([ + { + uri: `http://test.xn--e1afmkfd.com/`, + }, + ]); + let context = createContext("test", { isPrivate: false }); + await check_results({ + context, + autofilled: "test.xn--e1afmkfd.com/", + completed: "http://test.xn--e1afmkfd.com/", + matches: [ + makeVisitResult(context, { + uri: "http://test.xn--e1afmkfd.com/", + title: "test visit for http://test.xn--e1afmkfd.com/", + displayUrl: "http://test.пример.com", + heuristic: true, + }), + ], + }); + 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..b7c17d8cb3 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_avoid_stripping_to_empty_tokens.js @@ -0,0 +1,117 @@ +/* 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: UrlbarTestUtils.trimURL(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: UrlbarTestUtils.trimURL(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..0671b87a94 --- /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: UrlbarTestUtils.trimURL("http://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: UrlbarTestUtils.trimURL("http://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: UrlbarTestUtils.trimURL("http://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_embedded_url_param.js b/browser/components/urlbar/tests/unit/test_dedupe_embedded_url_param.js new file mode 100644 index 0000000000..eaf42feb2d --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_dedupe_embedded_url_param.js @@ -0,0 +1,226 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +testEngine_setup(); + +add_task(async function test_embedded_url_show_up_as_places_result() { + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com/?url=http://kitten.com/", + title: "kitten", + }, + ]); + + let context = createContext("kitten", { + isPrivate: false, + }); + + await check_results({ + context, + matches: [ + makeSearchResult(context, { + heuristic: true, + query: "kitten", + engineName: Services.search.defaultEngine.name, + }), + makeVisitResult(context, { + uri: "http://example.com/?url=http://kitten.com/", + title: "kitten", + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + }), + ], + }); + + await cleanupPlaces(); +}); + +add_task(async function test_deduplication_of_embedded_url_autofill_result() { + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com/?url=http://kitten.com/", + title: "kitten", + }, + { + uri: "http://kitten.com/", + title: "kitten", + }, + ]); + + let context = createContext("kitten", { + isPrivate: false, + }); + + await check_results({ + context, + matches: [ + makeVisitResult(context, { + uri: "http://kitten.com/", + title: "kitten", + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + heuristic: true, + providerName: "Autofill", + }), + ], + }); + + await cleanupPlaces(); +}); + +add_task(async function test_deduplication_of_embedded_url_places_result() { + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com/?url=http://kitten.com/", + title: "kitten", + }, + { + uri: "http://kitten.com/", + title: "kitten", + }, + ]); + + let context = createContext("kitten", { + isPrivate: false, + allowAutofill: false, + }); + + await check_results({ + context, + matches: [ + makeSearchResult(context, { + heuristic: true, + query: "kitten", + engineName: Services.search.defaultEngine.name, + }), + makeVisitResult(context, { + uri: "http://kitten.com/", + title: "kitten", + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + }), + ], + }); + + await cleanupPlaces(); +}); + +add_task( + async function test_deduplication_of_higher_frecency_embedded_url_places_result() { + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com/?url=http://kitten.com/", + title: "kitten", + }, + { + uri: "http://example.com/?url=http://kitten.com/", + title: "kitten", + }, + { + uri: "http://kitten.com/", + title: "kitten", + }, + ]); + + let context = createContext("kitten", { + isPrivate: false, + allowAutofill: false, + }); + + await check_results({ + context, + matches: [ + makeSearchResult(context, { + heuristic: true, + query: "kitten", + engineName: Services.search.defaultEngine.name, + }), + makeVisitResult(context, { + uri: "http://kitten.com/", + title: "kitten", + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + }), + ], + }); + + await cleanupPlaces(); + } +); + +add_task( + async function test_deduplication_of_embedded_encoded_url_places_result() { + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com/?url=http%3A%2F%2Fkitten.com%2F", + title: "kitten", + }, + { + uri: "http://kitten.com/", + title: "kitten", + }, + ]); + + let context = createContext("kitten", { + isPrivate: false, + allowAutofill: false, + }); + + await check_results({ + context, + matches: [ + makeSearchResult(context, { + heuristic: true, + query: "kitten", + engineName: Services.search.defaultEngine.name, + }), + makeVisitResult(context, { + uri: "http://kitten.com/", + title: "kitten", + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + }), + ], + }); + + await cleanupPlaces(); + } +); + +add_task(async function test_deduplication_of_embedded_url_switchTab_result() { + let uri = Services.io.newURI("http://kitten.com/"); + + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com/?url=http://kitten.com/", + title: "kitten", + }, + { + uri, + title: "kitten", + }, + ]); + + await addOpenPages(uri, 1); + + let context = createContext("kitten", { + isPrivate: false, + allowAutofill: false, + }); + + await check_results({ + context, + matches: [ + makeSearchResult(context, { + heuristic: true, + query: "kitten", + engineName: Services.search.defaultEngine.name, + }), + makeTabSwitchResult(context, { + source: UrlbarUtils.RESULT_SOURCE.TAB, + uri: "http://kitten.com/", + title: "kitten", + }), + ], + }); + + await removeOpenPages(uri, 1); + 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_dont_autofill_cases.js b/browser/components/urlbar/tests/unit/test_dont_autofill_cases.js new file mode 100644 index 0000000000..fefdd68452 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_dont_autofill_cases.js @@ -0,0 +1,59 @@ +/* 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 some cases where autofill should not happen. + */ + +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(); +}); 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..470b93a2b2 --- /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/@", + 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..e3ce0b8479 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_exposure.js @@ -0,0 +1,271 @@ +/* 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 = [ + QuickSuggestTestUtils.ampRemoteSettings({ + keywords: ["test"], + }), + QuickSuggestTestUtils.wikipediaRemoteSettings({ + keywords: ["non_sponsored"], + }), +]; + +const EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT = makeAmpResult({ + keyword: "test", +}); + +const EXPECTED_NON_SPONSORED_REMOTE_SETTINGS_RESULT = makeWikipediaResult({ + keyword: "non_sponsored", +}); + +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(); + + // Set up the remote settings client with the test data. + await QuickSuggestTestUtils.ensureQuickSuggestInit({ + remoteSettingsRecords: [ + { + type: "data", + attachment: REMOTE_SETTINGS_RESULTS, + }, + ], + prefs: [ + ["suggest.quicksuggest.nonsponsored", true], + ["suggest.quicksuggest.sponsored", true], + ], + }); +}); + +add_task(async function testExposureCheck() { + UrlbarPrefs.set("exposureResults", suggestResultType("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, + suggestResultType("adm_sponsored") + ); + Assert.equal(context.results[0].exposureResultHidden, false); +}); + +add_task(async function testExposureCheckMultiple() { + UrlbarPrefs.set( + "exposureResults", + [ + suggestResultType("adm_sponsored"), + suggestResultType("adm_nonsponsored"), + ].join(",") + ); + 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, + suggestResultType("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, + suggestResultType("adm_nonsponsored") + ); + Assert.equal(context.results[0].exposureResultHidden, false); +}); + +add_task(async function exposureDisplayFiltering() { + UrlbarPrefs.set("exposureResults", suggestResultType("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, + suggestResultType("adm_sponsored") + ); + Assert.equal(context.results[0].exposureResultHidden, true); +}); + +function suggestResultType(typeWithoutSource) { + let source = UrlbarPrefs.get("quickSuggestRustEnabled") ? "rust" : "rs"; + return `${source}_${typeWithoutSource}`; +} + +// Copied from quicksuggest/unit/head.js +function makeAmpResult({ + source, + provider, + keyword = "amp", + title = "Amp Suggestion", + url = "http://example.com/amp", + originalUrl = "http://example.com/amp", + icon = null, + iconBlob = new Blob([new Uint8Array([])]), + impressionUrl = "http://example.com/amp-impression", + clickUrl = "http://example.com/amp-click", + blockId = 1, + advertiser = "Amp", + iabCategory = "22 - Shopping", + suggestedIndex = -1, + isSuggestedIndexRelativeToGroup = true, + requestId = undefined, +} = {}) { + let result = { + suggestedIndex, + isSuggestedIndexRelativeToGroup, + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + heuristic: false, + payload: { + title, + url, + originalUrl, + requestId, + displayUrl: url.replace(/^https:\/\//, ""), + isSponsored: true, + qsSuggestion: keyword, + sponsoredImpressionUrl: impressionUrl, + sponsoredClickUrl: clickUrl, + sponsoredBlockId: blockId, + sponsoredAdvertiser: advertiser, + sponsoredIabCategory: iabCategory, + helpUrl: QuickSuggest.HELP_URL, + helpL10n: { + id: "urlbar-result-menu-learn-more-about-firefox-suggest", + }, + isBlockable: true, + blockL10n: { + id: "urlbar-result-menu-dismiss-firefox-suggest", + }, + telemetryType: "adm_sponsored", + descriptionL10n: { id: "urlbar-result-action-sponsored" }, + }, + }; + + if (UrlbarPrefs.get("quickSuggestRustEnabled")) { + result.payload.source = source || "rust"; + result.payload.provider = provider || "Amp"; + if (result.payload.source == "rust") { + result.payload.iconBlob = iconBlob; + } else { + result.payload.icon = icon; + } + } else { + result.payload.source = source || "remote-settings"; + result.payload.provider = provider || "AdmWikipedia"; + result.payload.icon = icon; + } + + return result; +} + +// Copied from quicksuggest/unit/head.js +function makeWikipediaResult({ + source, + provider, + keyword = "wikipedia", + title = "Wikipedia Suggestion", + url = "http://example.com/wikipedia", + originalUrl = "http://example.com/wikipedia", + icon = null, + iconBlob = new Blob([new Uint8Array([])]), + impressionUrl = "http://example.com/wikipedia-impression", + clickUrl = "http://example.com/wikipedia-click", + blockId = 1, + advertiser = "Wikipedia", + iabCategory = "5 - Education", + suggestedIndex = -1, + isSuggestedIndexRelativeToGroup = true, +}) { + let result = { + suggestedIndex, + isSuggestedIndexRelativeToGroup, + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + heuristic: false, + payload: { + title, + url, + originalUrl, + displayUrl: url.replace(/^https:\/\//, ""), + isSponsored: false, + qsSuggestion: keyword, + sponsoredAdvertiser: "Wikipedia", + sponsoredIabCategory: "5 - Education", + helpUrl: QuickSuggest.HELP_URL, + helpL10n: { + id: "urlbar-result-menu-learn-more-about-firefox-suggest", + }, + isBlockable: true, + blockL10n: { + id: "urlbar-result-menu-dismiss-firefox-suggest", + }, + telemetryType: "adm_nonsponsored", + }, + }; + + if (UrlbarPrefs.get("quickSuggestRustEnabled")) { + result.payload.source = source || "rust"; + result.payload.provider = provider || "Wikipedia"; + result.payload.iconBlob = iconBlob; + } else { + result.payload.source = source || "remote-settings"; + result.payload.provider = provider || "AdmWikipedia"; + result.payload.icon = icon; + result.payload.sponsoredImpressionUrl = impressionUrl; + result.payload.sponsoredClickUrl = clickUrl; + result.payload.sponsoredBlockId = blockId; + result.payload.sponsoredAdvertiser = advertiser; + result.payload.sponsoredIabCategory = iabCategory; + } + + return result; +} 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..d50d5314ad --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_frecency_alternative_nimbus.js @@ -0,0 +1,77 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function () { + const tests = [ + { + enableVariable: "originsAlternativeEnable", + enablePref: "places.frecency.origins.alternative.featureGate", + variables: { + originsDaysCutOff: "places.frecency.origins.alternative.daysCutOff", + }, + }, + { + enableVariable: "pagesAlternativeEnable", + enablePref: "places.frecency.pages.alternative.featureGate", + variables: { + pagesNumSampledVisits: + "places.frecency.pages.alternative.numSampledVisits", + pagesHalfLifeDays: "places.frecency.pages.alternative.halfLifeDays", + pagesHighWeight: "places.frecency.pages.alternative.highWeight", + pagesMediumWeight: "places.frecency.pages.alternative.mediumWeight", + pagesLowWeight: "places.frecency.pages.alternative.lowWeight", + }, + }, + ]; + for (let test of tests) { + await doTest(test.enableVariable, test.enablePref, test.variables); + } +}); + +async function doTest(enableVariable, enablePref, otherVariables) { + info(`Testing ${enableVariable}`); + let reset = await UrlbarTestUtils.initNimbusFeature( + { + // Empty for sanity check. + }, + "urlbar", + "config" + ); + Assert.ok(!Services.prefs.prefHasUserValue(enablePref)); + Assert.ok(!Services.prefs.getBoolPref(enablePref, false)); + for (let pref of Object.values(otherVariables)) { + Assert.ok(!Services.prefs.prefHasUserValue(pref)); + } + await reset(); + + reset = await UrlbarTestUtils.initNimbusFeature( + { + [enableVariable]: true, + }, + "urlbar", + "config" + ); + Assert.ok(Services.prefs.prefHasUserValue(enablePref)); + Assert.equal(Services.prefs.getBoolPref(enablePref), true); + for (let pref of Object.values(otherVariables)) { + Assert.ok(!Services.prefs.prefHasUserValue(pref)); + } + await reset(); + + const FAKE_VALUE = 777; + let config = { + [enableVariable]: true, + }; + for (let variable of Object.keys(otherVariables)) { + config[variable] = FAKE_VALUE; + } + reset = await UrlbarTestUtils.initNimbusFeature(config, "urlbar", "config"); + Assert.ok(Services.prefs.prefHasUserValue(enablePref)); + Assert.equal(Services.prefs.getBoolPref(enablePref), true); + for (let pref of Object.values(otherVariables)) { + Assert.ok(Services.prefs.prefHasUserValue(pref)); + Assert.equal(Services.prefs.getIntPref(pref, 90), FAKE_VALUE); + } + + 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..6f6f2fbd8a --- /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_setup(async function () { + 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..d49aaf2fb7 --- /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.getIconURL(), + }), + ]; + 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_history_bookmark_results_on_search_service_failure.js b/browser/components/urlbar/tests/unit/test_history_bookmark_results_on_search_service_failure.js new file mode 100644 index 0000000000..32b3441f5e --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_history_bookmark_results_on_search_service_failure.js @@ -0,0 +1,116 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests history and bookmark results show up when search service + * initialization has failed. + */ + +const { PromiseTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromiseTestUtils.sys.mjs" +); + +const searchService = Services.search.wrappedJSObject; + +add_setup(async function setup() { + searchService.errorToThrowInTest = "Settings"; + + // When search service fails, we want the promise rejection to be uncaught + // so we can continue running the test. + PromiseTestUtils.expectUncaughtRejection( + /Fake Settings error during search service initialization./ + ); + + registerCleanupFunction(async () => { + searchService.errorToThrowInTest = null; + await cleanupPlaces(); + }); +}); + +add_task( + async function test_bookmark_results_are_shown_when_search_service_failed() { + Assert.equal( + searchService.isInitialized, + false, + "Search Service should not be initialized." + ); + + info("Add a bookmark"); + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://cat.com/", + title: "cat", + }); + + let context = createContext("cat", { + isPrivate: false, + allowAutofill: false, + }); + + await check_results({ + context, + matches: [ + makeVisitResult(context, { + uri: "http://cat/", + heuristic: true, + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + fallbackTitle: "http://cat/", + }), + makeBookmarkResult(context, { + title: "cat", + uri: "http://cat.com/", + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + }), + ], + }); + + Assert.equal( + searchService.isInitialized, + true, + "Search Service should have finished its attempt to initialize." + ); + + Assert.equal( + searchService.hasSuccessfullyInitialized, + false, + "Search Service should have failed to initialize." + ); + await cleanupPlaces(); + } +); + +add_task( + async function test_history_results_are_shown_when_search_service_failed() { + Assert.equal( + searchService.isInitialized, + true, + "Search Service should have finished its attempt to initialize in the previous test." + ); + + Assert.equal( + searchService.hasSuccessfullyInitialized, + false, + "Search Service should have failed to initialize." + ); + + info("visit a url in history"); + await PlacesTestUtils.addVisits({ + uri: "http://example.com/", + title: "example", + }); + + let context = createContext("example", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + type: 3, + title: "example", + uri: "http://example.com/", + heuristic: true, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + }), + ], + }); + } +); 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..1773768a5c --- /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: UrlbarTestUtils.trimURL("http://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: UrlbarTestUtils.trimURL("http://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: UrlbarTestUtils.trimURL("http://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/"); + await 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: "A bookmark", + 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..192265661a --- /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_setup(async () => { + 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..3d3eab19ba --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_match_javascript.js @@ -0,0 +1,153 @@ +/* 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.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.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..8d4eef4ba2 --- /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_setup(async function () { + 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_pages_alt_frecency.js b/browser/components/urlbar/tests/unit/test_pages_alt_frecency.js new file mode 100644 index 0000000000..41452587d4 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_pages_alt_frecency.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/. */ + +// This is a basic autocomplete test to ensure enabling the alternative frecency +// algorithm doesn't break results and sorts them appropriately. +// A more comprehensive testing of the algorithm itself is not included since it +// is something that may change frequently according to experimentation results. +// Other existing tests will, of course, need to be adapted once an algorithm +// is promoted to be the default. + +testEngine_setup(); + +add_task(async function test_autofill() { + const searchString = "match"; + const singleVisitUrl = "https://singlevisit-match.org/"; + const singleVisitBookmarkedUrl = "https://singlevisitbookmarked-match.org/"; + const adaptiveVisitUrl = "https://adaptivevisit-match.org/"; + const adaptiveManyVisitsUrl = "https://adaptivemanyvisit-match.org/"; + const manyVisitsUrl = "https://manyvisits-match.org/"; + const sampledVisitsUrl = "https://sampledvisits-match.org/"; + const bookmarkedUrl = "https://bookmarked-match.org/"; + + await PlacesUtils.bookmarks.insert({ + url: bookmarkedUrl, + title: "bookmark", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + }); + await PlacesUtils.bookmarks.insert({ + url: singleVisitBookmarkedUrl, + title: "visited bookmark", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + }); + await PlacesTestUtils.addVisits([ + singleVisitUrl, + singleVisitBookmarkedUrl, + adaptiveVisitUrl, + ...new Array(10).fill(adaptiveManyVisitsUrl), + ...new Array(100).fill(manyVisitsUrl), + ...new Array(10).fill(sampledVisitsUrl), + ]); + await UrlbarUtils.addToInputHistory(adaptiveVisitUrl, searchString); + await UrlbarUtils.addToInputHistory(adaptiveManyVisitsUrl, searchString); + + let context = createContext(searchString, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: "Suggestions", + heuristic: true, + }), + makeVisitResult(context, { + uri: adaptiveManyVisitsUrl, + title: `test visit for ${adaptiveManyVisitsUrl}`, + }), + makeVisitResult(context, { + uri: adaptiveVisitUrl, + title: `test visit for ${adaptiveVisitUrl}`, + }), + makeVisitResult(context, { + uri: manyVisitsUrl, + title: `test visit for ${manyVisitsUrl}`, + }), + makeVisitResult(context, { + uri: sampledVisitsUrl, + title: `test visit for ${sampledVisitsUrl}`, + }), + makeBookmarkResult(context, { + uri: singleVisitBookmarkedUrl, + title: "visited bookmark", + }), + makeBookmarkResult(context, { + uri: bookmarkedUrl, + title: "bookmark", + }), + makeVisitResult(context, { + uri: singleVisitUrl, + title: `test visit for ${singleVisitUrl}`, + }), + ], + }); + + await PlacesUtils.history.clear(); +}); 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..4640b167f5 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_protocol_swap.js @@ -0,0 +1,302 @@ +/* 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.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..bf2ce13e7e --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_providerAliasEngines.js @@ -0,0 +1,146 @@ +/* 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", + }), + ], + }); + + context = createContext(`${alias} kitten?`, { + isPrivate: false, + }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: `Aliased${alias.toUpperCase()}MozSearch`, + query: "kitten?", + alias, + heuristic: true, + providerName: "AliasEngines", + }), + ], + }); + + context = createContext(`${alias} kitten ?`, { + isPrivate: false, + }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: `Aliased${alias.toUpperCase()}MozSearch`, + query: "kitten ?", + 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..7b331b346b --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_providerHeuristicFallback.js @@ -0,0 +1,775 @@ +/* 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_setup(async function () { + 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, + }), + ], + }); + + await PlacesUtils.history.clear(); + // Check that punycode results are properly decoded before being displayed. + info("visit url, host matching visited host but not visited url"); + await PlacesTestUtils.addVisits([ + { + uri: Services.io.newURI("http://test.пример.com/"), + title: "test.пример.com", + transition: PlacesUtils.history.TRANSITION_TYPED, + }, + ]); + context = createContext("test", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + uri: `http://test.xn--e1afmkfd.com/`, + displayUrl: `test.пример.com`, + heuristic: true, + iconUri: "page-icon:http://test.xn--e1afmkfd.com/", + }), + ], + }); + await PlacesUtils.history.clear(); +}); + +add_task(async function dont_fixup_urls_with_at_symbol() { + info("don't fixup search string if it contains no protocol and spaces."); + let query = "Lorem Ipsum @mozilla.org"; + let context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + ], + }); + + query = "http://Lorem Ipsum @mozilla.org"; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: `http://Lorem%20Ipsum%20@mozilla.org/`, + fallbackTitle: `${query}/`, + heuristic: true, + }), + ], + }); + query = "https://Lorem Ipsum @mozilla.org"; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: `https://Lorem%20Ipsum%20@mozilla.org/`, + fallbackTitle: `${query}/`, + heuristic: true, + }), + ], + }); + + query = "LoremIpsum@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, + }), + ], + }); +}); + +/** + * 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..7eb62fbeea --- /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_setup(async function () { + 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..e0958b8296 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_providerKeywords.js @@ -0,0 +1,407 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * 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("Keyword query with *"); + // We need a space before the asterisk to ensure it's considered a restriction + // token otherwise it will be a regular string character. + context = createContext("key blocking *", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeKeywordSearchResult(context, { + uri: "http://abc/?search=blocking%20*", + keyword: "key", + title: "abc: blocking *", + 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%3F", + keyword: "key", + title: "abc: blocking?", + 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%20%3F", + 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..4e4ef02e0c --- /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_setup(function () { + 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..f85f547ac3 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_providerOpenTabs.js @@ -0,0 +1,80 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_openTabs() { + const userContextId1 = 3; + const userContextId2 = 5; + const url = "http://foo.mozilla.org/"; + const url2 = "http://foo2.mozilla.org/"; + UrlbarProviderOpenTabs.registerOpenTab(url, userContextId1, false); + UrlbarProviderOpenTabs.registerOpenTab(url, userContextId1, false); + UrlbarProviderOpenTabs.registerOpenTab(url2, userContextId1, false); + UrlbarProviderOpenTabs.registerOpenTab(url, userContextId2, false); + Assert.deepEqual( + [url, url2], + UrlbarProviderOpenTabs.getOpenTabs(userContextId1), + "Found all the expected tabs" + ); + Assert.deepEqual( + [url], + UrlbarProviderOpenTabs.getOpenTabs(userContextId2), + "Found all the expected tabs" + ); + await PlacesUtils.promiseLargeCacheDBConnection(); + await UrlbarProviderOpenTabs.promiseDBPopulated; + Assert.deepEqual( + [ + { url, userContextId: userContextId1, count: 2 }, + { url: url2, userContextId: userContextId1, count: 1 }, + { url, userContextId: userContextId2, count: 1 }, + ], + await UrlbarProviderOpenTabs.getDatabaseRegisteredOpenTabsForTests(), + "Found all the expected tabs" + ); + + await UrlbarProviderOpenTabs.unregisterOpenTab(url2, userContextId1, false); + Assert.deepEqual( + [url], + UrlbarProviderOpenTabs.getOpenTabs(userContextId1), + "Found all the expected tabs" + ); + await UrlbarProviderOpenTabs.unregisterOpenTab(url, userContextId1, false); + Assert.deepEqual( + [url], + UrlbarProviderOpenTabs.getOpenTabs(userContextId1), + "Found all the expected tabs" + ); + Assert.deepEqual( + [ + { url, userContextId: userContextId1, count: 1 }, + { url, userContextId: userContextId2, count: 1 }, + ], + await UrlbarProviderOpenTabs.getDatabaseRegisteredOpenTabsForTests(), + "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, 2, "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_providerRecentSearches.js b/browser/components/urlbar/tests/unit/test_providerRecentSearches.js new file mode 100644 index 0000000000..c7b542e317 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_providerRecentSearches.js @@ -0,0 +1,167 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + setTimeout: "resource://gre/modules/Timer.sys.mjs", + UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs", +}); + +let ENABLED_PREF = "recentsearches.featureGate"; +let EXPIRE_PREF = "recentsearches.expirationMs"; +let SUGGESTS_PREF = "suggest.recentsearches"; + +let TEST_SEARCHES = ["Bob Vylan", "Glasgow Weather", "Joy Formidable"]; +let defaultEngine; + +function makeRecentSearchResult(context, engine, suggestion) { + let result = makeFormHistoryResult(context, { + suggestion, + engineName: engine.name, + }); + delete result.payload.lowerCaseSuggestion; + return result; +} + +async function addSearches(searches = TEST_SEARCHES) { + // Add the searches sequentially so they get a new timestamp + // and we can order by the time added. + for (let search of searches) { + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(r => setTimeout(r, 10)); + await UrlbarTestUtils.formHistory.add([ + { value: search, source: defaultEngine.name }, + ]); + } +} + +add_setup(async () => { + defaultEngine = await addTestSuggestionsEngine(); + await Services.search.setDefault( + defaultEngine, + Ci.nsISearchService.CHANGE_REASON_ADDON_INSTALL + ); + + let oldCurrentEngine = Services.search.defaultEngine; + + registerCleanupFunction(() => { + Services.search.defaultEngine = oldCurrentEngine; + UrlbarPrefs.clear(ENABLED_PREF); + UrlbarPrefs.clear(SUGGESTS_PREF); + }); +}); + +add_task(async function test_enabled() { + UrlbarPrefs.set(ENABLED_PREF, true); + UrlbarPrefs.set(SUGGESTS_PREF, true); + await addSearches(); + let context = createContext("", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeRecentSearchResult(context, defaultEngine, "Joy Formidable"), + makeRecentSearchResult(context, defaultEngine, "Glasgow Weather"), + makeRecentSearchResult(context, defaultEngine, "Bob Vylan"), + ], + }); +}); + +add_task(async function test_disabled() { + UrlbarPrefs.set(ENABLED_PREF, false); + UrlbarPrefs.set(SUGGESTS_PREF, false); + await addSearches(); + await check_results({ + context: createContext("", { isPrivate: false }), + matches: [], + }); +}); + +add_task(async function test_most_recent_shown() { + UrlbarPrefs.set(ENABLED_PREF, true); + UrlbarPrefs.set(SUGGESTS_PREF, true); + + await addSearches(Array.from(Array(10).keys()).map(i => `Search ${i}`)); + let context = createContext("", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeRecentSearchResult(context, defaultEngine, "Search 9"), + makeRecentSearchResult(context, defaultEngine, "Search 8"), + makeRecentSearchResult(context, defaultEngine, "Search 7"), + makeRecentSearchResult(context, defaultEngine, "Search 6"), + makeRecentSearchResult(context, defaultEngine, "Search 5"), + ], + }); + await UrlbarTestUtils.formHistory.clear(); +}); + +add_task(async function test_per_engine() { + UrlbarPrefs.set(ENABLED_PREF, true); + UrlbarPrefs.set(SUGGESTS_PREF, true); + + let oldEngine = defaultEngine; + await addSearches(); + + defaultEngine = await addTestSuggestionsEngine(null, { + name: "NewTestEngine", + }); + await Services.search.setDefault( + defaultEngine, + Ci.nsISearchService.CHANGE_REASON_ADDON_INSTALL + ); + + await addSearches(); + + let context = createContext("", { + isPrivate: false, + formHistoryName: "test", + }); + await check_results({ + context, + matches: [ + makeRecentSearchResult(context, defaultEngine, "Joy Formidable"), + makeRecentSearchResult(context, defaultEngine, "Glasgow Weather"), + makeRecentSearchResult(context, defaultEngine, "Bob Vylan"), + ], + }); + + defaultEngine = oldEngine; + await Services.search.setDefault( + defaultEngine, + Ci.nsISearchService.CHANGE_REASON_ADDON_INSTALL + ); + + info("We only show searches made since last default engine change"); + context = createContext("", { isPrivate: false }); + await check_results({ + context, + matches: [], + }); + await UrlbarTestUtils.formHistory.clear(); +}); + +add_task(async function test_expiry() { + UrlbarPrefs.set(ENABLED_PREF, true); + UrlbarPrefs.set(SUGGESTS_PREF, true); + await addSearches(); + let context = createContext("", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeRecentSearchResult(context, defaultEngine, "Joy Formidable"), + makeRecentSearchResult(context, defaultEngine, "Glasgow Weather"), + makeRecentSearchResult(context, defaultEngine, "Bob Vylan"), + ], + }); + + let shortExpiration = 100; + UrlbarPrefs.set(EXPIRE_PREF, shortExpiration.toString()); + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(r => setTimeout(r, shortExpiration * 2)); + + await check_results({ + context: createContext("", { isPrivate: false }), + matches: [], + }); +}); 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..0a8bfbead5 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_providerTabToSearch.js @@ -0,0 +1,536 @@ +/* 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_setup(async () => { + // 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.getIconURL(), + 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.getIconURL(), + 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."); + engine.hideOneOffButton = true; + + 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", + }), + ], + }); + engine.hideOneOffButton = false; + + 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..98c1081b84 --- /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_setup(async function () { + 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..094eb42437 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_providersManager_filtering.js @@ -0,0 +1,405 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_filtering_disable_only_source() { + let match = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + UrlbarUtils.RESULT_SOURCE.TABS, + { url: "http://mozilla.org/foo/" } + ); + let 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({ priority, 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..3b478c3cf3 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_query_url.js @@ -0,0 +1,123 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const 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: UrlbarTestUtils.trimURL("http://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: UrlbarTestUtils.trimURL("http://file.org/", { + removeSingleTrailingSlash: false, + }), + 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..00206c77b2 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_quickactions.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/. */ + +"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", + inQuickActionsSearchMode: false, + helpUrl: UrlbarProviderQuickActions.helpUrl, + inputLength, + }, +}); + +testEngine_setup(); + +add_setup(async () => { + 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..bb0e708162 --- /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_setup(async function () { + // 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: "cached-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..5d8cdd53d3 --- /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_setup(async function () { + // 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_richsuggestions.js b/browser/components/urlbar/tests/unit/test_richsuggestions.js new file mode 100644 index 0000000000..b6ceaa6db5 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_richsuggestions.js @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that rich suggestion results results are shown without + * rich data if richSuggestions are disabled. + */ + +const SUGGEST_ENABLED_PREF = "browser.search.suggest.enabled"; +const RICH_SUGGESTIONS_PREF = "browser.urlbar.richSuggestions.featureGate"; +const QUICKACTIONS_URLBAR_PREF = "quickactions.enabled"; + +add_setup(async function () { + let engine = await addTestTailSuggestionsEngine(defaultRichSuggestionsFn); + // Install the test engine. + let oldDefaultEngine = await Services.search.getDefault(); + registerCleanupFunction(async () => { + Services.search.setDefault( + oldDefaultEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + Services.prefs.clearUserPref(RICH_SUGGESTIONS_PREF); + Services.prefs.clearUserPref(SUGGEST_ENABLED_PREF); + UrlbarPrefs.clear(QUICKACTIONS_URLBAR_PREF); + }); + Services.search.setDefault(engine, Ci.nsISearchService.CHANGE_REASON_UNKNOWN); + Services.prefs.setBoolPref(RICH_SUGGESTIONS_PREF, true); + Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true); + UrlbarPrefs.set(QUICKACTIONS_URLBAR_PREF, false); +}); + +/** + * Test that suggestions with rich data are still shown + */ +add_task(async function test_richsuggestions_disabled() { + Services.prefs.setBoolPref(RICH_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, + }), + makeSearchResult(context, { + engineName: TAIL_SUGGESTIONS_ENGINE_NAME, + suggestion: query + "oronto", + }), + makeSearchResult(context, { + engineName: TAIL_SUGGESTIONS_ENGINE_NAME, + suggestion: query + "unisia", + }), + makeSearchResult(context, { + engineName: TAIL_SUGGESTIONS_ENGINE_NAME, + suggestion: query + "acoma", + }), + makeSearchResult(context, { + engineName: TAIL_SUGGESTIONS_ENGINE_NAME, + suggestion: query + "aipei", + }), + ], + }); +}); diff --git a/browser/components/urlbar/tests/unit/test_richsuggestions_order.js b/browser/components/urlbar/tests/unit/test_richsuggestions_order.js new file mode 100644 index 0000000000..7e918b4e5e --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_richsuggestions_order.js @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that rich suggestion results are ordered in the + * same order they were returned from the API. + */ + +const SUGGEST_ENABLED_PREF = "browser.search.suggest.enabled"; +const RICH_SUGGESTIONS_PREF = "browser.urlbar.richSuggestions.featureGate"; + +const QUICKACTIONS_URLBAR_PREF = "quickactions.enabled"; + +add_setup(async function () { + let engine = await addTestTailSuggestionsEngine(defaultRichSuggestionsFn); + + // Install the test engine. + let oldDefaultEngine = await Services.search.getDefault(); + registerCleanupFunction(async () => { + Services.search.setDefault( + oldDefaultEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + Services.prefs.clearUserPref(RICH_SUGGESTIONS_PREF); + Services.prefs.clearUserPref(SUGGEST_ENABLED_PREF); + UrlbarPrefs.clear(QUICKACTIONS_URLBAR_PREF); + }); + Services.search.setDefault(engine, Ci.nsISearchService.CHANGE_REASON_UNKNOWN); + Services.prefs.setBoolPref(RICH_SUGGESTIONS_PREF, true); + Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true); + UrlbarPrefs.set(QUICKACTIONS_URLBAR_PREF, false); +}); + +/** + * Tests that non-tail suggestion providers still return results correctly when + * the tailSuggestions pref is enabled. + */ +add_task(async function test_richsuggestions_order() { + const query = "what time is it in t"; + let context = createContext(query, { isPrivate: false }); + + let defaultRichResult = { + engineName: TAIL_SUGGESTIONS_ENGINE_NAME, + isRichSuggestion: true, + }; + + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: TAIL_SUGGESTIONS_ENGINE_NAME, + heuristic: true, + }), + makeSearchResult( + context, + Object.assign(defaultRichResult, { + suggestion: query + "oronto", + }) + ), + makeSearchResult(context, { + engineName: TAIL_SUGGESTIONS_ENGINE_NAME, + suggestion: query + "unisia", + }), + makeSearchResult( + context, + Object.assign(defaultRichResult, { + suggestion: query + "acoma", + }) + ), + makeSearchResult(context, { + engineName: TAIL_SUGGESTIONS_ENGINE_NAME, + suggestion: query + "aipei", + }), + ], + }); +}); 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..6c415c1283 --- /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_setup(async function () { + 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..dc7185149f --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_search_suggestions.js @@ -0,0 +1,2077 @@ +/* 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(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_setup(async function () { + 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..a21317428f --- /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_setup(async function () { + 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..c7e6905ff5 --- /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_setup(async function () { + 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..7d9cc8fef0 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_suggestedIndex.js @@ -0,0 +1,599 @@ +/* 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() { + 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]), + }, + { + desc: "{ suggestedIndex: 0, maxRichResults: 0 }", + maxRichResults: 0, + suggestedIndexes: [0], + expected: [], + }, + { + desc: "{ suggestedIndex: 1, maxRichResults: 0 }", + maxRichResults: 0, + suggestedIndexes: [1], + expected: [], + }, + { + desc: "{ suggestedIndex: -1, maxRichResults: 0 }", + maxRichResults: 0, + suggestedIndexes: [-1], + expected: [], + }, + { + desc: "{ suggestedIndex: 0, maxRichResults: 1 }", + maxRichResults: 1, + suggestedIndexes: [0], + expected: indexes([10, 1]), + }, + { + desc: "{ suggestedIndex: 1, maxRichResults: 1 }", + maxRichResults: 1, + suggestedIndexes: [1], + expected: indexes([10, 1]), + }, + { + desc: "{ suggestedIndex: -1, maxRichResults: 1 }", + maxRichResults: 1, + suggestedIndexes: [-1], + expected: indexes([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. + * @param {number} [options.maxRichResults] + * The `maxRichResults` pref will be set to this value. + */ +async function doSuggestedIndexTest({ + suggestedIndexes, + expected, + spansByIndex = {}, + resultCount = MAX_RESULTS, + maxRichResults = 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. + UrlbarPrefs.set("maxRichResults", maxRichResults); + 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..b69c17f50b --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_suggestedIndexRelativeToGroup.js @@ -0,0 +1,645 @@ +/* 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; + +// 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_setup(async () => { + sandbox = lazy.sinon.createSandbox(); + registerCleanupFunction(() => { + sandbox.restore(); + }); +}); + +add_task(async function test() { + // 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. + // * {number} [maxRichResults] + // The `maxRichResults` pref will be set to this value. If not specified + // `MAX_RESULTS` 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, + }, + ], + }, + + { + desc: "Last result in REMOTE_SUGGESTION, maxRichResults too small to add any REMOTE_SUGGESTION", + maxRichResults: 2, + suggestedIndexResults: [ + { + suggestedIndex: -1, + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + }, + ], + expected: [ + { + group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY, + count: 1, + }, + { + group: UrlbarUtils.RESULT_GROUP.GENERAL, + count: 1, + }, + // The suggestedIndex result should not be added. + ], + }, + + { + desc: "Last result in REMOTE_SUGGESTION, maxRichResults just big enough to show one REMOTE_SUGGESTION", + maxRichResults: 3, + suggestedIndexResults: [ + { + suggestedIndex: -1, + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + }, + ], + expected: [ + { + group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY, + count: 1, + }, + { + group: UrlbarUtils.RESULT_GROUP.GENERAL, + count: 1, + }, + { + group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION, + suggestedIndex: -1, + }, + ], + }, + ]; + + let controller = UrlbarTestUtils.newMockController(); + + for (let { + desc, + suggestedIndexResults, + expected, + resultGroups, + otherResults, + maxRichResults = MAX_RESULTS, + } of tests) { + info(`Running test: ${desc}`); + + setResultGroups(resultGroups || RESULT_GROUPS); + + UrlbarPrefs.set("maxRichResults", maxRichResults); + + // 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..640a629911 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_tab_matches.js @@ -0,0 +1,366 @@ +/* -*- 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", + }), + ], + }); + + // This covers the following 3 tests. Container tests are in a dedicated + // test file anyway, so these are left to cover the disabled pref case. + UrlbarPrefs.set("switchTabs.searchAllContainers", false); + + 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", + userContextId: 3, + }), + ], + }); + + 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", + }), + ], + }); + + UrlbarPrefs.clear("switchTabs.searchAllContainers"); + if (UrlbarPrefs.get("switchTabs.searchAllContainers")) { + // This would confuse the next tests, so remove it, containers are tested + // in a separate test file. + await removeOpenPages(uri5, 1, /* userContextId: */ 3); + } + + 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..bf90f69d9f --- /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_setup(async function () { + 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: UrlbarTestUtils.trimURL("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: UrlbarTestUtils.trimURL("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: UrlbarTestUtils.trimURL("http://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..ab9ea9bca4 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_unitConversion.js @@ -0,0 +1,503 @@ +/* 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}`); + + if (timezone) { + 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 (timezone) { + // Reset timezone to default + Cu.getJSTestingFunctions().setTimeZone(undefined); + } + } + } +}); + +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..7d94fd4379 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_word_boundary_search.js @@ -0,0 +1,401 @@ +/* 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", 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"); + }); + + 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.toml b/browser/components/urlbar/tests/unit/xpcshell.toml new file mode 100644 index 0000000000..188f4390c7 --- /dev/null +++ b/browser/components/urlbar/tests/unit/xpcshell.toml @@ -0,0 +1,201 @@ +[DEFAULT] +skip-if = ["os == 'android'"] # bug 1730213 +head = "head.js" +firefox-appdir = "browser" +support-files = ["data/engine.xml"] + +["test_000_frecency.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_copySnakeKeysToCamel.js"] + +["test_UrlbarUtils_getShortcutOrURIAndPostData.js"] + +["test_UrlbarUtils_getTokenMatches.js"] + +["test_UrlbarUtils_skippableTimer.js"] + +["test_UrlbarUtils_unEscapeURIForUI.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_originsAndQueries.js"] + +["test_autofill_origins_alt_frecency.js"] +prefs = [ + "places.frecency.origins.alternative.featureGate=true", + "browser.urlbar.suggest.quickactions=false", +] + +["test_autofill_prefix_fallback.js"] + +["test_autofill_search_engine_aliases.js"] + +["test_autofill_urls.js"] + +["test_avoid_stripping_to_empty_tokens.js"] + +["test_calculator.js"] + +["test_casing.js"] + +["test_dedupe_embedded_url_param.js"] + +["test_dedupe_prefix.js"] + +["test_dedupe_switchTab.js"] + +["test_dont_autofill_cases.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_history_bookmark_results_on_search_service_failure.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_pages_alt_frecency.js"] +prefs = [ + "places.frecency.pages.alternative.featureGate=true", + "browser.urlbar.suggest.quickactions=false", +] + +["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_providerRecentSearches.js"] + +["test_providerTabToSearch.js"] + +["test_providerTabToSearch_partialHost.js"] + +["test_providersManager.js"] + +["test_providersManager_filtering.js"] + +["test_providersManager_maxResults.js"] + +["test_queryScorer.js"] + +["test_query_url.js"] + +["test_quickactions.js"] + +["test_remote_tabs.js"] +skip-if = ["!sync"] + +["test_resultGroups.js"] + +["test_richsuggestions.js"] + +["test_richsuggestions_order.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_word_boundary_search.js"] -- cgit v1.2.3