summaryrefslogtreecommitdiffstats
path: root/browser/components/urlbar/tests
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/urlbar/tests')
-rw-r--r--browser/components/urlbar/tests/UrlbarTestUtils.sys.mjs1432
-rw-r--r--browser/components/urlbar/tests/browser-tips/README.txt7
-rw-r--r--browser/components/urlbar/tests/browser-tips/browser.ini26
-rw-r--r--browser/components/urlbar/tests/browser-tips/browser_interventions.js271
-rw-r--r--browser/components/urlbar/tests/browser-tips/browser_picks.js223
-rw-r--r--browser/components/urlbar/tests/browser-tips/browser_searchTips.js657
-rw-r--r--browser/components/urlbar/tests/browser-tips/browser_searchTips_interaction.js837
-rw-r--r--browser/components/urlbar/tests/browser-tips/browser_selection.js269
-rw-r--r--browser/components/urlbar/tests/browser-tips/browser_updateAsk.js74
-rw-r--r--browser/components/urlbar/tests/browser-tips/browser_updateRefresh.js54
-rw-r--r--browser/components/urlbar/tests/browser-tips/browser_updateRestart.js48
-rw-r--r--browser/components/urlbar/tests/browser-tips/browser_updateWeb.js52
-rw-r--r--browser/components/urlbar/tests/browser-tips/head.js771
-rw-r--r--browser/components/urlbar/tests/browser-tips/slow-page.html7
-rw-r--r--browser/components/urlbar/tests/browser-updateResults/browser.ini17
-rw-r--r--browser/components/urlbar/tests/browser-updateResults/browser_appendSpanCount.js183
-rw-r--r--browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_10_search_10_url.js1102
-rw-r--r--browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_10_search_5_url.js661
-rw-r--r--browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_10_url_10_search.js1185
-rw-r--r--browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_10_url_5_search.js707
-rw-r--r--browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_5_search_10_url.js1015
-rw-r--r--browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_5_search_5_url.js1131
-rw-r--r--browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_5_url_10_search.js1057
-rw-r--r--browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_5_url_5_search.js1203
-rw-r--r--browser/components/urlbar/tests/browser-updateResults/head.js550
-rw-r--r--browser/components/urlbar/tests/browser/POSTSearchEngine.xml6
-rw-r--r--browser/components/urlbar/tests/browser/add_search_engine_0.xml7
-rw-r--r--browser/components/urlbar/tests/browser/add_search_engine_1.xml7
-rw-r--r--browser/components/urlbar/tests/browser/add_search_engine_2.xml7
-rw-r--r--browser/components/urlbar/tests/browser/add_search_engine_3.xml7
-rw-r--r--browser/components/urlbar/tests/browser/add_search_engine_invalid.html11
-rw-r--r--browser/components/urlbar/tests/browser/add_search_engine_many.html24
-rw-r--r--browser/components/urlbar/tests/browser/add_search_engine_one.html12
-rw-r--r--browser/components/urlbar/tests/browser/add_search_engine_same_names.html15
-rw-r--r--browser/components/urlbar/tests/browser/add_search_engine_two.html16
-rw-r--r--browser/components/urlbar/tests/browser/authenticate.sjs218
-rw-r--r--browser/components/urlbar/tests/browser/browser.ini434
-rw-r--r--browser/components/urlbar/tests/browser/browser_UrlbarInput_formatValue.js178
-rw-r--r--browser/components/urlbar/tests/browser/browser_UrlbarInput_formatValue_detachedTab.js76
-rw-r--r--browser/components/urlbar/tests/browser/browser_UrlbarInput_hiddenFocus.js21
-rw-r--r--browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow.js156
-rw-r--r--browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow_resize.js58
-rw-r--r--browser/components/urlbar/tests/browser/browser_UrlbarInput_privateFeature.js74
-rw-r--r--browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms.js306
-rw-r--r--browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_backgroundTabs.js63
-rw-r--r--browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_modifiedUrl.js100
-rw-r--r--browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_moveTab.js133
-rw-r--r--browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_popup.js145
-rw-r--r--browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_revert.js170
-rw-r--r--browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_searchBar.js104
-rw-r--r--browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_searchMode.js81
-rw-r--r--browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_switch_tab.js139
-rw-r--r--browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_telemetry.js378
-rw-r--r--browser/components/urlbar/tests/browser/browser_UrlbarInput_setURI.js128
-rw-r--r--browser/components/urlbar/tests/browser/browser_UrlbarInput_tooltip.js83
-rw-r--r--browser/components/urlbar/tests/browser/browser_UrlbarInput_trimURLs.js127
-rw-r--r--browser/components/urlbar/tests/browser/browser_aboutHomeLoading.js196
-rw-r--r--browser/components/urlbar/tests/browser/browser_acknowledgeFeedbackAndDismissal.js304
-rw-r--r--browser/components/urlbar/tests/browser/browser_action_searchengine.js125
-rw-r--r--browser/components/urlbar/tests/browser/browser_action_searchengine_alias.js63
-rw-r--r--browser/components/urlbar/tests/browser/browser_add_search_engine.js325
-rw-r--r--browser/components/urlbar/tests/browser/browser_autoFill_backspaced.js268
-rw-r--r--browser/components/urlbar/tests/browser/browser_autoFill_canonize.js62
-rw-r--r--browser/components/urlbar/tests/browser/browser_autoFill_caretNotAtEnd.js34
-rw-r--r--browser/components/urlbar/tests/browser/browser_autoFill_firstResult.js200
-rw-r--r--browser/components/urlbar/tests/browser/browser_autoFill_paste.js38
-rw-r--r--browser/components/urlbar/tests/browser/browser_autoFill_placeholder.js1017
-rw-r--r--browser/components/urlbar/tests/browser/browser_autoFill_preserve.js257
-rw-r--r--browser/components/urlbar/tests/browser/browser_autoFill_trimURLs.js183
-rw-r--r--browser/components/urlbar/tests/browser/browser_autoFill_typed.js172
-rw-r--r--browser/components/urlbar/tests/browser/browser_autoFill_undo.js50
-rw-r--r--browser/components/urlbar/tests/browser/browser_autoOpen.js93
-rw-r--r--browser/components/urlbar/tests/browser/browser_autocomplete_a11y_label.js185
-rw-r--r--browser/components/urlbar/tests/browser/browser_autocomplete_autoselect.js122
-rw-r--r--browser/components/urlbar/tests/browser/browser_autocomplete_cursor.js37
-rw-r--r--browser/components/urlbar/tests/browser/browser_autocomplete_edit_completed.js75
-rw-r--r--browser/components/urlbar/tests/browser/browser_autocomplete_enter_race.js193
-rw-r--r--browser/components/urlbar/tests/browser/browser_autocomplete_no_title.js34
-rw-r--r--browser/components/urlbar/tests/browser/browser_autocomplete_readline_navigation.js71
-rw-r--r--browser/components/urlbar/tests/browser/browser_autocomplete_tag_star_visibility.js167
-rw-r--r--browser/components/urlbar/tests/browser/browser_bestMatch.js229
-rw-r--r--browser/components/urlbar/tests/browser/browser_blanking.js54
-rw-r--r--browser/components/urlbar/tests/browser/browser_bufferer_onQueryResults.js82
-rw-r--r--browser/components/urlbar/tests/browser/browser_calculator.js33
-rw-r--r--browser/components/urlbar/tests/browser/browser_canonizeURL.js277
-rw-r--r--browser/components/urlbar/tests/browser/browser_caret_position.js359
-rw-r--r--browser/components/urlbar/tests/browser/browser_click_row_border.js36
-rw-r--r--browser/components/urlbar/tests/browser/browser_closePanelOnClick.js34
-rw-r--r--browser/components/urlbar/tests/browser/browser_content_opener.js23
-rw-r--r--browser/components/urlbar/tests/browser/browser_contextualsearch.js119
-rw-r--r--browser/components/urlbar/tests/browser/browser_copy_during_load.js51
-rw-r--r--browser/components/urlbar/tests/browser/browser_copying.js416
-rw-r--r--browser/components/urlbar/tests/browser/browser_customizeMode.js73
-rw-r--r--browser/components/urlbar/tests/browser/browser_cutting.js17
-rw-r--r--browser/components/urlbar/tests/browser/browser_decode.js144
-rw-r--r--browser/components/urlbar/tests/browser/browser_delete.js51
-rw-r--r--browser/components/urlbar/tests/browser/browser_deleteAllText.js100
-rw-r--r--browser/components/urlbar/tests/browser/browser_display_selectedAction_Extensions.js57
-rw-r--r--browser/components/urlbar/tests/browser/browser_dns_first_for_single_words.js52
-rw-r--r--browser/components/urlbar/tests/browser/browser_downArrowKeySearch.js83
-rw-r--r--browser/components/urlbar/tests/browser/browser_dragdropURL.js106
-rw-r--r--browser/components/urlbar/tests/browser/browser_dynamicResults.js799
-rw-r--r--browser/components/urlbar/tests/browser/browser_edit_invalid_url.js91
-rw-r--r--browser/components/urlbar/tests/browser/browser_engagement.js206
-rw-r--r--browser/components/urlbar/tests/browser/browser_enter.js331
-rw-r--r--browser/components/urlbar/tests/browser/browser_enterAfterMouseOver.js97
-rw-r--r--browser/components/urlbar/tests/browser/browser_focusedCmdK.js15
-rw-r--r--browser/components/urlbar/tests/browser/browser_groupLabels.js629
-rw-r--r--browser/components/urlbar/tests/browser/browser_handleCommand_fallback.js142
-rw-r--r--browser/components/urlbar/tests/browser/browser_hashChangeProxyState.js151
-rw-r--r--browser/components/urlbar/tests/browser/browser_helpUrl.js428
-rw-r--r--browser/components/urlbar/tests/browser/browser_heuristicNotAddedFirst.js159
-rw-r--r--browser/components/urlbar/tests/browser/browser_hideHeuristic.js513
-rw-r--r--browser/components/urlbar/tests/browser/browser_ime_composition.js327
-rw-r--r--browser/components/urlbar/tests/browser/browser_inputHistory.js548
-rw-r--r--browser/components/urlbar/tests/browser/browser_inputHistory_autofill.js207
-rw-r--r--browser/components/urlbar/tests/browser/browser_inputHistory_emptystring.js94
-rw-r--r--browser/components/urlbar/tests/browser/browser_keepStateAcrossTabSwitches.js224
-rw-r--r--browser/components/urlbar/tests/browser/browser_keyword.js238
-rw-r--r--browser/components/urlbar/tests/browser/browser_keywordBookmarklets.js133
-rw-r--r--browser/components/urlbar/tests/browser/browser_keywordSearch.js57
-rw-r--r--browser/components/urlbar/tests/browser/browser_keywordSearch_postData.js74
-rw-r--r--browser/components/urlbar/tests/browser/browser_keyword_override.js61
-rw-r--r--browser/components/urlbar/tests/browser/browser_keyword_select_and_type.js97
-rw-r--r--browser/components/urlbar/tests/browser/browser_loadRace.js90
-rw-r--r--browser/components/urlbar/tests/browser/browser_locationBarCommand.js291
-rw-r--r--browser/components/urlbar/tests/browser/browser_locationBarExternalLoad.js94
-rw-r--r--browser/components/urlbar/tests/browser/browser_locationchange_urlbar_edit_dos.js67
-rw-r--r--browser/components/urlbar/tests/browser/browser_middleClick.js255
-rw-r--r--browser/components/urlbar/tests/browser/browser_new_tab_urlbar_reset.js39
-rw-r--r--browser/components/urlbar/tests/browser/browser_oneOffs.js980
-rw-r--r--browser/components/urlbar/tests/browser/browser_oneOffs_contextMenu.js80
-rw-r--r--browser/components/urlbar/tests/browser/browser_oneOffs_heuristicRestyle.js516
-rw-r--r--browser/components/urlbar/tests/browser/browser_oneOffs_keyModifiers.js392
-rw-r--r--browser/components/urlbar/tests/browser/browser_oneOffs_searchSuggestions.js358
-rw-r--r--browser/components/urlbar/tests/browser/browser_oneOffs_settings.js89
-rw-r--r--browser/components/urlbar/tests/browser/browser_pasteAndGo.js80
-rw-r--r--browser/components/urlbar/tests/browser/browser_paste_multi_lines.js239
-rw-r--r--browser/components/urlbar/tests/browser/browser_paste_then_focus.js60
-rw-r--r--browser/components/urlbar/tests/browser/browser_paste_then_switch_tab.js73
-rw-r--r--browser/components/urlbar/tests/browser/browser_percent_encoded.js59
-rw-r--r--browser/components/urlbar/tests/browser/browser_placeholder.js412
-rw-r--r--browser/components/urlbar/tests/browser/browser_populateAfterPushState.js32
-rw-r--r--browser/components/urlbar/tests/browser/browser_primary_selection_safe_on_new_tab.js73
-rw-r--r--browser/components/urlbar/tests/browser/browser_privateBrowsingWindowChange.js51
-rw-r--r--browser/components/urlbar/tests/browser/browser_queryContextCache.js482
-rw-r--r--browser/components/urlbar/tests/browser/browser_quickactions.js783
-rw-r--r--browser/components/urlbar/tests/browser/browser_quickactions_devtools.js176
-rw-r--r--browser/components/urlbar/tests/browser/browser_quickactions_tab_refocus.js194
-rw-r--r--browser/components/urlbar/tests/browser/browser_raceWithTabs.js86
-rw-r--r--browser/components/urlbar/tests/browser/browser_redirect_error.js137
-rw-r--r--browser/components/urlbar/tests/browser/browser_remoteness_switch.js56
-rw-r--r--browser/components/urlbar/tests/browser/browser_remotetab.js111
-rw-r--r--browser/components/urlbar/tests/browser/browser_removeUnsafeProtocolsFromURLBarPaste.js95
-rw-r--r--browser/components/urlbar/tests/browser/browser_remove_match.js297
-rw-r--r--browser/components/urlbar/tests/browser/browser_restoreEmptyInput.js64
-rw-r--r--browser/components/urlbar/tests/browser/browser_resultSpan.js254
-rw-r--r--browser/components/urlbar/tests/browser/browser_result_menu.js266
-rw-r--r--browser/components/urlbar/tests/browser/browser_result_onSelection.js67
-rw-r--r--browser/components/urlbar/tests/browser/browser_retainedResultsOnFocus.js435
-rw-r--r--browser/components/urlbar/tests/browser/browser_revert.js33
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchFunction.js278
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchHistoryLimit.js87
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchMode_alias_replacement.js274
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchMode_autofill.js133
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchMode_clickLink.js94
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchMode_engineRemoval.js109
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchMode_excludeResults.js217
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchMode_heuristic.js221
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchMode_indicator.js377
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchMode_indicator_clickthrough.js100
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchMode_localOneOffs_actionText.js459
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchMode_newWindow.js40
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchMode_no_results.js290
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchMode_oneOffButton.js108
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchMode_pickResult.js89
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchMode_preview.js489
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchMode_sessionStore.js332
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchMode_setURI.js119
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchMode_suggestions.js579
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchMode_switchTabs.js317
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchSettings.js30
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchSingleWordNotification.js372
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchSuggestions.js341
-rw-r--r--browser/components/urlbar/tests/browser/browser_searchTelemetry.js220
-rw-r--r--browser/components/urlbar/tests/browser/browser_search_bookmarks_from_bookmarks_menu.js55
-rw-r--r--browser/components/urlbar/tests/browser/browser_search_history_from_history_panel.js97
-rw-r--r--browser/components/urlbar/tests/browser/browser_selectStaleResults.js311
-rw-r--r--browser/components/urlbar/tests/browser/browser_selectionKeyNavigation.js200
-rw-r--r--browser/components/urlbar/tests/browser/browser_separatePrivateDefault.js223
-rw-r--r--browser/components/urlbar/tests/browser/browser_separatePrivateDefault_differentEngine.js354
-rw-r--r--browser/components/urlbar/tests/browser/browser_shortcuts_add_search_engine.js243
-rw-r--r--browser/components/urlbar/tests/browser/browser_speculative_connect.js199
-rw-r--r--browser/components/urlbar/tests/browser/browser_speculative_connect_not_with_client_cert.js236
-rw-r--r--browser/components/urlbar/tests/browser/browser_stop.js75
-rw-r--r--browser/components/urlbar/tests/browser/browser_stopSearchOnSelection.js113
-rw-r--r--browser/components/urlbar/tests/browser/browser_stop_pending.js459
-rw-r--r--browser/components/urlbar/tests/browser/browser_strip_on_share.js125
-rw-r--r--browser/components/urlbar/tests/browser/browser_suggestedIndex.js120
-rw-r--r--browser/components/urlbar/tests/browser/browser_suppressFocusBorder.js391
-rw-r--r--browser/components/urlbar/tests/browser/browser_switchTab_closesUrlbarPopup.js42
-rw-r--r--browser/components/urlbar/tests/browser/browser_switchTab_currentTab.js41
-rw-r--r--browser/components/urlbar/tests/browser/browser_switchTab_decodeuri.js51
-rw-r--r--browser/components/urlbar/tests/browser/browser_switchTab_inputHistory.js91
-rw-r--r--browser/components/urlbar/tests/browser/browser_switchTab_override.js100
-rw-r--r--browser/components/urlbar/tests/browser/browser_switchToTabHavingURI_aOpenParams.js217
-rw-r--r--browser/components/urlbar/tests/browser/browser_switchToTab_chiclet.js110
-rw-r--r--browser/components/urlbar/tests/browser/browser_switchToTab_closes_newtab.js63
-rw-r--r--browser/components/urlbar/tests/browser/browser_switchToTab_fullUrl_repeatedKeydown.js60
-rw-r--r--browser/components/urlbar/tests/browser/browser_tabKeyBehavior.js378
-rw-r--r--browser/components/urlbar/tests/browser/browser_tabMatchesInAwesomebar.js224
-rw-r--r--browser/components/urlbar/tests/browser/browser_tabMatchesInAwesomebar_perwindowpb.js120
-rw-r--r--browser/components/urlbar/tests/browser/browser_tabToSearch.js641
-rw-r--r--browser/components/urlbar/tests/browser/browser_textruns.js55
-rw-r--r--browser/components/urlbar/tests/browser/browser_tokenAlias.js861
-rw-r--r--browser/components/urlbar/tests/browser/browser_top_sites.js481
-rw-r--r--browser/components/urlbar/tests/browser/browser_top_sites_private.js174
-rw-r--r--browser/components/urlbar/tests/browser/browser_typed_value.js69
-rw-r--r--browser/components/urlbar/tests/browser/browser_unitConversion.js88
-rw-r--r--browser/components/urlbar/tests/browser/browser_updateForDomainCompletion.js51
-rw-r--r--browser/components/urlbar/tests/browser/browser_urlbar_annotation.js333
-rw-r--r--browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry_abandonment.js357
-rw-r--r--browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry_engagement.js1340
-rw-r--r--browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry_noEvent.js81
-rw-r--r--browser/components/urlbar/tests/browser/browser_urlbar_selection.js307
-rw-r--r--browser/components/urlbar/tests/browser/browser_urlbar_telemetry.js1218
-rw-r--r--browser/components/urlbar/tests/browser/browser_urlbar_telemetry_autofill.js733
-rw-r--r--browser/components/urlbar/tests/browser/browser_urlbar_telemetry_dynamic.js136
-rw-r--r--browser/components/urlbar/tests/browser/browser_urlbar_telemetry_extension.js155
-rw-r--r--browser/components/urlbar/tests/browser/browser_urlbar_telemetry_handoff.js182
-rw-r--r--browser/components/urlbar/tests/browser/browser_urlbar_telemetry_persisted.js270
-rw-r--r--browser/components/urlbar/tests/browser/browser_urlbar_telemetry_places.js270
-rw-r--r--browser/components/urlbar/tests/browser/browser_urlbar_telemetry_quickactions.js133
-rw-r--r--browser/components/urlbar/tests/browser/browser_urlbar_telemetry_remotetab.js185
-rw-r--r--browser/components/urlbar/tests/browser/browser_urlbar_telemetry_searchmode.js592
-rw-r--r--browser/components/urlbar/tests/browser/browser_urlbar_telemetry_sponsored_topsites.js181
-rw-r--r--browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tabtosearch.js416
-rw-r--r--browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tip.js130
-rw-r--r--browser/components/urlbar/tests/browser/browser_urlbar_telemetry_topsite.js136
-rw-r--r--browser/components/urlbar/tests/browser/browser_urlbar_telemetry_zeroPrefix.js266
-rw-r--r--browser/components/urlbar/tests/browser/browser_userTypedValue.js46
-rw-r--r--browser/components/urlbar/tests/browser/browser_valueOnTabSwitch.js166
-rw-r--r--browser/components/urlbar/tests/browser/browser_view_emptyResultSet.js40
-rw-r--r--browser/components/urlbar/tests/browser/browser_view_resultDisplay.js354
-rw-r--r--browser/components/urlbar/tests/browser/browser_view_resultTypes_display.js317
-rw-r--r--browser/components/urlbar/tests/browser/browser_view_selectionByMouse.js607
-rw-r--r--browser/components/urlbar/tests/browser/browser_waitForLoadOrTimeout.js37
-rw-r--r--browser/components/urlbar/tests/browser/browser_whereToOpen.js192
-rw-r--r--browser/components/urlbar/tests/browser/dummy_page.html9
-rw-r--r--browser/components/urlbar/tests/browser/dynamicResult0.css50
-rw-r--r--browser/components/urlbar/tests/browser/dynamicResult1.css50
-rw-r--r--browser/components/urlbar/tests/browser/file_blank_but_not_blank.html2
-rw-r--r--browser/components/urlbar/tests/browser/file_urlbar_edit_dos.html18
-rw-r--r--browser/components/urlbar/tests/browser/file_userTypedValue.html1
-rw-r--r--browser/components/urlbar/tests/browser/head-common.js156
-rw-r--r--browser/components/urlbar/tests/browser/head.js125
-rw-r--r--browser/components/urlbar/tests/browser/moz.pngbin0 -> 580 bytes
-rw-r--r--browser/components/urlbar/tests/browser/print_postdata.sjs25
-rw-r--r--browser/components/urlbar/tests/browser/redirect_error.sjs16
-rw-r--r--browser/components/urlbar/tests/browser/redirect_to.sjs9
-rw-r--r--browser/components/urlbar/tests/browser/searchSuggestionEngine.sjs57
-rw-r--r--browser/components/urlbar/tests/browser/searchSuggestionEngine.xml11
-rw-r--r--browser/components/urlbar/tests/browser/searchSuggestionEngine2.xml13
-rw-r--r--browser/components/urlbar/tests/browser/searchSuggestionEngineMany.xml11
-rw-r--r--browser/components/urlbar/tests/browser/searchSuggestionEngineSlow.xml11
-rw-r--r--browser/components/urlbar/tests/browser/slow-page.sjs23
-rw-r--r--browser/components/urlbar/tests/browser/urlbarTelemetrySearchSuggestions.sjs9
-rw-r--r--browser/components/urlbar/tests/browser/urlbarTelemetrySearchSuggestions.xml6
-rw-r--r--browser/components/urlbar/tests/browser/urlbarTelemetryUrlbarDynamic.css45
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser.ini52
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_groups.js202
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_interaction.js58
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_interaction_persisted_search_terms_disabled.js48
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_interaction_persisted_search_terms_enabled.js53
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_n_chars_n_words.js36
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_sap.js39
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_search_mode.js54
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_tips.js88
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_edge_cases.js218
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_groups.js259
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_interaction.js87
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_interaction_persisted_search_terms_disabled.js61
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_interaction_persisted_search_terms_enabled.js60
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_n_chars_n_words.js36
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_sap.js33
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_search_mode.js63
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_selected_result.js920
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_tips.js175
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_type.js151
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_exposure.js107
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_groups.js223
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_interaction.js63
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_interaction_persisted_search_terms_disabled.js57
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_interaction_persisted_search_terms_enabled.js61
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_n_chars_n_words.js40
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_preferences.js41
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_sap.js38
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_search_mode.js72
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_timing.js91
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_record_preferences.js54
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/head-exposure.js47
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/head-groups.js294
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/head-interaction.js238
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/head-n_chars_n_words.js56
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/head-sap.js66
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/head-search_mode.js93
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/head.js458
-rw-r--r--browser/components/urlbar/tests/ext/api.js260
-rw-r--r--browser/components/urlbar/tests/ext/browser/.eslintrc.js7
-rw-r--r--browser/components/urlbar/tests/ext/browser/browser.ini18
-rw-r--r--browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_attributionURL.js16
-rw-r--r--browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_clearInput.js31
-rw-r--r--browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_dynamicResult.js137
-rw-r--r--browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_engagementTelemetry.js18
-rw-r--r--browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_extensionTimeout.js16
-rw-r--r--browser/components/urlbar/tests/ext/browser/dynamicResult.css36
-rw-r--r--browser/components/urlbar/tests/ext/browser/head.js253
-rw-r--r--browser/components/urlbar/tests/ext/schema.json113
-rw-r--r--browser/components/urlbar/tests/quicksuggest/MerinoTestUtils.sys.mjs765
-rw-r--r--browser/components/urlbar/tests/quicksuggest/QuickSuggestTestUtils.sys.mjs1017
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser.ini37
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest.js88
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_addons.js560
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_block.js445
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_configuration.js2101
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_indexes.js425
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_merinoSessions.js142
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_onboardingDialog.js1596
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_dynamicWikipedia.js114
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_impressionEdgeCases.js477
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_navigationalSuggestions.js353
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_nonsponsored.js368
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_other.js409
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_sponsored.js367
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_weather.js152
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_weather.js379
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/head.js569
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/searchSuggestionEngine.sjs57
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/searchSuggestionEngine.xml11
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/subdialog.xhtml14
-rw-r--r--browser/components/urlbar/tests/quicksuggest/unit/head.js227
-rw-r--r--browser/components/urlbar/tests/quicksuggest/unit/test_merinoClient.js647
-rw-r--r--browser/components/urlbar/tests/quicksuggest/unit/test_merinoClient_sessions.js402
-rw-r--r--browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest.js1341
-rw-r--r--browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_addons.js728
-rw-r--r--browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_bestMatch.js463
-rw-r--r--browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_dynamicWikipedia.js95
-rw-r--r--browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_impressionCaps.js3888
-rw-r--r--browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_merino.js681
-rw-r--r--browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_merinoSessions.js174
-rw-r--r--browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_migrate_v1.js490
-rw-r--r--browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_migrate_v2.js1355
-rw-r--r--browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_nonUniqueKeywords.js282
-rw-r--r--browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_offlineDefault.js127
-rw-r--r--browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_positionInSuggestions.js487
-rw-r--r--browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_topPicks.js284
-rw-r--r--browser/components/urlbar/tests/quicksuggest/unit/test_suggestionsMap.js217
-rw-r--r--browser/components/urlbar/tests/quicksuggest/unit/test_weather.js1394
-rw-r--r--browser/components/urlbar/tests/quicksuggest/unit/test_weather_keywords.js1395
-rw-r--r--browser/components/urlbar/tests/quicksuggest/unit/xpcshell.ini23
-rw-r--r--browser/components/urlbar/tests/unit/data/engine.xml10
-rw-r--r--browser/components/urlbar/tests/unit/head.js1127
-rw-r--r--browser/components/urlbar/tests/unit/test_000_frecency.js245
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarController_integration.js83
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarController_telemetry.js256
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarController_unit.js389
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarPrefs.js449
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarQueryContext.js73
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarQueryContext_restrictSource.js132
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarSearchUtils.js462
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarUtils_addToUrlbarHistory.js63
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarUtils_getShortcutOrURIAndPostData.js249
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarUtils_getTokenMatches.js294
-rw-r--r--browser/components/urlbar/tests/unit/test_UrlbarUtils_unEscapeURIForUI.js36
-rw-r--r--browser/components/urlbar/tests/unit/test_about_urls.js176
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_adaptiveHistory.js1441
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_bookmarked.js150
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_do_not_trim.js140
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_functional.js115
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_origins.js1041
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_originsAndQueries.js2471
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_origins_alt_frecency.js243
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_prefix_fallback.js76
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_search_engine_aliases.js85
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_search_engines.js246
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_urls.js881
-rw-r--r--browser/components/urlbar/tests/unit/test_avoid_middle_complete.js270
-rw-r--r--browser/components/urlbar/tests/unit/test_avoid_stripping_to_empty_tokens.js119
-rw-r--r--browser/components/urlbar/tests/unit/test_calculator.js46
-rw-r--r--browser/components/urlbar/tests/unit/test_casing.js370
-rw-r--r--browser/components/urlbar/tests/unit/test_dedupe_prefix.js277
-rw-r--r--browser/components/urlbar/tests/unit/test_dedupe_switchTab.js34
-rw-r--r--browser/components/urlbar/tests/unit/test_download_embed_bookmarks.js137
-rw-r--r--browser/components/urlbar/tests/unit/test_empty_search.js181
-rw-r--r--browser/components/urlbar/tests/unit/test_encoded_urls.js97
-rw-r--r--browser/components/urlbar/tests/unit/test_escaping_badEscapedURI.js37
-rw-r--r--browser/components/urlbar/tests/unit/test_escaping_escapeSelf.js62
-rw-r--r--browser/components/urlbar/tests/unit/test_exposure.js195
-rw-r--r--browser/components/urlbar/tests/unit/test_frecency.js403
-rw-r--r--browser/components/urlbar/tests/unit/test_frecency_alternative_nimbus.js50
-rw-r--r--browser/components/urlbar/tests/unit/test_heuristic_cancel.js238
-rw-r--r--browser/components/urlbar/tests/unit/test_hideSponsoredHistory.js104
-rw-r--r--browser/components/urlbar/tests/unit/test_keywords.js212
-rw-r--r--browser/components/urlbar/tests/unit/test_l10nCache.js685
-rw-r--r--browser/components/urlbar/tests/unit/test_local_suggest_prefs.js126
-rw-r--r--browser/components/urlbar/tests/unit/test_match_javascript.js155
-rw-r--r--browser/components/urlbar/tests/unit/test_multi_word_search.js126
-rw-r--r--browser/components/urlbar/tests/unit/test_muxer.js731
-rw-r--r--browser/components/urlbar/tests/unit/test_protocol_ignore.js42
-rw-r--r--browser/components/urlbar/tests/unit/test_protocol_swap.js303
-rw-r--r--browser/components/urlbar/tests/unit/test_providerAliasEngines.js114
-rw-r--r--browser/components/urlbar/tests/unit/test_providerHeuristicFallback.js691
-rw-r--r--browser/components/urlbar/tests/unit/test_providerHistoryUrlHeuristic.js197
-rw-r--r--browser/components/urlbar/tests/unit/test_providerKeywords.js360
-rw-r--r--browser/components/urlbar/tests/unit/test_providerOmnibox.js887
-rw-r--r--browser/components/urlbar/tests/unit/test_providerOpenTabs.js45
-rw-r--r--browser/components/urlbar/tests/unit/test_providerPlaces.js250
-rw-r--r--browser/components/urlbar/tests/unit/test_providerPlaces_duplicate_entries.js42
-rw-r--r--browser/components/urlbar/tests/unit/test_providerPlaces_nonEnglish.js43
-rw-r--r--browser/components/urlbar/tests/unit/test_providerPreloaded.js578
-rw-r--r--browser/components/urlbar/tests/unit/test_providerTabToSearch.js535
-rw-r--r--browser/components/urlbar/tests/unit/test_providerTabToSearch_partialHost.js214
-rw-r--r--browser/components/urlbar/tests/unit/test_providersManager.js74
-rw-r--r--browser/components/urlbar/tests/unit/test_providersManager_filtering.js407
-rw-r--r--browser/components/urlbar/tests/unit/test_providersManager_maxResults.js37
-rw-r--r--browser/components/urlbar/tests/unit/test_queryScorer.js405
-rw-r--r--browser/components/urlbar/tests/unit/test_query_url.js121
-rw-r--r--browser/components/urlbar/tests/unit/test_quickactions.js126
-rw-r--r--browser/components/urlbar/tests/unit/test_remote_tabs.js695
-rw-r--r--browser/components/urlbar/tests/unit/test_resultGroups.js1576
-rw-r--r--browser/components/urlbar/tests/unit/test_search_engine_host.js98
-rw-r--r--browser/components/urlbar/tests/unit/test_search_engine_restyle.js124
-rw-r--r--browser/components/urlbar/tests/unit/test_search_suggestions.js2078
-rw-r--r--browser/components/urlbar/tests/unit/test_search_suggestions_aliases.js364
-rw-r--r--browser/components/urlbar/tests/unit/test_search_suggestions_tail.js379
-rw-r--r--browser/components/urlbar/tests/unit/test_special_search.js543
-rw-r--r--browser/components/urlbar/tests/unit/test_suggestedIndex.js562
-rw-r--r--browser/components/urlbar/tests/unit/test_suggestedIndexRelativeToGroup.js596
-rw-r--r--browser/components/urlbar/tests/unit/test_tab_matches.js354
-rw-r--r--browser/components/urlbar/tests/unit/test_tags_caseInsensitivity.js137
-rw-r--r--browser/components/urlbar/tests/unit/test_tags_extendedUnicode.js66
-rw-r--r--browser/components/urlbar/tests/unit/test_tags_general.js207
-rw-r--r--browser/components/urlbar/tests/unit/test_tags_matchBookmarkTitles.js42
-rw-r--r--browser/components/urlbar/tests/unit/test_tags_returnedInSearches.js125
-rw-r--r--browser/components/urlbar/tests/unit/test_tokenizer.js449
-rw-r--r--browser/components/urlbar/tests/unit/test_trimming.js171
-rw-r--r--browser/components/urlbar/tests/unit/test_unitConversion.js504
-rw-r--r--browser/components/urlbar/tests/unit/test_word_boundary_search.js403
-rw-r--r--browser/components/urlbar/tests/unit/xpcshell.ini99
449 files changed, 121071 insertions, 0 deletions
diff --git a/browser/components/urlbar/tests/UrlbarTestUtils.sys.mjs b/browser/components/urlbar/tests/UrlbarTestUtils.sys.mjs
new file mode 100644
index 0000000000..399fb7cc11
--- /dev/null
+++ b/browser/components/urlbar/tests/UrlbarTestUtils.sys.mjs
@@ -0,0 +1,1432 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+
+import {
+ UrlbarProvider,
+ UrlbarUtils,
+} from "resource:///modules/UrlbarUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ AddonTestUtils: "resource://testing-common/AddonTestUtils.sys.mjs",
+ BrowserTestUtils: "resource://testing-common/BrowserTestUtils.sys.mjs",
+ ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs",
+ ExperimentFakes: "resource://testing-common/NimbusTestUtils.sys.mjs",
+ ExperimentManager: "resource://nimbus/lib/ExperimentManager.sys.mjs",
+
+ FormHistoryTestUtils:
+ "resource://testing-common/FormHistoryTestUtils.sys.mjs",
+
+ NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
+ TestUtils: "resource://testing-common/TestUtils.sys.mjs",
+ UrlbarController: "resource:///modules/UrlbarController.sys.mjs",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
+ UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.sys.mjs",
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ BrowserUIUtils: "resource:///modules/BrowserUIUtils.jsm",
+ BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
+});
+
+export var UrlbarTestUtils = {
+ /**
+ * This maps the categories used by the FX_URLBAR_SELECTED_RESULT_METHOD and
+ * FX_SEARCHBAR_SELECTED_RESULT_METHOD histograms to their indexes in the
+ * `labels` array. This only needs to be used by tests that need to map from
+ * category names to indexes in histogram snapshots. Actual app code can use
+ * these category names directly when they add to a histogram.
+ */
+ SELECTED_RESULT_METHODS: {
+ enter: 0,
+ enterSelection: 1,
+ click: 2,
+ arrowEnterSelection: 3,
+ tabEnterSelection: 4,
+ rightClickEnter: 5,
+ },
+
+ // Fallback to the console.
+ info: console.log,
+
+ /**
+ * Running this init allows helpers to access test scope helpers, like Assert
+ * and SimpleTest. Note this initialization is not enforced, thus helpers
+ * should always check the properties set here and provide a fallback path.
+ *
+ * @param {object} scope The global scope where tests are being run.
+ */
+ init(scope) {
+ if (!scope) {
+ throw new Error("Must initialize UrlbarTestUtils with a test scope");
+ }
+ // If you add other properties to `this`, null them in uninit().
+ this.Assert = scope.Assert;
+ this.info = scope.info;
+ this.registerCleanupFunction = scope.registerCleanupFunction;
+
+ if (Services.env.exists("XPCSHELL_TEST_PROFILE_DIR")) {
+ this.initXPCShellDependencies();
+ } else {
+ // xpcshell doesn't support EventUtils.
+ this.EventUtils = scope.EventUtils;
+ this.SimpleTest = scope.SimpleTest;
+ }
+
+ this.registerCleanupFunction(() => {
+ this.Assert = null;
+ this.info = console.log;
+ this.registerCleanupFunction = null;
+ this.EventUtils = null;
+ this.SimpleTest = null;
+ });
+ },
+
+ /**
+ * Waits to a search to be complete.
+ *
+ * @param {object} win The window containing the urlbar
+ * @returns {Promise} Resolved when done.
+ */
+ async promiseSearchComplete(win) {
+ let waitForQuery = () => {
+ return this.promisePopupOpen(win, () => {}).then(
+ () => win.gURLBar.lastQueryContextPromise
+ );
+ };
+ let context = await waitForQuery();
+ if (win.gURLBar.searchMode) {
+ // Search mode may start a second query.
+ context = await waitForQuery();
+ }
+ return context;
+ },
+
+ /**
+ * Starts a search for a given string and waits for the search to be complete.
+ *
+ * @param {object} options The options object.
+ * @param {object} options.window The window containing the urlbar
+ * @param {string} options.value the search string
+ * @param {Function} options.waitForFocus The SimpleTest function
+ * @param {boolean} [options.fireInputEvent] whether an input event should be
+ * used when starting the query (simulates the user's typing, sets
+ * userTypedValued, triggers engagement event telemetry, etc.)
+ * @param {number} [options.selectionStart] The input's selectionStart
+ * @param {number} [options.selectionEnd] The input's selectionEnd
+ */
+ async promiseAutocompleteResultPopup({
+ window,
+ value,
+ waitForFocus,
+ fireInputEvent = true,
+ selectionStart = -1,
+ selectionEnd = -1,
+ } = {}) {
+ if (this.SimpleTest) {
+ await this.SimpleTest.promiseFocus(window);
+ } else {
+ await new Promise(resolve => waitForFocus(resolve, window));
+ }
+
+ const setup = () => {
+ window.gURLBar.inputField.focus();
+ // Using the value setter in some cases may trim and fetch unexpected
+ // results, then pick an alternate path.
+ if (
+ lazy.UrlbarPrefs.get("trimURLs") &&
+ value != lazy.BrowserUIUtils.trimURL(value)
+ ) {
+ window.gURLBar.inputField.value = value;
+ fireInputEvent = true;
+ } else {
+ window.gURLBar.value = value;
+ }
+ if (selectionStart >= 0 && selectionEnd >= 0) {
+ window.gURLBar.selectionEnd = selectionEnd;
+ window.gURLBar.selectionStart = selectionStart;
+ }
+
+ // An input event will start a new search, so be careful not to start a
+ // search if we fired an input event since that would start two searches.
+ if (fireInputEvent) {
+ // This is necessary to get the urlbar to set gBrowser.userTypedValue.
+ this.fireInputEvent(window);
+ } else {
+ window.gURLBar.setPageProxyState("invalid");
+ window.gURLBar.startQuery();
+ }
+ };
+ setup();
+
+ // In Linux TV test, as there is case that the input field lost the focus
+ // until showing popup, timeout failure happens since the expected poup
+ // never be shown. To avoid this, if losing the focus, retry setup to open
+ // popup.
+ const blurListener = () => {
+ setup();
+ };
+ window.gURLBar.inputField.addEventListener("blur", blurListener, {
+ once: true,
+ });
+ const result = await this.promiseSearchComplete(window);
+ window.gURLBar.inputField.removeEventListener("blur", blurListener);
+ return result;
+ },
+
+ /**
+ * Waits for a result to be added at a certain index. Since we implement lazy
+ * results replacement, even if we have a result at an index, it may be
+ * related to the previous query, this methods ensures the result is current.
+ *
+ * @param {object} win The window containing the urlbar
+ * @param {number} index The index to look for
+ * @returns {HtmlElement|XulElement} the result's element.
+ */
+ async waitForAutocompleteResultAt(win, index) {
+ // TODO Bug 1530338: Quantum Bar doesn't yet implement lazy results replacement.
+ await this.promiseSearchComplete(win);
+ let container = this.getResultsContainer(win);
+ if (index >= container.children.length) {
+ throw new Error("Not enough results");
+ }
+ return container.children[index];
+ },
+
+ /**
+ * Returns the oneOffSearchButtons object for the urlbar.
+ *
+ * @param {object} win The window containing the urlbar
+ * @returns {object} The oneOffSearchButtons
+ */
+ getOneOffSearchButtons(win) {
+ return win.gURLBar.view.oneOffSearchButtons;
+ },
+
+ /**
+ * Returns a specific button of a result.
+ *
+ * @param {object} win The window containing the urlbar
+ * @param {string} buttonName The name of the button, e.g. "menu", "0", etc.
+ * @param {number} resultIndex The index of the result
+ * @returns {HtmlElement} The button
+ */
+ getButtonForResultIndex(win, buttonName, resultIndex) {
+ return this.getRowAt(win, resultIndex).querySelector(
+ `.urlbarView-button-${buttonName}`
+ );
+ },
+
+ /**
+ * Show the result menu button regardless of the result being hovered or
+ + selected.
+ *
+ * @param {object} win The window containing the urlbar
+ */
+ disableResultMenuAutohide(win) {
+ let container = this.getResultsContainer(win);
+ let attr = "disable-resultmenu-autohide";
+ container.toggleAttribute(attr, true);
+ this.registerCleanupFunction?.(() => {
+ container.toggleAttribute(attr, false);
+ });
+ },
+
+ /**
+ * Opens the result menu of a specific result.
+ *
+ * @param {object} win The window containing the urlbar
+ * @param {object} [options] The options object.
+ * @param {number} [options.resultIndex] The index of the result. Defaults
+ * to the current selected index.
+ * @param {boolean} [options.byMouse] Whether to open the menu by mouse or
+ * keyboard.
+ * @param {string} [options.activationKey] Key to activate the button with,
+ * defaults to KEY_Enter.
+ */
+ async openResultMenu(
+ win,
+ {
+ resultIndex = win.gURLBar.view.selectedRowIndex,
+ byMouse = false,
+ activationKey = "KEY_Enter",
+ } = {}
+ ) {
+ this.Assert?.ok(win.gURLBar.view.isOpen, "view should be open");
+ let menuButton = this.getButtonForResultIndex(win, "menu", resultIndex);
+ this.Assert?.ok(
+ menuButton,
+ `found the menu button at result index ${resultIndex}`
+ );
+ let promiseMenuOpen = lazy.BrowserTestUtils.waitForEvent(
+ win.gURLBar.view.resultMenu,
+ "popupshown"
+ );
+ if (byMouse) {
+ this.info(
+ `synthesizing mousemove on row to make the menu button visible`
+ );
+ await this.EventUtils.promiseElementReadyForUserInput(
+ menuButton.closest(".urlbarView-row"),
+ win,
+ this.info
+ );
+ this.info(`got mousemove, now clicking the menu button`);
+ this.EventUtils.synthesizeMouseAtCenter(menuButton, {}, win);
+ this.info(`waiting for the menu popup to open via mouse`);
+ } else {
+ this.info(`selecting the result at index ${resultIndex}`);
+ while (win.gURLBar.view.selectedRowIndex != resultIndex) {
+ this.EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ }
+ if (this.getSelectedElement(win) != menuButton) {
+ this.EventUtils.synthesizeKey("KEY_Tab", {}, win);
+ }
+ this.Assert?.equal(
+ this.getSelectedElement(win),
+ menuButton,
+ `selected the menu button at result index ${resultIndex}`
+ );
+ this.EventUtils.synthesizeKey(activationKey, {}, win);
+ this.info(`waiting for ${activationKey} to open the menu popup`);
+ }
+ await promiseMenuOpen;
+ this.Assert?.equal(
+ win.gURLBar.view.resultMenu.state,
+ "open",
+ "Checking popup state"
+ );
+ },
+
+ /**
+ * Opens the result menu of a specific result and gets a menu item by either
+ * accesskey or command name. Either `accesskey` or `command` must be given.
+ *
+ * @param {object} options
+ * The options object.
+ * @param {object} options.window
+ * The window containing the urlbar.
+ * @param {string} options.accesskey
+ * The access key of the menu item to return.
+ * @param {string} options.command
+ * The command name of the menu item to return.
+ * @param {number} options.resultIndex
+ * The index of the result. Defaults to the current selected index.
+ * @param {boolean} options.openByMouse
+ * Whether to open the menu by mouse or keyboard.
+ * @param {Array} options.submenuSelectors
+ * If the command is in the top-level result menu, leave this as an empty
+ * array. If it's in a submenu, set this to an array where each element i is
+ * a selector that can be used to get the i'th menu item that opens a
+ * submenu.
+ */
+ async openResultMenuAndGetItem({
+ window,
+ accesskey,
+ command,
+ resultIndex = window.gURLBar.view.selectedRowIndex,
+ openByMouse = false,
+ submenuSelectors = [],
+ }) {
+ await this.openResultMenu(window, { resultIndex, byMouse: openByMouse });
+
+ // Open the sequence of submenus that contains the item.
+ for (let selector of submenuSelectors) {
+ let menuitem = window.gURLBar.view.resultMenu.querySelector(selector);
+ if (!menuitem) {
+ throw new Error("Submenu item not found for selector: " + selector);
+ }
+
+ let promisePopup = lazy.BrowserTestUtils.waitForEvent(
+ window.gURLBar.view.resultMenu,
+ "popupshown"
+ );
+
+ if (AppConstants.platform == "macosx") {
+ // Synthesized clicks don't work in the native Mac menu.
+ this.info(
+ "Calling openMenu() on submenu item with selector: " + selector
+ );
+ menuitem.openMenu(true);
+ } else {
+ this.info("Clicking submenu item with selector: " + selector);
+ this.EventUtils.synthesizeMouseAtCenter(menuitem, {}, window);
+ }
+
+ this.info("Waiting for submenu popupshown event");
+ await promisePopup;
+ this.info("Got the submenu popupshown event");
+ }
+
+ // Now get the item.
+ let menuitem;
+ if (accesskey) {
+ await lazy.BrowserTestUtils.waitForCondition(() => {
+ menuitem = window.gURLBar.view.resultMenu.querySelector(
+ `menuitem[accesskey=${accesskey}]`
+ );
+ return menuitem;
+ }, "Waiting for strings to load");
+ } else if (command) {
+ menuitem = window.gURLBar.view.resultMenu.querySelector(
+ `menuitem[data-command=${command}]`
+ );
+ } else {
+ throw new Error("accesskey or command must be specified");
+ }
+
+ return menuitem;
+ },
+
+ /**
+ * Opens the result menu of a specific result and presses an access key to
+ * activate a menu item.
+ *
+ * @param {object} win The window containing the urlbar
+ * @param {string} accesskey The access key to press once the menu is open
+ * @param {object} [options] The options object.
+ * @param {number} [options.resultIndex] The index of the result. Defaults
+ * to the current selected index.
+ * @param {boolean} [options.openByMouse] Whether to open the menu by mouse
+ * or keyboard.
+ */
+ async openResultMenuAndPressAccesskey(
+ win,
+ accesskey,
+ {
+ resultIndex = win.gURLBar.view.selectedRowIndex,
+ openByMouse = false,
+ } = {}
+ ) {
+ let menuitem = await this.openResultMenuAndGetItem({
+ accesskey,
+ resultIndex,
+ openByMouse,
+ window: win,
+ });
+ if (!menuitem) {
+ throw new Error("Menu item not found for accesskey: " + accesskey);
+ }
+
+ let promiseCommand = lazy.BrowserTestUtils.waitForEvent(
+ win.gURLBar.view.resultMenu,
+ "command"
+ );
+
+ if (AppConstants.platform == "macosx") {
+ // The native Mac menu doesn't support access keys.
+ this.info("calling doCommand() to activate menu item");
+ menuitem.doCommand();
+ win.gURLBar.view.resultMenu.hidePopup(true);
+ } else {
+ this.info(`pressing access key (${accesskey}) to activate menu item`);
+ this.EventUtils.synthesizeKey(accesskey, {}, win);
+ }
+
+ this.info("waiting for command event");
+ await promiseCommand;
+ this.info("got the command event");
+ },
+
+ /**
+ * Opens the result menu of a specific result and clicks a menu item with a
+ * specified command name.
+ *
+ * @param {object} win
+ * The window containing the urlbar.
+ * @param {string|Array} commandOrArray
+ * If the command is in the top-level result menu, set this to the command
+ * name. If it's in a submenu, set this to an array where each element i is
+ * a selector that can be used to click the i'th menu item that opens a
+ * submenu, and the last element is the command name.
+ * @param {object} options
+ * The options object.
+ * @param {number} options.resultIndex
+ * The index of the result. Defaults to the current selected index.
+ * @param {boolean} options.openByMouse
+ * Whether to open the menu by mouse or keyboard.
+ */
+ async openResultMenuAndClickItem(
+ win,
+ commandOrArray,
+ {
+ resultIndex = win.gURLBar.view.selectedRowIndex,
+ openByMouse = false,
+ } = {}
+ ) {
+ let submenuSelectors = Array.isArray(commandOrArray)
+ ? commandOrArray
+ : [commandOrArray];
+ let command = submenuSelectors.pop();
+
+ let menuitem = await this.openResultMenuAndGetItem({
+ resultIndex,
+ openByMouse,
+ command,
+ submenuSelectors,
+ window: win,
+ });
+ if (!menuitem) {
+ throw new Error("Menu item not found for command: " + command);
+ }
+
+ let promiseCommand = lazy.BrowserTestUtils.waitForEvent(
+ win.gURLBar.view.resultMenu,
+ "command"
+ );
+
+ if (AppConstants.platform == "macosx") {
+ // Synthesized clicks don't work in the native Mac menu.
+ this.info("calling doCommand() to activate menu item");
+ menuitem.doCommand();
+ win.gURLBar.view.resultMenu.hidePopup(true);
+ } else {
+ this.info("Clicking menu item with command: " + command);
+ this.EventUtils.synthesizeMouseAtCenter(menuitem, {}, win);
+ }
+
+ this.info("Waiting for command event");
+ await promiseCommand;
+ this.info("Got the command event");
+ },
+
+ /**
+ * Returns true if the oneOffSearchButtons are visible.
+ *
+ * @param {object} win The window containing the urlbar
+ * @returns {boolean} True if the buttons are visible.
+ */
+ getOneOffSearchButtonsVisible(win) {
+ let buttons = this.getOneOffSearchButtons(win);
+ return buttons.style.display != "none" && !buttons.container.hidden;
+ },
+
+ /**
+ * Gets an abstracted representation of the result at an index.
+ *
+ * @param {object} win The window containing the urlbar
+ * @param {number} index The index to look for
+ * @returns {object} An object with numerous properties describing the result.
+ */
+ async getDetailsOfResultAt(win, index) {
+ let element = await this.waitForAutocompleteResultAt(win, index);
+ let details = {};
+ let result = element.result;
+ details.result = result;
+ let { url, postData } = UrlbarUtils.getUrlFromResult(result);
+ details.url = url;
+ details.postData = postData;
+ details.type = result.type;
+ details.source = result.source;
+ details.heuristic = result.heuristic;
+ details.autofill = !!result.autofill;
+ details.image =
+ element.getElementsByClassName("urlbarView-favicon")[0]?.src;
+ details.title = result.title;
+ details.tags = "tags" in result.payload ? result.payload.tags : [];
+ details.isSponsored = result.payload.isSponsored;
+ let actions = element.getElementsByClassName("urlbarView-action");
+ let urls = element.getElementsByClassName("urlbarView-url");
+ let typeIcon = element.querySelector(".urlbarView-type-icon");
+ await win.document.l10n.translateFragment(element);
+ details.displayed = {
+ title: element.getElementsByClassName("urlbarView-title")[0]?.textContent,
+ action: actions.length ? actions[0].textContent : null,
+ url: urls.length ? urls[0].textContent : null,
+ typeIcon: typeIcon
+ ? win.getComputedStyle(typeIcon)["background-image"]
+ : null,
+ };
+ details.element = {
+ action: element.getElementsByClassName("urlbarView-action")[0],
+ row: element,
+ separator: element.getElementsByClassName(
+ "urlbarView-title-separator"
+ )[0],
+ title: element.getElementsByClassName("urlbarView-title")[0],
+ url: element.getElementsByClassName("urlbarView-url")[0],
+ };
+ if (details.type == UrlbarUtils.RESULT_TYPE.SEARCH) {
+ details.searchParams = {
+ engine: result.payload.engine,
+ keyword: result.payload.keyword,
+ query: result.payload.query,
+ suggestion: result.payload.suggestion,
+ inPrivateWindow: result.payload.inPrivateWindow,
+ isPrivateEngine: result.payload.isPrivateEngine,
+ };
+ } else if (details.type == UrlbarUtils.RESULT_TYPE.KEYWORD) {
+ details.keyword = result.payload.keyword;
+ } else if (details.type == UrlbarUtils.RESULT_TYPE.DYNAMIC) {
+ details.dynamicType = result.payload.dynamicType;
+ }
+ return details;
+ },
+
+ /**
+ * Gets the currently selected element.
+ *
+ * @param {object} win The window containing the urlbar.
+ * @returns {HtmlElement|XulElement} The selected element.
+ */
+ getSelectedElement(win) {
+ return win.gURLBar.view.selectedElement || null;
+ },
+
+ /**
+ * Gets the index of the currently selected element.
+ *
+ * @param {object} win The window containing the urlbar.
+ * @returns {number} The selected index.
+ */
+ getSelectedElementIndex(win) {
+ return win.gURLBar.view.selectedElementIndex;
+ },
+
+ /**
+ * Gets the row at a specific index.
+ *
+ * @param {object} win The window containing the urlbar.
+ * @param {number} index The index to look for.
+ * @returns {HTMLElement|XulElement} The selected row.
+ */
+ getRowAt(win, index) {
+ return this.getResultsContainer(win).children.item(index);
+ },
+
+ /**
+ * Gets the currently selected row. If the selected element is a descendant of
+ * a row, this will return the ancestor row.
+ *
+ * @param {object} win The window containing the urlbar.
+ * @returns {HTMLElement|XulElement} The selected row.
+ */
+ getSelectedRow(win) {
+ return this.getRowAt(win, this.getSelectedRowIndex(win));
+ },
+
+ /**
+ * Gets the index of the currently selected element.
+ *
+ * @param {object} win The window containing the urlbar.
+ * @returns {number} The selected row index.
+ */
+ getSelectedRowIndex(win) {
+ return win.gURLBar.view.selectedRowIndex;
+ },
+
+ /**
+ * Selects the element at the index specified.
+ *
+ * @param {object} win The window containing the urlbar.
+ * @param {index} index The index to select.
+ */
+ setSelectedRowIndex(win, index) {
+ win.gURLBar.view.selectedRowIndex = index;
+ },
+
+ getResultsContainer(win) {
+ return win.gURLBar.view.panel.querySelector(".urlbarView-results");
+ },
+
+ /**
+ * Gets the number of results.
+ * You must wait for the query to be complete before using this.
+ *
+ * @param {object} win The window containing the urlbar
+ * @returns {number} the number of results.
+ */
+ getResultCount(win) {
+ return this.getResultsContainer(win).children.length;
+ },
+
+ /**
+ * Ensures at least one search suggestion is present.
+ *
+ * @param {object} win The window containing the urlbar
+ * @returns {boolean} whether at least one search suggestion is present.
+ */
+ promiseSuggestionsPresent(win) {
+ // TODO Bug 1530338: Quantum Bar doesn't yet implement lazy results replacement. When
+ // we do that, we'll have to be sure the suggestions we find are relevant
+ // for the current query. For now let's just wait for the search to be
+ // complete.
+ return this.promiseSearchComplete(win).then(context => {
+ // Look for search suggestions.
+ let firstSearchSuggestionIndex = context.results.findIndex(
+ r => r.type == UrlbarUtils.RESULT_TYPE.SEARCH && r.payload.suggestion
+ );
+ if (firstSearchSuggestionIndex == -1) {
+ throw new Error("Cannot find a search suggestion");
+ }
+ return firstSearchSuggestionIndex;
+ });
+ },
+
+ /**
+ * Waits for the given number of connections to an http server.
+ *
+ * @param {object} httpserver an HTTP Server instance
+ * @param {number} count Number of connections to wait for
+ * @returns {Promise} resolved when all the expected connections were started.
+ */
+ promiseSpeculativeConnections(httpserver, count) {
+ if (!httpserver) {
+ throw new Error("Must provide an http server");
+ }
+ return lazy.BrowserTestUtils.waitForCondition(
+ () => httpserver.connectionNumber == count,
+ "Waiting for speculative connection setup"
+ );
+ },
+
+ /**
+ * Waits for the popup to be shown.
+ *
+ * @param {object} win The window containing the urlbar
+ * @param {Function} openFn Function to be used to open the popup.
+ * @returns {Promise} resolved once the popup is closed
+ */
+ async promisePopupOpen(win, openFn) {
+ if (!openFn) {
+ throw new Error("openFn should be supplied to promisePopupOpen");
+ }
+ await openFn();
+ if (win.gURLBar.view.isOpen) {
+ return;
+ }
+ this.info("Awaiting for the urlbar panel to open");
+ await new Promise(resolve => {
+ win.gURLBar.controller.addQueryListener({
+ onViewOpen() {
+ win.gURLBar.controller.removeQueryListener(this);
+ resolve();
+ },
+ });
+ });
+ this.info("Urlbar panel opened");
+ },
+
+ /**
+ * Waits for the popup to be hidden.
+ *
+ * @param {object} win The window containing the urlbar
+ * @param {Function} [closeFn] Function to be used to close the popup, if not
+ * supplied it will default to a closing the popup directly.
+ * @returns {Promise} resolved once the popup is closed
+ */
+ async promisePopupClose(win, closeFn = null) {
+ if (closeFn) {
+ await closeFn();
+ } else {
+ win.gURLBar.view.close();
+ }
+ if (!win.gURLBar.view.isOpen) {
+ return;
+ }
+ this.info("Awaiting for the urlbar panel to close");
+ await new Promise(resolve => {
+ win.gURLBar.controller.addQueryListener({
+ onViewClose() {
+ win.gURLBar.controller.removeQueryListener(this);
+ resolve();
+ },
+ });
+ });
+ this.info("Urlbar panel closed");
+ },
+
+ /**
+ * Open the input field context menu and run a task on it.
+ *
+ * @param {nsIWindow} win the current window
+ * @param {Function} task a task function to run, gets the contextmenu popup
+ * as argument.
+ */
+ async withContextMenu(win, task) {
+ let textBox = win.gURLBar.querySelector("moz-input-box");
+ let cxmenu = textBox.menupopup;
+ let openPromise = lazy.BrowserTestUtils.waitForEvent(cxmenu, "popupshown");
+ this.EventUtils.synthesizeMouseAtCenter(
+ win.gURLBar.inputField,
+ {
+ type: "contextmenu",
+ button: 2,
+ },
+ win
+ );
+ await openPromise;
+ // On Mac sometimes the menuitems are not ready.
+ await new Promise(win.requestAnimationFrame);
+ try {
+ await task(cxmenu);
+ } finally {
+ // Close the context menu if the task didn't pick anything.
+ if (cxmenu.state == "open" || cxmenu.state == "showing") {
+ let closePromise = lazy.BrowserTestUtils.waitForEvent(
+ cxmenu,
+ "popuphidden"
+ );
+ cxmenu.hidePopup();
+ await closePromise;
+ }
+ }
+ },
+
+ /**
+ * @param {object} win The browser window
+ * @returns {boolean} Whether the popup is open
+ */
+ isPopupOpen(win) {
+ return win.gURLBar.view.isOpen;
+ },
+
+ /**
+ * Asserts that the input is in a given search mode, or no search mode. Can
+ * only be used if UrlbarTestUtils has been initialized with init().
+ *
+ * @param {Window} window
+ * The browser window.
+ * @param {object} expectedSearchMode
+ * The expected search mode object.
+ */
+ async assertSearchMode(window, expectedSearchMode) {
+ this.Assert.equal(
+ !!window.gURLBar.searchMode,
+ window.gURLBar.hasAttribute("searchmode"),
+ "Urlbar should never be in search mode without the corresponding attribute."
+ );
+
+ this.Assert.equal(
+ !!window.gURLBar.searchMode,
+ !!expectedSearchMode,
+ "gURLBar.searchMode should exist as expected"
+ );
+
+ if (
+ window.gURLBar.searchMode?.source &&
+ window.gURLBar.searchMode.source !== UrlbarUtils.RESULT_SOURCE.SEARCH
+ ) {
+ this.Assert.equal(
+ window.gURLBar.getAttribute("searchmodesource"),
+ UrlbarUtils.getResultSourceName(window.gURLBar.searchMode.source),
+ "gURLBar has proper searchmodesource attribute"
+ );
+ } else {
+ this.Assert.ok(
+ !window.gURLBar.hasAttribute("searchmodesource"),
+ "gURLBar does not have searchmodesource attribute"
+ );
+ }
+
+ if (!expectedSearchMode) {
+ // Check the input's placeholder.
+ const prefName =
+ "browser.urlbar.placeholderName" +
+ (lazy.PrivateBrowsingUtils.isWindowPrivate(window) ? ".private" : "");
+ let engineName = Services.prefs.getStringPref(prefName, "");
+ this.Assert.deepEqual(
+ window.document.l10n.getAttributes(window.gURLBar.inputField),
+ engineName
+ ? { id: "urlbar-placeholder-with-name", args: { name: engineName } }
+ : { id: "urlbar-placeholder", args: null },
+ "Expected placeholder l10n when search mode is inactive"
+ );
+ return;
+ }
+
+ // Default to full search mode for less verbose tests.
+ expectedSearchMode = { ...expectedSearchMode };
+ if (!expectedSearchMode.hasOwnProperty("isPreview")) {
+ expectedSearchMode.isPreview = false;
+ }
+
+ let isGeneralPurposeEngine = false;
+ if (expectedSearchMode.engineName) {
+ let engine = Services.search.getEngineByName(
+ expectedSearchMode.engineName
+ );
+ isGeneralPurposeEngine = engine.isGeneralPurposeEngine;
+ expectedSearchMode.isGeneralPurposeEngine = isGeneralPurposeEngine;
+ }
+
+ // expectedSearchMode may come from UrlbarUtils.LOCAL_SEARCH_MODES. The
+ // objects in that array include useful metadata like icon URIs and pref
+ // names that are not usually included in actual search mode objects. For
+ // convenience, ignore those properties if they aren't also present in the
+ // urlbar's actual search mode object.
+ let ignoreProperties = ["icon", "pref", "restrict", "telemetryLabel"];
+ for (let prop of ignoreProperties) {
+ if (prop in expectedSearchMode && !(prop in window.gURLBar.searchMode)) {
+ this.info(
+ `Ignoring unimportant property '${prop}' in expected search mode`
+ );
+ delete expectedSearchMode[prop];
+ }
+ }
+
+ this.Assert.deepEqual(
+ window.gURLBar.searchMode,
+ expectedSearchMode,
+ "Expected searchMode"
+ );
+
+ // Check the textContent and l10n attributes of the indicator and label.
+ let expectedTextContent = "";
+ let expectedL10n = { id: null, args: null };
+ if (expectedSearchMode.engineName) {
+ expectedTextContent = expectedSearchMode.engineName;
+ } else if (expectedSearchMode.source) {
+ let name = UrlbarUtils.getResultSourceName(expectedSearchMode.source);
+ this.Assert.ok(name, "Expected result source should have a name");
+ expectedL10n = { id: `urlbar-search-mode-${name}`, args: null };
+ } else {
+ this.Assert.ok(false, "Unexpected searchMode");
+ }
+
+ for (let element of [
+ window.gURLBar._searchModeIndicatorTitle,
+ window.gURLBar._searchModeLabel,
+ ]) {
+ if (expectedTextContent) {
+ this.Assert.equal(
+ element.textContent,
+ expectedTextContent,
+ "Expected textContent"
+ );
+ }
+ this.Assert.deepEqual(
+ window.document.l10n.getAttributes(element),
+ expectedL10n,
+ "Expected l10n"
+ );
+ }
+
+ // Check the input's placeholder.
+ let expectedPlaceholderL10n;
+ if (expectedSearchMode.engineName) {
+ expectedPlaceholderL10n = {
+ id: isGeneralPurposeEngine
+ ? "urlbar-placeholder-search-mode-web-2"
+ : "urlbar-placeholder-search-mode-other-engine",
+ args: { name: expectedSearchMode.engineName },
+ };
+ } else if (expectedSearchMode.source) {
+ let name = UrlbarUtils.getResultSourceName(expectedSearchMode.source);
+ expectedPlaceholderL10n = {
+ id: `urlbar-placeholder-search-mode-other-${name}`,
+ args: null,
+ };
+ }
+ this.Assert.deepEqual(
+ window.document.l10n.getAttributes(window.gURLBar.inputField),
+ expectedPlaceholderL10n,
+ "Expected placeholder l10n when search mode is active"
+ );
+
+ // If this is an engine search mode, check that all results are either
+ // search results with the same engine or have the same host as the engine.
+ // Search mode preview can show other results since it is not supposed to
+ // start a query.
+ if (
+ expectedSearchMode.engineName &&
+ !expectedSearchMode.isPreview &&
+ this.isPopupOpen(window)
+ ) {
+ let resultCount = this.getResultCount(window);
+ for (let i = 0; i < resultCount; i++) {
+ let result = await this.getDetailsOfResultAt(window, i);
+ if (result.source == UrlbarUtils.RESULT_SOURCE.SEARCH) {
+ this.Assert.equal(
+ expectedSearchMode.engineName,
+ result.searchParams.engine,
+ "Search mode result matches engine name."
+ );
+ } else {
+ let engine = Services.search.getEngineByName(
+ expectedSearchMode.engineName
+ );
+ let engineRootDomain =
+ lazy.UrlbarSearchUtils.getRootDomainFromEngine(engine);
+ let resultUrl = new URL(result.url);
+ this.Assert.ok(
+ resultUrl.hostname.includes(engineRootDomain),
+ "Search mode result matches engine host."
+ );
+ }
+ }
+ }
+ },
+
+ /**
+ * Enters search mode by clicking a one-off. The view must already be open
+ * before you call this. Can only be used if UrlbarTestUtils has been
+ * initialized with init().
+ *
+ * @param {object} window
+ * The window to operate on.
+ * @param {object} searchMode
+ * If given, the one-off matching this search mode will be clicked; it
+ * should be a full search mode object as described in
+ * UrlbarInput.setSearchMode. If not given, the first one-off is clicked.
+ */
+ async enterSearchMode(window, searchMode = null) {
+ this.info(`Enter Search Mode ${JSON.stringify(searchMode)}`);
+
+ // Ensure any pending query is complete.
+ await this.promiseSearchComplete(window);
+
+ // Ensure the the one-offs are finished rebuilding and visible.
+ let oneOffs = this.getOneOffSearchButtons(window);
+ await lazy.TestUtils.waitForCondition(
+ () => !oneOffs._rebuilding,
+ "Waiting for one-offs to finish rebuilding"
+ );
+ this.Assert.equal(
+ UrlbarTestUtils.getOneOffSearchButtonsVisible(window),
+ true,
+ "One-offs are visible"
+ );
+
+ let buttons = oneOffs.getSelectableButtons(true);
+ if (!searchMode) {
+ searchMode = { engineName: buttons[0].engine.name };
+ let engine = Services.search.getEngineByName(searchMode.engineName);
+ if (engine.isGeneralPurposeEngine) {
+ searchMode.source = UrlbarUtils.RESULT_SOURCE.SEARCH;
+ }
+ }
+
+ if (!searchMode.entry) {
+ searchMode.entry = "oneoff";
+ }
+
+ let oneOff = buttons.find(o =>
+ searchMode.engineName
+ ? o.engine.name == searchMode.engineName
+ : o.source == searchMode.source
+ );
+ this.Assert.ok(oneOff, "Found one-off button for search mode");
+ this.EventUtils.synthesizeMouseAtCenter(oneOff, {}, window);
+ await this.promiseSearchComplete(window);
+ this.Assert.ok(this.isPopupOpen(window), "Urlbar view is still open.");
+ await this.assertSearchMode(window, searchMode);
+ },
+
+ /**
+ * Exits search mode. If neither `backspace` nor `clickClose` is given, we'll
+ * default to backspacing. Can only be used if UrlbarTestUtils has been
+ * initialized with init().
+ *
+ * @param {object} window
+ * The window to operate on.
+ * @param {object} options
+ * Options object
+ * @param {boolean} options.backspace
+ * Exits search mode by backspacing at the beginning of the search string.
+ * @param {boolean} options.clickClose
+ * Exits search mode by clicking the close button on the search mode
+ * indicator.
+ * @param {boolean} [options.waitForSearch]
+ * Whether the test should wait for a search after exiting search mode.
+ * Defaults to true.
+ */
+ async exitSearchMode(
+ window,
+ { backspace, clickClose, waitForSearch = true } = {}
+ ) {
+ let urlbar = window.gURLBar;
+ // If the Urlbar is not extended, ignore the clickClose parameter. The close
+ // button is not clickable in this state. This state might be encountered on
+ // Linux, where prefers-reduced-motion is enabled in automation.
+ if (!urlbar.hasAttribute("breakout-extend") && clickClose) {
+ if (waitForSearch) {
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ urlbar.searchMode = null;
+ await searchPromise;
+ } else {
+ urlbar.searchMode = null;
+ }
+ return;
+ }
+
+ if (!backspace && !clickClose) {
+ backspace = true;
+ }
+
+ if (backspace) {
+ let urlbarValue = urlbar.value;
+ urlbar.selectionStart = urlbar.selectionEnd = 0;
+ if (waitForSearch) {
+ let searchPromise = this.promiseSearchComplete(window);
+ this.EventUtils.synthesizeKey("KEY_Backspace", {}, window);
+ await searchPromise;
+ } else {
+ this.EventUtils.synthesizeKey("KEY_Backspace", {}, window);
+ }
+ this.Assert.equal(
+ urlbar.value,
+ urlbarValue,
+ "Urlbar value hasn't changed."
+ );
+ this.assertSearchMode(window, null);
+ } else if (clickClose) {
+ // We need to hover the indicator to make the close button clickable in the
+ // test.
+ let indicator = urlbar.querySelector("#urlbar-search-mode-indicator");
+ this.EventUtils.synthesizeMouseAtCenter(
+ indicator,
+ { type: "mouseover" },
+ window
+ );
+ let closeButton = urlbar.querySelector(
+ "#urlbar-search-mode-indicator-close"
+ );
+ if (waitForSearch) {
+ let searchPromise = this.promiseSearchComplete(window);
+ this.EventUtils.synthesizeMouseAtCenter(closeButton, {}, window);
+ await searchPromise;
+ } else {
+ this.EventUtils.synthesizeMouseAtCenter(closeButton, {}, window);
+ }
+ await this.assertSearchMode(window, null);
+ }
+ },
+
+ /**
+ * Returns the userContextId (container id) for the last search.
+ *
+ * @param {object} win The browser window
+ * @returns {Promise<number>}
+ * resolved when fetching is complete. Its value is a userContextId
+ */
+ async promiseUserContextId(win) {
+ const defaultId = Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID;
+ let context = await win.gURLBar.lastQueryContextPromise;
+ return context.userContextId || defaultId;
+ },
+
+ /**
+ * Dispatches an input event to the input field.
+ *
+ * @param {object} win The browser window
+ */
+ fireInputEvent(win) {
+ // Set event.data to the last character in the input, for a couple of
+ // reasons: It simulates the user typing, and it's necessary for autofill.
+ let event = new InputEvent("input", {
+ data: win.gURLBar.value[win.gURLBar.value.length - 1] || null,
+ });
+ win.gURLBar.inputField.dispatchEvent(event);
+ },
+
+ /**
+ * Returns a new mock controller. This is useful for xpcshell tests.
+ *
+ * @param {object} options Additional options to pass to the UrlbarController
+ * constructor.
+ * @returns {UrlbarController} A new controller.
+ */
+ newMockController(options = {}) {
+ return new lazy.UrlbarController(
+ Object.assign(
+ {
+ input: {
+ isPrivate: false,
+ onFirstResult() {
+ return false;
+ },
+ getSearchSource() {
+ return "dummy-search-source";
+ },
+ window: {
+ location: {
+ href: AppConstants.BROWSER_CHROME_URL,
+ },
+ },
+ },
+ },
+ options
+ )
+ );
+ },
+
+ /**
+ * Initializes some external components used by the urlbar. This is necessary
+ * in xpcshell tests but not in browser tests.
+ */
+ async initXPCShellDependencies() {
+ // The FormHistoryStartup component must be initialized since urlbar uses
+ // form history.
+ Cc["@mozilla.org/satchel/form-history-startup;1"]
+ .getService(Ci.nsIObserver)
+ .observe(null, "profile-after-change", null);
+
+ // This is necessary because UrlbarMuxerUnifiedComplete.sort calls
+ // Services.search.parseSubmissionURL, so we need engines.
+ try {
+ await lazy.AddonTestUtils.promiseStartupManager();
+ } catch (error) {
+ if (!error.message.includes("already started")) {
+ throw error;
+ }
+ }
+ },
+
+ /**
+ * Enrolls in a mock Nimbus feature.
+ *
+ * If you call UrlbarPrefs.updateFirefoxSuggestScenario() from an xpcshell
+ * test, you must call this first to intialize the Nimbus urlbar feature.
+ *
+ * @param {object} value
+ * Define any desired Nimbus variables in this object.
+ * @param {string} [feature]
+ * The feature to init.
+ * @param {string} [enrollmentType]
+ * The enrollment type, either "rollout" (default) or "config".
+ * @returns {Function}
+ * A cleanup function that will unenroll the feature, returns a promise.
+ */
+ async initNimbusFeature(
+ value = {},
+ feature = "urlbar",
+ enrollmentType = "rollout"
+ ) {
+ this.info("initNimbusFeature awaiting ExperimentManager.onStartup");
+ await lazy.ExperimentManager.onStartup();
+
+ this.info("initNimbusFeature awaiting ExperimentAPI.ready");
+ await lazy.ExperimentAPI.ready();
+
+ let method =
+ enrollmentType == "rollout"
+ ? "enrollWithRollout"
+ : "enrollWithFeatureConfig";
+ this.info(`initNimbusFeature awaiting ExperimentFakes.${method}`);
+ let doCleanup = await lazy.ExperimentFakes[method]({
+ featureId: lazy.NimbusFeatures[feature].featureId,
+ value: { enabled: true, ...value },
+ });
+
+ this.info("initNimbusFeature done");
+
+ this.registerCleanupFunction?.(async () => {
+ // If `doCleanup()` has already been called (i.e., by the caller), it will
+ // throw an error here.
+ try {
+ await doCleanup();
+ } catch (error) {}
+ });
+
+ return doCleanup;
+ },
+
+ /**
+ * Simulate that user clicks URLBar and inputs text into it.
+ *
+ * @param {object} win
+ * The browser window containing target gURLBar.
+ * @param {string} text
+ * The text to be input.
+ */
+ async inputIntoURLBar(win, text) {
+ this.EventUtils.synthesizeMouseAtCenter(win.gURLBar.inputField, {}, win);
+ await lazy.BrowserTestUtils.waitForCondition(
+ () => win.document.activeElement === win.gURLBar.inputField
+ );
+ this.EventUtils.sendString(text, win);
+ },
+};
+
+UrlbarTestUtils.formHistory = {
+ /**
+ * Adds values to the urlbar's form history.
+ *
+ * @param {Array} values
+ * The form history entries to remove.
+ * @param {object} window
+ * The window containing the urlbar.
+ * @returns {Promise} resolved once the operation is complete.
+ */
+ add(values = [], window = lazy.BrowserWindowTracker.getTopWindow()) {
+ let fieldname = this.getFormHistoryName(window);
+ return lazy.FormHistoryTestUtils.add(fieldname, values);
+ },
+
+ /**
+ * Removes values from the urlbar's form history. If you want to remove all
+ * history, use clearFormHistory.
+ *
+ * @param {Array} values
+ * The form history entries to remove.
+ * @param {object} window
+ * The window containing the urlbar.
+ * @returns {Promise} resolved once the operation is complete.
+ */
+ remove(values = [], window = lazy.BrowserWindowTracker.getTopWindow()) {
+ let fieldname = this.getFormHistoryName(window);
+ return lazy.FormHistoryTestUtils.remove(fieldname, values);
+ },
+
+ /**
+ * Removes all values from the urlbar's form history. If you want to remove
+ * individual values, use removeFormHistory.
+ *
+ * @param {object} window
+ * The window containing the urlbar.
+ * @returns {Promise} resolved once the operation is complete.
+ */
+ clear(window = lazy.BrowserWindowTracker.getTopWindow()) {
+ let fieldname = this.getFormHistoryName(window);
+ return lazy.FormHistoryTestUtils.clear(fieldname);
+ },
+
+ /**
+ * Searches the urlbar's form history.
+ *
+ * @param {object} criteria
+ * Criteria to narrow the search. See FormHistory.search.
+ * @param {object} window
+ * The window containing the urlbar.
+ * @returns {Promise}
+ * A promise resolved with an array of found form history entries.
+ */
+ search(criteria = {}, window = lazy.BrowserWindowTracker.getTopWindow()) {
+ let fieldname = this.getFormHistoryName(window);
+ return lazy.FormHistoryTestUtils.search(fieldname, criteria);
+ },
+
+ /**
+ * Returns a promise that's resolved on the next form history change.
+ *
+ * @param {string} change
+ * Null to listen for any change, or one of: add, remove, update
+ * @returns {Promise}
+ * Resolved on the next specified form history change.
+ */
+ promiseChanged(change = null) {
+ return lazy.TestUtils.topicObserved(
+ "satchel-storage-changed",
+ (subject, data) => !change || data == "formhistory-" + change
+ );
+ },
+
+ /**
+ * Returns the form history name for the urlbar in a window.
+ *
+ * @param {object} window
+ * The window.
+ * @returns {string}
+ * The form history name of the urlbar in the window.
+ */
+ getFormHistoryName(window = lazy.BrowserWindowTracker.getTopWindow()) {
+ return window ? window.gURLBar.formHistoryName : "searchbar-history";
+ },
+};
+
+/**
+ * A test provider. If you need a test provider whose behavior is different
+ * from this, then consider modifying the implementation below if you think the
+ * new behavior would be useful for other tests. Otherwise, you can create a
+ * new TestProvider instance and then override its methods.
+ */
+class TestProvider extends UrlbarProvider {
+ /**
+ * Constructor.
+ *
+ * @param {object} options
+ * Constructor options
+ * @param {Array} options.results
+ * An array of UrlbarResult objects that will be the provider's results.
+ * @param {string} [options.name]
+ * The provider's name. Provider names should be unique.
+ * @param {UrlbarUtils.PROVIDER_TYPE} [options.type]
+ * The provider's type.
+ * @param {number} [options.priority]
+ * The provider's priority. Built-in providers have a priority of zero.
+ * @param {number} [options.addTimeout]
+ * If non-zero, each result will be added on this timeout. If zero, all
+ * results will be added immediately and synchronously.
+ * @param {Function} [options.onCancel]
+ * If given, a function that will be called when the provider's cancelQuery
+ * method is called.
+ * @param {Function} [options.onSelection]
+ * If given, a function that will be called when
+ * {@link UrlbarView.#selectElement} method is called.
+ * @param {Function} [options.onEngagement]
+ * If given, a function that will be called when engagement.
+ */
+ constructor({
+ results,
+ name = "TestProvider" + Services.uuid.generateUUID(),
+ type = UrlbarUtils.PROVIDER_TYPE.PROFILE,
+ priority = 0,
+ addTimeout = 0,
+ onCancel = null,
+ onSelection = null,
+ onEngagement = null,
+ } = {}) {
+ super();
+ this._results = results;
+ this._name = name;
+ this._type = type;
+ this._priority = priority;
+ this._addTimeout = addTimeout;
+ this._onCancel = onCancel;
+ this._onSelection = onSelection;
+ this._onEngagement = onEngagement;
+ }
+ get name() {
+ return this._name;
+ }
+ get type() {
+ return this._type;
+ }
+ getPriority(context) {
+ return this._priority;
+ }
+ isActive(context) {
+ return true;
+ }
+ async startQuery(context, addCallback) {
+ for (let result of this._results) {
+ if (!this._addTimeout) {
+ addCallback(this, result);
+ } else {
+ await new Promise(resolve => {
+ lazy.setTimeout(() => {
+ addCallback(this, result);
+ resolve();
+ }, this._addTimeout);
+ });
+ }
+ }
+ }
+ cancelQuery(context) {
+ if (this._onCancel) {
+ this._onCancel();
+ }
+ }
+
+ onSelection(result, element) {
+ if (this._onSelection) {
+ this._onSelection(result, element);
+ }
+ }
+
+ onEngagement(isPrivate, state, queryContext, details) {
+ if (this._onEngagement) {
+ this._onEngagement(isPrivate, state, queryContext, details);
+ }
+ }
+}
+
+UrlbarTestUtils.TestProvider = TestProvider;
diff --git a/browser/components/urlbar/tests/browser-tips/README.txt b/browser/components/urlbar/tests/browser-tips/README.txt
new file mode 100644
index 0000000000..04a7b09707
--- /dev/null
+++ b/browser/components/urlbar/tests/browser-tips/README.txt
@@ -0,0 +1,7 @@
+If you're running these tests and you get an error like this:
+
+FAIL head.js import threw an exception - Error opening input stream (invalid filename?): chrome://mochitests/content/browser/toolkit/mozapps/update/tests/browser/head.js
+
+Then run `mach test toolkit/mozapps/update/tests/browser` first. You can
+stop mach as soon as it starts the first test, but this is necessary so that
+mach builds the update tests in your objdir.
diff --git a/browser/components/urlbar/tests/browser-tips/browser.ini b/browser/components/urlbar/tests/browser-tips/browser.ini
new file mode 100644
index 0000000000..d7674161dc
--- /dev/null
+++ b/browser/components/urlbar/tests/browser-tips/browser.ini
@@ -0,0 +1,26 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+[DEFAULT]
+support-files =
+ head.js
+
+[browser_interventions.js]
+[browser_picks.js]
+[browser_searchTips.js]
+support-files =
+ ../browser/slow-page.sjs
+ slow-page.html
+https_first_disabled = true
+[browser_searchTips_interaction.js]
+https_first_disabled = true
+[browser_selection.js]
+[browser_updateAsk.js]
+skip-if = os == 'win' && msix # Updater is disabled in MSIX builds
+[browser_updateRefresh.js]
+skip-if = os == 'win' && msix # Updater is disabled in MSIX builds
+[browser_updateRestart.js]
+skip-if = os == 'win' && msix # Updater is disabled in MSIX builds
+[browser_updateWeb.js]
+skip-if = os == 'win' && msix # Updater is disabled in MSIX builds
diff --git a/browser/components/urlbar/tests/browser-tips/browser_interventions.js b/browser/components/urlbar/tests/browser-tips/browser_interventions.js
new file mode 100644
index 0000000000..ebac90ad85
--- /dev/null
+++ b/browser/components/urlbar/tests/browser-tips/browser_interventions.js
@@ -0,0 +1,271 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarProviderInterventions:
+ "resource:///modules/UrlbarProviderInterventions.sys.mjs",
+});
+
+add_setup(async function () {
+ Services.telemetry.clearEvents();
+ Services.telemetry.clearScalars();
+ makeProfileResettable();
+});
+
+// Tests the refresh tip.
+add_task(async function refresh() {
+ // Pick the tip, which should open the refresh dialog. Click its cancel
+ // button.
+ await checkIntervention({
+ searchString: SEARCH_STRINGS.REFRESH,
+ tip: UrlbarProviderInterventions.TIP_TYPE.REFRESH,
+ title:
+ "Restore default settings and remove old add-ons for optimal performance.",
+ button: /^Refresh .+…$/,
+ awaitCallback() {
+ return BrowserTestUtils.promiseAlertDialog(
+ "cancel",
+ "chrome://global/content/resetProfile.xhtml",
+ { isSubDialog: true }
+ );
+ },
+ });
+});
+
+// Tests the clear tip.
+add_task(async function clear() {
+ // Pick the tip, which should open the refresh dialog. Click its cancel
+ // button.
+ await checkIntervention({
+ searchString: SEARCH_STRINGS.CLEAR,
+ tip: UrlbarProviderInterventions.TIP_TYPE.CLEAR,
+ title: "Clear your cache, cookies, history and more.",
+ button: "Choose What to Clear…",
+ awaitCallback() {
+ return BrowserTestUtils.promiseAlertDialog(
+ "cancel",
+ "chrome://browser/content/sanitize.xhtml",
+ {
+ isSubDialog: true,
+ }
+ );
+ },
+ });
+});
+
+// Tests the clear tip in a private window. The clear tip shouldn't appear in
+// private windows.
+add_task(async function clear_private() {
+ let win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+ // First, make sure the extension works in PBM by triggering a non-clear
+ // tip.
+ let result = (await awaitTip(SEARCH_STRINGS.REFRESH, win))[0];
+ Assert.strictEqual(
+ result.payload.type,
+ UrlbarProviderInterventions.TIP_TYPE.REFRESH
+ );
+
+ // Blur the urlbar so that the engagement is ended.
+ await UrlbarTestUtils.promisePopupClose(win, () => win.gURLBar.blur());
+
+ // Now do a search that would trigger the clear tip.
+ await awaitNoTip(SEARCH_STRINGS.CLEAR, win);
+
+ // Blur the urlbar so that the engagement is ended.
+ await UrlbarTestUtils.promisePopupClose(win, () => win.gURLBar.blur());
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+// Tests that if multiple interventions of the same type are seen in the same
+// engagement, only one instance is recorded in Telemetry.
+add_task(async function multipleInterventionsInOneEngagement() {
+ Services.telemetry.clearScalars();
+ let result = (await awaitTip(SEARCH_STRINGS.REFRESH, window))[0];
+ Assert.strictEqual(
+ result.payload.type,
+ UrlbarProviderInterventions.TIP_TYPE.REFRESH
+ );
+ result = (await awaitTip(SEARCH_STRINGS.CLEAR, window))[0];
+ Assert.strictEqual(
+ result.payload.type,
+ UrlbarProviderInterventions.TIP_TYPE.CLEAR
+ );
+ result = (await awaitTip(SEARCH_STRINGS.REFRESH, window))[0];
+ Assert.strictEqual(
+ result.payload.type,
+ UrlbarProviderInterventions.TIP_TYPE.REFRESH
+ );
+
+ // Blur the urlbar so that the engagement is ended.
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ // We should only record one impression for the Refresh tip. Although it was
+ // seen twice, it was in the same engagement.
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ `${UrlbarProviderInterventions.TIP_TYPE.REFRESH}-shown`,
+ 1
+ );
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ `${UrlbarProviderInterventions.TIP_TYPE.CLEAR}-shown`,
+ 1
+ );
+});
+
+// Test the result of UrlbarProviderInterventions.isActive()
+// and whether or not the function calucates the score.
+add_task(async function testIsActive() {
+ const testData = [
+ {
+ description: "Test for search string that activates the intervention",
+ searchString: "firefox slow",
+ expectedActive: true,
+ expectedScoreCalculated: true,
+ },
+ {
+ description:
+ "Test for search string that does not activate the intervention",
+ searchString: "example slow",
+ expectedActive: false,
+ expectedScoreCalculated: true,
+ },
+ {
+ description: "Test for empty search string",
+ searchString: "",
+ expectedActive: false,
+ expectedScoreCalculated: false,
+ },
+ {
+ description: "Test for an URL",
+ searchString: "https://firefox/slow",
+ expectedActive: false,
+ expectedScoreCalculated: false,
+ },
+ {
+ description: "Test for a data URL",
+ searchString: "data:text/html,<div>firefox slow</div>",
+ expectedActive: false,
+ expectedScoreCalculated: false,
+ },
+ {
+ description: "Test for string like URL",
+ searchString: "firefox://slow",
+ expectedActive: false,
+ expectedScoreCalculated: false,
+ },
+ ];
+
+ for (const {
+ description,
+ searchString,
+ expectedActive,
+ expectedScoreCalculated,
+ } of testData) {
+ info(description);
+
+ // Set null to currentTip to know whether or not UrlbarProviderInterventions
+ // calculated the score.
+ UrlbarProviderInterventions.currentTip = null;
+
+ const isActive = UrlbarProviderInterventions.isActive({ searchString });
+ Assert.equal(isActive, expectedActive, "Result of isAcitive is correct");
+ const isScoreCalculated = UrlbarProviderInterventions.currentTip !== null;
+ Assert.equal(
+ isScoreCalculated,
+ expectedScoreCalculated,
+ "The score is calculated correctly"
+ );
+ }
+});
+
+add_task(async function tipsAreEnglishOnly() {
+ // Test that Interventions are working in en-US.
+ let result = (await awaitTip(SEARCH_STRINGS.REFRESH, window))[0];
+ Assert.strictEqual(
+ result.payload.type,
+ UrlbarProviderInterventions.TIP_TYPE.REFRESH
+ );
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+
+ // We will need to fetch new engines when we switch locales.
+ let enginesReloaded =
+ SearchTestUtils.promiseSearchNotification("engines-reloaded");
+
+ const originalAvailable = Services.locale.availableLocales;
+ const originalRequested = Services.locale.requestedLocales;
+ Services.locale.availableLocales = ["en-US", "de"];
+ Services.locale.requestedLocales = ["de"];
+
+ let cleanup = async () => {
+ let reloadPromise =
+ SearchTestUtils.promiseSearchNotification("engines-reloaded");
+ Services.locale.requestedLocales = originalRequested;
+ Services.locale.availableLocales = originalAvailable;
+ await reloadPromise;
+ cleanup = null;
+ };
+ registerCleanupFunction(() => cleanup?.());
+
+ let appLocales = Services.locale.appLocalesAsBCP47;
+ Assert.equal(appLocales[0], "de");
+
+ await enginesReloaded;
+
+ // Interventions should no longer work in the new locale.
+ await awaitNoTip(SEARCH_STRINGS.CLEAR, window);
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+
+ await cleanup();
+});
+
+// Tests the help command (using the clear intervention). It should open the
+// help page and it should not trigger the primary intervention behavior.
+add_task(async function pickHelp() {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ // Do a search that triggers the clear tip.
+ let [result] = await awaitTip(SEARCH_STRINGS.CLEAR);
+ Assert.strictEqual(
+ result.payload.type,
+ UrlbarProviderInterventions.TIP_TYPE.CLEAR
+ );
+
+ // Click the help command and wait for the help page to load.
+ Assert.ok(
+ !!result.payload.helpUrl,
+ "The result's helpUrl should be defined and non-empty: " +
+ JSON.stringify(result.payload.helpUrl)
+ );
+ let loadPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ result.payload.helpUrl
+ );
+ await UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "h", {
+ openByMouse: true,
+ resultIndex: 1,
+ });
+ info("Waiting for help URL to load in the current tab");
+ await loadPromise;
+
+ // Wait a bit and make sure the clear recent history dialog did not open.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 2000));
+ Assert.strictEqual(gDialogBox.isOpen, false, "No dialog should be open");
+
+ // Check telemetry.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ `${UrlbarProviderInterventions.TIP_TYPE.CLEAR}-help`,
+ 1
+ );
+ });
+});
diff --git a/browser/components/urlbar/tests/browser-tips/browser_picks.js b/browser/components/urlbar/tests/browser-tips/browser_picks.js
new file mode 100644
index 0000000000..60a7676668
--- /dev/null
+++ b/browser/components/urlbar/tests/browser-tips/browser_picks.js
@@ -0,0 +1,223 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests clicks and enter key presses on UrlbarUtils.RESULT_TYPE.TIP results.
+
+"use strict";
+
+const TIP_URL = "http://example.com/tip";
+const HELP_URL = "http://example.com/help";
+
+add_setup(async function () {
+ window.windowUtils.disableNonTestMouseEvents(true);
+ registerCleanupFunction(() => {
+ window.windowUtils.disableNonTestMouseEvents(false);
+ });
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.eventTelemetry.enabled", true]],
+ });
+});
+
+add_task(async function enter_mainButton_url() {
+ await doTest({ click: false, buttonUrl: TIP_URL });
+});
+
+add_task(async function enter_mainButton_noURL() {
+ await doTest({ click: false });
+});
+
+add_task(async function enter_help() {
+ await doTest({ click: false, helpUrl: HELP_URL });
+});
+
+add_task(async function mouse_mainButton_url() {
+ await doTest({ click: true, buttonUrl: TIP_URL });
+});
+
+add_task(async function mouse_mainButton_noURL() {
+ await doTest({ click: true });
+});
+
+add_task(async function mouse_help() {
+ await doTest({ click: true, helpUrl: HELP_URL });
+});
+
+// Clicks inside a tip but not on any button.
+add_task(async function mouse_insideTipButNotOnButtons() {
+ let results = [makeTipResult({ buttonUrl: TIP_URL, helpUrl: HELP_URL })];
+ let provider = new UrlbarTestUtils.TestProvider({ results, priority: 1 });
+ UrlbarProvidersManager.registerProvider(provider);
+
+ // Click inside the tip but outside the buttons. Nothing should happen. Make
+ // the result the heuristic to check that the selection on the main button
+ // isn't lost.
+ results[0].heuristic = true;
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ value: "test",
+ window,
+ fireInputEvent: true,
+ });
+ let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElementIndex(window),
+ 0,
+ "The main button's index should be selected initially"
+ );
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElement(window),
+ row._buttons.get("0"),
+ "The main button element should be selected initially"
+ );
+ EventUtils.synthesizeMouseAtCenter(row, {});
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 500));
+ Assert.ok(gURLBar.view.isOpen, "The view should remain open");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElementIndex(window),
+ 0,
+ "The main button's index should remain selected"
+ );
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElement(window),
+ row._buttons.get("0"),
+ "The main button element should remain selected"
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ UrlbarProvidersManager.unregisterProvider(provider);
+});
+
+/**
+ * Runs this test's main checks.
+ *
+ * @param {object} options
+ * Options for the test.
+ * @param {boolean} options.click
+ * Pass true to trigger a click, false to trigger an enter key.
+ * @param {string} [options.buttonUrl]
+ * Pass a URL if picking the main button should open a URL. Pass nothing if
+ * a URL shouldn't be opened or if you want to pick the help button instead of
+ * the main button.
+ * @param {string} [options.helpUrl]
+ * Pass a URL if you want to pick the help button. Pass nothing if you want
+ * to pick the main button instead.
+ */
+async function doTest({ click, buttonUrl = undefined, helpUrl = undefined }) {
+ // Open a new tab for the test if we expect to load a URL.
+ let tab;
+ if (buttonUrl || helpUrl) {
+ tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:blank",
+ });
+ }
+
+ // Add our test provider.
+ let provider = new UrlbarTestUtils.TestProvider({
+ results: [makeTipResult({ buttonUrl, helpUrl })],
+ priority: 1,
+ });
+ UrlbarProvidersManager.registerProvider(provider);
+
+ let onEngagementPromise = new Promise(
+ resolve => (provider.onEngagement = resolve)
+ );
+
+ // Do a search to show our tip result.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ value: "test",
+ window,
+ fireInputEvent: true,
+ });
+ let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
+ let mainButton = row._buttons.get("0");
+ let target = helpUrl
+ ? row._buttons.get(UrlbarPrefs.get("resultMenu") ? "menu" : "help")
+ : mainButton;
+
+ // If we're picking the tip with the keyboard, TAB to select the proper
+ // target.
+ if (!click) {
+ EventUtils.synthesizeKey("KEY_Tab", { repeat: helpUrl ? 2 : 1 });
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElement(window),
+ target,
+ `${target.className} should be selected.`
+ );
+ }
+
+ // Now pick the target and wait for provider.onEngagement to be called and
+ // the URL to load if necessary.
+ let loadPromise;
+ if (buttonUrl || helpUrl) {
+ loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ }
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ if (helpUrl && UrlbarPrefs.get("resultMenu")) {
+ UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "h", {
+ openByMouse: click,
+ resultIndex: 0,
+ });
+ } else if (click) {
+ EventUtils.synthesizeMouseAtCenter(target, {});
+ } else {
+ EventUtils.synthesizeKey("KEY_Enter");
+ }
+ });
+ await onEngagementPromise;
+ await loadPromise;
+
+ // Check telemetry.
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ helpUrl ? "test-help" : "test-picked",
+ 1
+ );
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ category: "urlbar",
+ method: "engagement",
+ object:
+ click && !(helpUrl && UrlbarPrefs.get("resultMenu"))
+ ? "click"
+ : "enter",
+ value: "typed",
+ },
+ ],
+ { category: "urlbar" }
+ );
+
+ // Done.
+ UrlbarProvidersManager.unregisterProvider(provider);
+ if (tab) {
+ BrowserTestUtils.removeTab(tab);
+ }
+}
+
+function makeTipResult({ buttonUrl, helpUrl }) {
+ return new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TIP,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ type: "test",
+ titleL10n: { id: "urlbar-search-tips-confirm" },
+ buttons: [
+ {
+ url: buttonUrl,
+ l10n: { id: "urlbar-search-tips-confirm" },
+ },
+ ],
+ helpUrl,
+ helpL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-tip-get-help"
+ : "urlbar-tip-help-icon",
+ },
+ }
+ );
+}
diff --git a/browser/components/urlbar/tests/browser-tips/browser_searchTips.js b/browser/components/urlbar/tests/browser-tips/browser_searchTips.js
new file mode 100644
index 0000000000..ff592bc831
--- /dev/null
+++ b/browser/components/urlbar/tests/browser-tips/browser_searchTips.js
@@ -0,0 +1,657 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests the Search Tips feature, which displays a prompt to use the Urlbar on
+// the newtab page and on the user's default search engine's homepage.
+// Specifically, it tests that the Tips appear when they should be appearing.
+// This doesn't test the max-shown-count limit or the restriction on tips when
+// we show the default browser prompt because those require restarting the
+// browser.
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ AppMenuNotifications: "resource://gre/modules/AppMenuNotifications.sys.mjs",
+ ProfileAge: "resource://gre/modules/ProfileAge.sys.mjs",
+ PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
+ UrlbarProviderSearchTips:
+ "resource:///modules/UrlbarProviderSearchTips.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ HttpServer: "resource://testing-common/httpd.js",
+});
+
+// These should match the same consts in UrlbarProviderSearchTips.jsm.
+const MAX_SHOWN_COUNT = 4;
+const LAST_UPDATE_THRESHOLD_MS = 24 * 60 * 60 * 1000;
+
+// We test some of the bigger Google domains.
+const GOOGLE_DOMAINS = [
+ "www.google.com",
+ "www.google.ca",
+ "www.google.co.uk",
+ "www.google.com.au",
+ "www.google.co.nz",
+];
+
+// In order for the persist tip to appear, the scheme of the
+// search engine has to be the same as the scheme of the SERP url.
+// withDNSRedirect() loads an http: url while the searchform
+// of the default engine uses https. To enable the search term
+// to be shown, we use the Example engine because it doesn't require
+// a redirect.
+const SEARCH_SERP_URL = "https://example.com/?q=chocolate";
+
+add_setup(async function () {
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ `browser.urlbar.tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.ONBOARD}`,
+ 0,
+ ],
+ [
+ `browser.urlbar.tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.PERSIST}`,
+ 0,
+ ],
+ [
+ `browser.urlbar.tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.REDIRECT}`,
+ 0,
+ ],
+ ],
+ });
+
+ // Write an old profile age so tips are actually shown.
+ let age = await ProfileAge();
+ let originalTimes = age._times;
+ let date = Date.now() - LAST_UPDATE_THRESHOLD_MS - 30000;
+ age._times = { created: date, firstUse: date };
+ await age.writeTimes();
+
+ // Remove update history and the current active update so tips are shown.
+ let updateRootDir = Services.dirsvc.get("UpdRootD", Ci.nsIFile);
+ let updatesFile = updateRootDir.clone();
+ updatesFile.append("updates.xml");
+ let activeUpdateFile = updateRootDir.clone();
+ activeUpdateFile.append("active-update.xml");
+ try {
+ updatesFile.remove(false);
+ } catch (e) {}
+ try {
+ activeUpdateFile.remove(false);
+ } catch (e) {}
+
+ let defaultEngine = await Services.search.getDefault();
+ let defaultEngineName = defaultEngine.name;
+ Assert.equal(defaultEngineName, "Google", "Default engine should be Google.");
+
+ // Add a mock engine so we don't hit the network loading the SERP.
+ await SearchTestUtils.installSearchExtension();
+
+ registerCleanupFunction(async () => {
+ let age2 = await ProfileAge();
+ age2._times = originalTimes;
+ await age2.writeTimes();
+ await setDefaultEngine(defaultEngineName);
+ resetSearchTipsProvider();
+ });
+});
+
+// The onboarding tip should be shown on about:newtab.
+add_task(async function newtab() {
+ await checkTab(
+ window,
+ "about:newtab",
+ UrlbarProviderSearchTips.TIP_TYPE.ONBOARD
+ );
+});
+
+// The onboarding tip should be shown on about:home.
+add_task(async function home() {
+ await checkTab(
+ window,
+ "about:home",
+ UrlbarProviderSearchTips.TIP_TYPE.ONBOARD
+ );
+});
+
+// The redirect tip should be shown for www.google.com when it's the default
+// engine.
+add_task(async function google() {
+ await setDefaultEngine("Google");
+ for (let domain of GOOGLE_DOMAINS) {
+ await withDNSRedirect(domain, "/", async url => {
+ await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.REDIRECT);
+ });
+ }
+});
+
+// The redirect tip should be shown for www.google.com/webhp when it's the
+// default engine.
+add_task(async function googleWebhp() {
+ await setDefaultEngine("Google");
+ for (let domain of GOOGLE_DOMAINS) {
+ await withDNSRedirect(domain, "/webhp", async url => {
+ await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.REDIRECT);
+ });
+ }
+});
+
+// The redirect tip should be shown for the Google homepage when query strings
+// are appended.
+add_task(async function googleQueryString() {
+ await setDefaultEngine("Google");
+ for (let domain of GOOGLE_DOMAINS) {
+ await withDNSRedirect(domain, "/webhp", async url => {
+ await checkTab(
+ window,
+ `${url}?hl=en`,
+ UrlbarProviderSearchTips.TIP_TYPE.REDIRECT
+ );
+ });
+ }
+});
+
+// The redirect tip should not be shown on Google results pages.
+add_task(async function googleResults() {
+ await setDefaultEngine("Google");
+ for (let domain of GOOGLE_DOMAINS) {
+ await withDNSRedirect(domain, "/search", async url => {
+ await checkTab(
+ window,
+ `${url}?q=firefox`,
+ UrlbarProviderSearchTips.TIP_TYPE.NONE
+ );
+ });
+ }
+});
+
+// The redirect tip should not be shown for www.google.com when it's not the
+// default engine.
+add_task(async function googleNotDefault() {
+ await setDefaultEngine("Bing");
+ for (let domain of GOOGLE_DOMAINS) {
+ await withDNSRedirect(domain, "/", async url => {
+ await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.NONE);
+ });
+ }
+});
+
+// The redirect tip should not be shown for www.google.com/webhp when it's not
+// the default engine.
+add_task(async function googleWebhpNotDefault() {
+ await setDefaultEngine("Bing");
+ for (let domain of GOOGLE_DOMAINS) {
+ await withDNSRedirect(domain, "/webhp", async url => {
+ await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.NONE);
+ });
+ }
+});
+
+// The redirect tip should be shown for www.bing.com when it's the default
+// engine.
+add_task(async function bing() {
+ await setDefaultEngine("Bing");
+ await withDNSRedirect("www.bing.com", "/", async url => {
+ await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.REDIRECT);
+ });
+});
+
+// The redirect tip should be shown on the Bing homepage even when Bing appends
+// query strings.
+add_task(async function bingQueryString() {
+ await setDefaultEngine("Bing");
+ await withDNSRedirect("www.bing.com", "/", async url => {
+ await checkTab(
+ window,
+ `${url}?toWww=1`,
+ UrlbarProviderSearchTips.TIP_TYPE.REDIRECT
+ );
+ });
+});
+
+// The redirect tip should not be shown on Bing results pages.
+add_task(async function bingResults() {
+ await setDefaultEngine("Bing");
+ await withDNSRedirect("www.bing.com", "/search", async url => {
+ await checkTab(
+ window,
+ `${url}?q=firefox`,
+ UrlbarProviderSearchTips.TIP_TYPE.NONE
+ );
+ });
+});
+
+// The redirect tip should not be shown for www.bing.com when it's not the
+// default engine.
+add_task(async function bingNotDefault() {
+ await setDefaultEngine("Google");
+ await withDNSRedirect("www.bing.com", "/", async url => {
+ await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.NONE);
+ });
+});
+
+// The redirect tip should be shown for duckduckgo.com when it's the default
+// engine.
+add_task(async function ddg() {
+ await setDefaultEngine("DuckDuckGo");
+ await withDNSRedirect("duckduckgo.com", "/", async url => {
+ await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.REDIRECT);
+ });
+});
+
+// The redirect tip should be shown for start.duckduckgo.com when it's the
+// default engine.
+add_task(async function ddgStart() {
+ await setDefaultEngine("DuckDuckGo");
+ await withDNSRedirect("start.duckduckgo.com", "/", async url => {
+ await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.REDIRECT);
+ });
+});
+
+// The redirect tip should not be shown for duckduckgo.com when it's not the
+// default engine.
+add_task(async function ddgNotDefault() {
+ await setDefaultEngine("Google");
+ await withDNSRedirect("duckduckgo.com", "/", async url => {
+ await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.NONE);
+ });
+});
+
+// The redirect tip should not be shown for start.duckduckgo.com when it's not
+// the default engine.
+add_task(async function ddgStartNotDefault() {
+ await setDefaultEngine("Google");
+ await withDNSRedirect("start.duckduckgo.com", "/", async url => {
+ await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.NONE);
+ });
+});
+
+// The redirect tip should not be shown for duckduckgo.com/?q=foo, the search
+// results page, which happens to have the same domain and path as the home
+// page.
+add_task(async function ddgSearchResultsPage() {
+ await setDefaultEngine("DuckDuckGo");
+ await withDNSRedirect("duckduckgo.com", "/", async url => {
+ await checkTab(
+ window,
+ `${url}?q=test`,
+ UrlbarProviderSearchTips.TIP_TYPE.NONE
+ );
+ });
+});
+
+// The redirect tip should not be shown on a non-engine page.
+add_task(async function nonEnginePage() {
+ await checkTab(
+ window,
+ "http://example.com/",
+ UrlbarProviderSearchTips.TIP_TYPE.NONE
+ );
+});
+
+// The persist tip should show on default SERPs.
+// This test also has an implied check that the SERP
+// is receiving an originalURI.
+// This is because the page the test is attempting to load
+// will differ from the page that's actually loaded due to
+// the DNS redirect.
+add_task(async function persistTipOnDefault() {
+ await setDefaultEngine("Example");
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.showSearchTerms.featureGate", true]],
+ });
+ await checkTab(
+ window,
+ SEARCH_SERP_URL,
+ UrlbarProviderSearchTips.TIP_TYPE.PERSIST
+ );
+ await SpecialPowers.popPrefEnv();
+});
+
+// The persist tip should not show on non-default SERPs.
+add_task(async function noPersistTipOnNonDefault() {
+ await setDefaultEngine("DuckDuckGo");
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.showSearchTerms.featureGate", true]],
+ });
+ await checkTab(
+ window,
+ SEARCH_SERP_URL,
+ UrlbarProviderSearchTips.TIP_TYPE.NONE
+ );
+ await SpecialPowers.popPrefEnv();
+});
+
+// The persist tip should only show up once a session.
+add_task(async function persistTipOnceOnDefaultSerp() {
+ await setDefaultEngine("Example");
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.showSearchTerms.featureGate", true]],
+ });
+ await checkTab(
+ window,
+ SEARCH_SERP_URL,
+ UrlbarProviderSearchTips.TIP_TYPE.PERSIST
+ );
+ await checkTab(
+ window,
+ SEARCH_SERP_URL,
+ UrlbarProviderSearchTips.TIP_TYPE.NONE
+ );
+ await SpecialPowers.popPrefEnv();
+});
+
+// The persist tip should not show in a window
+// with a selected tab containing a non-SERP url.
+add_task(async function noPersistTipInWindowWithNonSerpTab() {
+ await setDefaultEngine("Example");
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.showSearchTerms.featureGate", true]],
+ });
+
+ // Create a new window for the SERP to be loaded into.
+ let newWindow = await BrowserTestUtils.openNewBrowserWindow();
+
+ // Focus on the original window.
+ window.focus();
+ await waitForBrowserWindowActive(window);
+
+ // Load the SERP in the new window to initiate a background load.
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ newWindow.gBrowser.selectedBrowser,
+ false,
+ SEARCH_SERP_URL
+ );
+ BrowserTestUtils.loadURIString(
+ newWindow.gBrowser.selectedBrowser,
+ SEARCH_SERP_URL
+ );
+ await browserLoadedPromise;
+
+ // Wait longer than the persist tip delay to check that the search tip
+ // doesn't show on the non-SERP tab.
+ await new Promise(resolve =>
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(resolve, UrlbarProviderSearchTips.SHOW_PERSIST_TIP_DELAY_MS * 2)
+ );
+ Assert.ok(!window.gURLBar.view.isOpen);
+
+ // Clean up.
+ await BrowserTestUtils.closeWindow(newWindow);
+ await SpecialPowers.popPrefEnv();
+ resetSearchTipsProvider();
+});
+
+// Tips should be shown at most once per session regardless of their type.
+add_task(async function oncePerSession() {
+ await setDefaultEngine("Google");
+ await checkTab(
+ window,
+ "about:newtab",
+ UrlbarProviderSearchTips.TIP_TYPE.ONBOARD,
+ false
+ );
+ await checkTab(
+ window,
+ "about:newtab",
+ UrlbarProviderSearchTips.TIP_TYPE.NONE,
+ false
+ );
+ await withDNSRedirect("www.google.com", "/", async url => {
+ await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.NONE);
+ });
+ await setDefaultEngine("Example");
+ await checkTab(
+ window,
+ SEARCH_SERP_URL,
+ UrlbarProviderSearchTips.TIP_TYPE.NONE
+ );
+});
+
+// The one-off search buttons should not be shown when
+// a search tip is shown even though the search string is empty.
+add_task(async function shortcut_buttons_with_tip() {
+ await checkTab(
+ window,
+ "about:newtab",
+ UrlbarProviderSearchTips.TIP_TYPE.ONBOARD
+ );
+});
+
+// Don't show the persist search tip when the browser loads
+// a different page from the page the tip was supposed to show on.
+add_task(async function noSearchTipWhileAnotherPageLoads() {
+ await setDefaultEngine("Example");
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.showSearchTerms.featureGate", true]],
+ });
+
+ // Create a slow endpoint.
+ const SLOW_PAGE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://www.example.com"
+ ) + "slow-page.sjs";
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: SEARCH_SERP_URL,
+ });
+
+ // Load a slow URI to cause an onStateChange event but
+ // not an onLocationChange event.
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, SLOW_PAGE);
+
+ // Wait roughly for the amount of time it would take for the
+ // persist search tip to show.
+ await new Promise(resolve =>
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(resolve, UrlbarProviderSearchTips.SHOW_PERSIST_TIP_DELAY_MS * 2)
+ );
+
+ // Check the search tip didn't show while the page was loading.
+ Assert.equal(
+ UrlbarPrefs.get(
+ `tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.PERSIST}`
+ ),
+ 0,
+ "The shownCount pref should be 0."
+ );
+
+ Assert.equal(false, window.gURLBar.view.isOpen, "Urlbar should be closed.");
+
+ // Clean up.
+ await SpecialPowers.popPrefEnv();
+ resetSearchTipsProvider();
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Show the persist search tip when the browser is still loading
+// resources from the page the tip is supposed to show on.
+add_task(async function searchTipWhilePageLoads() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.showSearchTerms.featureGate", true]],
+ });
+
+ // Create a search engine endpoint that will still
+ // be loading resources on the page load.
+ const SLOW_PAGE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://www.example.com"
+ ) + "slow-page.html";
+
+ await SearchTestUtils.installSearchExtension({
+ name: "Slow Engine",
+ search_url: SLOW_PAGE,
+ search_url_get_params: "search={searchTerms}",
+ });
+ await setDefaultEngine("Slow Engine");
+
+ let engine = Services.search.getEngineByName("Slow Engine");
+ let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl(engine, "chocolate");
+
+ // Load a slow SERP.
+ await checkTab(
+ window,
+ expectedSearchUrl,
+ UrlbarProviderSearchTips.TIP_TYPE.PERSIST
+ );
+
+ // Clean up.
+ await SpecialPowers.popPrefEnv();
+ resetSearchTipsProvider();
+});
+
+// Search tips modify the userTypedValue of a tab. The next time
+// the pageproxystate is updated, the existence of the userTypedValue
+// can change the pageproxystate. In the case of the Persist Search Tip,
+// we don't want to change the pageproxystate while the Urlbar is non-focused,
+// so check that when an event causes the pageproxystate to update
+// (e.g. a SERP pushing state), the pageproxystate remains the same.
+add_task(async function persistSearchTipAfterPushState() {
+ await setDefaultEngine("Example");
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.showSearchTerms.featureGate", true]],
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: SEARCH_SERP_URL,
+ });
+
+ // Ensure the search tip is visible.
+ await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.PERSIST, false);
+ Assert.equal(
+ gURLBar.getAttribute("pageproxystate"),
+ "valid",
+ "Urlbar is should be in a valid pageproxystate."
+ );
+
+ // Mock the default SERP using the History API on an exising website.
+ // This is to trigger another call to setURI.
+ await SpecialPowers.spawn(tab.linkedBrowser, [SEARCH_SERP_URL], async url => {
+ content.history.pushState({}, "", url);
+ });
+
+ Assert.equal(
+ gURLBar.getAttribute("pageproxystate"),
+ "valid",
+ "Urlbar is should be in a valid pageproxystate."
+ );
+
+ // Clean up.
+ await SpecialPowers.popPrefEnv();
+ resetSearchTipsProvider();
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Ensure a the Persist Search Tip is non-visible when a PopupNotification
+// is already visible.
+add_task(async function persistSearchTipBeforePopupShown() {
+ await setDefaultEngine("Example");
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.showSearchTerms.featureGate", true]],
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: SEARCH_SERP_URL,
+ });
+
+ let promisePopupShown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+ PopupNotifications.show(
+ gBrowser.selectedBrowser,
+ "test-notification",
+ "This is a sample popup.",
+ "geo-notification-icon"
+ );
+ await promisePopupShown;
+
+ // Wait roughly for the amount of time it would take for the
+ // persist search tip to show.
+ await new Promise(resolve =>
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(resolve, UrlbarProviderSearchTips.SHOW_PERSIST_TIP_DELAY_MS * 2)
+ );
+
+ // Check the search tip didn't show while the page was loading.
+ Assert.equal(
+ UrlbarPrefs.get(
+ `tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.PERSIST}`
+ ),
+ 0,
+ "The shownCount pref should be 0."
+ );
+ Assert.equal(false, window.gURLBar.view.isOpen, "Urlbar should be closed.");
+
+ // Clean up.
+ await SpecialPowers.popPrefEnv();
+ resetSearchTipsProvider();
+ BrowserTestUtils.removeTab(tab);
+});
+
+// The Persist Search Tip should be hidden when a PopupNotification appears.
+add_task(async function persistSearchTipAfterPopupShown() {
+ await setDefaultEngine("Example");
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.showSearchTerms.featureGate", true]],
+ });
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: SEARCH_SERP_URL,
+ });
+
+ // Ensure the search tip is visible.
+ await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.PERSIST, false);
+
+ // Show a popup after the search tip is shown.
+ let promisePopupShown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+ PopupNotifications.show(
+ gBrowser.selectedBrowser,
+ "test-notification",
+ "This is a sample popup.",
+ "geo-notification-icon"
+ );
+ await promisePopupShown;
+
+ // The search tip should not be visible.
+ Assert.equal(false, window.gURLBar.view.isOpen, "Urlbar should be closed.");
+ Assert.equal(
+ gURLBar.getAttribute("pageproxystate"),
+ "valid",
+ "Urlbar is should be in a valid pageproxystate."
+ );
+
+ // Clean up.
+ await SpecialPowers.popPrefEnv();
+ resetSearchTipsProvider();
+ BrowserTestUtils.removeTab(tab);
+});
+
+function waitForBrowserWindowActive(win) {
+ return new Promise(resolve => {
+ if (Services.focus.activeWindow == win) {
+ resolve();
+ } else {
+ win.addEventListener(
+ "activate",
+ () => {
+ resolve();
+ },
+ { once: true }
+ );
+ }
+ });
+}
diff --git a/browser/components/urlbar/tests/browser-tips/browser_searchTips_interaction.js b/browser/components/urlbar/tests/browser-tips/browser_searchTips_interaction.js
new file mode 100644
index 0000000000..76281dfaf2
--- /dev/null
+++ b/browser/components/urlbar/tests/browser-tips/browser_searchTips_interaction.js
@@ -0,0 +1,837 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests the Search Tips feature, which displays a prompt to use the Urlbar on
+// the newtab page and on the user's default search engine's homepage.
+// Specifically, it tests that the Tips appear when they should be appearing.
+// This doesn't test the max-shown-count limit because it requires restarting
+// the browser.
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ AppMenuNotifications: "resource://gre/modules/AppMenuNotifications.sys.mjs",
+ ProfileAge: "resource://gre/modules/ProfileAge.sys.mjs",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
+ UrlbarProviderSearchTips:
+ "resource:///modules/UrlbarProviderSearchTips.sys.mjs",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ HttpServer: "resource://testing-common/httpd.js",
+});
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "clipboardHelper",
+ "@mozilla.org/widget/clipboardhelper;1",
+ "nsIClipboardHelper"
+);
+
+// These should match the same consts in UrlbarProviderSearchTips.jsm.
+const MAX_SHOWN_COUNT = 4;
+const LAST_UPDATE_THRESHOLD_MS = 24 * 60 * 60 * 1000;
+
+// We test some of the bigger Google domains.
+const GOOGLE_DOMAINS = [
+ "www.google.com",
+ "www.google.ca",
+ "www.google.co.uk",
+ "www.google.com.au",
+ "www.google.co.nz",
+];
+
+// In order for the persist tip to appear, the scheme of the
+// search engine has to be the same as the scheme of the SERP url.
+// withDNSRedirect() loads an http: url while the searchform
+// of the default engine uses https. To enable the search term
+// to be shown, we use the Example engine because it doesn't require
+// a redirect.
+const SEARCH_TERM = "chocolate";
+const SEARCH_SERP_URL = `https://example.com/?q=${SEARCH_TERM}`;
+
+add_setup(async function () {
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ `browser.urlbar.tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.ONBOARD}`,
+ 0,
+ ],
+ [
+ `browser.urlbar.tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.PERSIST}`,
+ 0,
+ ],
+ [
+ `browser.urlbar.tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.REDIRECT}`,
+ 0,
+ ],
+ ],
+ });
+
+ // Write an old profile age so tips are actually shown.
+ let age = await ProfileAge();
+ let originalTimes = age._times;
+ let date = Date.now() - LAST_UPDATE_THRESHOLD_MS - 30000;
+ age._times = { created: date, firstUse: date };
+ await age.writeTimes();
+
+ // Remove update history and the current active update so tips are shown.
+ let updateRootDir = Services.dirsvc.get("UpdRootD", Ci.nsIFile);
+ let updatesFile = updateRootDir.clone();
+ updatesFile.append("updates.xml");
+ let activeUpdateFile = updateRootDir.clone();
+ activeUpdateFile.append("active-update.xml");
+ try {
+ updatesFile.remove(false);
+ } catch (e) {}
+ try {
+ activeUpdateFile.remove(false);
+ } catch (e) {}
+
+ let defaultEngine = await Services.search.getDefault();
+ let defaultEngineName = defaultEngine.name;
+ Assert.equal(defaultEngineName, "Google", "Default engine should be Google.");
+
+ // Add a mock engine so we don't hit the network loading the SERP.
+ await SearchTestUtils.installSearchExtension();
+
+ registerCleanupFunction(async () => {
+ let age2 = await ProfileAge();
+ age2._times = originalTimes;
+ await age2.writeTimes();
+ await setDefaultEngine(defaultEngineName);
+ resetSearchTipsProvider();
+ });
+});
+
+// Picking the tip's button should cause the Urlbar to blank out and the tip to
+// be not to be shown again in any session. Telemetry should be updated.
+add_task(async function pickButton_onboard() {
+ UrlbarProviderSearchTips.disableTipsForCurrentSession = false;
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.eventTelemetry.enabled", true]],
+ });
+
+ Services.telemetry.clearEvents();
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:newtab",
+ waitForLoad: false,
+ });
+ await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.ONBOARD, false);
+
+ // Click the tip button.
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ let button = result.element.row._buttons.get("0");
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ });
+ gURLBar.blur();
+
+ // Check telemetry.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ `${UrlbarProviderSearchTips.TIP_TYPE.ONBOARD}-picked`,
+ 1
+ );
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ category: "urlbar",
+ method: "engagement",
+ object: "click",
+ value: "typed",
+ },
+ ],
+ { category: "urlbar" }
+ );
+
+ Assert.equal(
+ UrlbarPrefs.get(
+ `tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.ONBOARD}`
+ ),
+ MAX_SHOWN_COUNT,
+ "Onboarding tips are disabled after tip button is picked."
+ );
+ Assert.equal(gURLBar.value, "", "The Urlbar should be empty.");
+ resetSearchTipsProvider();
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+// Picking the tip's button should cause the Urlbar to blank out and the tip to
+// be not to be shown again in any session. Telemetry should be updated.
+add_task(async function pickButton_redirect() {
+ UrlbarProviderSearchTips.disableTipsForCurrentSession = false;
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.eventTelemetry.enabled", true]],
+ });
+ Services.telemetry.clearEvents();
+
+ await setDefaultEngine("Google");
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await withDNSRedirect("www.google.com", "/", async url => {
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, url);
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.REDIRECT, false);
+
+ // Click the tip button.
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ let button = result.element.row._buttons.get("0");
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ });
+ gURLBar.blur();
+ });
+ });
+
+ // Check telemetry.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ `${UrlbarProviderSearchTips.TIP_TYPE.REDIRECT}-picked`,
+ 1
+ );
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ category: "urlbar",
+ method: "engagement",
+ object: "click",
+ value: "typed",
+ },
+ ],
+ { category: "urlbar" }
+ );
+
+ Assert.equal(
+ UrlbarPrefs.get(
+ `tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.REDIRECT}`
+ ),
+ MAX_SHOWN_COUNT,
+ "Redirect tips are disabled after tip button is picked."
+ );
+ Assert.equal(gURLBar.value, "", "The Urlbar should be empty.");
+ resetSearchTipsProvider();
+ await SpecialPowers.popPrefEnv();
+});
+
+// Picking the tip's button should cause the Urlbar to keep its current
+// value and the tip to be not to be shown again in any session.
+// Telemetry should be updated.
+add_task(async function pickButton_persist() {
+ UrlbarProviderSearchTips.disableTipsForCurrentSession = false;
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.eventTelemetry.enabled", true],
+ ["browser.urlbar.showSearchTerms.featureGate", true],
+ ],
+ });
+ Services.telemetry.clearEvents();
+
+ await setDefaultEngine("Example");
+
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ SEARCH_SERP_URL
+ );
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, SEARCH_SERP_URL);
+ await browserLoadedPromise;
+ await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.PERSIST, false);
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ let button = result.element.row._buttons.get("0");
+
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ });
+ gURLBar.blur();
+
+ Assert.equal(
+ gURLBar.value,
+ SEARCH_TERM,
+ "The Urlbar should keep its existing value."
+ );
+ });
+
+ // Check telemetry.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ `${UrlbarProviderSearchTips.TIP_TYPE.PERSIST}-picked`,
+ 1
+ );
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ category: "urlbar",
+ method: "engagement",
+ object: "click",
+ value: "typed",
+ },
+ ],
+ { category: "urlbar" }
+ );
+
+ Assert.equal(
+ UrlbarPrefs.get(
+ `tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.PERSIST}`
+ ),
+ MAX_SHOWN_COUNT,
+ "Persist tips are disabled after tip button is picked."
+ );
+ Assert.equal(gURLBar.value, "", "The Urlbar should be empty.");
+ resetSearchTipsProvider();
+ await SpecialPowers.popPrefEnv();
+});
+
+// Clicking in the input while the onboard tip is showing should have the same
+// effect as picking the tip.
+add_task(async function clickInInput_onboard() {
+ UrlbarProviderSearchTips.disableTipsForCurrentSession = false;
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.eventTelemetry.enabled", true]],
+ });
+ Services.telemetry.clearEvents();
+
+ await setDefaultEngine("Google");
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:newtab",
+ waitForLoad: false,
+ });
+ await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.ONBOARD, false);
+
+ // Click in the input.
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeMouseAtCenter(gURLBar.textbox.parentNode, {});
+ });
+ gURLBar.blur();
+
+ // Check telemetry.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ `${UrlbarProviderSearchTips.TIP_TYPE.ONBOARD}-picked`,
+ 1
+ );
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ category: "urlbar",
+ method: "engagement",
+ object: "click",
+ value: "typed",
+ },
+ ],
+ { category: "urlbar" }
+ );
+
+ Assert.equal(
+ UrlbarPrefs.get(
+ `tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.ONBOARD}`
+ ),
+ MAX_SHOWN_COUNT,
+ "Onboarding tips are disabled after tip button is picked."
+ );
+ Assert.equal(gURLBar.value, "", "The Urlbar should be empty.");
+ resetSearchTipsProvider();
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+// Pressing Ctrl+L (the open location command) while the onboard tip is showing
+// should have the same effect as picking the tip.
+add_task(async function openLocation_onboard() {
+ UrlbarProviderSearchTips.disableTipsForCurrentSession = false;
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.eventTelemetry.enabled", true]],
+ });
+ Services.telemetry.clearEvents();
+
+ await setDefaultEngine("Google");
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:newtab",
+ waitForLoad: false,
+ });
+ await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.ONBOARD, false);
+
+ // Trigger the open location command.
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ document.getElementById("Browser:OpenLocation").doCommand();
+ });
+ gURLBar.blur();
+
+ // Check telemetry.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ `${UrlbarProviderSearchTips.TIP_TYPE.ONBOARD}-picked`,
+ 1
+ );
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ },
+ ],
+ { category: "urlbar" }
+ );
+
+ Assert.equal(
+ UrlbarPrefs.get(
+ `tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.ONBOARD}`
+ ),
+ MAX_SHOWN_COUNT,
+ "Onboarding tips are disabled after tip button is picked."
+ );
+ Assert.equal(gURLBar.value, "", "The Urlbar should be empty.");
+ resetSearchTipsProvider();
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+// Clicking in the input while the redirect tip is showing should have the same
+// effect as picking the tip.
+add_task(async function clickInInput_redirect() {
+ UrlbarProviderSearchTips.disableTipsForCurrentSession = false;
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.eventTelemetry.enabled", true]],
+ });
+ Services.telemetry.clearEvents();
+
+ await setDefaultEngine("Google");
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await withDNSRedirect("www.google.com", "/", async url => {
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, url);
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.REDIRECT, false);
+
+ // Click in the input.
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeMouseAtCenter(gURLBar.textbox.parentNode, {});
+ });
+ gURLBar.blur();
+ });
+ });
+
+ // Check telemetry.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ `${UrlbarProviderSearchTips.TIP_TYPE.REDIRECT}-picked`,
+ 1
+ );
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ category: "urlbar",
+ method: "engagement",
+ object: "click",
+ value: "typed",
+ },
+ ],
+ { category: "urlbar" }
+ );
+
+ Assert.equal(
+ UrlbarPrefs.get(
+ `tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.REDIRECT}`
+ ),
+ MAX_SHOWN_COUNT,
+ "Redirect tips are disabled after tip button is picked."
+ );
+ Assert.equal(gURLBar.value, "", "The Urlbar should be empty.");
+ resetSearchTipsProvider();
+ await SpecialPowers.popPrefEnv();
+});
+
+// Clicking in the input while the persist tip is showing should have the same
+// effect as picking the tip.
+add_task(async function clickInInput_persist() {
+ UrlbarProviderSearchTips.disableTipsForCurrentSession = false;
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.eventTelemetry.enabled", true],
+ ["browser.urlbar.showSearchTerms.featureGate", true],
+ ],
+ });
+ Services.telemetry.clearEvents();
+
+ await setDefaultEngine("Example");
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ SEARCH_SERP_URL
+ );
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, SEARCH_SERP_URL);
+ await browserLoadedPromise;
+ await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.PERSIST, false);
+
+ // Click in the input.
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeMouseAtCenter(gURLBar.textbox.parentNode, {});
+ });
+ gURLBar.blur();
+ Assert.equal(
+ gURLBar.value,
+ SEARCH_TERM,
+ "The Urlbar should keep its existing value."
+ );
+ });
+
+ // Check telemetry.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ `${UrlbarProviderSearchTips.TIP_TYPE.PERSIST}-picked`,
+ 1
+ );
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ category: "urlbar",
+ method: "engagement",
+ object: "click",
+ value: "typed",
+ },
+ ],
+ { category: "urlbar" }
+ );
+
+ Assert.equal(
+ UrlbarPrefs.get(
+ `tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.PERSIST}`
+ ),
+ MAX_SHOWN_COUNT,
+ "Persist tips are disabled after tip button is picked."
+ );
+ Assert.equal(gURLBar.value, "", "The Urlbar should be empty.");
+ resetSearchTipsProvider();
+ await SpecialPowers.popPrefEnv();
+});
+
+// Pressing Ctrl+L (the open location command) while the redirect tip is showing
+// should have the same effect as picking the tip.
+add_task(async function openLocation_redirect() {
+ UrlbarProviderSearchTips.disableTipsForCurrentSession = false;
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.eventTelemetry.enabled", true]],
+ });
+ Services.telemetry.clearEvents();
+
+ await setDefaultEngine("Google");
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await withDNSRedirect("www.google.com", "/", async url => {
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, url);
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.REDIRECT, false);
+
+ // Trigger the open location command.
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ document.getElementById("Browser:OpenLocation").doCommand();
+ });
+ gURLBar.blur();
+ });
+ });
+
+ // Check telemetry.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ `${UrlbarProviderSearchTips.TIP_TYPE.REDIRECT}-picked`,
+ 1
+ );
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ },
+ ],
+ { category: "urlbar" }
+ );
+
+ Assert.equal(
+ UrlbarPrefs.get(
+ `tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.REDIRECT}`
+ ),
+ MAX_SHOWN_COUNT,
+ "Redirect tips are disabled after tip button is picked."
+ );
+ Assert.equal(gURLBar.value, "", "The Urlbar should be empty.");
+ resetSearchTipsProvider();
+ await SpecialPowers.popPrefEnv();
+});
+
+// Pressing Ctrl+L (the open location command) while the persist tip is showing
+// should have the same effect as picking the tip.
+add_task(async function openLocation_persist() {
+ UrlbarProviderSearchTips.disableTipsForCurrentSession = false;
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.eventTelemetry.enabled", true],
+ ["browser.urlbar.showSearchTerms.featureGate", true],
+ ],
+ });
+ Services.telemetry.clearEvents();
+
+ await setDefaultEngine("Example");
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ SEARCH_SERP_URL
+ );
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, SEARCH_SERP_URL);
+ await browserLoadedPromise;
+ await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.PERSIST, false);
+
+ // Trigger the open location command.
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ document.getElementById("Browser:OpenLocation").doCommand();
+ });
+ gURLBar.blur();
+ Assert.equal(
+ gURLBar.value,
+ SEARCH_TERM,
+ "The Urlbar should keep its existing value."
+ );
+ });
+
+ // Check telemetry.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ `${UrlbarProviderSearchTips.TIP_TYPE.PERSIST}-picked`,
+ 1
+ );
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ },
+ ],
+ { category: "urlbar" }
+ );
+
+ Assert.equal(
+ UrlbarPrefs.get(
+ `tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.PERSIST}`
+ ),
+ MAX_SHOWN_COUNT,
+ "Persist tips are disabled after tip button is picked."
+ );
+ Assert.equal(gURLBar.value, "", "The Urlbar should be empty.");
+ resetSearchTipsProvider();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function pickingTipDoesNotDisableOtherKinds() {
+ UrlbarProviderSearchTips.disableTipsForCurrentSession = false;
+ await setDefaultEngine("Google");
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:newtab",
+ waitForLoad: false,
+ });
+ await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.ONBOARD, false);
+
+ // Click the tip button.
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ let button = result.element.row._buttons.get("0");
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ });
+
+ gURLBar.blur();
+ Assert.equal(
+ UrlbarPrefs.get(
+ `tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.ONBOARD}`
+ ),
+ MAX_SHOWN_COUNT,
+ "Onboarding tips are disabled after tip button is picked."
+ );
+
+ BrowserTestUtils.removeTab(tab);
+
+ // Simulate a new session.
+ UrlbarProviderSearchTips.disableTipsForCurrentSession = false;
+
+ // Onboarding tips should no longer be shown.
+ let tab2 = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:newtab",
+ waitForLoad: false,
+ });
+ await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.NONE);
+
+ // We should still show redirect tips.
+ await withDNSRedirect("www.google.com", "/", async url => {
+ await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.REDIRECT);
+ });
+
+ BrowserTestUtils.removeTab(tab2);
+ resetSearchTipsProvider();
+});
+
+// The tip shouldn't be shown when there's another notification present.
+add_task(async function notification() {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ let box = gBrowser.getNotificationBox();
+ let note = box.appendNotification("urlbar-test", {
+ label: "Test",
+ priority: box.PRIORITY_INFO_HIGH,
+ });
+ // Give it a big persistence so it doesn't go away on page load.
+ note.persistence = 100;
+ await withDNSRedirect("www.google.com", "/", async url => {
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, url);
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.NONE);
+ box.removeNotification(note, true);
+ });
+ });
+ resetSearchTipsProvider();
+});
+
+// The tip should be shown when switching to a tab where it should be shown.
+add_task(async function tabSwitch() {
+ let tab = BrowserTestUtils.addTab(gBrowser, "about:newtab");
+ UrlbarProviderSearchTips.disableTipsForCurrentSession = false;
+ Services.telemetry.clearScalars();
+ await BrowserTestUtils.switchTab(gBrowser, tab);
+ await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.ONBOARD);
+ BrowserTestUtils.removeTab(tab);
+ resetSearchTipsProvider();
+});
+
+// The engagement event should be ended if the user ignores a tip.
+// See bug 1610024.
+add_task(async function ignoreEndsEngagement() {
+ await setDefaultEngine("Google");
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await withDNSRedirect("www.google.com", "/", async url => {
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, url);
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.REDIRECT, false);
+ // We're just looking for any target outside the Urlbar.
+ let spring = gURLBar.inputField
+ .closest("#nav-bar")
+ .querySelector("toolbarspring");
+ await UrlbarTestUtils.promisePopupClose(window, async () => {
+ await EventUtils.synthesizeMouseAtCenter(spring, {});
+ });
+ Assert.ok(
+ UrlbarProviderSearchTips.showedTipTypeInCurrentEngagement ==
+ UrlbarProviderSearchTips.TIP_TYPE.NONE,
+ "The engagement should have ended after the tip was ignored."
+ );
+ });
+ });
+ resetSearchTipsProvider();
+});
+
+add_task(async function pasteAndGo_url() {
+ await doPasteAndGoTest("http://example.com/", "http://example.com/");
+});
+
+add_task(async function pasteAndGo_nonURL() {
+ await setDefaultEngine("Example");
+ await doPasteAndGoTest(
+ "pasteAndGo_nonURL",
+ "https://example.com/?q=pasteAndGo_nonURL"
+ );
+ await setDefaultEngine("Google");
+});
+
+async function doPasteAndGoTest(searchString, expectedURL) {
+ UrlbarProviderSearchTips.disableTipsForCurrentSession = false;
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:newtab",
+ waitForLoad: false,
+ });
+ await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.ONBOARD, false);
+
+ await SimpleTest.promiseClipboardChange(searchString, () => {
+ clipboardHelper.copyString(searchString);
+ });
+
+ let textBox = gURLBar.querySelector("moz-input-box");
+ let cxmenu = textBox.menupopup;
+ let cxmenuPromise = BrowserTestUtils.waitForEvent(cxmenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {
+ type: "contextmenu",
+ button: 2,
+ });
+ await cxmenuPromise;
+ let menuitem = textBox.getMenuItem("paste-and-go");
+
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ expectedURL
+ );
+ cxmenu.activateItem(menuitem);
+ await browserLoadedPromise;
+ BrowserTestUtils.removeTab(tab);
+ resetSearchTipsProvider();
+}
+
+// Since we coupled the logic that decides whether to show the tip with our
+// gURLBar.search call, we should make sure search isn't called when
+// the conditions for a tip are met but the provider is disabled.
+add_task(async function noActionWhenDisabled() {
+ await setDefaultEngine("Bing");
+ await withDNSRedirect("www.bing.com", "/", async url => {
+ await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.REDIRECT);
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features",
+ false,
+ ],
+ ],
+ });
+
+ await withDNSRedirect("www.bing.com", "/", async url => {
+ Assert.ok(
+ !UrlbarTestUtils.isPopupOpen(window),
+ "The UrlbarView should not be open."
+ );
+ });
+
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/urlbar/tests/browser-tips/browser_selection.js b/browser/components/urlbar/tests/browser-tips/browser_selection.js
new file mode 100644
index 0000000000..89206b123e
--- /dev/null
+++ b/browser/components/urlbar/tests/browser-tips/browser_selection.js
@@ -0,0 +1,269 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests keyboard selection within UrlbarUtils.RESULT_TYPE.TIP results.
+
+"use strict";
+
+const HELP_URL = "about:mozilla";
+const TIP_URL = "about:about";
+
+add_task(async function tipIsSecondResult() {
+ let results = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/a" }
+ ),
+ makeTipResult({ buttonUrl: TIP_URL, helpUrl: HELP_URL }),
+ ];
+
+ let provider = new UrlbarTestUtils.TestProvider({ results, priority: 1 });
+ UrlbarProvidersManager.registerProvider(provider);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ value: "test",
+ window,
+ });
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 2,
+ "There should be two results in the view."
+ );
+ let secondResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(
+ secondResult.type,
+ UrlbarUtils.RESULT_TYPE.TIP,
+ "The second result should be a tip."
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElementIndex(window),
+ 0,
+ "The first element should be selected."
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.ok(
+ UrlbarTestUtils.getSelectedElement(window).classList.contains(
+ "urlbarView-button-0"
+ ),
+ "The selected element should be the tip button."
+ );
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElementIndex(window),
+ UrlbarPrefs.get("resultMenu") ? 2 : 1,
+ "Selected element index"
+ );
+
+ EventUtils.synthesizeKey("KEY_Tab");
+ Assert.ok(
+ UrlbarTestUtils.getSelectedElement(window).classList.contains(
+ UrlbarPrefs.get("resultMenu")
+ ? "urlbarView-button-menu"
+ : "urlbarView-button-help"
+ ),
+ UrlbarPrefs.get("resultMenu")
+ ? "The selected element should be the tip menu button."
+ : "The selected element should be the tip help button."
+ );
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 1,
+ UrlbarPrefs.get("resultMenu")
+ ? "getSelectedRowIndex should return 1 even though the menu button is selected."
+ : "getSelectedRowIndex should return 1 even though the help button is selected."
+ );
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElementIndex(window),
+ UrlbarPrefs.get("resultMenu") ? 3 : 2,
+ "Selected element index"
+ );
+
+ // If this test is running alone, the one-offs will rebuild themselves when
+ // the view is opened above, and they may not be visible yet. Wait for the
+ // first one to become visible before trying to select it.
+ await TestUtils.waitForCondition(() => {
+ return (
+ gURLBar.view.oneOffSearchButtons.buttons.firstElementChild &&
+ BrowserTestUtils.is_visible(
+ gURLBar.view.oneOffSearchButtons.buttons.firstElementChild
+ )
+ );
+ }, "Waiting for first one-off to become visible.");
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await TestUtils.waitForCondition(() => {
+ return gURLBar.view.oneOffSearchButtons.selectedButton;
+ }, "Waiting for one-off to become selected.");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElementIndex(window),
+ -1,
+ "No results should be selected."
+ );
+
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
+ Assert.ok(
+ UrlbarTestUtils.getSelectedElement(window).classList.contains(
+ UrlbarPrefs.get("resultMenu")
+ ? "urlbarView-button-menu"
+ : "urlbarView-button-help"
+ ),
+ UrlbarPrefs.get("resultMenu")
+ ? "The selected element should be the tip menu button."
+ : "The selected element should be the tip help button."
+ );
+
+ gURLBar.view.close();
+ UrlbarProvidersManager.unregisterProvider(provider);
+});
+
+add_task(async function tipIsOnlyResult() {
+ let results = [makeTipResult({ buttonUrl: TIP_URL, helpUrl: HELP_URL })];
+
+ let provider = new UrlbarTestUtils.TestProvider({ results, priority: 1 });
+ UrlbarProvidersManager.registerProvider(provider);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ value: "test",
+ window,
+ });
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 1,
+ "There should be one result in the view."
+ );
+ let firstResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ firstResult.type,
+ UrlbarUtils.RESULT_TYPE.TIP,
+ "The first and only result should be a tip."
+ );
+
+ EventUtils.synthesizeKey("KEY_Tab");
+ Assert.ok(
+ UrlbarTestUtils.getSelectedElement(window).classList.contains(
+ "urlbarView-button-0"
+ ),
+ "The selected element should be the tip button."
+ );
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElementIndex(window),
+ 0,
+ "The first element should be selected."
+ );
+
+ EventUtils.synthesizeKey("KEY_Tab");
+ Assert.ok(
+ UrlbarTestUtils.getSelectedElement(window).classList.contains(
+ UrlbarPrefs.get("resultMenu")
+ ? "urlbarView-button-menu"
+ : "urlbarView-button-help"
+ ),
+ UrlbarPrefs.get("resultMenu")
+ ? "The selected element should be the tip menu button."
+ : "The selected element should be the tip help button."
+ );
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElementIndex(window),
+ 1,
+ "The second element should be selected."
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElementIndex(window),
+ -1,
+ "There should be no selection."
+ );
+
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
+ Assert.ok(
+ UrlbarTestUtils.getSelectedElement(window).classList.contains(
+ UrlbarPrefs.get("resultMenu")
+ ? "urlbarView-button-menu"
+ : "urlbarView-button-help"
+ ),
+ UrlbarPrefs.get("resultMenu")
+ ? "The selected element should be the tip menu button."
+ : "The selected element should be the tip help button."
+ );
+
+ gURLBar.view.close();
+ UrlbarProvidersManager.unregisterProvider(provider);
+});
+
+add_task(async function tipHasNoHelpButton() {
+ let results = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/a" }
+ ),
+ makeTipResult({ buttonUrl: TIP_URL }),
+ ];
+
+ let provider = new UrlbarTestUtils.TestProvider({ results, priority: 1 });
+ UrlbarProvidersManager.registerProvider(provider);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ value: "test",
+ window,
+ });
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 2,
+ "There should be two results in the view."
+ );
+ let secondResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(
+ secondResult.type,
+ UrlbarUtils.RESULT_TYPE.TIP,
+ "The second result should be a tip."
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElementIndex(window),
+ 0,
+ "The first element should be selected."
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.ok(
+ UrlbarTestUtils.getSelectedElement(window).classList.contains(
+ "urlbarView-button-0"
+ ),
+ "The selected element should be the tip button."
+ );
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElementIndex(window),
+ UrlbarPrefs.get("resultMenu") ? 2 : 1,
+ "Selected element index"
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await TestUtils.waitForCondition(() => {
+ return gURLBar.view.oneOffSearchButtons.selectedButton;
+ }, "Waiting for one-off to become selected.");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElementIndex(window),
+ -1,
+ "No results should be selected."
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ Assert.ok(
+ UrlbarTestUtils.getSelectedElement(window).classList.contains(
+ "urlbarView-button-0"
+ ),
+ "The selected element should be the tip button."
+ );
+
+ gURLBar.view.close();
+ UrlbarProvidersManager.unregisterProvider(provider);
+});
diff --git a/browser/components/urlbar/tests/browser-tips/browser_updateAsk.js b/browser/components/urlbar/tests/browser-tips/browser_updateAsk.js
new file mode 100644
index 0000000000..3f2014d8c0
--- /dev/null
+++ b/browser/components/urlbar/tests/browser-tips/browser_updateAsk.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Checks the UPDATE_ASK tip.
+//
+// The update parts of this test are adapted from:
+// https://searchfox.org/mozilla-central/source/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_downloadOptIn.js
+
+"use strict";
+
+let params = { queryString: "&invalidCompleteSize=1" };
+
+let downloadInfo = [];
+if (Services.prefs.getBoolPref(PREF_APP_UPDATE_BITS_ENABLED, false)) {
+ downloadInfo[0] = { patchType: "partial", bitsResult: "0" };
+} else {
+ downloadInfo[0] = { patchType: "partial", internalResult: "0" };
+}
+
+let preSteps = [
+ {
+ panelId: "checkingForUpdates",
+ checkActiveUpdate: null,
+ continueFile: CONTINUE_CHECK,
+ },
+ {
+ panelId: "downloadAndInstall",
+ checkActiveUpdate: null,
+ continueFile: null,
+ },
+];
+
+let postSteps = [
+ {
+ panelId: "downloading",
+ checkActiveUpdate: { state: STATE_DOWNLOADING },
+ continueFile: CONTINUE_DOWNLOAD,
+ downloadInfo,
+ },
+ {
+ panelId: "apply",
+ checkActiveUpdate: { state: STATE_PENDING },
+ continueFile: null,
+ },
+];
+
+add_task(async function test() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.quickactions", false]],
+ });
+
+ // Disable the pref that automatically downloads and installs updates.
+ await UpdateUtils.setAppUpdateAutoEnabled(false);
+
+ // Set up the "download and install" update state.
+ await initUpdate(params);
+ UrlbarProviderInterventions.checkForBrowserUpdate(true);
+ await processUpdateSteps(preSteps);
+
+ // Pick the tip and continue with the mock update, which should attempt to
+ // restart the browser.
+ await doUpdateTest({
+ searchString: SEARCH_STRINGS.UPDATE,
+ tip: UrlbarProviderInterventions.TIP_TYPE.UPDATE_ASK,
+ title: /^A new version of .+ is available\.$/,
+ button: "Install and Restart to Update",
+ awaitCallback() {
+ return Promise.all([
+ processUpdateSteps(postSteps),
+ awaitAppRestartRequest(),
+ ]);
+ },
+ });
+});
diff --git a/browser/components/urlbar/tests/browser-tips/browser_updateRefresh.js b/browser/components/urlbar/tests/browser-tips/browser_updateRefresh.js
new file mode 100644
index 0000000000..5e94298996
--- /dev/null
+++ b/browser/components/urlbar/tests/browser-tips/browser_updateRefresh.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Checks the UPDATE_REFRESH tip.
+//
+// The update parts of this test are adapted from:
+// https://searchfox.org/mozilla-central/source/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_check_noUpdate.js
+
+"use strict";
+
+let params = { queryString: "&noUpdates=1" };
+
+let preSteps = [
+ {
+ panelId: "checkingForUpdates",
+ checkActiveUpdate: null,
+ continueFile: CONTINUE_CHECK,
+ },
+ {
+ panelId: "noUpdatesFound",
+ checkActiveUpdate: null,
+ continueFile: null,
+ },
+];
+
+add_task(async function test() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.quickactions", false]],
+ });
+
+ makeProfileResettable();
+
+ // Set up the "no updates" update state.
+ await initUpdate(params);
+ UrlbarProviderInterventions.checkForBrowserUpdate(true);
+ await processUpdateSteps(preSteps);
+
+ // Picking the tip should open the refresh dialog. Click its cancel
+ // button.
+ await doUpdateTest({
+ searchString: SEARCH_STRINGS.UPDATE,
+ tip: UrlbarProviderInterventions.TIP_TYPE.UPDATE_REFRESH,
+ title:
+ /^.+ is up to date\. Trying to fix a problem\? Restore default settings and remove old add-ons for optimal performance\.$/,
+ button: /^Refresh .+…$/,
+ awaitCallback() {
+ return BrowserTestUtils.promiseAlertDialog(
+ "cancel",
+ "chrome://global/content/resetProfile.xhtml",
+ { isSubDialog: true }
+ );
+ },
+ });
+});
diff --git a/browser/components/urlbar/tests/browser-tips/browser_updateRestart.js b/browser/components/urlbar/tests/browser-tips/browser_updateRestart.js
new file mode 100644
index 0000000000..75e92910f0
--- /dev/null
+++ b/browser/components/urlbar/tests/browser-tips/browser_updateRestart.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Checks the UPDATE_RESTART tip.
+//
+// The update parts of this test are adapted from:
+// https://searchfox.org/mozilla-central/source/toolkit/mozapps/update/tests/browser/browser_aboutDialog_bc_downloaded_staged.js
+
+"use strict";
+
+let params = {
+ queryString: "&invalidCompleteSize=1",
+ backgroundUpdate: true,
+ continueFile: CONTINUE_STAGING,
+ waitForUpdateState: STATE_APPLIED,
+};
+
+let preSteps = [
+ {
+ panelId: "apply",
+ checkActiveUpdate: { state: STATE_APPLIED },
+ continueFile: null,
+ },
+];
+
+add_task(async function test() {
+ // Enable the pref that automatically downloads and installs updates.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [PREF_APP_UPDATE_STAGING_ENABLED, true],
+ ["browser.urlbar.suggest.quickactions", false],
+ ],
+ });
+
+ // Set up the "apply" update state.
+ await initUpdate(params);
+ UrlbarProviderInterventions.checkForBrowserUpdate(true);
+ await processUpdateSteps(preSteps);
+
+ // Picking the tip should attempt to restart the browser.
+ await doUpdateTest({
+ searchString: SEARCH_STRINGS.UPDATE,
+ tip: UrlbarProviderInterventions.TIP_TYPE.UPDATE_RESTART,
+ title: /^The latest .+ is downloaded and ready to install\.$/,
+ button: "Restart to Update",
+ awaitCallback: awaitAppRestartRequest,
+ });
+});
diff --git a/browser/components/urlbar/tests/browser-tips/browser_updateWeb.js b/browser/components/urlbar/tests/browser-tips/browser_updateWeb.js
new file mode 100644
index 0000000000..daca12fea4
--- /dev/null
+++ b/browser/components/urlbar/tests/browser-tips/browser_updateWeb.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Checks the UPDATE_WEB tip.
+//
+// The update parts of this test are adapted from:
+// https://searchfox.org/mozilla-central/source/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_check_unsupported.js
+
+"use strict";
+
+let params = { queryString: "&unsupported=1" };
+
+let preSteps = [
+ {
+ panelId: "checkingForUpdates",
+ checkActiveUpdate: null,
+ continueFile: CONTINUE_CHECK,
+ },
+ {
+ panelId: "unsupportedSystem",
+ checkActiveUpdate: null,
+ continueFile: null,
+ },
+];
+
+add_task(async function test() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.quickactions", false]],
+ });
+
+ // Set up the "unsupported update" update state.
+ await initUpdate(params);
+ UrlbarProviderInterventions.checkForBrowserUpdate(true);
+ await processUpdateSteps(preSteps);
+
+ // Picking the tip should open the download page in a new tab.
+ let downloadTab = await doUpdateTest({
+ searchString: SEARCH_STRINGS.UPDATE,
+ tip: UrlbarProviderInterventions.TIP_TYPE.UPDATE_WEB,
+ title: /^Get the latest .+ browser\.$/,
+ button: "Download Now",
+ awaitCallback() {
+ return BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "https://www.mozilla.org/firefox/new/"
+ );
+ },
+ });
+
+ Assert.equal(gBrowser.selectedTab, downloadTab);
+ BrowserTestUtils.removeTab(downloadTab);
+});
diff --git a/browser/components/urlbar/tests/browser-tips/head.js b/browser/components/urlbar/tests/browser-tips/head.js
new file mode 100644
index 0000000000..d6797f5338
--- /dev/null
+++ b/browser/components/urlbar/tests/browser-tips/head.js
@@ -0,0 +1,771 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This directory contains tests that check tips and interventions, and in
+// particular the update-related interventions.
+// We mock updates by using the test helpers in
+// toolkit/mozapps/update/tests/browser.
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/mozapps/update/tests/browser/head.js",
+ this
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ ResetProfile: "resource://gre/modules/ResetProfile.sys.mjs",
+ TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
+ UrlbarProviderInterventions:
+ "resource:///modules/UrlbarProviderInterventions.sys.mjs",
+ UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs",
+ UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ HttpServer: "resource://testing-common/httpd.js",
+});
+
+XPCOMUtils.defineLazyGetter(this, "UrlbarTestUtils", () => {
+ const { UrlbarTestUtils: module } = ChromeUtils.importESModule(
+ "resource://testing-common/UrlbarTestUtils.sys.mjs"
+ );
+ module.init(this);
+ return module;
+});
+
+XPCOMUtils.defineLazyGetter(this, "SearchTestUtils", () => {
+ const { SearchTestUtils: module } = ChromeUtils.importESModule(
+ "resource://testing-common/SearchTestUtils.sys.mjs"
+ );
+ module.init(this);
+ return module;
+});
+
+// For each intervention type, a search string that trigger the intervention.
+const SEARCH_STRINGS = {
+ CLEAR: "firefox history",
+ REFRESH: "firefox slow",
+ UPDATE: "firefox update",
+};
+
+registerCleanupFunction(() => {
+ // We need to reset the provider's appUpdater.status between tests so that
+ // each test doesn't interfere with the next.
+ UrlbarProviderInterventions.resetAppUpdater();
+});
+
+/**
+ * Override our binary path so that the update lock doesn't think more than one
+ * instance of this test is running.
+ * This is a heavily pared down copy of the function in xpcshellUtilsAUS.js.
+ */
+function adjustGeneralPaths() {
+ let dirProvider = {
+ getFile(aProp, aPersistent) {
+ // Set the value of persistent to false so when this directory provider is
+ // unregistered it will revert back to the original provider.
+ aPersistent.value = false;
+ // The sync manager only uses XRE_EXECUTABLE_FILE, so that's all we need
+ // to override, we won't bother handling anything else.
+ if (aProp == XRE_EXECUTABLE_FILE) {
+ // The temp directory that the mochitest runner creates is unique per
+ // test, so its path can serve to provide the unique key that the update
+ // sync manager requires (it doesn't need for this to be the actual
+ // path to any real file, it's only used as an opaque string).
+ let tempPath = Services.env.get("MOZ_PROCESS_LOG");
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.initWithPath(tempPath);
+ return file;
+ }
+ return null;
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIDirectoryServiceProvider"]),
+ };
+
+ let ds = Services.dirsvc.QueryInterface(Ci.nsIDirectoryService);
+ try {
+ ds.QueryInterface(Ci.nsIProperties).undefine(XRE_EXECUTABLE_FILE);
+ } catch (_ex) {
+ // We only override one property, so we have nothing to do if that fails.
+ return;
+ }
+ ds.registerProvider(dirProvider);
+ registerCleanupFunction(() => {
+ ds.unregisterProvider(dirProvider);
+ // Reset the update lock once again so that we know the lock we're
+ // interested in here will be closed properly (normally that happens during
+ // XPCOM shutdown, but that isn't consistent during tests).
+ let syncManager = Cc[
+ "@mozilla.org/updates/update-sync-manager;1"
+ ].getService(Ci.nsIUpdateSyncManager);
+ syncManager.resetLock();
+ });
+
+ // Now that we've overridden the directory provider, the name of the update
+ // lock needs to be changed to match the overridden path.
+ let syncManager = Cc["@mozilla.org/updates/update-sync-manager;1"].getService(
+ Ci.nsIUpdateSyncManager
+ );
+ syncManager.resetLock();
+}
+
+/**
+ * Initializes a mock app update. Adapted from runAboutDialogUpdateTest:
+ * https://searchfox.org/mozilla-central/source/toolkit/mozapps/update/tests/browser/head.js
+ *
+ * @param {object} params
+ * See the files in toolkit/mozapps/update/tests/browser.
+ */
+async function initUpdate(params) {
+ Services.env.set("MOZ_TEST_SLOW_SKIP_UPDATE_STAGE", "1");
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [PREF_APP_UPDATE_DISABLEDFORTESTING, false],
+ [PREF_APP_UPDATE_URL_MANUAL, gDetailsURL],
+ ],
+ });
+
+ adjustGeneralPaths();
+ await setupTestUpdater();
+
+ let queryString = params.queryString ? params.queryString : "";
+ let updateURL =
+ URL_HTTP_UPDATE_SJS +
+ "?detailsURL=" +
+ gDetailsURL +
+ queryString +
+ getVersionParams();
+ if (params.backgroundUpdate) {
+ setUpdateURL(updateURL);
+ gAUS.checkForBackgroundUpdates();
+ if (params.continueFile) {
+ await continueFileHandler(params.continueFile);
+ }
+ if (params.waitForUpdateState) {
+ let whichUpdate =
+ params.waitForUpdateState == STATE_DOWNLOADING
+ ? "downloadingUpdate"
+ : "readyUpdate";
+ await TestUtils.waitForCondition(
+ () =>
+ gUpdateManager[whichUpdate] &&
+ gUpdateManager[whichUpdate].state == params.waitForUpdateState,
+ "Waiting for update state: " + params.waitForUpdateState,
+ undefined,
+ 200
+ ).catch(e => {
+ // Instead of throwing let the check below fail the test so the panel
+ // ID and the expected panel ID is printed in the log.
+ logTestInfo(e);
+ });
+ // Display the UI after the update state equals the expected value.
+ Assert.equal(
+ gUpdateManager[whichUpdate].state,
+ params.waitForUpdateState,
+ "The update state value should equal " + params.waitForUpdateState
+ );
+ }
+ } else {
+ updateURL += "&slowUpdateCheck=1&useSlowDownloadMar=1";
+ setUpdateURL(updateURL);
+ }
+}
+
+/**
+ * Performs steps in a mock update. Adapted from runAboutDialogUpdateTest:
+ * https://searchfox.org/mozilla-central/source/toolkit/mozapps/update/tests/browser/head.js
+ *
+ * @param {Array} steps
+ * See the files in toolkit/mozapps/update/tests/browser.
+ */
+async function processUpdateSteps(steps) {
+ for (let step of steps) {
+ await processUpdateStep(step);
+ }
+}
+
+/**
+ * Performs a step in a mock update. Adapted from runAboutDialogUpdateTest:
+ * https://searchfox.org/mozilla-central/source/toolkit/mozapps/update/tests/browser/head.js
+ *
+ * @param {object} step
+ * See the files in toolkit/mozapps/update/tests/browser.
+ */
+async function processUpdateStep(step) {
+ if (typeof step == "function") {
+ step();
+ return;
+ }
+
+ const { panelId, checkActiveUpdate, continueFile, downloadInfo } = step;
+
+ if (
+ panelId == "downloading" &&
+ gAUS.currentState == Ci.nsIApplicationUpdateService.STATE_IDLE
+ ) {
+ // Now that `AUS.downloadUpdate` is async, we start showing the
+ // downloading panel while `AUS.downloadUpdate` is still resolving.
+ // But the below checks assume that this resolution has already
+ // happened. So we need to wait for things to actually resolve.
+ await gAUS.stateTransition;
+ }
+
+ if (checkActiveUpdate) {
+ let whichUpdate =
+ checkActiveUpdate.state == STATE_DOWNLOADING
+ ? "downloadingUpdate"
+ : "readyUpdate";
+ await TestUtils.waitForCondition(
+ () => gUpdateManager[whichUpdate],
+ "Waiting for active update"
+ );
+ Assert.ok(
+ !!gUpdateManager[whichUpdate],
+ "There should be an active update"
+ );
+ Assert.equal(
+ gUpdateManager[whichUpdate].state,
+ checkActiveUpdate.state,
+ "The active update state should equal " + checkActiveUpdate.state
+ );
+ } else {
+ Assert.ok(
+ !gUpdateManager.readyUpdate,
+ "There should not be a ready update"
+ );
+ Assert.ok(
+ !gUpdateManager.downloadingUpdate,
+ "There should not be a downloadingUpdate update"
+ );
+ }
+
+ if (panelId == "downloading") {
+ for (let i = 0; i < downloadInfo.length; ++i) {
+ let data = downloadInfo[i];
+ // The About Dialog tests always specify a continue file.
+ await continueFileHandler(continueFile);
+ let patch = getPatchOfType(
+ data.patchType,
+ gUpdateManager.downloadingUpdate
+ );
+ // The update is removed early when the last download fails so check
+ // that there is a patch before proceeding.
+ let isLastPatch = i == downloadInfo.length - 1;
+ if (!isLastPatch || patch) {
+ let resultName = data.bitsResult ? "bitsResult" : "internalResult";
+ patch.QueryInterface(Ci.nsIWritablePropertyBag);
+ await TestUtils.waitForCondition(
+ () => patch.getProperty(resultName) == data[resultName],
+ "Waiting for expected patch property " +
+ resultName +
+ " value: " +
+ data[resultName],
+ undefined,
+ 200
+ ).catch(e => {
+ // Instead of throwing let the check below fail the test so the
+ // property value and the expected property value is printed in
+ // the log.
+ logTestInfo(e);
+ });
+ Assert.equal(
+ patch.getProperty(resultName),
+ data[resultName],
+ "The patch property " +
+ resultName +
+ " value should equal " +
+ data[resultName]
+ );
+ }
+ }
+ } else if (continueFile) {
+ await continueFileHandler(continueFile);
+ }
+}
+
+/**
+ * Checks an intervention tip. This works by starting a search that should
+ * trigger a tip, picks the tip, and waits for the tip's action to happen.
+ *
+ * @param {object} options
+ * Options for the test
+ * @param {string} options.searchString
+ * The search string.
+ * @param {string} options.tip
+ * The expected tip type.
+ * @param {string | RegExp} options.title
+ * The expected tip title.
+ * @param {string | RegExp} options.button
+ * The expected button title.
+ * @param {Function} options.awaitCallback
+ * A function that checks the tip's action. Should return a promise (or be
+ * async).
+ * @returns {object}
+ * The value returned from `awaitCallback`.
+ */
+async function doUpdateTest({
+ searchString,
+ tip,
+ title,
+ button,
+ awaitCallback,
+} = {}) {
+ // Do a search that triggers the tip.
+ let [result, element] = await awaitTip(searchString);
+ Assert.strictEqual(result.payload.type, tip, "Tip type");
+ await element.ownerDocument.l10n.translateFragment(element);
+
+ let actualTitle = element._elements.get("title").textContent;
+ if (typeof title == "string") {
+ Assert.equal(actualTitle, title, "Title string");
+ } else {
+ // regexp
+ Assert.ok(title.test(actualTitle), "Title regexp");
+ }
+
+ let actualButton = element._buttons.get("0").textContent;
+ if (typeof button == "string") {
+ Assert.equal(actualButton, button, "Button string");
+ } else {
+ // regexp
+ Assert.ok(button.test(actualButton), "Button regexp");
+ }
+
+ if (UrlbarPrefs.get("resultMenu")) {
+ Assert.ok(element._buttons.has("menu"), "Tip has a menu button");
+ } else {
+ Assert.ok(element._buttons.has("help"), "Tip has a help button");
+ }
+
+ // Pick the tip and wait for the action.
+ let values = await Promise.all([awaitCallback(), pickTip()]);
+
+ // Check telemetry.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ `${tip}-shown`,
+ 1
+ );
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ `${tip}-picked`,
+ 1
+ );
+
+ return values[0] || null;
+}
+
+/**
+ * Starts a search and asserts that the second result is a tip.
+ *
+ * @param {string} searchString
+ * The search string.
+ * @param {window} win
+ * The window.
+ * @returns {(result| element)[]}
+ * The result and its element in the DOM.
+ */
+async function awaitTip(searchString, win = window) {
+ let context = await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: searchString,
+ waitForFocus,
+ fireInputEvent: true,
+ });
+ Assert.ok(context.results.length >= 2);
+ let result = context.results[1];
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.TIP);
+ let element = await UrlbarTestUtils.waitForAutocompleteResultAt(win, 1);
+ return [result, element];
+}
+
+/**
+ * Picks the current tip's button. The view should be open and the second
+ * result should be a tip.
+ */
+async function pickTip() {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ let button = result.element.row._buttons.get("0");
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ });
+}
+
+/**
+ * Waits for the quit-application-requested notification and cancels it (so that
+ * the app isn't actually restarted).
+ */
+async function awaitAppRestartRequest() {
+ await TestUtils.topicObserved(
+ "quit-application-requested",
+ (cancelQuit, data) => {
+ if (data == "restart") {
+ cancelQuit.QueryInterface(Ci.nsISupportsPRBool).data = true;
+ return true;
+ }
+ return false;
+ }
+ );
+}
+
+/**
+ * Sets up the profile so that it can be reset.
+ */
+function makeProfileResettable() {
+ // Make reset possible.
+ let profileService = Cc["@mozilla.org/toolkit/profile-service;1"].getService(
+ Ci.nsIToolkitProfileService
+ );
+ let currentProfileDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ let profileName = "mochitest-test-profile-temp-" + Date.now();
+ let tempProfile = profileService.createProfile(
+ currentProfileDir,
+ profileName
+ );
+ Assert.ok(
+ ResetProfile.resetSupported(),
+ "Should be able to reset from mochitest's temporary profile once it's in the profile manager."
+ );
+
+ registerCleanupFunction(() => {
+ tempProfile.remove(false);
+ Assert.ok(
+ !ResetProfile.resetSupported(),
+ "Shouldn't be able to reset from mochitest's temporary profile once removed from the profile manager."
+ );
+ });
+}
+
+/**
+ * Starts a search that should trigger a tip, picks the tip, and waits for the
+ * tip's action to happen.
+ *
+ * @param {object} options
+ * Options for the test
+ * @param {string} options.searchString
+ * The search string.
+ * @param {TIPS} options.tip
+ * The expected tip type.
+ * @param {string} options.title
+ * The expected tip title.
+ * @param {string} options.button
+ * The expected button title.
+ * @param {Function} options.awaitCallback
+ * A function that checks the tip's action. Should return a promise (or be
+ * async).
+ * @returns {*}
+ * The value returned from `awaitCallback`.
+ */
+function checkIntervention({
+ searchString,
+ tip,
+ title,
+ button,
+ awaitCallback,
+} = {}) {
+ // Opening modal dialogs confuses focus on Linux just after them, thus run
+ // these checks in separate tabs to better isolate them.
+ return BrowserTestUtils.withNewTab("about:blank", async () => {
+ // Do a search that triggers the tip.
+ let [result, element] = await awaitTip(searchString);
+ Assert.strictEqual(result.payload.type, tip);
+ await element.ownerDocument.l10n.translateFragment(element);
+
+ let actualTitle = element._elements.get("title").textContent;
+ if (typeof title == "string") {
+ Assert.equal(actualTitle, title, "Title string");
+ } else {
+ // regexp
+ Assert.ok(title.test(actualTitle), "Title regexp");
+ }
+
+ let actualButton = element._buttons.get("0").textContent;
+ if (typeof button == "string") {
+ Assert.equal(actualButton, button, "Button string");
+ } else {
+ // regexp
+ Assert.ok(button.test(actualButton), "Button regexp");
+ }
+
+ if (UrlbarPrefs.get("resultMenu")) {
+ let menuButton = element._buttons.get("menu");
+ Assert.ok(menuButton, "Menu button exists");
+ Assert.ok(
+ BrowserTestUtils.is_visible(menuButton),
+ "Menu button is visible"
+ );
+ } else {
+ let helpButton = element._buttons.get("help");
+ Assert.ok(helpButton, "Help button exists");
+ Assert.ok(
+ BrowserTestUtils.is_visible(helpButton),
+ "Help button is visible"
+ );
+ }
+
+ let values = await Promise.all([awaitCallback(), pickTip()]);
+ Assert.ok(true, "Refresh dialog opened");
+
+ // Ensure the urlbar is closed so that the engagement is ended.
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ `${tip}-shown`,
+ 1
+ );
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ `${tip}-picked`,
+ 1
+ );
+
+ return values[0] || null;
+ });
+}
+
+/**
+ * Starts a search and asserts that there are no tips.
+ *
+ * @param {string} searchString
+ * The search string.
+ * @param {Window} win
+ * The host window.
+ */
+async function awaitNoTip(searchString, win = window) {
+ let context = await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: searchString,
+ waitForFocus,
+ fireInputEvent: true,
+ });
+ for (let result of context.results) {
+ Assert.notEqual(result.type, UrlbarUtils.RESULT_TYPE.TIP);
+ }
+}
+
+/**
+ * Search tips helper. Asserts that a particular search tip is shown or that no
+ * search tip is shown.
+ *
+ * @param {window} win
+ * A browser window.
+ * @param {UrlbarProviderSearchTips.TIP_TYPE} expectedTip
+ * The expected search tip. Pass a falsey value (like zero) for none.
+ * @param {boolean} closeView
+ * If true, this function closes the urlbar view before returning.
+ */
+async function checkTip(win, expectedTip, closeView = true) {
+ if (!expectedTip) {
+ // Wait a bit for the tip to not show up.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 100));
+ Assert.ok(!win.gURLBar.view.isOpen);
+ return;
+ }
+
+ // Wait for the view to open, and then check the tip result.
+ await UrlbarTestUtils.promisePopupOpen(win, () => {});
+ Assert.ok(true, "View opened");
+ Assert.equal(UrlbarTestUtils.getResultCount(win), 1);
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(win, 0);
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.TIP);
+ let heuristic;
+ let title;
+ let name = Services.search.defaultEngine.name;
+ switch (expectedTip) {
+ case UrlbarProviderSearchTips.TIP_TYPE.ONBOARD:
+ heuristic = true;
+ title =
+ `Type less, find more: Search ${name} right from your ` +
+ `address bar.`;
+ break;
+ case UrlbarProviderSearchTips.TIP_TYPE.REDIRECT:
+ heuristic = false;
+ title =
+ `Start your search in the address bar to see suggestions from ` +
+ `${name} and your browsing history.`;
+ break;
+ case UrlbarProviderSearchTips.TIP_TYPE.PERSIST:
+ heuristic = false;
+ title =
+ "Searching just got simpler." +
+ " Try making your search more specific here in the address bar." +
+ " To show the URL instead, visit Search, in settings.";
+ break;
+ }
+ Assert.equal(result.heuristic, heuristic);
+ Assert.equal(result.displayed.title, title);
+ Assert.equal(
+ result.element.row._buttons.get("0").textContent,
+ expectedTip == UrlbarProviderSearchTips.TIP_TYPE.PERSIST
+ ? `Got it`
+ : `Okay, Got It`
+ );
+ Assert.ok(!result.element.row._buttons.has("help"));
+
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ `${expectedTip}-shown`,
+ 1
+ );
+
+ Assert.ok(
+ !UrlbarTestUtils.getOneOffSearchButtonsVisible(window),
+ "One-offs should be hidden when showing a search tip"
+ );
+
+ if (closeView) {
+ await UrlbarTestUtils.promisePopupClose(win);
+ }
+}
+
+function makeTipResult({ buttonUrl, helpUrl = undefined }) {
+ return new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TIP,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ helpUrl,
+ type: "test",
+ titleL10n: { id: "urlbar-search-tips-confirm" },
+ buttons: [
+ {
+ url: buttonUrl,
+ l10n: { id: "urlbar-search-tips-confirm" },
+ },
+ ],
+ }
+ );
+}
+
+/**
+ * Search tips helper. Opens a foreground tab and asserts that a particular
+ * search tip is shown or that no search tip is shown.
+ *
+ * @param {window} win
+ * A browser window.
+ * @param {string} url
+ * The URL to load in a new foreground tab.
+ * @param {UrlbarProviderSearchTips.TIP_TYPE} expectedTip
+ * The expected search tip. Pass a falsey value (like zero) for none.
+ * @param {boolean} reset
+ * If true, the search tips provider will be reset before this function
+ * returns. See resetSearchTipsProvider.
+ */
+async function checkTab(win, url, expectedTip, reset = true) {
+ // BrowserTestUtils.withNewTab always waits for tab load, which hangs on
+ // about:newtab for some reason, so don't use it.
+ let shownCount;
+ if (expectedTip) {
+ shownCount = UrlbarPrefs.get(`tipShownCount.${expectedTip}`);
+ }
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser: win.gBrowser,
+ url,
+ waitForLoad: url != "about:newtab",
+ });
+
+ await checkTip(win, expectedTip, true);
+ if (expectedTip) {
+ Assert.equal(
+ UrlbarPrefs.get(`tipShownCount.${expectedTip}`),
+ shownCount + 1,
+ "The shownCount pref should have been incremented by one."
+ );
+ }
+
+ if (reset) {
+ resetSearchTipsProvider();
+ }
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+/**
+ * This lets us visit www.google.com (for example) and have it redirect to
+ * our test HTTP server instead of visiting the actual site.
+ *
+ * @param {string} domain
+ * The domain to which we are redirecting.
+ * @param {string} path
+ * The pathname on the domain.
+ * @param {Function} callback
+ * Executed when the test suite thinks `domain` is loaded.
+ */
+async function withDNSRedirect(domain, path, callback) {
+ // Some domains have special security requirements, like www.bing.com. We
+ // need to override them to successfully load them. This part is adapted from
+ // testing/marionette/cert.js.
+ const certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+ ].getService(Ci.nsICertOverrideService);
+ Services.prefs.setBoolPref(
+ "network.stricttransportsecurity.preloadlist",
+ false
+ );
+ Services.prefs.setIntPref("security.cert_pinning.enforcement_level", 0);
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ true
+ );
+
+ // Now set network.dns.localDomains to redirect the domain to localhost and
+ // set up an HTTP server.
+ Services.prefs.setCharPref("network.dns.localDomains", domain);
+
+ let server = new HttpServer();
+ server.registerPathHandler(path, (req, resp) => {
+ resp.write(`Test! http://${domain}${path}`);
+ });
+ server.start(-1);
+ server.identity.setPrimary("http", domain, server.identity.primaryPort);
+ let url = `http://${domain}:${server.identity.primaryPort}${path}`;
+
+ await callback(url);
+
+ // Reset network.dns.localDomains and stop the server.
+ Services.prefs.clearUserPref("network.dns.localDomains");
+ await new Promise(resolve => server.stop(resolve));
+
+ // Reset the security stuff.
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ false
+ );
+ Services.prefs.clearUserPref("network.stricttransportsecurity.preloadlist");
+ Services.prefs.clearUserPref("security.cert_pinning.enforcement_level");
+ const sss = Cc["@mozilla.org/ssservice;1"].getService(
+ Ci.nsISiteSecurityService
+ );
+ sss.clearAll();
+}
+
+function resetSearchTipsProvider() {
+ Services.prefs.clearUserPref(
+ `browser.urlbar.tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.ONBOARD}`
+ );
+ Services.prefs.clearUserPref(
+ `browser.urlbar.tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.PERSIST}`
+ );
+ Services.prefs.clearUserPref(
+ `browser.urlbar.tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.REDIRECT}`
+ );
+ UrlbarProviderSearchTips.disableTipsForCurrentSession = false;
+}
+
+async function setDefaultEngine(name) {
+ let engine = (await Services.search.getEngines()).find(e => e.name == name);
+ Assert.ok(engine);
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+}
diff --git a/browser/components/urlbar/tests/browser-tips/slow-page.html b/browser/components/urlbar/tests/browser-tips/slow-page.html
new file mode 100644
index 0000000000..f58a44dc62
--- /dev/null
+++ b/browser/components/urlbar/tests/browser-tips/slow-page.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <h1>Search Engine Results Page that is loading a slow resource.</h1>
+ </body>
+ <script src="https://www.example.com/browser/browser/components/urlbar/tests/browser-tips/slow-page.sjs"></script>
+</html>
diff --git a/browser/components/urlbar/tests/browser-updateResults/browser.ini b/browser/components/urlbar/tests/browser-updateResults/browser.ini
new file mode 100644
index 0000000000..f20fed7e13
--- /dev/null
+++ b/browser/components/urlbar/tests/browser-updateResults/browser.ini
@@ -0,0 +1,17 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+[DEFAULT]
+support-files =
+ head.js
+
+[browser_appendSpanCount.js]
+[browser_suggestedIndex_10_search_10_url.js]
+[browser_suggestedIndex_10_search_5_url.js]
+[browser_suggestedIndex_10_url_10_search.js]
+[browser_suggestedIndex_10_url_5_search.js]
+[browser_suggestedIndex_5_search_10_url.js]
+[browser_suggestedIndex_5_search_5_url.js]
+[browser_suggestedIndex_5_url_10_search.js]
+[browser_suggestedIndex_5_url_5_search.js]
diff --git a/browser/components/urlbar/tests/browser-updateResults/browser_appendSpanCount.js b/browser/components/urlbar/tests/browser-updateResults/browser_appendSpanCount.js
new file mode 100644
index 0000000000..bcf9609597
--- /dev/null
+++ b/browser/components/urlbar/tests/browser-updateResults/browser_appendSpanCount.js
@@ -0,0 +1,183 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This test makes sure that when the view updates itself and appends new rows,
+// the new rows start out hidden when they exceed the current visible result
+// span count. It includes a tip result so that it tests a row with > 1 result
+// span.
+
+"use strict";
+
+add_task(async function viewUpdateAppendHidden() {
+ // We'll use this test provider to test specific results. We assume that
+ // history and bookmarks have been cleared (by init() above).
+ let provider = new DelayingTestProvider();
+ UrlbarProvidersManager.registerProvider(provider);
+ registerCleanupFunction(() => {
+ UrlbarProvidersManager.unregisterProvider(provider);
+ });
+
+ // We do two searches below without closing the panel. Use "firefox cach" as
+ // the first query and "firefox cache" as the second so that (1) an
+ // intervention tip is triggered both times but also so that (2) the queries
+ // are different each time.
+ let baseQuery = "firefox cache";
+ let queries = [baseQuery.substring(0, baseQuery.length - 1), baseQuery];
+ let maxResults = UrlbarPrefs.get("maxRichResults");
+
+ let queryStrings = [];
+ for (let i = 0; i < maxResults; i++) {
+ queryStrings.push(`${baseQuery} ${i}`);
+ }
+
+ // First search: Trigger the intervention tip and a view full of search
+ // suggestions.
+ provider._results = queryStrings.map(
+ suggestion =>
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ {
+ query: queries[0],
+ suggestion,
+ lowerCaseSuggestion: suggestion.toLocaleLowerCase(),
+ engine: Services.search.defaultEngine.name,
+ }
+ )
+ );
+ provider.finishQueryPromise = Promise.resolve();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: queries[0],
+ });
+
+ // Sanity check the tip result and row count.
+ let tipResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(
+ tipResult.type,
+ UrlbarUtils.RESULT_TYPE.TIP,
+ "Result at index 1 is a tip"
+ );
+ let tipResultSpan = UrlbarUtils.getSpanForResult(
+ tipResult.element.row.result
+ );
+ Assert.greater(tipResultSpan, 1, "Sanity check: Tip has large result span");
+ let expectedRowCount = maxResults - tipResultSpan + 1;
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ expectedRowCount,
+ "Sanity check: Initial row count takes tip result span into account"
+ );
+
+ // Second search: Change the provider's results so that it has enough history
+ // to fill up the view. Search suggestion rows cannot be updated to history
+ // results, so the view will append the history results as new rows.
+ provider._results = queryStrings.map(title => {
+ let url = "http://example.com/" + title;
+ return new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ {
+ title,
+ url,
+ displayUrl: "http://example.com/" + title,
+ }
+ );
+ });
+
+ // Don't allow the search to finish until we check the updated rows. We'll
+ // accomplish that by adding a mutation observer on the rows and delaying
+ // resolving the provider's finishQueryPromise. When all new rows have been
+ // added, we expect the new row count to be:
+ //
+ // expectedRowCount // the original row count
+ // + (expectedRowCount - 2) // the newly added history row count (hidden)
+ // --------------------------
+ // (2 * expectedRowCount) - 2
+ //
+ // The `- 2` subtracts the heuristic and tip result.
+ let newExpectedRowCount = 2 * expectedRowCount - 2;
+ let mutationPromise = new Promise(resolve => {
+ let observer = new MutationObserver(mutations => {
+ let childCount = UrlbarTestUtils.getResultCount(window);
+ info(`Rows mutation observer called, childCount now ${childCount}`);
+ if (newExpectedRowCount <= childCount) {
+ observer.disconnect();
+ resolve();
+ }
+ });
+ observer.observe(UrlbarTestUtils.getResultsContainer(window), {
+ childList: true,
+ });
+ });
+
+ // Now do the second search but don't wait for it to finish.
+ let resolveQuery;
+ provider.finishQueryPromise = new Promise(
+ resolve => (resolveQuery = resolve)
+ );
+ let queryPromise = UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: queries[1],
+ });
+
+ // Wait for the history rows to be added.
+ await mutationPromise;
+
+ // Check the rows. We can't use UrlbarTestUtils.getDetailsOfResultAt() here
+ // because it waits for the query to finish.
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ newExpectedRowCount,
+ "New expected row count"
+ );
+ // stale search rows
+ let rows = UrlbarTestUtils.getResultsContainer(window).children;
+ for (let i = 2; i < expectedRowCount; i++) {
+ let row = rows[i];
+ Assert.equal(
+ row.result.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ `Result at index ${i} is a search result`
+ );
+ Assert.ok(
+ BrowserTestUtils.is_visible(row),
+ `Search result at index ${i} is visible`
+ );
+ Assert.equal(
+ row.getAttribute("stale"),
+ "true",
+ `Search result at index ${i} is stale`
+ );
+ }
+ // new hidden history rows
+ for (let i = expectedRowCount; i < newExpectedRowCount; i++) {
+ let row = rows[i];
+ Assert.equal(
+ row.result.type,
+ UrlbarUtils.RESULT_TYPE.URL,
+ `Result at index ${i} is a URL result`
+ );
+ Assert.ok(
+ !BrowserTestUtils.is_visible(row),
+ `URL result at index ${i} is hidden`
+ );
+ Assert.ok(
+ !row.hasAttribute("stale"),
+ `URL result at index ${i} is not stale`
+ );
+ }
+
+ // Finish the query, and we're done.
+ resolveQuery();
+ await queryPromise;
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ gURLBar.handleRevert();
+
+ // We unregister the provider above in a cleanup function so we don't
+ // accidentally interfere with later tests, but do it here too in case we add
+ // more tasks to this test. It's harmless to call more than once.
+ UrlbarProvidersManager.unregisterProvider(provider);
+});
diff --git a/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_10_search_10_url.js b/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_10_search_10_url.js
new file mode 100644
index 0000000000..755c453850
--- /dev/null
+++ b/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_10_search_10_url.js
@@ -0,0 +1,1102 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This test checks row visibility during view updates when rows with suggested
+// indexes are added and removed. Each task performs two searches: Search 1
+// returns 10 results with search suggestions, and search 2 returns 10 results
+// with URL results.
+
+"use strict";
+
+// Search 1:
+// 10 results, no suggestedIndex
+// Search 2:
+// 10 results including suggestedIndex = 1
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results, no suggestedIndex
+// Search 2:
+// 10 results including suggestedIndex = 2
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 2,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 2,
+ hidden: true,
+ },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results, no suggestedIndex
+// Search 2:
+// 10 results including suggestedIndex = 9
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results, no suggestedIndex
+// Search 2:
+// 10 results including suggestedIndex = -1
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results, no suggestedIndex
+// Search 2:
+// 10 results including suggestedIndex = -2
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ hidden: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results, no suggestedIndex
+// Search 2:
+// 10 results including suggestedIndex = -9
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -9,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -9,
+ hidden: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = 1
+// Search 2:
+// 10 results including suggestedIndex = 1
+// Expected visible rows during update:
+// 10 original rows with no changes (because the original search results can't
+// be replaced with URL results)
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = 1
+// Search 2:
+// 10 results including suggestedIndex = 2
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 2,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ stale: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 2,
+ hidden: true,
+ },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = 1
+// Search 2:
+// 10 results including suggestedIndex = 9
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ stale: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = 1
+// Search 2:
+// 10 results including suggestedIndex = -1
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ stale: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = 1
+// Search 2:
+// 10 results including suggestedIndex = -2
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ stale: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ hidden: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = 1
+// Search 2:
+// 10 results including suggestedIndex = -9
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -9,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ stale: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -9,
+ hidden: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = 9
+// Search 2:
+// 10 results including suggestedIndex = 1
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 9,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = 9
+// Search 2:
+// 10 results including suggestedIndex = 9
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 9,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ stale: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = 9
+// Search 2:
+// 10 results including suggestedIndex = -1
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 9,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ stale: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = 9
+// Search 2:
+// 10 results including suggestedIndex = -9
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 9,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -9,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -9,
+ hidden: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = -1
+// Search 2:
+// 10 results including suggestedIndex = 1
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = -1
+// Search 2:
+// 10 results including suggestedIndex = 9
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ stale: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = -1
+// Search 2:
+// 10 results including suggestedIndex = -1
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ stale: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = -1
+// Search 2:
+// 10 results including suggestedIndex = -9
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -9,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -9,
+ hidden: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = -9
+// Search 2:
+// 10 results including suggestedIndex = 1
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -9,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -9,
+ stale: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = -9
+// Search 2:
+// 10 results including suggestedIndex = 9
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -9,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -9,
+ stale: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = -9
+// Search 2:
+// 10 results including suggestedIndex = -1
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -9,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -9,
+ stale: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = -9
+// Search 2:
+// 10 results including suggestedIndex = -9
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -9,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -9,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -9,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results, no suggestedIndex
+// Search 2:
+// 10 results including suggestedIndex = 1 and suggestedIndex = -1
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndexes: [1, -1],
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = 1
+// Search 2:
+// 10 results including suggestedIndex = 1 and suggestedIndex = -1
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndexes: [1, -1],
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = -1
+// Search 2:
+// 10 results including suggestedIndex = 1 and suggestedIndex = -1
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndexes: [1, -1],
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results, no suggestedIndex
+// Search 2:
+// 9 results including suggestedIndex = 1 with resultSpan = 2
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ resultSpan: 2,
+ viewCount: 9,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ resultSpan: 2,
+ hidden: true,
+ },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results, no suggestedIndex
+// Search 2:
+// 9 results including:
+// suggestedIndex = 1 with resultSpan = 2
+// suggestedIndex = -1
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndexes: [[1, 2], -1],
+ viewCount: 9,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ resultSpan: 2,
+ hidden: true,
+ },
+ { count: 6, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 9 results including suggestedIndex = 1 with resultSpan = 2
+// Search 2:
+// 9 results including:
+// suggestedIndex = 1 with resultSpan = 2
+// suggestedIndex = -1
+// Expected visible rows during update:
+// 9 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndexes: [[1, 2]],
+ viewCount: 9,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndexes: [[1, 2], -1],
+ viewCount: 9,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ resultSpan: 2,
+ },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 6, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
diff --git a/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_10_search_5_url.js b/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_10_search_5_url.js
new file mode 100644
index 0000000000..bd707dd422
--- /dev/null
+++ b/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_10_search_5_url.js
@@ -0,0 +1,661 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This test checks row visibility during view updates when rows with suggested
+// indexes are added and removed. Each task performs two searches: Search 1
+// returns 10 results with search suggestions, and search 2 returns 5 results
+// with URL results.
+
+"use strict";
+
+// Search 1:
+// 10 results, no suggestedIndex
+// Search 2:
+// 5 results including suggestedIndex = 1
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results, no suggestedIndex
+// Search 2:
+// 5 results including suggestedIndex = 2
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 2,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 2,
+ hidden: true,
+ },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results, no suggestedIndex
+// Search 2:
+// 5 results including suggestedIndex = 9
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results, no suggestedIndex
+// Search 2:
+// 5 results including suggestedIndex = -1
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results, no suggestedIndex
+// Search 2:
+// 5 results including suggestedIndex = -2
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ hidden: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = 1
+// Search 2:
+// 5 results including suggestedIndex = 1
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = 1
+// Search 2:
+// 5 results including suggestedIndex = 9
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ stale: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = 1
+// Search 2:
+// 5 results including suggestedIndex = -1
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ stale: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = 1
+// Search 2:
+// 5 results including suggestedIndex = -3
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -3,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ stale: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -3,
+ hidden: true,
+ },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = 9
+// Search 2:
+// 5 results including suggestedIndex = 1
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 9,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = 9
+// Search 2:
+// 5 results including suggestedIndex = 9
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 9,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ stale: true,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = -1
+// Search 2:
+// 5 results including suggestedIndex = 1
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = -1
+// Search 2:
+// 5 results including suggestedIndex = 9
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ stale: true,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = -1
+// Search 2:
+// 5 results including suggestedIndex = -1
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ stale: true,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results, no suggestedIndex
+// Search 2:
+// 5 results including suggestedIndex = 1 and suggestedIndex = -1
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 2,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndexes: [1, -1],
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = 1
+// Search 2:
+// 5 results including suggestedIndex = 1 and suggestedIndex = -1
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 2,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndexes: [1, -1],
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = -1
+// Search 2:
+// 5 results including suggestedIndex = 1 and suggestedIndex = -1
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 2,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndexes: [1, -1],
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 9 results including suggestedIndex = 1 with resultSpan = 2
+// Search 2:
+// 5 results including:
+// suggestedIndex = 1 with resultSpan = 2
+// suggestedIndex = -1
+// Expected visible rows during update:
+// 9 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndexes: [[1, 2]],
+ viewCount: 9,
+ },
+ search2: {
+ otherCount: 2,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndexes: [[1, 2], -1],
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ resultSpan: 2,
+ },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
diff --git a/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_10_url_10_search.js b/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_10_url_10_search.js
new file mode 100644
index 0000000000..57f172adba
--- /dev/null
+++ b/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_10_url_10_search.js
@@ -0,0 +1,1185 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This test checks row visibility during view updates when rows with suggested
+// indexes are added and removed. Each task performs two searches: Search 1
+// returns 10 results where the first result is a search suggestion and the
+// remainder are URL results, and search 2 returns 10 results with search
+// suggestions. This tests the view-update logic that allows search suggestions
+// to replace other results once an existing suggestion row is encountered.
+
+"use strict";
+
+// Search 1:
+// 10 results, no suggestedIndex
+// Search 2:
+// 10 results including suggestedIndex = 1
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results, no suggestedIndex
+// Search 2:
+// 10 results including suggestedIndex = 2
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 2,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 2,
+ hidden: true,
+ },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results, no suggestedIndex
+// Search 2:
+// 10 results including suggestedIndex = 9
+// Expected visible rows during update:
+// Indexes 2-8 replaced with search suggestions
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 9,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results, no suggestedIndex
+// Search 2:
+// 10 results including suggestedIndex = -1
+// Expected visible rows during update:
+// Indexes 2-8 replaced with search suggestions
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -1,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results, no suggestedIndex
+// Search 2:
+// 10 results including suggestedIndex = -2
+// Expected visible rows during update:
+// Indexes 2-7 replaced with search suggestions
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -2,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ hidden: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results, no suggestedIndex
+// Search 2:
+// 10 results including suggestedIndex = -9
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -9,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -9,
+ hidden: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = 1
+// Search 2:
+// 10 results including suggestedIndex = 1
+// Expected visible rows during update:
+// All search-2 rows
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ {
+ count: 7,
+ type: UrlbarPrefs.get("resultMenu")
+ ? UrlbarUtils.RESULT_TYPE.URL
+ : UrlbarUtils.RESULT_TYPE.SEARCH,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = 1
+// Search 2:
+// 10 results including suggestedIndex = 2
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 2,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ stale: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 2,
+ hidden: true,
+ },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = 1
+// Search 2:
+// 10 results including suggestedIndex = 9
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 9,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ stale: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = 1
+// Search 2:
+// 10 results including suggestedIndex = -1
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -1,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ stale: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = 1
+// Search 2:
+// 10 results including suggestedIndex = -2
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -2,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ stale: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ hidden: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = 1
+// Search 2:
+// 10 results including suggestedIndex = -9
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -9,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ stale: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -9,
+ hidden: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = 9
+// Search 2:
+// 10 results including suggestedIndex = 1
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 9,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = 9
+// Search 2:
+// 10 results including suggestedIndex = 9
+// Expected visible rows during update:
+// All search-2 rows
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 9,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 9,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ {
+ count: 7,
+ type: UrlbarPrefs.get("resultMenu")
+ ? UrlbarUtils.RESULT_TYPE.URL
+ : UrlbarUtils.RESULT_TYPE.SEARCH,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = 9
+// Search 2:
+// 10 results including suggestedIndex = -1
+// Expected visible rows during update:
+// Indexes 2-8 replaced with search suggestions
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 9,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -1,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = 9
+// Search 2:
+// 10 results including suggestedIndex = -9
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 9,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -9,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -9,
+ hidden: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = -1
+// Search 2:
+// 10 results including suggestedIndex = 1
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: -1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = -1
+// Search 2:
+// 10 results including suggestedIndex = 9
+// Expected visible rows during update:
+// Indexes 2-8 replaced with search suggestions
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: -1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 9,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = -1
+// Search 2:
+// 10 results including suggestedIndex = -1
+// Expected visible rows during update:
+// All search-2 rows
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: -1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -1,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ {
+ count: 7,
+ type: UrlbarPrefs.get("resultMenu")
+ ? UrlbarUtils.RESULT_TYPE.URL
+ : UrlbarUtils.RESULT_TYPE.SEARCH,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = -1
+// Search 2:
+// 10 results including suggestedIndex = -9
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: -1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -9,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -9,
+ hidden: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = -9
+// Search 2:
+// 10 results including suggestedIndex = 1
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: -9,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -9,
+ stale: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = -9
+// Search 2:
+// 10 results including suggestedIndex = 9
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: -9,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 9,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -9,
+ stale: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = -9
+// Search 2:
+// 10 results including suggestedIndex = -1
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: -9,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -1,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -9,
+ stale: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = -9
+// Search 2:
+// 10 results including suggestedIndex = -9
+// Expected visible rows during update:
+// All search-2 rows
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: -9,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -9,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -9,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ {
+ count: 7,
+ type: UrlbarPrefs.get("resultMenu")
+ ? UrlbarUtils.RESULT_TYPE.URL
+ : UrlbarUtils.RESULT_TYPE.SEARCH,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results, no suggestedIndex
+// Search 2:
+// 10 results including suggestedIndex = 1 and suggestedIndex = -1
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndexes: [1, -1],
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = 1
+// Search 2:
+// 10 results including suggestedIndex = 1 and suggestedIndex = -1
+// Expected visible rows during update:
+// Indexes 2-8 replaced with search suggestions
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndexes: [1, -1],
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = -1
+// Search 2:
+// 10 results including suggestedIndex = 1 and suggestedIndex = -1
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: -1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndexes: [1, -1],
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results, no suggestedIndex
+// Search 2:
+// 9 results including suggestedIndex = 1 with resultSpan = 2
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ resultSpan: 2,
+ viewCount: 9,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ resultSpan: 2,
+ hidden: true,
+ },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results, no suggestedIndex
+// Search 2:
+// 9 results including:
+// suggestedIndex = 1 with resultSpan = 2
+// suggestedIndex = -1
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndexes: [[1, 2], -1],
+ viewCount: 9,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ resultSpan: 2,
+ hidden: true,
+ },
+ { count: 6, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 9 results including suggestedIndex = 1 with resultSpan = 2
+// Search 2:
+// 9 results including:
+// suggestedIndex = 1 with resultSpan = 2
+// suggestedIndex = -1
+// Expected visible rows during update:
+// Indexes 2-8 replaced with search suggestions
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndexes: [[1, 2]],
+ viewCount: 9,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndexes: [[1, 2], -1],
+ viewCount: 9,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ resultSpan: 2,
+ },
+ { count: 6, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
diff --git a/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_10_url_5_search.js b/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_10_url_5_search.js
new file mode 100644
index 0000000000..03d6f158f4
--- /dev/null
+++ b/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_10_url_5_search.js
@@ -0,0 +1,707 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This test checks row visibility during view updates when rows with suggested
+// indexes are added and removed. Each task performs two searches: Search 1
+// returns 10 results where the first result is a search suggestion and the
+// remainder are URL results, and search 2 returns 5 results with search
+// suggestions. This tests the view-update logic that allows search suggestions
+// to replace other results once an existing suggestion row is encountered.
+
+"use strict";
+
+// Search 1:
+// 10 results, no suggestedIndex
+// Search 2:
+// 5 results including suggestedIndex = 1
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results, no suggestedIndex
+// Search 2:
+// 5 results including suggestedIndex = 2
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 2,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 2,
+ hidden: true,
+ },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results, no suggestedIndex
+// Search 2:
+// 5 results including suggestedIndex = 9
+// Expected visible rows during update:
+// Indexes 2 and 3 replaced with search suggestions, no other changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 6, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results, no suggestedIndex
+// Search 2:
+// 5 results including suggestedIndex = -1
+// Expected visible rows during update:
+// Indexes 2 and 3 replaced with search suggestions, no other changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -1,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 6, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results, no suggestedIndex
+// Search 2:
+// 5 results including suggestedIndex = -2
+// Expected visible rows during update:
+// Indexes 1 and 2 replaced with search suggestions, no other changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -2,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ hidden: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = 1
+// Search 2:
+// 5 results including suggestedIndex = 1
+// Expected visible rows during update:
+// Indexes 3 and 4 replaced with search suggestions, no other changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 5, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = 1
+// Search 2:
+// 5 results including suggestedIndex = 9
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ stale: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = 1
+// Search 2:
+// 5 results including suggestedIndex = -1
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -1,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ stale: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = 1
+// Search 2:
+// 5 results including suggestedIndex = -3
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -3,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ stale: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -3,
+ hidden: true,
+ },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = 9
+// Search 2:
+// 5 results including suggestedIndex = 1
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 9,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = 9
+// Search 2:
+// 5 results including suggestedIndex = 9
+// Expected visible rows during update:
+// Indexes 2 and 3 replaced with search suggestions, no other changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 9,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 5, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = -1
+// Search 2:
+// 5 results including suggestedIndex = 1
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: -1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = -1
+// Search 2:
+// 5 results including suggestedIndex = 9
+// Expected visible rows during update:
+// Indexes 2 and 3 replaced with search suggestions, no other changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: -1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 5, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = -1
+// Search 2:
+// 5 results including suggestedIndex = -1
+// Expected visible rows during update:
+// Indexes 2 and 3 replaced with search suggestions, no other changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: -1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -1,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 5, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results, no suggestedIndex
+// Search 2:
+// 5 results including suggestedIndex = 1 and suggestedIndex = -1
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 2,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndexes: [1, -1],
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = 1
+// Search 2:
+// 5 results including suggestedIndex = 1 and suggestedIndex = -1
+// Expected visible rows during update:
+// Index 3 replaced with search suggestion, no other changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 2,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndexes: [1, -1],
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 6, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 10 results including suggestedIndex = -1
+// Search 2:
+// 5 results including suggestedIndex = 1 and suggestedIndex = -1
+// Expected visible rows during update:
+// 10 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: -1,
+ viewCount: 10,
+ },
+ search2: {
+ otherCount: 2,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndexes: [1, -1],
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 9 results including suggestedIndex = 1 with resultSpan = 2
+// Search 2:
+// 5 results including:
+// suggestedIndex = 1 with resultSpan = 2
+// suggestedIndex = -1
+// Expected visible rows during update:
+// Index 3 replaced with search suggestion, no other changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 9, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndexes: [[1, 2]],
+ viewCount: 9,
+ },
+ search2: {
+ otherCount: 2,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndexes: [[1, 2], -1],
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ resultSpan: 2,
+ },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 5, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
diff --git a/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_5_search_10_url.js b/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_5_search_10_url.js
new file mode 100644
index 0000000000..dfd626d701
--- /dev/null
+++ b/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_5_search_10_url.js
@@ -0,0 +1,1015 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This test checks row visibility during view updates when rows with suggested
+// indexes are added and removed. Each task performs two searches: Search 1
+// returns 5 results with search suggestions, and search 2 returns 10 results
+// with URL results.
+
+"use strict";
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 10 results including suggestedIndex = 1
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 4,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 10 results including suggestedIndex = 2
+// Expected visible rows during update:
+// 5 search-1 rows + 1 search-2 row (the one before the suggestedIndex row)
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 4,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 2,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 2,
+ hidden: true,
+ },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 10 results including suggestedIndex = 4
+// Expected visible rows during update:
+// 5 search-1 rows + 3 search-2 rows (the ones before the suggestedIndex row)
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 4,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 4,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 4,
+ hidden: true,
+ },
+ { count: 5, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 10 results including suggestedIndex = 6
+// Expected visible rows during update:
+// 5 search-1 rows + 5 search-2 rows (the ones before the suggestedIndex row)
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 4,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 6,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 5, type: UrlbarUtils.RESULT_TYPE.URL },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 6,
+ hidden: true,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 10 results including suggestedIndex = 8
+// Expected visible rows during update:
+// 5 search-1 rows + 5 search-2 rows (some of the ones before the
+// suggestedIndex)
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 4,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 8,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 5, type: UrlbarUtils.RESULT_TYPE.URL },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 8,
+ hidden: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 10 results including suggestedIndex = 9
+// Expected visible rows during update:
+// 5 search-1 rows + 5 search-2 rows (some of the ones before the
+// suggestedIndex)
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 4,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 5, type: UrlbarUtils.RESULT_TYPE.URL },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 10 results including suggestedIndex = -1
+// Expected visible rows during update:
+// 5 search-1 rows + 5 search-2 rows
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 4,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 5, type: UrlbarUtils.RESULT_TYPE.URL },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 10 results including suggestedIndex = -2
+// Expected visible rows during update:
+// 5 search-1 rows + 5 search-2 rows
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 4,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 5, type: UrlbarUtils.RESULT_TYPE.URL },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ hidden: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 10 results including suggestedIndex = -4
+// Expected visible rows during update:
+// 5 search-1 rows + 5 search-2 rows
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 4,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -4,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 5, type: UrlbarUtils.RESULT_TYPE.URL },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -4,
+ hidden: true,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 10 results including suggestedIndex = -6
+// Expected visible rows during update:
+// 5 search-1 rows + 3 search-2 rows (the ones before the suggestedIndex row)
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 4,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -6,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -6,
+ hidden: true,
+ },
+ { count: 5, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 10 results including suggestedIndex = -8
+// Expected visible rows during update:
+// 5 search-1 rows + 1 search-2 row (the one before the suggestedIndex row)
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 4,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -8,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -8,
+ hidden: true,
+ },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 1
+// Search 2:
+// 10 results including suggestedIndex = 1
+// Expected visible rows during update:
+// 5 search-1 rows + 5 search-2 rows
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 5, type: UrlbarUtils.RESULT_TYPE.URL },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 1
+// Search 2:
+// 10 results including suggestedIndex = 2
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 2,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ stale: true,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 2,
+ hidden: true,
+ },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 1
+// Search 2:
+// 10 results including suggestedIndex = 9
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ stale: true,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 1
+// Search 2:
+// 10 results including suggestedIndex = -1
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ stale: true,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 1
+// Search 2:
+// 10 results including suggestedIndex = -2
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ stale: true,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ hidden: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 1
+// Search 2:
+// 10 results including suggestedIndex = -9
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -9,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ stale: true,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -9,
+ hidden: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 9
+// Search 2:
+// 10 results including suggestedIndex = 1
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 9
+// Search 2:
+// 10 results including suggestedIndex = 3
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 3,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ stale: true,
+ },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 3,
+ hidden: true,
+ },
+ { count: 6, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 9
+// Search 2:
+// 10 results including suggestedIndex = 9
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ stale: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 9
+// Search 2:
+// 10 results including suggestedIndex = -1
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ stale: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 9
+// Search 2:
+// 10 results including suggestedIndex = -7
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -7,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ stale: true,
+ },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -7,
+ hidden: true,
+ },
+ { count: 6, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 10 results including suggestedIndex = 1 and suggestedIndex = -1
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 4,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndexes: [1, -1],
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 1
+// Search 2:
+// 10 results including suggestedIndex = 1 and suggestedIndex = -1
+// Expected visible rows during update:
+// 5 search-1 rows + 5 search-2 rows (some of the ones before the
+// suggestedIndex)
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndexes: [1, -1],
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 5, type: UrlbarUtils.RESULT_TYPE.URL },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = -1
+// Search 2:
+// 10 results including suggestedIndex = 1 and suggestedIndex = -1
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndexes: [1, -1],
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 9 results including suggestedIndex = 1 with resultSpan = 2
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 4,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ resultSpan: 2,
+ viewCount: 9,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ resultSpan: 2,
+ hidden: true,
+ },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 9 results including:
+// suggestedIndex = 1 with resultSpan = 2
+// suggestedIndex = -1
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 4,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndexes: [[1, 2], -1],
+ viewCount: 9,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ resultSpan: 2,
+ hidden: true,
+ },
+ { count: 6, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 1 with resultSpan = 2
+// Search 2:
+// 9 results including:
+// suggestedIndex = 1 with resultSpan = 2
+// suggestedIndex = -1
+// Expected visible rows during update:
+// 5 search-1 rows + 4 search-2 rows
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndexes: [[1, 2]],
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndexes: [[1, 2], -1],
+ viewCount: 9,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ resultSpan: 2,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 4, type: UrlbarUtils.RESULT_TYPE.URL },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
diff --git a/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_5_search_5_url.js b/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_5_search_5_url.js
new file mode 100644
index 0000000000..49d6ac0663
--- /dev/null
+++ b/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_5_search_5_url.js
@@ -0,0 +1,1131 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This test checks row visibility during view updates when rows with suggested
+// indexes are added and removed. Each task performs two searches: Search 1
+// returns 5 results with search suggestions, and search 2 returns 5 results
+// with URL results.
+
+"use strict";
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 5 results including suggestedIndex = 1
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 4,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 5 results including suggestedIndex = 2
+// Expected visible rows during update:
+// 5 search-1 rows + 1 search-2 row (the one before the suggestedIndex row)
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 4,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 2,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 2,
+ hidden: true,
+ },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 5 results including suggestedIndex = 9
+// Expected visible rows during update:
+// 5 search-1 rows + 3 search-2 rows (the ones before the suggestedIndex)
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 4,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 5 results including suggestedIndex = -1
+// Expected visible rows during update:
+// 5 search-1 rows + 3 search-2 rows (the ones before the suggestedIndex row)
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 4,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 5 results including suggestedIndex = -2
+// Expected visible rows during update:
+// 5 search-1 rows + 2 search-2 rows (the one before the suggestedIndex row)
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 4,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ hidden: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 1
+// Search 2:
+// 5 results including suggestedIndex = 1
+// Expected visible rows during update:
+// 5 search-1 rows + 3 search-2 rows (i.e., all rows from both searches)
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 1
+// Search 2:
+// 5 results including suggestedIndex = 2
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 2,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ stale: true,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 2,
+ hidden: true,
+ },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 1
+// Search 2:
+// 5 results including suggestedIndex = 9
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ stale: true,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 1
+// Search 2:
+// 5 results including suggestedIndex = -1
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ stale: true,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 1
+// Search 2:
+// 5 results including suggestedIndex = -2
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ stale: true,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ hidden: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 1
+// Search 2:
+// 5 results including suggestedIndex = -3
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -3,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ stale: true,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -3,
+ hidden: true,
+ },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 9
+// Search 2:
+// 5 results including suggestedIndex = 1
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 9
+// Search 2:
+// 5 results including suggestedIndex = 2
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 2,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ stale: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 2,
+ hidden: true,
+ },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 9
+// Search 2:
+// 5 results including suggestedIndex = 9
+// Expected visible rows during update:
+// 5 original rows with no changes (because the original search results can't
+// be replaced with URL results)
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ stale: true,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 9
+// Search 2:
+// 5 results including suggestedIndex = -1
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ stale: true,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 9
+// Search 2:
+// 5 results including suggestedIndex = -2
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ stale: true,
+ },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ hidden: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 9
+// Search 2:
+// 5 results including suggestedIndex = -3
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -3,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ stale: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -3,
+ hidden: true,
+ },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = -1
+// Search 2:
+// 5 results including suggestedIndex = 1
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = -1
+// Search 2:
+// 5 results including suggestedIndex = 2
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 2,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ stale: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 2,
+ hidden: true,
+ },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = -1
+// Search 2:
+// 5 results including suggestedIndex = 9
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ stale: true,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = -1
+// Search 2:
+// 5 results including suggestedIndex = -1
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ stale: true,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = -1
+// Search 2:
+// 5 results including suggestedIndex = -2
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ stale: true,
+ },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ hidden: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = -2
+// Search 2:
+// 5 results including suggestedIndex = 1
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -2,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ stale: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = -2
+// Search 2:
+// 5 results including suggestedIndex = 2
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -2,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 2,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ stale: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 2,
+ hidden: true,
+ },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = -2
+// Search 2:
+// 5 results including suggestedIndex = 9
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -2,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ stale: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = -2
+// Search 2:
+// 5 results including suggestedIndex = -1
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -2,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ stale: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = -2
+// Search 2:
+// 5 results including suggestedIndex = -2
+// Expected visible rows during update:
+// 5 original rows with no changes (because the original search results can't
+// be replaced with URL results)
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -2,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ stale: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ hidden: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 5 results including suggestedIndex = 1 and suggestedIndex = -1
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 4,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 2,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndexes: [1, -1],
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 4, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 1
+// Search 2:
+// 5 results including suggestedIndex = 1 and suggestedIndex = -1
+// Expected visible rows during update:
+// 5 search-1 rows + 2 search-2 rows (the ones before the suggestedIndex row)
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 2,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndexes: [1, -1],
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = -1
+// Search 2:
+// 5 results including suggestedIndex = 1 and suggestedIndex = -1
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 2,
+ otherType: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndexes: [1, -1],
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
diff --git a/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_5_url_10_search.js b/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_5_url_10_search.js
new file mode 100644
index 0000000000..4306efbeff
--- /dev/null
+++ b/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_5_url_10_search.js
@@ -0,0 +1,1057 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This test checks row visibility during view updates when rows with suggested
+// indexes are added and removed. Each task performs two searches: Search 1
+// returns 5 results where the first result is a search suggestion and the
+// remainder are URL results, and search 2 returns 10 results with search
+// suggestions. This tests the view-update logic that allows search suggestions
+// to replace other results once an existing suggestion row is encountered.
+
+"use strict";
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 10 results including suggestedIndex = 1
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 10 results including suggestedIndex = 2
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 2,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 2,
+ hidden: true,
+ },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 10 results including suggestedIndex = 4
+// Expected visible rows during update:
+// Indexes 2 and 3 replaced with search suggestions, no other changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 4,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 4,
+ hidden: true,
+ },
+ { count: 5, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 10 results including suggestedIndex = 6
+// Expected visible rows during update:
+// All search-2 rows
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 6,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 5, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 6,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ ],
+});
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 10 results including suggestedIndex = 8
+// Expected visible rows during update:
+// All search-2 rows
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 8,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 8,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ ],
+});
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 10 results including suggestedIndex = 9
+// Expected visible rows during update:
+// All search-2 rows
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 9,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 10 results including suggestedIndex = -1
+// Expected visible rows during update:
+// All search-2 rows
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -1,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 10 results including suggestedIndex = -2
+// Expected visible rows during update:
+// All search-2 rows
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -2,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ ],
+});
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 10 results including suggestedIndex = -4
+// Expected visible rows during update:
+// All search-2 rows
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -4,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 5, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -4,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ ],
+});
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 10 results including suggestedIndex = -6
+// Expected visible rows during update:
+// Indexes 2 and 3 replaced with search suggestions, no other changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -6,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -6,
+ hidden: true,
+ },
+ { count: 5, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 10 results including suggestedIndex = -8
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -8,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -8,
+ hidden: true,
+ },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 1
+// Search 2:
+// 10 results including suggestedIndex = 1
+// Expected visible rows during update:
+// All search-2 rows
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 1
+// Search 2:
+// 10 results including suggestedIndex = 2
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 2,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ stale: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 2,
+ hidden: true,
+ },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 1
+// Search 2:
+// 10 results including suggestedIndex = 9
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 9,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ stale: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 1
+// Search 2:
+// 10 results including suggestedIndex = -1
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -1,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ stale: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 1
+// Search 2:
+// 10 results including suggestedIndex = -2
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -2,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ stale: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ hidden: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 1
+// Search 2:
+// 10 results including suggestedIndex = -9
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -9,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ stale: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -9,
+ hidden: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 9
+// Search 2:
+// 10 results including suggestedIndex = 1
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 8, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 9
+// Search 2:
+// 10 results including suggestedIndex = 3
+// Expected visible rows during update:
+// Index 2 replaced with search suggestion, no other changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 3,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 3,
+ hidden: true,
+ },
+ { count: 6, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 9
+// Search 2:
+// 10 results including suggestedIndex = 9
+// Expected visible rows during update:
+// Indexes 2 and 3 replaced with search suggestions, no other changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 9,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ stale: true,
+ },
+ { count: 5, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 9
+// Search 2:
+// 10 results including suggestedIndex = -1
+// Expected visible rows during update:
+// Indexes 2 and 3 replaced with search suggestions, no other changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -1,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ stale: true,
+ },
+ { count: 5, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 9
+// Search 2:
+// 10 results including suggestedIndex = -7
+// Expected visible rows during update:
+// Index 2 replaced with search suggestion, no other changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -7,
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -7,
+ hidden: true,
+ },
+ { count: 6, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 10 results including suggestedIndex = 1 and suggestedIndex = -1
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndexes: [1, -1],
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 1
+// Search 2:
+// 10 results including suggestedIndex = 1 and suggestedIndex = -1
+// Expected visible rows during update:
+// All search-2 rows
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndexes: [1, -1],
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = -1
+// Search 2:
+// 10 results including suggestedIndex = 1 and suggestedIndex = -1
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: -1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndexes: [1, -1],
+ viewCount: 10,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 9 results including suggestedIndex = 1 with resultSpan = 2
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ resultSpan: 2,
+ viewCount: 9,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ resultSpan: 2,
+ hidden: true,
+ },
+ { count: 7, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 9 results including:
+// suggestedIndex = 1 with resultSpan = 2
+// suggestedIndex = -1
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndexes: [[1, 2], -1],
+ viewCount: 9,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ resultSpan: 2,
+ hidden: true,
+ },
+ { count: 6, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 1 with resultSpan = 2
+// Search 2:
+// 9 results including:
+// suggestedIndex = 1 with resultSpan = 2
+// suggestedIndex = -1
+// Expected visible rows during update:
+// All search-2 rows
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndexes: [[1, 2]],
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 10,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndexes: [[1, 2], -1],
+ viewCount: 9,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ resultSpan: 2,
+ },
+ { count: 6, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ },
+ ],
+});
diff --git a/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_5_url_5_search.js b/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_5_url_5_search.js
new file mode 100644
index 0000000000..44078c2251
--- /dev/null
+++ b/browser/components/urlbar/tests/browser-updateResults/browser_suggestedIndex_5_url_5_search.js
@@ -0,0 +1,1203 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This test checks row visibility during view updates when rows with suggested
+// indexes are added and removed. Each task performs two searches: Search 1
+// returns 5 results where the first result is a search suggestion and the
+// remainder are URL results, and search 2 returns 5 results with search
+// suggestions. This tests the view-update logic that allows search suggestions
+// to replace other results once an existing suggestion row is encountered.
+
+"use strict";
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 5 results including suggestedIndex = 1
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 5 results including suggestedIndex = 2
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 2,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 2,
+ hidden: true,
+ },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 5 results including suggestedIndex = 9
+// Expected visible rows during update:
+// Indexes 2 and 3 replaced with search suggestions, no other changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 5 results including suggestedIndex = -1
+// Expected visible rows during update:
+// Indexes 2 and 3 replaced with search suggestions, no other changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -1,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 5 results including suggestedIndex = -2
+// Expected visible rows during update:
+// Index 2 replaced with search suggestion, no other changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -2,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ hidden: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 1
+// Search 2:
+// 5 results including suggestedIndex = 1
+// Expected visible rows during update:
+// All search-2 rows
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ {
+ count: 2,
+ type: UrlbarPrefs.get("resultMenu")
+ ? UrlbarUtils.RESULT_TYPE.URL
+ : UrlbarUtils.RESULT_TYPE.SEARCH,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 1
+// Search 2:
+// 5 results including suggestedIndex = 2
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 2,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ stale: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 2,
+ hidden: true,
+ },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 1
+// Search 2:
+// 5 results including suggestedIndex = 9
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ stale: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 1
+// Search 2:
+// 5 results including suggestedIndex = -1
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -1,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ stale: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 1
+// Search 2:
+// 5 results including suggestedIndex = -2
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -2,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ stale: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ hidden: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 1
+// Search 2:
+// 5 results including suggestedIndex = -3
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -3,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ stale: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -3,
+ hidden: true,
+ },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 9
+// Search 2:
+// 5 results including suggestedIndex = 1
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 9
+// Search 2:
+// 5 results including suggestedIndex = 2
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 2,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 2,
+ hidden: true,
+ },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 9
+// Search 2:
+// 5 results including suggestedIndex = 9
+// Expected visible rows during update:
+// All search-2 rows
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ {
+ count: 2,
+ type: UrlbarPrefs.get("resultMenu")
+ ? UrlbarUtils.RESULT_TYPE.URL
+ : UrlbarUtils.RESULT_TYPE.SEARCH,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 9
+// Search 2:
+// 5 results including suggestedIndex = -1
+// Expected visible rows during update:
+// Indexes 2 and 3 replaced with search suggestions, no other changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -1,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 9
+// Search 2:
+// 5 results including suggestedIndex = -2
+// Expected visible rows during update:
+// Index 2 replaced with search suggestion, no other changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -2,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ hidden: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 9
+// Search 2:
+// 5 results including suggestedIndex = -3
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -3,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -3,
+ hidden: true,
+ },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = -1
+// Search 2:
+// 5 results including suggestedIndex = 1
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: -1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = -1
+// Search 2:
+// 5 results including suggestedIndex = 2
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: -1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 2,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 2,
+ hidden: true,
+ },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = -1
+// Search 2:
+// 5 results including suggestedIndex = 9
+// Expected visible rows during update:
+// Indexes 2 and 3 replaced with search suggestions, no other changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: -1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = -1
+// Search 2:
+// 5 results including suggestedIndex = -1
+// Expected visible rows during update:
+// All search-2 rows
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: -1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -1,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ {
+ count: 2,
+ type: UrlbarPrefs.get("resultMenu")
+ ? UrlbarUtils.RESULT_TYPE.URL
+ : UrlbarUtils.RESULT_TYPE.SEARCH,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = -1
+// Search 2:
+// 5 results including suggestedIndex = -2
+// Expected visible rows during update:
+// Index 2 replaced with search suggestion, no other changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: -1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -2,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ hidden: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = -2
+// Search 2:
+// 5 results including suggestedIndex = 1
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: -2,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ stale: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = -2
+// Search 2:
+// 5 results including suggestedIndex = 2
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: -2,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 2,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ stale: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 2,
+ hidden: true,
+ },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = -2
+// Search 2:
+// 5 results including suggestedIndex = 9
+// Expected visible rows during update:
+// Index 2 replaced with search suggestion, no other changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: -2,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: 9,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ stale: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 9,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = -2
+// Search 2:
+// 5 results including suggestedIndex = -1
+// Expected visible rows during update:
+// Index 2 replaced with search suggestion, no other changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: -2,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -1,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ stale: true,
+ },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = -2
+// Search 2:
+// 5 results including suggestedIndex = -2
+// Expected visible rows during update:
+// All search-2 rows
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: -2,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 3,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndex: -2,
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ {
+ count: 1,
+ type: UrlbarPrefs.get("resultMenu")
+ ? UrlbarUtils.RESULT_TYPE.URL
+ : UrlbarUtils.RESULT_TYPE.SEARCH,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -2,
+ },
+ {
+ count: 1,
+ type: UrlbarPrefs.get("resultMenu")
+ ? UrlbarUtils.RESULT_TYPE.URL
+ : UrlbarUtils.RESULT_TYPE.SEARCH,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results, no suggestedIndex
+// Search 2:
+// 5 results including suggestedIndex = 1 and suggestedIndex = -1
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 2,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndexes: [1, -1],
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 3, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = 1
+// Search 2:
+// 5 results including suggestedIndex = 1 and suggestedIndex = -1
+// Expected visible rows during update:
+// Index 3 replaced with search suggestion, no other changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: 1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 2,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndexes: [1, -1],
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
+
+// Search 1:
+// 5 results including suggestedIndex = -1
+// Search 2:
+// 5 results including suggestedIndex = 1 and suggestedIndex = -1
+// Expected visible rows during update:
+// 5 original rows with no changes
+add_suggestedIndex_task({
+ search1: {
+ other: [
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL },
+ ],
+ suggestedIndex: -1,
+ viewCount: 5,
+ },
+ search2: {
+ otherCount: 2,
+ otherType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ suggestedIndexes: [1, -1],
+ viewCount: 5,
+ },
+ duringUpdate: [
+ { count: 1 },
+ { count: 1, type: UrlbarUtils.RESULT_TYPE.SEARCH, stale: true },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.URL, stale: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ stale: true,
+ },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: 1,
+ hidden: true,
+ },
+ { count: 2, type: UrlbarUtils.RESULT_TYPE.SEARCH, hidden: true },
+ {
+ count: 1,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ suggestedIndex: -1,
+ hidden: true,
+ },
+ ],
+});
diff --git a/browser/components/urlbar/tests/browser-updateResults/head.js b/browser/components/urlbar/tests/browser-updateResults/head.js
new file mode 100644
index 0000000000..7bdb1bcb1f
--- /dev/null
+++ b/browser/components/urlbar/tests/browser-updateResults/head.js
@@ -0,0 +1,550 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// The files in this directory test UrlbarView.#updateResults().
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+ UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs",
+ UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
+ UrlbarView: "resource:///modules/UrlbarView.sys.mjs",
+});
+
+XPCOMUtils.defineLazyGetter(this, "UrlbarTestUtils", () => {
+ const { UrlbarTestUtils: module } = ChromeUtils.importESModule(
+ "resource://testing-common/UrlbarTestUtils.sys.mjs"
+ );
+ module.init(this);
+ return module;
+});
+
+add_setup(async function headInit() {
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Make absolutely sure the panel stays open during the test. There are
+ // spurious blurs on WebRender TV tests as the test starts that cause the
+ // panel to close and the query to be canceled, resulting in intermittent
+ // failures without this.
+ ["ui.popup.disable_autohide", true],
+
+ // Make sure maxRichResults is 10 for sanity.
+ ["browser.urlbar.maxRichResults", 10],
+ ],
+ });
+
+ // Increase the timeout of the remove-stale-rows timer so that it doesn't
+ // interfere with the tests.
+ let originalRemoveStaleRowsTimeout = UrlbarView.removeStaleRowsTimeout;
+ UrlbarView.removeStaleRowsTimeout = 30000;
+ registerCleanupFunction(() => {
+ UrlbarView.removeStaleRowsTimeout = originalRemoveStaleRowsTimeout;
+ });
+});
+
+/**
+ * A test provider that doesn't finish startQuery() until `finishQueryPromise`
+ * is resolved.
+ */
+class DelayingTestProvider extends UrlbarTestUtils.TestProvider {
+ finishQueryPromise = null;
+ async startQuery(context, addCallback) {
+ for (let result of this._results) {
+ addCallback(this, result);
+ }
+ await this.finishQueryPromise;
+ }
+}
+
+/**
+ * Makes a result with a suggested index.
+ *
+ * @param {number} suggestedIndex
+ * The preferred index of the result.
+ * @param {number} resultSpan
+ * The result will have this span.
+ * @returns {UrlbarResult}
+ */
+function makeSuggestedIndexResult(suggestedIndex, resultSpan = 1) {
+ return Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ {
+ url: "http://example.com/si",
+ displayUrl: "http://example.com/si",
+ title: "suggested index",
+ }
+ ),
+ { suggestedIndex, resultSpan }
+ );
+}
+
+/**
+ * Makes an array of results for the suggestedIndex tests. The array will
+ * include a heuristic followed by the specified results.
+ *
+ * @param {object} options
+ * The options object
+ * @param {number} [options.count]
+ * The number of results to return other than the heuristic. This and
+ * `type` must be given together.
+ * @param {UrlbarUtils.RESULT_TYPE} [options.type]
+ * The type of results to return other than the heuristic. This and `count`
+ * must be given together.
+ * @param {Array} [options.specs]
+ * If you want a mix of result types instead of only one type, then use this
+ * param instead of `count` and `type`. Each item in this array must be an
+ * object with the following properties:
+ * {number} count
+ * The number of results to return for the given `type`.
+ * {UrlbarUtils.RESULT_TYPE} type
+ * The type of results.
+ * @returns {Array}
+ * An array of results.
+ */
+function makeProviderResults({ count = 0, type = undefined, specs = [] }) {
+ if (count) {
+ specs.push({ count, type });
+ }
+
+ let query = "test";
+ let results = [
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ {
+ query,
+ engine: Services.search.defaultEngine.name,
+ }
+ ),
+ { heuristic: true }
+ ),
+ ];
+
+ for (let { count: specCount, type: specType } of specs) {
+ for (let i = 0; i < specCount; i++) {
+ let str = `${query} ${results.length}`;
+ switch (specType) {
+ case UrlbarUtils.RESULT_TYPE.SEARCH:
+ results.push(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ {
+ query,
+ suggestion: str,
+ lowerCaseSuggestion: str.toLowerCase(),
+ engine: Services.search.defaultEngine.name,
+ }
+ )
+ );
+ break;
+ case UrlbarUtils.RESULT_TYPE.URL:
+ results.push(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ {
+ url: "http://example.com/" + i,
+ displayUrl: "http://example.com/" + i,
+ title: str,
+ }
+ )
+ );
+ break;
+ default:
+ throw new Error(`Unsupported makeProviderResults type: ${specType}`);
+ }
+ }
+ }
+
+ return results;
+}
+
+let gSuggestedIndexTaskIndex = 0;
+
+/**
+ * Adds a suggestedIndex test task. See doSuggestedIndexTest() for params.
+ *
+ * @param {object} options
+ * See doSuggestedIndexTest().
+ */
+function add_suggestedIndex_task(options) {
+ if (!gSuggestedIndexTaskIndex) {
+ initSuggestedIndexTest();
+ }
+ let testIndex = gSuggestedIndexTaskIndex++;
+ let testName = "test_" + testIndex;
+ let testDesc = JSON.stringify(options);
+ let func = async () => {
+ info(`Running task at index ${testIndex}: ${testDesc}`);
+ await doSuggestedIndexTest(options);
+ };
+ Object.defineProperty(func, "name", { value: testName });
+ add_task(func);
+}
+
+/**
+ * Initializes suggestedIndex tests. You don't normally need to call this from
+ * your test because add_suggestedIndex_task() calls it automatically.
+ */
+function initSuggestedIndexTest() {
+ // These tests can time out on Mac TV WebRender just because they do so much,
+ // so request a longer timeout.
+ if (AppConstants.platform == "macosx") {
+ requestLongerTimeout(3);
+ }
+ registerCleanupFunction(() => {
+ gSuggestedIndexTaskIndex = 0;
+ });
+}
+
+/**
+ * @typedef {object} SuggestedIndexTestOptions
+ * @property {number} [otherCount]
+ * The number of results other than the heuristic and suggestedIndex results
+ * that the provider should return for search 1. This and `otherType` must be
+ * given together.
+ * @property {UrlbarUtils.RESULT_TYPE} [otherType]
+ * The type of results other than the heuristic and suggestedIndex results
+ * that the provider should return for search 1. This and `otherCount` must be
+ * given together.
+ * @property {Array} [other]
+ * If you want the provider to return a mix of result types instead of only
+ * one type, then use this param instead of `otherCount` and `otherType`. Each
+ * item in this array must be an object with the following properties:
+ * {number} count
+ * The number of results to return for the given `type`.
+ * {UrlbarUtils.RESULT_TYPE} type
+ * The type of results.
+ * @property {number} viewCount
+ * The total number of results expected in the view after search 1 finishes,
+ * including the heuristic and suggestedIndex results.
+ * @param {number} [suggestedIndex]
+ * If given, the provider will return a result with this suggested index for
+ * search 1.
+ * @property {number} [resultSpan]
+ * If this and `search1.suggestedIndex` are given, then the suggestedIndex
+ * result for search 1 will have this resultSpan.
+ * @property {Array} [suggestedIndexes]
+ * If you want the provider to return more than one suggestedIndex result for
+ * search 1, then use this instead of `search1.suggestedIndex`. Each item in
+ * this array must be one of the following:
+ * suggestedIndex value
+ * [suggestedIndex, resultSpan] tuple
+ */
+
+/**
+ * Runs a suggestedIndex test. Performs two searches and checks the results just
+ * after the view update and after the second search finishes. The caller is
+ * responsible for passing in a description of what the rows should look like
+ * just after the view update finishes but before the second search finishes,
+ * i.e., before stale rows are removed and hidden rows are shown -- this is the
+ * `duringUpdate` param. The important thing this checks is that the rows with
+ * suggested indexes don't move around or appear in the wrong places.
+ *
+ * @param {object} options
+ * The options object
+ * @param {SuggestedIndexTestOptions} options.search1
+ * The first search options object
+ * @param {SuggestedIndexTestOptions} options.search2
+ * This object has the same properties as the `search1` object but it applies
+ * to the second search.
+ * @param {Array<{ count: number, type: UrlbarUtils.RESULT_TYPE, suggestedIndex: ?number, stale: ?boolean, hidden: ?boolean }>} options.duringUpdate
+ * An array of expected row states during the view update. Each item in the
+ * array must be an object with the following properties:
+ * {number} count
+ * The number of rows in the view to which this row state object applies.
+ * {UrlbarUtils.RESULT_TYPE} type
+ * The expected type of the rows.
+ * {number} [suggestedIndex]
+ * The expected suggestedIndex of the row.
+ * {boolean} [stale]
+ * Whether the rows are expected to be stale. Defaults to false.
+ * {boolean} [hidden]
+ * Whether the rows are expected to be hidden. Defaults to false.
+ */
+async function doSuggestedIndexTest({ search1, search2, duringUpdate }) {
+ // We use this test provider to test specific results. It has an Infinity
+ // priority so that it provides all results in our test searches, including
+ // the heuristic. That lets us avoid any potential races with the built-in
+ // providers; testing them is not important here.
+ let provider = new DelayingTestProvider({ priority: Infinity });
+ UrlbarProvidersManager.registerProvider(provider);
+ registerCleanupFunction(() => {
+ UrlbarProvidersManager.unregisterProvider(provider);
+ });
+
+ // Set up the first search. First, add the non-suggestedIndex results to the
+ // provider.
+ provider._results = makeProviderResults({
+ specs: search1.other,
+ count: search1.otherCount,
+ type: search1.otherType,
+ });
+
+ // Set up `suggestedIndexes`. It's an array with [suggestedIndex, resultSpan]
+ // tuples.
+ if (!search1.suggestedIndexes) {
+ search1.suggestedIndexes = [];
+ }
+ search1.suggestedIndexes = search1.suggestedIndexes.map(value =>
+ typeof value == "number" ? [value, 1] : value
+ );
+ if (typeof search1.suggestedIndex == "number") {
+ search1.suggestedIndexes.push([
+ search1.suggestedIndex,
+ search1.resultSpan || 1,
+ ]);
+ }
+
+ // Add the suggestedIndex results to the provider.
+ for (let [suggestedIndex, resultSpan] of search1.suggestedIndexes) {
+ provider._results.push(
+ makeSuggestedIndexResult(suggestedIndex, resultSpan)
+ );
+ }
+
+ // Do the first search.
+ provider.finishQueryPromise = Promise.resolve();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+
+ // Sanity check the results.
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ search1.viewCount,
+ "Row count after first search"
+ );
+ for (let [suggestedIndex, resultSpan] of search1.suggestedIndexes) {
+ let index =
+ suggestedIndex >= 0
+ ? Math.min(search1.viewCount - 1, suggestedIndex)
+ : Math.max(0, search1.viewCount + suggestedIndex);
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index);
+ Assert.equal(
+ result.element.row.result.suggestedIndex,
+ suggestedIndex,
+ "suggestedIndex after first search"
+ );
+ Assert.equal(
+ UrlbarUtils.getSpanForResult(result.element.row.result),
+ resultSpan,
+ "resultSpan after first search"
+ );
+ }
+
+ // Set up the second search. First, add the non-suggestedIndex results to the
+ // provider.
+ provider._results = makeProviderResults({
+ specs: search2.other,
+ count: search2.otherCount,
+ type: search2.otherType,
+ });
+
+ // Set up `suggestedIndexes`. It's an array with [suggestedIndex, resultSpan]
+ // tuples.
+ if (!search2.suggestedIndexes) {
+ search2.suggestedIndexes = [];
+ }
+ search2.suggestedIndexes = search2.suggestedIndexes.map(value =>
+ typeof value == "number" ? [value, 1] : value
+ );
+ if (typeof search2.suggestedIndex == "number") {
+ search2.suggestedIndexes.push([
+ search2.suggestedIndex,
+ search2.resultSpan || 1,
+ ]);
+ }
+
+ // Add the suggestedIndex results to the provider.
+ for (let [suggestedIndex, resultSpan] of search2.suggestedIndexes) {
+ provider._results.push(
+ makeSuggestedIndexResult(suggestedIndex, resultSpan)
+ );
+ }
+
+ let rowCountDuringUpdate = duringUpdate.reduce(
+ (count, rowState) => count + rowState.count,
+ 0
+ );
+
+ // Don't allow the search to finish until we check the updated rows. We'll
+ // accomplish that by adding a mutation observer to observe completion of the
+ // update and delaying resolving the provider's finishQueryPromise.
+ let mutationPromise = new Promise(resolve => {
+ let lastRowState = duringUpdate[duringUpdate.length - 1];
+ let observer = new MutationObserver(mutations => {
+ observer.disconnect();
+ resolve();
+ });
+ if (lastRowState.stale) {
+ // The last row during the update is expected to become stale. Wait for
+ // the stale attribute to be set on it. We'll actually just wait for any
+ // attribute.
+ let { children } = UrlbarTestUtils.getResultsContainer(window);
+ observer.observe(children[children.length - 1], { attributes: true });
+ } else if (search1.viewCount == rowCountDuringUpdate) {
+ // No rows are expected to be added during the view update, so it must be
+ // the case that some rows will be updated for results in the the second
+ // search. Wait for any change to an existing row.
+ observer.observe(UrlbarTestUtils.getResultsContainer(window), {
+ subtree: true,
+ attributes: true,
+ characterData: true,
+ });
+ } else {
+ // Rows are expected to be added during the update. Wait for them.
+ observer.observe(UrlbarTestUtils.getResultsContainer(window), {
+ childList: true,
+ });
+ }
+ });
+
+ // Now do the second search but don't wait for it to finish.
+ let resolveQuery;
+ provider.finishQueryPromise = new Promise(
+ resolve => (resolveQuery = resolve)
+ );
+ let queryPromise = UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+
+ // Wait for the update to finish.
+ await mutationPromise;
+
+ // Check the rows. We can't use UrlbarTestUtils.getDetailsOfResultAt() here
+ // because it waits for the search to finish.
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ rowCountDuringUpdate,
+ "Row count during update"
+ );
+ let rows = UrlbarTestUtils.getResultsContainer(window).children;
+ let rowIndex = 0;
+ for (let rowState of duringUpdate) {
+ for (let i = 0; i < rowState.count; i++) {
+ let row = rows[rowIndex];
+
+ // type
+ if ("type" in rowState) {
+ Assert.equal(
+ row.result.type,
+ rowState.type,
+ `Type at index ${rowIndex} during update`
+ );
+ }
+
+ // suggestedIndex
+ if ("suggestedIndex" in rowState) {
+ Assert.ok(
+ row.result.hasSuggestedIndex,
+ `Row at index ${rowIndex} has suggestedIndex during update`
+ );
+ Assert.equal(
+ row.result.suggestedIndex,
+ rowState.suggestedIndex,
+ `suggestedIndex at index ${rowIndex} during update`
+ );
+ } else {
+ Assert.ok(
+ !row.result.hasSuggestedIndex,
+ `Row at index ${rowIndex} does not have suggestedIndex during update`
+ );
+ }
+
+ // resultSpan
+ Assert.equal(
+ UrlbarUtils.getSpanForResult(row.result),
+ rowState.resultSpan || 1,
+ `resultSpan at index ${rowIndex} during update`
+ );
+
+ // stale
+ if (rowState.stale) {
+ Assert.equal(
+ row.getAttribute("stale"),
+ "true",
+ `Row at index ${rowIndex} is stale during update`
+ );
+ } else {
+ Assert.ok(
+ !row.hasAttribute("stale"),
+ `Row at index ${rowIndex} is not stale during update`
+ );
+ }
+
+ // visible
+ Assert.equal(
+ BrowserTestUtils.is_visible(row),
+ !rowState.hidden,
+ `Visible at index ${rowIndex} during update`
+ );
+
+ rowIndex++;
+ }
+ }
+
+ // Finish the search.
+ resolveQuery();
+ await queryPromise;
+
+ // Check the rows now that the second search is done. First, build a map from
+ // real indexes to suggested index. e.g., if a suggestedIndex = -1, then the
+ // real index = the result count - 1.
+ let suggestedIndexesByRealIndex = new Map();
+ for (let [suggestedIndex, resultSpan] of search2.suggestedIndexes) {
+ let realIndex =
+ suggestedIndex >= 0
+ ? Math.min(suggestedIndex, search2.viewCount - 1)
+ : Math.max(0, search2.viewCount + suggestedIndex);
+ suggestedIndexesByRealIndex.set(realIndex, [suggestedIndex, resultSpan]);
+ }
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ search2.viewCount,
+ "Row count after update"
+ );
+ for (let i = 0; i < search2.viewCount; i++) {
+ let result = rows[i].result;
+ let tuple = suggestedIndexesByRealIndex.get(i);
+ if (tuple) {
+ let [suggestedIndex, resultSpan] = tuple;
+ Assert.ok(
+ result.hasSuggestedIndex,
+ `Row at index ${i} has suggestedIndex after update`
+ );
+ Assert.equal(
+ result.suggestedIndex,
+ suggestedIndex,
+ `suggestedIndex at index ${i} after update`
+ );
+ Assert.equal(
+ UrlbarUtils.getSpanForResult(result),
+ resultSpan,
+ `resultSpan at index ${i} after update`
+ );
+ } else {
+ Assert.ok(
+ !result.hasSuggestedIndex,
+ `Row at index ${i} does not have suggestedIndex after update`
+ );
+ }
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ gURLBar.handleRevert();
+ UrlbarProvidersManager.unregisterProvider(provider);
+}
diff --git a/browser/components/urlbar/tests/browser/POSTSearchEngine.xml b/browser/components/urlbar/tests/browser/POSTSearchEngine.xml
new file mode 100644
index 0000000000..8b387ea9ae
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/POSTSearchEngine.xml
@@ -0,0 +1,6 @@
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
+ <ShortName>POST Search</ShortName>
+ <Url type="text/html" method="POST" template="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/print_postdata.sjs">
+ <Param name="searchterms" value="{searchTerms}"/>
+ </Url>
+</OpenSearchDescription>
diff --git a/browser/components/urlbar/tests/browser/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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>add_search_engine_0</ShortName>
+<Url type="text/html" method="GET" template="http://mochi.test:8888/" rel="searchform">
+ <Param name="terms" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/browser/components/urlbar/tests/browser/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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>add_search_engine_1</ShortName>
+<Url type="text/html" method="GET" template="http://mochi.test:8888/" rel="searchform">
+ <Param name="terms" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/browser/components/urlbar/tests/browser/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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>add_search_engine_2</ShortName>
+<Url type="text/html" method="GET" template="http://mochi.test:8888/" rel="searchform">
+ <Param name="terms" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/browser/components/urlbar/tests/browser/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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>add_search_engine_3</ShortName>
+<Url type="text/html" method="GET" template="http://mochi.test:8888/" rel="searchform">
+ <Param name="terms" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/browser/components/urlbar/tests/browser/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 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<link rel="search"
+ type="application/opensearchdescription+xml"
+ title="add_search_engine_404"
+ href="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/add_search_engine_404.xml">
+</head>
+<body></body>
+</html>
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 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="/>
+<link rel="search"
+ type="application/opensearchdescription+xml"
+ title="add_search_engine_0"
+ href="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/add_search_engine_0.xml">
+<link rel="search"
+ type="application/opensearchdescription+xml"
+ title="add_search_engine_1"
+ href="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/add_search_engine_1.xml">
+<link rel="search"
+ type="application/opensearchdescription+xml"
+ title="add_search_engine_2"
+ href="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/add_search_engine_2.xml">
+<link rel="search"
+ type="application/opensearchdescription+xml"
+ title="add_search_engine_3"
+ href="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/add_search_engine_3.xml">
+</head>
+<body></body>
+</html>
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 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="/>
+<link rel="search"
+ type="application/opensearchdescription+xml"
+ title="add_search_engine_0"
+ href="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/add_search_engine_0.xml">
+</head>
+<body></body>
+</html>
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 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<link rel="search"
+ type="application/opensearchdescription+xml"
+ title="add_search_engine_0"
+ href="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/add_search_engine_0.xml">
+<link rel="search"
+ type="application/opensearchdescription+xml"
+ title="add_search_engine_0"
+ href="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/add_search_engine_0.xml">
+</head>
+<body></body>
+</html>
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 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="/>
+<link rel="search"
+ type="application/opensearchdescription+xml"
+ title="add_search_engine_0"
+ href="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/add_search_engine_0.xml">
+ <link rel="search"
+ type="application/opensearchdescription+xml"
+ title="add_search_engine_1"
+ href="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/add_search_engine_2.xml">
+</head>
+<body></body>
+</html>
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("<html xmlns='http://www.w3.org/1999/xhtml'>");
+ response.write(
+ "<p>Login: <span id='ok'>" +
+ (requestAuth ? "FAIL" : "PASS") +
+ "</span></p>\n"
+ );
+ response.write(
+ "<p>Proxy: <span id='proxy'>" +
+ (requestProxyAuth ? "FAIL" : "PASS") +
+ "</span></p>\n"
+ );
+ response.write("<p>Auth: <span id='auth'>" + authHeader + "</span></p>\n");
+ response.write("<p>User: <span id='user'>" + actual_user + "</span></p>\n");
+ response.write("<p>Pass: <span id='pass'>" + actual_pass + "</span></p>\n");
+
+ if (huge) {
+ response.write("<div style='display: none'>");
+ for (let i = 0; i < 100000; i++) {
+ response.write("123456789\n");
+ }
+ response.write("</div>");
+ response.write(
+ "<span id='footnote'>This is a footnote after the huge content fill</span>"
+ );
+ }
+
+ if (plugin) {
+ response.write(
+ "<embed id='embedtest' style='width: 400px; height: 100px;' " +
+ "type='application/x-test'></embed>\n"
+ );
+ }
+
+ response.write("</html>");
+}
diff --git a/browser/components/urlbar/tests/browser/browser.ini b/browser/components/urlbar/tests/browser/browser.ini
new file mode 100644
index 0000000000..64290236d3
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser.ini
@@ -0,0 +1,434 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+[DEFAULT]
+support-files =
+ dummy_page.html
+ head.js
+ head-common.js
+skip-if =
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+
+prefs =
+ browser.urlbar.trending.featureGate=false
+ extensions.screenshots.disabled=false
+ screenshots.browser.component.enabled=true
+
+[browser_UrlbarInput_formatValue.js]
+[browser_UrlbarInput_formatValue_detachedTab.js]
+skip-if =
+ apple_catalina # Bug 1756585
+ os == 'win' # Bug 1756585
+[browser_UrlbarInput_hiddenFocus.js]
+[browser_UrlbarInput_overflow.js]
+[browser_UrlbarInput_overflow_resize.js]
+[browser_UrlbarInput_privateFeature.js]
+[browser_UrlbarInput_searchTerms.js]
+[browser_UrlbarInput_searchTerms_backgroundTabs.js]
+[browser_UrlbarInput_searchTerms_modifiedUrl.js]
+[browser_UrlbarInput_searchTerms_moveTab.js]
+[browser_UrlbarInput_searchTerms_popup.js]
+[browser_UrlbarInput_searchTerms_revert.js]
+[browser_UrlbarInput_searchTerms_searchBar.js]
+[browser_UrlbarInput_searchTerms_searchMode.js]
+[browser_UrlbarInput_searchTerms_switch_tab.js]
+[browser_UrlbarInput_searchTerms_telemetry.js]
+[browser_UrlbarInput_setURI.js]
+https_first_disabled = true
+skip-if =
+ apple_catalina && debug # Bug 1773790
+[browser_UrlbarInput_tooltip.js]
+[browser_UrlbarInput_trimURLs.js]
+https_first_disabled = true
+[browser_aboutHomeLoading.js]
+skip-if =
+ tsan # Intermittently times out, see 1622698 (frequent on TSan).
+ os == 'linux' && bits == 64 && !debug # Bug 1622698
+[browser_acknowledgeFeedbackAndDismissal.js]
+[browser_action_searchengine.js]
+[browser_action_searchengine_alias.js]
+[browser_add_search_engine.js]
+support-files =
+ add_search_engine_0.xml
+ add_search_engine_1.xml
+ add_search_engine_2.xml
+ add_search_engine_3.xml
+ add_search_engine_invalid.html
+ add_search_engine_one.html
+ add_search_engine_many.html
+ add_search_engine_same_names.html
+ add_search_engine_two.html
+[browser_autoFill_backspaced.js]
+[browser_autoFill_canonize.js]
+https_first_disabled = true
+[browser_autoFill_caretNotAtEnd.js]
+[browser_autoFill_firstResult.js]
+[browser_autoFill_paste.js]
+[browser_autoFill_placeholder.js]
+[browser_autoFill_preserve.js]
+[browser_autoFill_trimURLs.js]
+[browser_autoFill_typed.js]
+[browser_autoFill_undo.js]
+[browser_autoOpen.js]
+[browser_autocomplete_a11y_label.js]
+support-files =
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+[browser_autocomplete_autoselect.js]
+[browser_autocomplete_cursor.js]
+[browser_autocomplete_edit_completed.js]
+[browser_autocomplete_enter_race.js]
+https_first_disabled = true
+[browser_autocomplete_no_title.js]
+[browser_autocomplete_readline_navigation.js]
+skip-if = os != "mac" # Mac only feature
+[browser_autocomplete_tag_star_visibility.js]
+[browser_bestMatch.js]
+[browser_blanking.js]
+support-files =
+ file_blank_but_not_blank.html
+[browser_bufferer_onQueryResults.js]
+[browser_calculator.js]
+[browser_canonizeURL.js]
+https_first_disabled = true
+support-files =
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+[browser_caret_position.js]
+[browser_click_row_border.js]
+[browser_closePanelOnClick.js]
+[browser_content_opener.js]
+[browser_contextualsearch.js]
+[browser_copy_during_load.js]
+support-files =
+ slow-page.sjs
+[browser_copying.js]
+https_first_disabled = true
+support-files =
+ authenticate.sjs
+[browser_customizeMode.js]
+[browser_cutting.js]
+[browser_decode.js]
+[browser_delete.js]
+[browser_deleteAllText.js]
+[browser_display_selectedAction_Extensions.js]
+[browser_dns_first_for_single_words.js]
+skip-if = verify && os == 'linux' # Bug 1581635
+[browser_downArrowKeySearch.js]
+https_first_disabled = true
+[browser_dragdropURL.js]
+[browser_dynamicResults.js]
+https_first_disabled = true
+support-files =
+ dynamicResult0.css
+ dynamicResult1.css
+[browser_edit_invalid_url.js]
+[browser_engagement.js]
+[browser_enter.js]
+[browser_enterAfterMouseOver.js]
+[browser_focusedCmdK.js]
+[browser_groupLabels.js]
+support-files =
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+[browser_handleCommand_fallback.js]
+[browser_hashChangeProxyState.js]
+[browser_helpUrl.js]
+[browser_heuristicNotAddedFirst.js]
+[browser_hideHeuristic.js]
+[browser_ime_composition.js]
+[browser_inputHistory.js]
+https_first_disabled = true
+support-files =
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+[browser_inputHistory_autofill.js]
+[browser_inputHistory_emptystring.js]
+[browser_keepStateAcrossTabSwitches.js]
+https_first_disabled = true
+[browser_keyword.js]
+support-files =
+ print_postdata.sjs
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+[browser_keywordBookmarklets.js]
+[browser_keywordSearch.js]
+support-files =
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+[browser_keywordSearch_postData.js]
+support-files =
+ POSTSearchEngine.xml
+ print_postdata.sjs
+[browser_keyword_override.js]
+[browser_keyword_select_and_type.js]
+[browser_loadRace.js]
+[browser_locationBarCommand.js]
+https_first_disabled = true
+skip-if =
+ os == 'linux' && bits == 64 && !debug # Bug 1787020
+[browser_locationBarExternalLoad.js]
+[browser_locationchange_urlbar_edit_dos.js]
+support-files =
+ file_urlbar_edit_dos.html
+[browser_middleClick.js]
+[browser_new_tab_urlbar_reset.js]
+[browser_oneOffs.js]
+support-files =
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+[browser_oneOffs_contextMenu.js]
+support-files =
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+[browser_oneOffs_heuristicRestyle.js]
+skip-if =
+ os == "linux" && bits == 64 && !debug # Bug 1775811
+[browser_oneOffs_keyModifiers.js]
+support-files =
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+[browser_oneOffs_searchSuggestions.js]
+support-files =
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+ searchSuggestionEngine2.xml
+[browser_oneOffs_settings.js]
+[browser_pasteAndGo.js]
+https_first_disabled = true
+[browser_paste_multi_lines.js]
+[browser_paste_then_focus.js]
+[browser_paste_then_switch_tab.js]
+[browser_percent_encoded.js]
+[browser_placeholder.js]
+support-files =
+ searchSuggestionEngine.xml
+ searchSuggestionEngine2.xml
+ searchSuggestionEngine.sjs
+[browser_populateAfterPushState.js]
+[browser_primary_selection_safe_on_new_tab.js]
+[browser_privateBrowsingWindowChange.js]
+[browser_queryContextCache.js]
+[browser_quickactions.js]
+skip-if =
+ os == "linux" # Bug 1806090
+[browser_quickactions_devtools.js]
+[browser_quickactions_tab_refocus.js]
+[browser_raceWithTabs.js]
+[browser_redirect_error.js]
+support-files = redirect_error.sjs
+[browser_remoteness_switch.js]
+https_first_disabled = true
+[browser_remotetab.js]
+[browser_removeUnsafeProtocolsFromURLBarPaste.js]
+[browser_remove_match.js]
+[browser_restoreEmptyInput.js]
+[browser_resultSpan.js]
+[browser_result_menu.js]
+[browser_result_onSelection.js]
+[browser_retainedResultsOnFocus.js]
+[browser_revert.js]
+[browser_searchFunction.js]
+[browser_searchHistoryLimit.js]
+[browser_searchMode_alias_replacement.js]
+support-files =
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+[browser_searchMode_autofill.js]
+[browser_searchMode_clickLink.js]
+https_first_disabled = true
+support-files =
+ dummy_page.html
+[browser_searchMode_engineRemoval.js]
+[browser_searchMode_excludeResults.js]
+[browser_searchMode_heuristic.js]
+https_first_disabled = true
+[browser_searchMode_indicator.js]
+https_first_disabled = true
+support-files =
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+[browser_searchMode_indicator_clickthrough.js]
+[browser_searchMode_localOneOffs_actionText.js]
+support-files =
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+[browser_searchMode_newWindow.js]
+[browser_searchMode_no_results.js]
+[browser_searchMode_oneOffButton.js]
+[browser_searchMode_pickResult.js]
+https_first_disabled = true
+[browser_searchMode_preview.js]
+[browser_searchMode_sessionStore.js]
+https_first_disabled = true
+skip-if = os == 'mac' && debug && verify # bug 1671045
+[browser_searchMode_setURI.js]
+https_first_disabled = true
+[browser_searchMode_suggestions.js]
+support-files =
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+ searchSuggestionEngineMany.xml
+[browser_searchMode_switchTabs.js]
+[browser_searchSettings.js]
+[browser_searchSingleWordNotification.js]
+https_first_disabled = true
+skip-if =
+ os == 'linux' && bits == 64 # Bug 1773830
+[browser_searchSuggestions.js]
+support-files =
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+[browser_searchTelemetry.js]
+support-files =
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+[browser_search_bookmarks_from_bookmarks_menu.js]
+[browser_search_history_from_history_panel.js]
+[browser_selectStaleResults.js]
+support-files =
+ searchSuggestionEngineSlow.xml
+ searchSuggestionEngine.sjs
+[browser_selectionKeyNavigation.js]
+[browser_separatePrivateDefault.js]
+support-files =
+ POSTSearchEngine.xml
+ print_postdata.sjs
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+ searchSuggestionEngine2.xml
+[browser_separatePrivateDefault_differentEngine.js]
+support-files =
+ POSTSearchEngine.xml
+ print_postdata.sjs
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+ searchSuggestionEngine2.xml
+[browser_shortcuts_add_search_engine.js]
+support-files =
+ add_search_engine_many.html
+ add_search_engine_two.html
+ add_search_engine_0.xml
+ add_search_engine_1.xml
+[browser_speculative_connect.js]
+support-files =
+ searchSuggestionEngine2.xml
+ searchSuggestionEngine.sjs
+[browser_speculative_connect_not_with_client_cert.js]
+[browser_stop.js]
+[browser_stopSearchOnSelection.js]
+support-files =
+ searchSuggestionEngineSlow.xml
+ searchSuggestionEngine.sjs
+[browser_stop_pending.js]
+https_first_disabled = true
+support-files =
+ slow-page.sjs
+[browser_strip_on_share.js]
+[browser_suggestedIndex.js]
+[browser_suppressFocusBorder.js]
+[browser_switchTab_closesUrlbarPopup.js]
+[browser_switchTab_decodeuri.js]
+[browser_switchTab_inputHistory.js]
+[browser_switchTab_override.js]
+[browser_switchToTabHavingURI_aOpenParams.js]
+[browser_switchToTab_chiclet.js]
+[browser_switchToTab_closes_newtab.js]
+[browser_switchToTab_fullUrl_repeatedKeydown.js]
+[browser_tabKeyBehavior.js]
+[browser_tabMatchesInAwesomebar.js]
+support-files =
+ moz.png
+[browser_tabMatchesInAwesomebar_perwindowpb.js]
+[browser_tabToSearch.js]
+[browser_textruns.js]
+[browser_tokenAlias.js]
+[browser_top_sites.js]
+https_first_disabled = true
+[browser_top_sites_private.js]
+https_first_disabled = true
+[browser_typed_value.js]
+[browser_unitConversion.js]
+[browser_updateForDomainCompletion.js]
+https_first_disabled = true
+[browser_urlbar_annotation.js]
+support-files =
+ redirect_to.sjs
+[browser_urlbar_event_telemetry_abandonment.js]
+support-files =
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+skip-if = os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[browser_urlbar_event_telemetry_engagement.js]
+https_first_disabled = true
+skip-if =
+ apple_catalina # Bug 1625690
+ apple_silicon # Disabled due to bleedover with other tests when run in regular suites; passes in "failures" jobs
+ os == 'linux' # Bug 1748986, bug 1775824
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+support-files =
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+[browser_urlbar_event_telemetry_noEvent.js]
+[browser_urlbar_selection.js]
+skip-if = (os == 'mac') # bug 1570474
+[browser_urlbar_telemetry.js]
+tags = search-telemetry
+support-files =
+ urlbarTelemetrySearchSuggestions.sjs
+ urlbarTelemetrySearchSuggestions.xml
+[browser_urlbar_telemetry_autofill.js]
+tags = search-telemetry
+[browser_urlbar_telemetry_dynamic.js]
+tags = search-telemetry
+support-files =
+ urlbarTelemetryUrlbarDynamic.css
+[browser_urlbar_telemetry_extension.js]
+tags = search-telemetry
+[browser_urlbar_telemetry_handoff.js]
+tags = search-telemetry
+[browser_urlbar_telemetry_persisted.js]
+tags = search-telemetry
+[browser_urlbar_telemetry_places.js]
+https_first_disabled = true
+tags = search-telemetry
+[browser_urlbar_telemetry_quickactions.js]
+tags = search-telemetry
+[browser_urlbar_telemetry_remotetab.js]
+tags = search-telemetry
+[browser_urlbar_telemetry_searchmode.js]
+tags = search-telemetry
+support-files =
+ urlbarTelemetrySearchSuggestions.sjs
+ urlbarTelemetrySearchSuggestions.xml
+[browser_urlbar_telemetry_sponsored_topsites.js]
+https_first_disabled = true
+tags = search-telemetry
+[browser_urlbar_telemetry_tabtosearch.js]
+tags = search-telemetry
+[browser_urlbar_telemetry_tip.js]
+tags = search-telemetry
+[browser_urlbar_telemetry_topsite.js]
+tags = search-telemetry
+[browser_urlbar_telemetry_zeroPrefix.js]
+tags = search-telemetry
+[browser_userTypedValue.js]
+support-files = file_userTypedValue.html
+[browser_valueOnTabSwitch.js]
+[browser_view_emptyResultSet.js]
+[browser_view_resultDisplay.js]
+[browser_view_resultTypes_display.js]
+support-files =
+ print_postdata.sjs
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+[browser_view_selectionByMouse.js]
+skip-if =
+ os == "linux" && asan # Bug 1789051
+[browser_waitForLoadOrTimeout.js]
+https_first_disabled = true
+skip-if =
+ tsan # Bug 1683730
+ os == "linux" && bits == 64 && !debug # Bug 1666092
+[browser_whereToOpen.js]
diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_formatValue.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_formatValue.js
new file mode 100644
index 0000000000..d4b73603f9
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_formatValue.js
@@ -0,0 +1,178 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Checks that the url formatter properly recognizes the host and de-emphasizes
+// the rest of the url.
+
+/**
+ * Tests a given url.
+ * The de-emphasized parts must be wrapped in "<" and ">" chars.
+ *
+ * @param {string} aExpected The url to test.
+ * @param {string} aClobbered [optional] Normally the url is de-emphasized
+ * in-place, thus it's enough to pass aExpected. Though, in some cases
+ * the formatter may decide to replace the url with a fixed one, because
+ * it can't properly guess a host. In that case aClobbered is the
+ * expected de-emphasized value.
+ * @param {boolean} synthesizeInput [optional] Whether to synthesize an input
+ * event to test.
+ */
+function testVal(aExpected, aClobbered = null, synthesizeInput = false) {
+ let str = aExpected.replace(/[<>]/g, "");
+ if (synthesizeInput) {
+ gURLBar.focus();
+ gURLBar.select();
+ EventUtils.sendString(str);
+ Assert.equal(
+ gURLBar.editor.rootElement.textContent,
+ str,
+ "Url is not highlighted"
+ );
+ gBrowser.selectedBrowser.focus();
+ } else {
+ gURLBar.value = str;
+ }
+
+ let selectionController = gURLBar.editor.selectionController;
+ let selection = selectionController.getSelection(
+ selectionController.SELECTION_URLSECONDARY
+ );
+ let value = gURLBar.editor.rootElement.textContent;
+ let result = "";
+ for (let i = 0; i < selection.rangeCount; i++) {
+ let range = selection.getRangeAt(i).toString();
+ let pos = value.indexOf(range);
+ result += value.substring(0, pos) + "<" + range + ">";
+ value = value.substring(pos + range.length);
+ }
+ result += value;
+ Assert.equal(
+ result,
+ aClobbered || aExpected,
+ "Correct part of the url is de-emphasized" +
+ (synthesizeInput ? " (with input simulation)" : "")
+ );
+
+ // Now re-test synthesizing input.
+ if (!synthesizeInput) {
+ testVal(aExpected, aClobbered, true);
+ }
+}
+
+function test() {
+ const prefname = "browser.urlbar.formatting.enabled";
+
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref(prefname);
+ gURLBar.setURI();
+ });
+
+ gBrowser.selectedBrowser.focus();
+
+ testVal("<https://>mozilla.org");
+ testVal("<https://>mözilla.org");
+ testVal("<https://>mozilla.imaginatory");
+
+ testVal("<https://www.>mozilla.org");
+ testVal("<https://sub.>mozilla.org");
+ testVal("<https://sub1.sub2.sub3.>mozilla.org");
+ testVal("<www.>mozilla.org");
+ testVal("<sub.>mozilla.org");
+ testVal("<sub1.sub2.sub3.>mozilla.org");
+ testVal("<mozilla.com.>mozilla.com");
+ testVal("<https://mozilla.com:mozilla.com@>mozilla.com");
+ testVal("<mozilla.com:mozilla.com@>mozilla.com");
+
+ testVal("<ftp.>mozilla.org");
+ testVal("<ftp://ftp.>mozilla.org");
+
+ testVal("<https://sub.>mozilla.org");
+ testVal("<https://sub1.sub2.sub3.>mozilla.org");
+ testVal("<https://user:pass@sub1.sub2.sub3.>mozilla.org");
+ testVal("<https://user:pass@>mozilla.org");
+ testVal("<user:pass@sub1.sub2.sub3.>mozilla.org");
+ testVal("<user:pass@>mozilla.org");
+
+ testVal("<https://>mozilla.org< >");
+ testVal("mozilla.org< >");
+ // RTL characters in domain change order of domain and suffix. Domain should
+ // be highlighted correctly.
+ testVal("<http://>اختبار.اختبار</www.mozilla.org/index.html>");
+
+ testVal("<https://>mozilla.org</file.ext>");
+ testVal("<https://>mozilla.org</sub/file.ext>");
+ testVal("<https://>mozilla.org</sub/file.ext?foo>");
+ testVal("<https://>mozilla.org</sub/file.ext?foo&bar>");
+ testVal("<https://>mozilla.org</sub/file.ext?foo&bar#top>");
+ testVal("<https://>mozilla.org</sub/file.ext?foo&bar#top>");
+ testVal("foo.bar<?q=test>");
+ testVal("foo.bar<#mozilla.org>");
+ testVal("foo.bar<?somewhere.mozilla.org>");
+ testVal("foo.bar<?@mozilla.org>");
+ testVal("foo.bar<#x@mozilla.org>");
+ testVal("foo.bar<#@x@mozilla.org>");
+ testVal("foo.bar<?x@mozilla.org>");
+ testVal("foo.bar<?@x@mozilla.org>");
+ testVal("<foo.bar@x@>mozilla.org");
+ testVal("<foo.bar@:baz@>mozilla.org");
+ testVal("<foo.bar:@baz@>mozilla.org");
+ testVal("<foo.bar@:ba:z@>mozilla.org");
+ testVal("<foo.:bar:@baz@>mozilla.org");
+ testVal(
+ "foopy:\\blah@somewhere.com//whatever/",
+ "foopy</blah@somewhere.com//whatever/>"
+ );
+
+ testVal("<https://sub.>mozilla.org<:666/file.ext>");
+ testVal("<sub.>mozilla.org<:666/file.ext>");
+ testVal("localhost<:666/file.ext>");
+
+ let IPs = [
+ "192.168.1.1",
+ "[::]",
+ "[::1]",
+ "[1::]",
+ "[::]",
+ "[::1]",
+ "[1::]",
+ "[1:2:3:4:5:6:7::]",
+ "[::1:2:3:4:5:6:7]",
+ "[1:2:a:B:c:D:e:F]",
+ "[1::8]",
+ "[1:2::8]",
+ "[fe80::222:19ff:fe11:8c76]",
+ "[0000:0123:4567:89AB:CDEF:abcd:ef00:0000]",
+ "[::192.168.1.1]",
+ "[1::0.0.0.0]",
+ "[1:2::255.255.255.255]",
+ "[1:2:3::255.255.255.255]",
+ "[1:2:3:4::255.255.255.255]",
+ "[1:2:3:4:5::255.255.255.255]",
+ "[1:2:3:4:5:6:255.255.255.255]",
+ ];
+ IPs.forEach(function (IP) {
+ testVal(IP);
+ testVal(IP + "</file.ext>");
+ testVal(IP + "<:666/file.ext>");
+ testVal("<https://>" + IP);
+ testVal(`<https://>${IP}</file.ext>`);
+ testVal(`<https://user:pass@>${IP}<:666/file.ext>`);
+ testVal(`<user:pass@>${IP}<:666/file.ext>`);
+ testVal(`user:\\pass@${IP}/`, `user</pass@${IP}/>`);
+ });
+
+ testVal("mailto:admin@mozilla.org");
+ testVal("gopher://mozilla.org/");
+ testVal("about:config");
+ testVal("jar:http://mozilla.org/example.jar!/");
+ testVal("view-source:http://mozilla.org/");
+ testVal("foo9://mozilla.org/");
+ testVal("foo+://mozilla.org/");
+ testVal("foo.://mozilla.org/");
+ testVal("foo-://mozilla.org/");
+
+ // Disable formatting.
+ Services.prefs.setBoolPref(prefname, false);
+
+ testVal("https://mozilla.org");
+}
diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_formatValue_detachedTab.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_formatValue_detachedTab.js
new file mode 100644
index 0000000000..02da2a8e2b
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_formatValue_detachedTab.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+// After detaching a tab into a new window, the input value in the new window
+// should be formatted.
+
+add_task(async function detach() {
+ // Sometimes the value isn't formatted on Mac when running in verify chaos
+ // mode. The usual, proper front-end code path is hit, and the path that
+ // removes formatting is not hit, so it seems like some kind of race in the
+ // editor or selection code in Gecko. Since this has only been observed on Mac
+ // in chaos mode and doesn't seem to be a problem in urlbar code, skip the
+ // test in that case.
+ if (AppConstants.platform == "macosx" && Services.env.get("MOZ_CHAOSMODE")) {
+ Assert.ok(true, "Skipping test in chaos mode on Mac");
+ return;
+ }
+
+ UrlbarPrefs.clear("formatting.enabled");
+ Assert.ok(
+ UrlbarPrefs.get("formatting.enabled"),
+ "Formatting is enabled by default"
+ );
+
+ info("Waiting for new tab");
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "https://example.com/detach",
+ });
+
+ let winPromise = BrowserTestUtils.waitForNewWindow();
+ info("Detaching tab");
+ let win = gBrowser.replaceTabWithWindow(tab, {});
+ info("Waiting for new window");
+ await winPromise;
+
+ // Wait an extra tick for good measure since the code itself also waits for
+ // `delayedStartupPromise`.
+ info("Waiting for delayed startup in new window");
+ await win.delayedStartupPromise;
+ info("Waiting for tick");
+ await TestUtils.waitForTick();
+
+ assertValue("<https://>example.com</detach>", win);
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Asserts formatting in the input is correct.
+ *
+ * @param {string} expectedValue
+ * The URL to test. The parts the are expected to be de-emphasized should be
+ * wrapped in "<" and ">" chars.
+ * @param {window} win
+ * The input in this window will be tested.
+ */
+function assertValue(expectedValue, win = window) {
+ let selectionController = win.gURLBar.editor.selectionController;
+ let selection = selectionController.getSelection(
+ selectionController.SELECTION_URLSECONDARY
+ );
+ let value = win.gURLBar.editor.rootElement.textContent;
+ let result = "";
+ for (let i = 0; i < selection.rangeCount; i++) {
+ let range = selection.getRangeAt(i).toString();
+ let pos = value.indexOf(range);
+ result += value.substring(0, pos) + "<" + range + ">";
+ value = value.substring(pos + range.length);
+ }
+ result += value;
+ Assert.equal(
+ result,
+ expectedValue,
+ "Correct part of the url is de-emphasized"
+ );
+}
diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_hiddenFocus.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_hiddenFocus.js
new file mode 100644
index 0000000000..08e5ae97d3
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_hiddenFocus.js
@@ -0,0 +1,21 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function () {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ registerCleanupFunction(async function () {
+ BrowserTestUtils.removeTab(tab);
+ gURLBar.setURI();
+ });
+
+ gURLBar.blur();
+ ok(!gURLBar.focused, "url bar is not focused");
+ ok(!gURLBar.hasAttribute("focused"), "url bar is not visibly focused");
+ gURLBar.setHiddenFocus();
+ ok(gURLBar.focused, "url bar is focused");
+ ok(!gURLBar.hasAttribute("focused"), "url bar is not visibly focused");
+ gURLBar.removeHiddenFocus();
+ ok(gURLBar.focused, "url bar is focused");
+ ok(gURLBar.hasAttribute("focused"), "url bar is visibly focused");
+});
diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow.js
new file mode 100644
index 0000000000..b0a3337d84
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow.js
@@ -0,0 +1,156 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+async function testVal(aExpected, overflowSide = "") {
+ info(`Testing ${aExpected}`);
+ try {
+ gURLBar.setURI(makeURI(aExpected));
+ } catch (ex) {
+ if (ex.result != Cr.NS_ERROR_MALFORMED_URI) {
+ throw ex;
+ }
+ // For values without a protocol fallback to setting the raw value.
+ gURLBar.value = aExpected;
+ }
+
+ Assert.equal(
+ gURLBar.selectionStart,
+ gURLBar.selectionEnd,
+ "Selection sanity check"
+ );
+
+ gURLBar.focus();
+ Assert.equal(
+ document.activeElement,
+ gURLBar.inputField,
+ "URL Bar should be focused"
+ );
+ Assert.equal(
+ gURLBar.valueFormatter.scheme.value,
+ "",
+ "Check the scheme value"
+ );
+ Assert.equal(
+ getComputedStyle(gURLBar.valueFormatter.scheme).visibility,
+ "hidden",
+ "Check the scheme box visibility"
+ );
+
+ gURLBar.blur();
+ await window.promiseDocumentFlushed(() => {});
+ // The attribute doesn't always change, so we can't use waitForAttribute.
+ await TestUtils.waitForCondition(
+ () => gURLBar.getAttribute("textoverflow") === overflowSide
+ );
+
+ let scheme = aExpected.match(/^([a-z]+:\/{0,2})/)?.[1] || "";
+ // We strip http, so we should not show the scheme for it.
+ if (
+ scheme == "http://" &&
+ Services.prefs.getBoolPref("browser.urlbar.trimURLs", true)
+ ) {
+ scheme = "";
+ }
+
+ Assert.equal(
+ gURLBar.valueFormatter.scheme.value,
+ scheme,
+ "Check the scheme value"
+ );
+ let isOverflowed =
+ gURLBar.inputField.scrollWidth > gURLBar.inputField.clientWidth;
+ Assert.equal(isOverflowed, !!overflowSide, "Check The input field overflow");
+ Assert.equal(
+ gURLBar.getAttribute("textoverflow"),
+ overflowSide,
+ "Check the textoverflow attribute"
+ );
+ if (overflowSide) {
+ let side = gURLBar.getAttribute("domaindir") == "ltr" ? "right" : "left";
+ Assert.equal(side, overflowSide, "Check the overflow side");
+ Assert.equal(
+ getComputedStyle(gURLBar.valueFormatter.scheme).visibility,
+ scheme && isOverflowed && overflowSide == "left" ? "visible" : "hidden",
+ "Check the scheme box visibility"
+ );
+
+ info("Focus, change scroll position and blur, to ensure proper restore");
+ gURLBar.focus();
+ EventUtils.synthesizeKey("KEY_End");
+ gURLBar.blur();
+ await window.promiseDocumentFlushed(() => {});
+ // The attribute doesn't always change, so we can't use waitForAttribute.
+ await TestUtils.waitForCondition(
+ () => gURLBar.getAttribute("textoverflow") === overflowSide
+ );
+
+ Assert.equal(side, overflowSide, "Check the overflow side");
+ Assert.equal(
+ getComputedStyle(gURLBar.valueFormatter.scheme).visibility,
+ scheme && isOverflowed && overflowSide == "left" ? "visible" : "hidden",
+ "Check the scheme box visibility"
+ );
+ }
+}
+
+add_task(async function () {
+ // We use a new tab for the test to be sure all the tab switching and loading
+ // is complete before starting, otherwise onLocationChange for this tab could
+ // override the value we set with an empty value.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ registerCleanupFunction(function () {
+ gURLBar.setURI();
+ BrowserTestUtils.removeTab(tab);
+ });
+
+ let lotsOfSpaces = "%20".repeat(200);
+
+ // اسماء.شبكة
+ let rtlDomain =
+ "\u0627\u0633\u0645\u0627\u0621\u002e\u0634\u0628\u0643\u0629";
+ let rtlChar = "\u0627";
+
+ // Mix the direction of the tests to cover more cases, and to ensure the
+ // textoverflow attribute changes every time, because tewtVal waits for that.
+ await testVal(`https://mozilla.org/${lotsOfSpaces}/test/`, "right");
+ await testVal(`https://mozilla.org/`);
+ await testVal(`https://${rtlDomain}/${lotsOfSpaces}/test/`, "left");
+ await testVal(`https://mozilla.org:8888/${lotsOfSpaces}/test/`, "right");
+ await testVal(`https://${rtlDomain}:8888/${lotsOfSpaces}/test/`, "left");
+
+ await testVal(`ftp://mozilla.org/${lotsOfSpaces}/test/`, "right");
+ await testVal(`ftp://${rtlDomain}/${lotsOfSpaces}/test/`, "left");
+ await testVal(`ftp://mozilla.org/`);
+
+ await testVal(`http://${rtlDomain}/${lotsOfSpaces}/test/`, "left");
+ await testVal(`http://mozilla.org/`);
+ await testVal(`http://mozilla.org/${lotsOfSpaces}/test/`, "right");
+ await testVal(`http://${rtlDomain}:8888/${lotsOfSpaces}/test/`, "left");
+ await testVal(`http://[::1]/${rtlChar}/${lotsOfSpaces}/test/`, "right");
+
+ info("Test with formatting disabled");
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.formatting.enabled", false],
+ ["browser.urlbar.trimURLs", false],
+ ],
+ });
+
+ await testVal(`https://mozilla.org/`);
+ await testVal(`https://${rtlDomain}/${lotsOfSpaces}/test/`, "left");
+ await testVal(`https://mozilla.org/${lotsOfSpaces}/test/`, "right");
+
+ info("Test with trimURLs disabled");
+ await testVal(`http://${rtlDomain}/${lotsOfSpaces}/test/`, "left");
+
+ await SpecialPowers.popPrefEnv();
+
+ info("Tests without protocol");
+ await testVal(`mozilla.org/${lotsOfSpaces}/test/`, "right");
+ await testVal(`mozilla.org/`);
+ await testVal(`${rtlDomain}/${lotsOfSpaces}/test/`, "left");
+ await testVal(`mozilla.org:8888/${lotsOfSpaces}/test/`, "right");
+ await testVal(`${rtlDomain}:8888/${lotsOfSpaces}/test/`, "left");
+ await testVal(`[::1]/${rtlChar}/${lotsOfSpaces}/test/`, "right");
+});
diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow_resize.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow_resize.js
new file mode 100644
index 0000000000..879911d703
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow_resize.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+async function testVal(win, url) {
+ info(`Testing ${url}`);
+ win.gURLBar.setURI(makeURI(url));
+
+ let urlbar = win.gURLBar;
+ urlbar.blur();
+
+ for (let width of [1000, 800]) {
+ win.resizeTo(width, 500);
+ await win.promiseDocumentFlushed(() => {});
+ Assert.greater(
+ urlbar.inputField.scrollWidth,
+ urlbar.inputField.clientWidth,
+ "Check The input field overflows"
+ );
+ // Resize is handled on a timer, so we must wait for it.
+ await TestUtils.waitForCondition(
+ () => urlbar.inputField.scrollLeft == urlbar.inputField.scrollLeftMax,
+ "The urlbar input field is completely scrolled to the end"
+ );
+ await TestUtils.waitForCondition(
+ () => urlbar.getAttribute("textoverflow") == "left",
+ "Wait for the textoverflow attribute"
+ );
+ }
+}
+
+add_task(async function () {
+ // We use a new tab for the test to be sure all the tab switching and loading
+ // is complete before starting, otherwise onLocationChange for this tab could
+ // override the value we set with an empty value.
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ registerCleanupFunction(() => BrowserTestUtils.closeWindow(win));
+
+ let lotsOfSpaces = "%20".repeat(200);
+
+ // اسماء.شبكة
+ let rtlDomain =
+ "\u0627\u0633\u0645\u0627\u0621\u002e\u0634\u0628\u0643\u0629";
+
+ // Mix the direction of the tests to cover more cases, and to ensure the
+ // textoverflow attribute changes every time, because tewtVal waits for that.
+ await testVal(win, `https://${rtlDomain}/${lotsOfSpaces}/test/`);
+
+ info("Test with formatting and trimurl disabled");
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.formatting.enabled", false],
+ ["browser.urlbar.trimURLs", false],
+ ],
+ });
+
+ await testVal(win, `https://${rtlDomain}/${lotsOfSpaces}/test/`);
+ await testVal(win, `http://${rtlDomain}/${lotsOfSpaces}/test/`);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_privateFeature.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_privateFeature.js
new file mode 100644
index 0000000000..fb81e9f536
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_privateFeature.js
@@ -0,0 +1,74 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Tests that _loadURL correctly sets and passes on the `private` window
+// attribute (or not) with various arguments.
+
+add_task(async function privateFeatureSetOnNewWindowImplicitly() {
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ let newWinOpened = BrowserTestUtils.waitForNewWindow();
+
+ privateWin.gURLBar._loadURL("about:blank", null, "window", {});
+
+ let newWin = await newWinOpened;
+ Assert.equal(
+ newWin.docShell.treeOwner
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIAppWindow).chromeFlags &
+ Ci.nsIWebBrowserChrome.CHROME_PRIVATE_WINDOW,
+ Ci.nsIWebBrowserChrome.CHROME_PRIVATE_WINDOW,
+ "New window opened from existing private window should be marked as private"
+ );
+ await BrowserTestUtils.closeWindow(newWin);
+ await BrowserTestUtils.closeWindow(privateWin);
+});
+
+add_task(async function privateFeatureSetOnNewWindowExplicitly() {
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ let newWinOpened = BrowserTestUtils.waitForNewWindow();
+
+ privateWin.gURLBar._loadURL("about:blank", null, "window", { private: true });
+
+ let newWin = await newWinOpened;
+ Assert.equal(
+ newWin.docShell.treeOwner
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIAppWindow).chromeFlags &
+ Ci.nsIWebBrowserChrome.CHROME_PRIVATE_WINDOW,
+ Ci.nsIWebBrowserChrome.CHROME_PRIVATE_WINDOW,
+ "New window opened from existing private window should be marked as private"
+ );
+ await BrowserTestUtils.closeWindow(newWin);
+ await BrowserTestUtils.closeWindow(privateWin);
+});
+
+add_task(async function privateFeatureNotSetOnNewWindowExplicitly() {
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ let newWinOpened = BrowserTestUtils.waitForNewWindow();
+
+ privateWin.gURLBar._loadURL("about:blank", null, "window", {
+ private: false,
+ });
+
+ let newWin = await newWinOpened;
+ Assert.notEqual(
+ newWin.docShell.treeOwner
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIAppWindow).chromeFlags &
+ Ci.nsIWebBrowserChrome.CHROME_PRIVATE_WINDOW,
+ Ci.nsIWebBrowserChrome.CHROME_PRIVATE_WINDOW,
+ "New window opened from existing private window should be marked as private"
+ );
+ await BrowserTestUtils.closeWindow(newWin);
+ await BrowserTestUtils.closeWindow(privateWin);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms.js
new file mode 100644
index 0000000000..3d38036d84
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms.js
@@ -0,0 +1,306 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// These tests check the behavior of the Urlbar when loading a page
+// whose url matches that of the default search engine.
+
+let defaultTestEngine;
+
+// The main search string used in tests
+const SEARCH_STRING = "chocolate cake";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.showSearchTerms.featureGate", true]],
+ });
+
+ await SearchTestUtils.installSearchExtension(
+ {
+ name: "MozSearch",
+ search_url: "https://www.example.com/",
+ search_url_get_params: "q={searchTerms}&pc=fake_code",
+ },
+ { setAsDefault: true }
+ );
+ defaultTestEngine = Services.search.getEngineByName("MozSearch");
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.history.clear();
+ });
+});
+
+// Starts a search with a tab and asserts that
+// the state of the Urlbar contains the search term
+async function searchWithTab(
+ searchString,
+ tab = null,
+ engine = defaultTestEngine
+) {
+ if (!tab) {
+ tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ }
+
+ let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl(engine, searchString);
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ expectedSearchUrl
+ );
+
+ gURLBar.focus();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value: searchString,
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("KEY_Enter");
+ await browserLoadedPromise;
+
+ assertSearchStringIsInUrlbar(searchString);
+
+ return { tab, expectedSearchUrl };
+}
+
+// Search terms should show up in the url bar if the pref is on
+// and the SERP url matches the one constructed in Firefox
+add_task(async function list_of_search_strings() {
+ const searches = [
+ {
+ // Single word
+ searchString: "chocolate",
+ },
+ {
+ // Word with space
+ searchString: "chocolate cake",
+ },
+ {
+ // Special characters
+ searchString: "chocolate;,?:@&=+$-_.!~*'()#cake",
+ },
+ {
+ searchString: '"chocolate cake" -recipes',
+ },
+ {
+ // Search with special characters
+ searchString: "site:example.com chocolate -cake",
+ },
+ ];
+
+ for (let { searchString } of searches) {
+ let { tab } = await searchWithTab(searchString);
+ BrowserTestUtils.removeTab(tab);
+ }
+});
+
+// If a user does a search, goes to another page, and then
+// goes back to the SERP, the search term should show.
+add_task(async function go_back() {
+ let { tab } = await searchWithTab(SEARCH_STRING);
+
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ BrowserTestUtils.loadURIString(
+ tab.linkedBrowser,
+ "http://www.example.com/some_url"
+ );
+ await browserLoadedPromise;
+
+ let pageShowPromise = BrowserTestUtils.waitForContentEvent(
+ tab.linkedBrowser,
+ "pageshow"
+ );
+ tab.linkedBrowser.goBack();
+ await pageShowPromise;
+
+ assertSearchStringIsInUrlbar(SEARCH_STRING);
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Manually loading a url that matches a search query url
+// should show the search term in the Urlbar.
+add_task(async function load_url() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl(
+ defaultTestEngine,
+ SEARCH_STRING
+ );
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ expectedSearchUrl
+ );
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, expectedSearchUrl);
+ await browserLoadedPromise;
+ assertSearchStringIsInUrlbar(SEARCH_STRING);
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Focusing and blurring the urlbar while the search terms
+// persist should change the pageproxystate.
+add_task(async function focus_and_unfocus() {
+ let { tab } = await searchWithTab(SEARCH_STRING);
+
+ gURLBar.focus();
+ Assert.equal(
+ gURLBar.getAttribute("pageproxystate"),
+ "invalid",
+ "Should have matching pageproxystate."
+ );
+
+ gURLBar.blur();
+ Assert.equal(
+ gURLBar.getAttribute("pageproxystate"),
+ "valid",
+ "Should have matching pageproxystate."
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// If the user modifies the search term, blurring the
+// urlbar should keep the urlbar in an invalid pageproxystate.
+add_task(async function focus_and_unfocus_modified() {
+ let { tab } = await searchWithTab(SEARCH_STRING);
+
+ gURLBar.focus();
+ Assert.equal(
+ gURLBar.getAttribute("pageproxystate"),
+ "invalid",
+ "Should have matching pageproxystate."
+ );
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value: "another search term",
+ fireInputEvent: true,
+ });
+
+ gURLBar.blur();
+ Assert.equal(
+ gURLBar.getAttribute("pageproxystate"),
+ "invalid",
+ "Should have matching pageproxystate."
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// If Top Sites is cached in the UrlbarView, don't show it if the search terms
+// persist in the Urlbar.
+add_task(async function focus_after_top_sites() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Prevent the persist tip from interrupting clicking the Urlbar
+ // after the the SERP has been loaded.
+ [
+ `browser.urlbar.tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.PERSIST}`,
+ 10000,
+ ],
+ ["browser.newtabpage.activity-stream.feeds.topsites", true],
+ ],
+ });
+
+ // Populate Top Sites on a clean version of Places.
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesTestUtils.promiseAsyncUpdates();
+ await TestUtils.waitForTick();
+
+ const urls = [];
+ const N_TOP_SITES = 5;
+ const N_VISITS = 5;
+
+ for (let i = 0; i < N_TOP_SITES; i++) {
+ let url = `https://${i}.example.com/hello_world${i}`;
+ urls.unshift(url);
+ // Each URL needs to be added several times to boost its frecency enough to
+ // qualify as a top site.
+ for (let j = 0; j < N_VISITS; j++) {
+ await PlacesTestUtils.addVisits(url);
+ }
+ }
+
+ let changedPromise = TestUtils.topicObserved("newtab-top-sites-changed").then(
+ () => info("Observed newtab-top-sites-changed")
+ );
+ await updateTopSites(sites => sites?.length == N_TOP_SITES);
+ await changedPromise;
+
+ // Ensure Top Sites is cached.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ Assert.ok(gURLBar.view.isOpen, "UrlbarView should be open.");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ N_TOP_SITES,
+ `The number of results should be the same as the number of Top Sites ${N_TOP_SITES}.`
+ );
+ for (let i = 0; i < urls.length; i++) {
+ let { url } = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ Assert.equal(url, urls[i], "The result url should be a Top Site.");
+ }
+
+ let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl(
+ defaultTestEngine,
+ SEARCH_STRING
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value: SEARCH_STRING,
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("KEY_Enter");
+
+ await BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ expectedSearchUrl
+ );
+ Assert.equal(
+ gBrowser.selectedBrowser.searchTerms,
+ SEARCH_STRING,
+ "The search term should be in the Urlbar."
+ );
+
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.notEqual(
+ details.url,
+ urls[0],
+ "The first result should not be a Top Site."
+ );
+ Assert.equal(
+ details.heuristic,
+ true,
+ "The first result should be the heuristic result."
+ );
+ Assert.equal(
+ details.url,
+ expectedSearchUrl,
+ "The first result url should be the same as the SERP."
+ );
+ Assert.equal(
+ details.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "The first result be a search result."
+ );
+ Assert.equal(
+ details.searchParams?.query,
+ SEARCH_STRING,
+ "The first result should have a matching query."
+ );
+
+ // Clean up.
+ SpecialPowers.popPrefEnv();
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_backgroundTabs.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_backgroundTabs.js
new file mode 100644
index 0000000000..8ed29a9c5b
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_backgroundTabs.js
@@ -0,0 +1,63 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// These tests check the behavior of the Urlbar when search terms are
+// expected to be shown and tabs are opened in the background.
+
+let defaultTestEngine;
+
+// The main search string used in tests
+const SEARCH_STRING = "chocolate cake";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.showSearchTerms.featureGate", true]],
+ });
+
+ await SearchTestUtils.installSearchExtension(
+ {
+ name: "MozSearch",
+ search_url: "https://www.example.com/",
+ search_url_get_params: "q={searchTerms}&pc=fake_code",
+ },
+ { setAsDefault: true }
+ );
+ defaultTestEngine = Services.search.getEngineByName("MozSearch");
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.history.clear();
+ });
+});
+
+// If a user opens background tab search from the Urlbar,
+// the search term should show when the tab is focused.
+add_task(async function ctrl_open() {
+ let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl(
+ defaultTestEngine,
+ SEARCH_STRING
+ );
+ // Search for the term in a new background tab.
+ let newTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ expectedSearchUrl
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: SEARCH_STRING,
+ fireInputEvent: true,
+ });
+ gURLBar.focus();
+
+ EventUtils.synthesizeKey("KEY_Enter", {
+ altKey: true,
+ shiftKey: true,
+ });
+
+ // Find the background tab that was created, and switch to it.
+ let backgroundTab = await newTabPromise;
+ await BrowserTestUtils.switchTab(gBrowser, backgroundTab);
+ assertSearchStringIsInUrlbar(SEARCH_STRING);
+
+ BrowserTestUtils.removeTab(backgroundTab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_modifiedUrl.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_modifiedUrl.js
new file mode 100644
index 0000000000..4c20864171
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_modifiedUrl.js
@@ -0,0 +1,100 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// These tests check the behavior of the Urlbar when search terms are
+// expected to be shown but the url is modified from what the browser expects.
+
+let defaultTestEngine;
+
+// The main search string used in tests
+const SEARCH_STRING = "chocolate cake";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.showSearchTerms.featureGate", true]],
+ });
+
+ await SearchTestUtils.installSearchExtension(
+ {
+ name: "MozSearch",
+ search_url: "https://www.example.com/",
+ search_url_get_params: "q={searchTerms}&pc=fake_code",
+ },
+ { setAsDefault: true }
+ );
+ defaultTestEngine = Services.search.getEngineByName("MozSearch");
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.history.clear();
+ });
+});
+
+// If a SERP uses the History API to modify the URI,
+// the search term should still show in the URL bar.
+add_task(async function history_push_state() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl(
+ defaultTestEngine,
+ SEARCH_STRING
+ );
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ expectedSearchUrl
+ );
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, expectedSearchUrl);
+ await browserLoadedPromise;
+
+ let locationChangePromise = BrowserTestUtils.waitForLocationChange(gBrowser);
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
+ let url = new URL(content.window.location);
+ url.searchParams.set("pc", "fake_code_2");
+ content.history.pushState({}, "", url);
+ });
+
+ await locationChangePromise;
+ // Check URI to make sure that it's actually been changed
+ Assert.equal(
+ gBrowser.currentURI.spec,
+ `https://www.example.com/?q=chocolate+cake&pc=fake_code_2`,
+ "URI of Urlbar should have changed"
+ );
+
+ Assert.equal(
+ gURLBar.value,
+ SEARCH_STRING,
+ `Search string ${SEARCH_STRING} should be in the url bar`
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Loading a url that looks like a search query url but has additional
+// query params should not show the search term in the Urlbar.
+add_task(async function url_with_additional_query_params() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl(
+ defaultTestEngine,
+ SEARCH_STRING
+ );
+ // Add a query param
+ expectedSearchUrl += "&another_code=something_else";
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ expectedSearchUrl
+ );
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, expectedSearchUrl);
+ await browserLoadedPromise;
+
+ Assert.equal(gURLBar.value, expectedSearchUrl, `URL should be in URL bar`);
+ Assert.equal(
+ gURLBar.getAttribute("pageproxystate"),
+ "valid",
+ "Pageproxystate should be valid"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_moveTab.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_moveTab.js
new file mode 100644
index 0000000000..59f0eca916
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_moveTab.js
@@ -0,0 +1,133 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ These tests check the behavior of the Urlbar when search terms are shown
+ and the tab with the default SERP moves from one window to another.
+
+ Unlike other searchTerm tests, these modify the currentURI to ensure
+ that the currentURI has a different spec than the default SERP so that
+ the search terms won't show if the originalURI wasn't properly copied
+ during the tab swap.
+*/
+
+let originalEngine, defaultTestEngine;
+
+// The main search keyword used in tests
+const SEARCH_STRING = "chocolate cake";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.showSearchTerms.featureGate", true]],
+ });
+
+ await SearchTestUtils.installSearchExtension({
+ name: "MozSearch",
+ search_url: "https://www.example.com/",
+ search_url_get_params: "q={searchTerms}&pc=fake_code",
+ });
+ defaultTestEngine = Services.search.getEngineByName("MozSearch");
+
+ originalEngine = await Services.search.getDefault();
+ await Services.search.setDefault(
+ defaultTestEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+
+ registerCleanupFunction(async function () {
+ await Services.search.setDefault(
+ originalEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ await PlacesUtils.history.clear();
+ });
+});
+
+async function searchWithTab(
+ searchString,
+ tab = null,
+ engine = defaultTestEngine
+) {
+ if (!tab) {
+ tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ }
+
+ let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl(engine, searchString);
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ expectedSearchUrl
+ );
+
+ gURLBar.focus();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value: searchString,
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("KEY_Enter");
+ await browserLoadedPromise;
+
+ return { tab, expectedSearchUrl };
+}
+
+// Move a tab showing the search term into its own window.
+add_task(async function move_tab_into_new_window() {
+ let { tab, expectedSearchUrl } = await searchWithTab(SEARCH_STRING);
+
+ // Mock the default SERP modifying the existing url
+ // so that the originalURI and currentURI differ.
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [expectedSearchUrl],
+ async url => {
+ content.history.pushState({}, "", url + "&pc2=firefox");
+ }
+ );
+
+ // Move the tab into its own window.
+ let newWindow = gBrowser.replaceTabWithWindow(tab);
+ await BrowserTestUtils.waitForEvent(tab.linkedBrowser, "SwapDocShells");
+
+ assertSearchStringIsInUrlbar(SEARCH_STRING, { win: newWindow });
+
+ // Clean up.
+ await BrowserTestUtils.closeWindow(newWindow);
+});
+
+// Move a tab from its own window into an existing window.
+add_task(async function move_tab_into_existing_window() {
+ // Load a second window with the default SERP.
+ let win = await BrowserTestUtils.openNewBrowserWindow({ remote: true });
+ let browser = win.gBrowser.selectedBrowser;
+ let tab = win.gBrowser.tabs[0];
+
+ // Load the default SERP into the second window.
+ let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl(
+ defaultTestEngine,
+ SEARCH_STRING
+ );
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ expectedSearchUrl
+ );
+ BrowserTestUtils.loadURIString(browser, expectedSearchUrl);
+ await browserLoadedPromise;
+
+ // Mock the default SERP modifying the existing url
+ // so that the originalURI and currentURI differ.
+ await SpecialPowers.spawn(browser, [expectedSearchUrl], async url => {
+ content.history.pushState({}, "", url + "&pc2=firefox");
+ });
+
+ // Make the first window adopt and switch to that tab.
+ tab = gBrowser.adoptTab(tab);
+ await BrowserTestUtils.switchTab(gBrowser, tab);
+ assertSearchStringIsInUrlbar(SEARCH_STRING);
+
+ // Clean up.
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_popup.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_popup.js
new file mode 100644
index 0000000000..d25e17d960
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_popup.js
@@ -0,0 +1,145 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// These tests check the behavior of the Urlbar when persist search terms
+// are either enabled or disabled, and a popup notification is shown.
+
+function waitForPopupNotification() {
+ let promisePopupShown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+ PopupNotifications.show(
+ gBrowser.selectedBrowser,
+ "test-notification",
+ "This is a sample popup."
+ );
+ return promisePopupShown;
+}
+
+// The main search string used in tests.
+const SEARCH_TERM = "chocolate";
+const PREF_FEATUREGATE = "browser.urlbar.showSearchTerms.featureGate";
+let defaultTestEngine;
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_FEATUREGATE, true]],
+ });
+ await SearchTestUtils.installSearchExtension(
+ {
+ name: "MozSearch",
+ search_url: "https://www.example.com/",
+ search_url_get_params: "q={searchTerms}&pc=fake_code",
+ },
+ { setAsDefault: true }
+ );
+
+ defaultTestEngine = Services.search.getEngineByName("MozSearch");
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.history.clear();
+ });
+});
+
+async function searchWithTab(
+ searchString,
+ tab = null,
+ engine = defaultTestEngine,
+ expectedPersistedSearchTerms = true
+) {
+ if (!tab) {
+ tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ }
+
+ let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl(engine, searchString);
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ expectedSearchUrl
+ );
+
+ gURLBar.focus();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value: searchString,
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("KEY_Enter");
+ await browserLoadedPromise;
+
+ if (expectedPersistedSearchTerms) {
+ assertSearchStringIsInUrlbar(searchString);
+ }
+
+ return { tab, expectedSearchUrl };
+}
+
+// A notification should cause the urlbar to revert while
+// the search term persists.
+add_task(async function generic_popup_when_persist_is_enabled() {
+ let { tab, expectedSearchUrl } = await searchWithTab(SEARCH_TERM);
+
+ await waitForPopupNotification();
+
+ Assert.equal(
+ gURLBar.getAttribute("pageproxystate"),
+ "valid",
+ "Urlbar should have a valid pageproxystate."
+ );
+
+ Assert.equal(
+ gURLBar.value,
+ expectedSearchUrl,
+ "Search url should be in the urlbar."
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+// Ensure the urlbar is not being reverted when a prompt is shown
+// and the persist feature is disabled.
+add_task(async function generic_popup_no_revert_when_persist_is_disabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_FEATUREGATE, false]],
+ });
+
+ let { tab } = await searchWithTab(
+ SEARCH_TERM,
+ null,
+ defaultTestEngine,
+ false
+ );
+
+ // Have a user typed value in the urlbar to make
+ // pageproxystate invalid.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: SEARCH_TERM,
+ });
+ gURLBar.blur();
+
+ await waitForPopupNotification();
+
+ // Wait a brief amount of time between when the popup is shown
+ // and when the event handler should fire if it's enabled.
+ await TestUtils.waitForTick();
+
+ Assert.equal(
+ gURLBar.getAttribute("pageproxystate"),
+ "invalid",
+ "Urlbar should not be reverted."
+ );
+
+ Assert.equal(
+ gURLBar.value,
+ SEARCH_TERM,
+ "User typed value should remain in urlbar."
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_revert.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_revert.js
new file mode 100644
index 0000000000..91d6ea403a
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_revert.js
@@ -0,0 +1,170 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// These tests check the behavior of the Urlbar when search terms are shown
+// and the user reverts the Urlbar.
+
+let defaultTestEngine;
+
+// The main search keyword used in tests
+const SEARCH_STRING = "chocolate cake";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.showSearchTerms.featureGate", true]],
+ });
+
+ await SearchTestUtils.installSearchExtension(
+ {
+ name: "MozSearch",
+ search_url: "https://www.example.com/",
+ search_url_get_params: "q={searchTerms}&pc=fake_code",
+ },
+ { setAsDefault: true }
+ );
+ defaultTestEngine = Services.search.getEngineByName("MozSearch");
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.history.clear();
+ });
+});
+
+async function searchWithTab(
+ searchString,
+ tab = null,
+ engine = defaultTestEngine
+) {
+ if (!tab) {
+ tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ }
+
+ let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl(engine, searchString);
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ expectedSearchUrl
+ );
+
+ gURLBar.focus();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value: searchString,
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("KEY_Enter");
+ await browserLoadedPromise;
+
+ assertSearchStringIsInUrlbar(searchString);
+
+ return { tab, expectedSearchUrl };
+}
+
+function synthesizeRevert() {
+ gURLBar.focus();
+ EventUtils.synthesizeKey("KEY_Escape", { repeat: 2 });
+}
+
+// Users should be able to revert the URL bar
+add_task(async function revert() {
+ let { tab, expectedSearchUrl } = await searchWithTab(SEARCH_STRING);
+ synthesizeRevert();
+
+ Assert.equal(
+ gURLBar.value,
+ expectedSearchUrl,
+ `Urlbar should have the reverted URI ${expectedSearchUrl} as its value.`
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Users should be able to revert the URL bar,
+// and go to the same page.
+add_task(async function revert_and_press_enter() {
+ let { tab, expectedSearchUrl } = await searchWithTab(SEARCH_STRING);
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ expectedSearchUrl
+ );
+
+ synthesizeRevert();
+ gURLBar.focus();
+ EventUtils.synthesizeKey("KEY_Enter");
+ await browserLoadedPromise;
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Users should be able to revert the URL, and then if they navigate
+// to another tab, the tab that was reverted will show the search term again.
+add_task(async function revert_and_change_tab() {
+ let { tab, expectedSearchUrl } = await searchWithTab(SEARCH_STRING);
+
+ synthesizeRevert();
+
+ Assert.notEqual(
+ gURLBar.value,
+ SEARCH_STRING,
+ `Search string ${SEARCH_STRING} should not be in the url bar`
+ );
+ Assert.equal(
+ gURLBar.value,
+ expectedSearchUrl,
+ `Urlbar should have ${expectedSearchUrl} as value.`
+ );
+
+ // Open another tab
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ // Switch back to the original tab.
+ await BrowserTestUtils.switchTab(gBrowser, tab);
+
+ // Because the urlbar is focused, the pageproxystate should be invalid.
+ assertSearchStringIsInUrlbar(SEARCH_STRING, { pageProxyState: "invalid" });
+
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(tab2);
+});
+
+// If a user reverts a tab, and then does another search,
+// they should be able to see the search term again.
+add_task(async function revert_and_search_again() {
+ let { tab } = await searchWithTab(SEARCH_STRING);
+ synthesizeRevert();
+ await searchWithTab("another search string", tab);
+ BrowserTestUtils.removeTab(tab);
+});
+
+// If a user reverts the Urlbar while on a default SERP,
+// and they navigate away from the page by visiting another
+// link or using the back/forward buttons, the Urlbar should
+// show the search term again when returning back to the default SERP.
+add_task(async function revert_when_using_content() {
+ let { tab } = await searchWithTab(SEARCH_STRING);
+ synthesizeRevert();
+ await searchWithTab("another search string", tab);
+
+ // Revert the page, and then go back and forth in history.
+ // The search terms should show up.
+ synthesizeRevert();
+ let pageShowPromise = BrowserTestUtils.waitForContentEvent(
+ tab.linkedBrowser,
+ "pageshow"
+ );
+ tab.linkedBrowser.goBack();
+ await pageShowPromise;
+ assertSearchStringIsInUrlbar(SEARCH_STRING);
+
+ pageShowPromise = BrowserTestUtils.waitForContentEvent(
+ tab.linkedBrowser,
+ "pageshow"
+ );
+ tab.linkedBrowser.goForward();
+ await pageShowPromise;
+ assertSearchStringIsInUrlbar("another search string");
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_searchBar.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_searchBar.js
new file mode 100644
index 0000000000..784d8932ac
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_searchBar.js
@@ -0,0 +1,104 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// These tests check the behavior of the Urlbar when a user enables
+// the search bar and showSearchTerms is true.
+
+const { CustomizableUITestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CustomizableUITestUtils.sys.mjs"
+);
+
+const gCUITestUtils = new CustomizableUITestUtils(window);
+const SEARCH_STRING = "example_string";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.search.widget.inNavBar", true],
+ ["browser.urlbar.showSearchTerms.featureGate", true],
+ ],
+ });
+
+ await SearchTestUtils.installSearchExtension(
+ {
+ name: "MozSearch",
+ search_url: "https://www.example.com/",
+ search_url_get_params: "q={searchTerms}&pc=fake_code",
+ },
+ { setAsDefault: true }
+ );
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.history.clear();
+ gCUITestUtils.removeSearchBar();
+ });
+});
+
+function assertSearchStringIsNotInUrlbar(searchString) {
+ Assert.notEqual(
+ gURLBar.value,
+ searchString,
+ `Search string ${searchString} should not be in the url bar.`
+ );
+ Assert.equal(
+ gURLBar.getAttribute("pageproxystate"),
+ "valid",
+ "Pageproxystate should be valid."
+ );
+ Assert.equal(
+ gBrowser.selectedBrowser.searchTerms,
+ "",
+ "searchTerms should be blank."
+ );
+}
+
+// When a user enables the search bar, and does a search in the search bar,
+// the search term should not show in the URL bar.
+add_task(async function search_bar_on() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ await gCUITestUtils.addSearchBar();
+
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ `https://www.example.com/?q=${SEARCH_STRING}&pc=fake_code`
+ );
+
+ let searchBar = BrowserSearch.searchBar;
+ searchBar.value = SEARCH_STRING;
+ searchBar.focus();
+ EventUtils.synthesizeKey("KEY_Enter");
+
+ await browserLoadedPromise;
+ assertSearchStringIsNotInUrlbar(SEARCH_STRING);
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// When a user enables the search bar, and does a search in the URL bar,
+// the search term should still not show in the URL bar.
+add_task(async function search_bar_on_with_url_bar_search() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ await gCUITestUtils.addSearchBar();
+
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ `https://www.example.com/?q=${SEARCH_STRING}&pc=fake_code`
+ );
+
+ gURLBar.focus();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value: SEARCH_STRING,
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("KEY_Enter");
+
+ await browserLoadedPromise;
+ assertSearchStringIsNotInUrlbar(SEARCH_STRING);
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_searchMode.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_searchMode.js
new file mode 100644
index 0000000000..880b597784
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_searchMode.js
@@ -0,0 +1,81 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// These tests check the behavior of the Urlbar when using search mode
+
+let defaultTestEngine;
+
+// The main search string used in tests
+const SEARCH_STRING = "chocolate cake";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.showSearchTerms.featureGate", true]],
+ });
+
+ await SearchTestUtils.installSearchExtension({
+ name: "MozSearch",
+ search_url: "https://www.example.com/",
+ search_url_get_params: "q={searchTerms}&pc=fake_code",
+ });
+ defaultTestEngine = Services.search.getEngineByName("MozSearch");
+
+ await SearchTestUtils.installSearchExtension(
+ {
+ name: "MochiSearch",
+ search_url: "https://mochi.test:8888/",
+ search_url_get_params: "q={searchTerms}&pc=fake_code",
+ },
+ { setAsDefault: true }
+ );
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.history.clear();
+ });
+});
+
+// When a user does a search with search mode, they should
+// not see the search term in the URL bar for that engine.
+add_task(async function non_default_search() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl(
+ defaultTestEngine,
+ SEARCH_STRING
+ );
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ expectedSearchUrl
+ );
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: SEARCH_STRING,
+ });
+ await UrlbarTestUtils.enterSearchMode(window, {
+ engineName: defaultTestEngine.name,
+ });
+ gURLBar.focus();
+ EventUtils.synthesizeKey("KEY_Enter");
+ await browserLoadedPromise;
+
+ Assert.equal(gURLBar.value, expectedSearchUrl, `URL should be in URL bar`);
+ Assert.equal(
+ gURLBar.getAttribute("pageproxystate"),
+ "valid",
+ "Pageproxystate should be valid"
+ );
+ Assert.equal(
+ gBrowser.userTypedValue,
+ null,
+ "There should not be a userTypedValue for a search on a non-default search engine"
+ );
+ Assert.equal(
+ gBrowser.selectedBrowser.searchTerms,
+ "",
+ "searchTerms should be empty."
+ );
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_switch_tab.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_switch_tab.js
new file mode 100644
index 0000000000..77ee3e19d7
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_switch_tab.js
@@ -0,0 +1,139 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// These tests check the behavior of the Urlbar when search terms are shown
+// and the user switches between tabs.
+
+let defaultTestEngine;
+
+// The main search keyword used in tests
+const SEARCH_STRING = "chocolate cake";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.showSearchTerms.featureGate", true]],
+ });
+
+ await SearchTestUtils.installSearchExtension(
+ {
+ name: "MozSearch",
+ search_url: "https://www.example.com/",
+ search_url_get_params: "q={searchTerms}&pc=fake_code",
+ },
+ { setAsDefault: true }
+ );
+ defaultTestEngine = Services.search.getEngineByName("MozSearch");
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.history.clear();
+ });
+});
+
+async function searchWithTab(
+ searchString,
+ tab = null,
+ engine = defaultTestEngine
+) {
+ if (!tab) {
+ tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ }
+
+ let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl(engine, searchString);
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ expectedSearchUrl
+ );
+
+ gURLBar.focus();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value: searchString,
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("KEY_Enter");
+ await browserLoadedPromise;
+
+ assertSearchStringIsInUrlbar(searchString);
+
+ return { tab, expectedSearchUrl };
+}
+
+// Users should be able to search, change the tab, and come
+// back to the original tab to see the search term again
+add_task(async function change_tab() {
+ let { tab: tab1 } = await searchWithTab(SEARCH_STRING);
+ let { tab: tab2 } = await searchWithTab("another keyword");
+ let { tab: tab3 } = await searchWithTab("yet another keyword");
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ assertSearchStringIsInUrlbar(SEARCH_STRING);
+
+ await BrowserTestUtils.switchTab(gBrowser, tab2);
+ assertSearchStringIsInUrlbar("another keyword");
+
+ await BrowserTestUtils.switchTab(gBrowser, tab3);
+ assertSearchStringIsInUrlbar("yet another keyword");
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab3);
+});
+
+// If a user types in the URL bar, and the user goes to a
+// different tab, the original tab should still contain the
+// text written by the user.
+add_task(async function user_overwrites_search_term() {
+ let { tab: tab1 } = await searchWithTab(SEARCH_STRING);
+
+ gURLBar.focus();
+ gURLBar.select();
+ EventUtils.sendString("another_word");
+
+ Assert.notEqual(
+ gURLBar.value,
+ SEARCH_STRING,
+ `Search string ${SEARCH_STRING} should not be in the url bar`
+ );
+
+ // Open a new tab, switch back to the first and
+ // check that the user typed value is still there.
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+
+ Assert.equal(
+ gURLBar.value,
+ "another_word",
+ "another_word should be in the url bar"
+ );
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
+
+// If a user clears the URL bar, and goes to a different tab,
+// and returns to the initial tab, it should show the search term again.
+add_task(async function user_overwrites_search_term() {
+ let { tab: tab1 } = await searchWithTab(SEARCH_STRING);
+
+ gURLBar.focus();
+ gURLBar.select();
+ EventUtils.sendKey("delete");
+
+ Assert.equal(gURLBar.value, "", "Empty string should be in url bar.");
+
+ // Open a new tab, switch back to the first and check
+ // the blank string is replaced with the search string.
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+
+ assertSearchStringIsInUrlbar(SEARCH_STRING, {
+ pageProxyState: "invalid",
+ userTypedValue: "",
+ });
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_telemetry.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_telemetry.js
new file mode 100644
index 0000000000..bdad68e0ef
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_telemetry.js
@@ -0,0 +1,378 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * These tests check that we record the number of times search terms
+ * persist in the Urlbar, and when search terms are cleared due to a
+ * PopupNotification.
+ *
+ * This is different from existing telemetry that tracks whether users
+ * interacted with the Urlbar or made another search while the search
+ * terms were peristed.
+ */
+
+let defaultTestEngine;
+
+// The main search string used in tests
+const SEARCH_STRING = "chocolate cake";
+
+// Telemetry keys.
+const PERSISTED_VIEWED = "urlbar.persistedsearchterms.view_count";
+const PERSISTED_REVERTED = "urlbar.persistedsearchterms.revert_by_popup_count";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.showSearchTerms.featureGate", true]],
+ });
+ await SearchTestUtils.installSearchExtension(
+ {
+ name: "MozSearch",
+ search_url: "https://www.example.com/",
+ search_url_get_params: "q={searchTerms}&pc=fake_code",
+ },
+ { setAsDefault: true }
+ );
+ defaultTestEngine = Services.search.getEngineByName("MozSearch");
+ Services.telemetry.clearScalars();
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.history.clear();
+ Services.telemetry.clearScalars();
+ });
+});
+
+// Starts a search with a tab and asserts that
+// the state of the Urlbar contains the search term.
+async function searchWithTab(
+ searchString,
+ tab = null,
+ engine = defaultTestEngine,
+ assertSearchString = true
+) {
+ if (!tab) {
+ tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ }
+
+ let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl(engine, searchString);
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ expectedSearchUrl
+ );
+
+ gURLBar.focus();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value: searchString,
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("KEY_Enter");
+ await browserLoadedPromise;
+
+ if (assertSearchString) {
+ assertSearchStringIsInUrlbar(searchString);
+ }
+
+ return { tab, expectedSearchUrl };
+}
+
+add_task(async function load_page_with_persisted_search() {
+ let { tab } = await searchWithTab(SEARCH_STRING);
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 1);
+ TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined);
+
+ // Clean up.
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function load_page_without_persisted_search() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.showSearchTerms.featureGate", false]],
+ });
+
+ let { tab } = await searchWithTab(
+ SEARCH_STRING,
+ null,
+ defaultTestEngine,
+ false
+ );
+
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, undefined);
+ TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined);
+
+ // Clean up.
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+// Multiple searches should result in the correct number of recorded views.
+add_task(async function load_page_n_times() {
+ let N = 5;
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ for (let index = 0; index < N; ++index) {
+ await searchWithTab(SEARCH_STRING, tab);
+ }
+
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, N);
+ TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined);
+
+ // Clean up.
+ BrowserTestUtils.removeTab(tab);
+});
+
+// A persisted search view event should not be recorded when unfocusing the urlbar.
+add_task(async function focus_and_unfocus() {
+ let { tab } = await searchWithTab(SEARCH_STRING);
+
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false);
+ TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 1);
+ TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined);
+
+ gURLBar.focus();
+ gURLBar.select();
+ gURLBar.blur();
+
+ // Focusing and unfocusing the urlbar shouldn't change the persisted view count.
+ scalars = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 1);
+ TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined);
+
+ // Clean up.
+ BrowserTestUtils.removeTab(tab);
+});
+
+// A persisted search view event should not be recorded by a
+// pushState event after a page has been loaded.
+add_task(async function history_api() {
+ let { tab } = await searchWithTab(SEARCH_STRING);
+
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false);
+ TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 1);
+ TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
+ let url = new URL(content.window.location);
+ let someState = { value: true };
+ url.searchParams.set("pc", "fake_code_2");
+ content.history.pushState(someState, "", url);
+ someState.value = false;
+ content.history.replaceState(someState, "", url);
+ });
+
+ scalars = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 1);
+ TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined);
+
+ // Clean up.
+ BrowserTestUtils.removeTab(tab);
+});
+
+// A persisted search view event should be recorded when switching back to a tab
+// that contains search terms.
+add_task(async function switch_tabs() {
+ let { tab } = await searchWithTab(SEARCH_STRING);
+
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false);
+ TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 1);
+ TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined);
+
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ await BrowserTestUtils.switchTab(gBrowser, tab);
+
+ scalars = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 2);
+ TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined);
+
+ // Clean up.
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(tab2);
+});
+
+// A telemetry event should be recorded when returning to a persisted SERP via tabhistory.
+add_task(async function tabhistory() {
+ let { tab } = await searchWithTab(SEARCH_STRING);
+
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false);
+ TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 1);
+ TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined);
+
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ BrowserTestUtils.loadURIString(
+ tab.linkedBrowser,
+ "https://www.example.com/some_url"
+ );
+ await browserLoadedPromise;
+
+ let pageShowPromise = BrowserTestUtils.waitForContentEvent(
+ tab.linkedBrowser,
+ "pageshow"
+ );
+ tab.linkedBrowser.goBack();
+ await pageShowPromise;
+
+ scalars = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 2);
+ TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined);
+
+ // Clean up.
+ BrowserTestUtils.removeTab(tab);
+});
+
+// PopupNotification's that rely on an anchor element in the urlbar should trigger
+// an increment of the revert counter.
+// This assumes the anchor element is present in the Urlbar during a valid pageproxystate.
+add_task(async function popup_in_urlbar() {
+ let { tab } = await searchWithTab(SEARCH_STRING);
+ let promisePopupShown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+ PopupNotifications.show(
+ gBrowser.selectedBrowser,
+ "test-notification",
+ "This is a sample popup."
+ );
+ await promisePopupShown;
+
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 1);
+ TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, 1);
+
+ // Clean up.
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Non-persistent PopupNotifications won't re-appear if a user switches
+// tabs and returns to the tab that had the Popup.
+add_task(async function non_persistent_popup_in_urlbar_switch_tab() {
+ let { tab } = await searchWithTab(SEARCH_STRING);
+ let promisePopupShown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+ PopupNotifications.show(
+ gBrowser.selectedBrowser,
+ "test-notification",
+ "This is a sample popup.",
+ "geo-notification-icon"
+ );
+ await promisePopupShown;
+
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", false);
+ TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 1);
+ TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, 1);
+
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ await BrowserTestUtils.switchTab(gBrowser, tab);
+
+ scalars = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 2);
+ TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, 1);
+
+ // Clean up.
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(tab2);
+});
+
+// Persistent PopupNotifications will constantly appear to users
+// even if they switch to another tab and switch back.
+add_task(async function persistent_popup_in_urlbar_switch_tab() {
+ let { tab } = await searchWithTab(SEARCH_STRING);
+ let promisePopupShown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+ PopupNotifications.show(
+ gBrowser.selectedBrowser,
+ "test-notification",
+ "This is a sample popup.",
+ "geo-notification-icon",
+ null,
+ null,
+ { persistent: true }
+ );
+ await promisePopupShown;
+
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", false);
+ TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 1);
+ TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, 1);
+
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ promisePopupShown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+ await BrowserTestUtils.switchTab(gBrowser, tab);
+ await promisePopupShown;
+
+ scalars = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 2);
+ TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, 2);
+
+ // Clean up.
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(tab2);
+});
+
+// If the persist feature is not enabled, a telemetry event should not be recorded
+// if a PopupNotification uses an anchor in the Urlbar.
+add_task(async function popup_in_urlbar_without_feature() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.showSearchTerms.featureGate", false]],
+ });
+
+ let { tab } = await searchWithTab(
+ SEARCH_STRING,
+ null,
+ defaultTestEngine,
+ false
+ );
+ let promisePopupShown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+ PopupNotifications.show(
+ gBrowser.selectedBrowser,
+ "test-notification",
+ "This is a sample popup."
+ );
+ await promisePopupShown;
+
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, undefined);
+ TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined);
+
+ // Clean up.
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+// If the anchor element for the PopupNotification is not located in the Urlbar,
+// a telemetry event should not be recorded.
+add_task(async function popup_not_in_urlbar() {
+ let { tab } = await searchWithTab(SEARCH_STRING);
+
+ let promisePopupShown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+ PopupNotifications.show(
+ gBrowser.selectedBrowser,
+ "test-notification",
+ "This is a sample popup that uses the unified extensions button.",
+ gUnifiedExtensions.getPopupAnchorID(gBrowser.selectedBrowser, window)
+ );
+ await promisePopupShown;
+
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 1);
+ TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined);
+
+ // Clean up.
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_setURI.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_setURI.js
new file mode 100644
index 0000000000..0fa365f8bc
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_setURI.js
@@ -0,0 +1,128 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ waitForExplicitFinish();
+
+ // avoid prompting about phishing
+ Services.prefs.setIntPref(phishyUserPassPref, 32);
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref(phishyUserPassPref);
+ });
+
+ nextTest();
+}
+
+const phishyUserPassPref = "network.http.phishy-userpass-length";
+
+function nextTest() {
+ let testCase = tests.shift();
+ if (testCase) {
+ testCase(function () {
+ executeSoon(nextTest);
+ });
+ } else {
+ executeSoon(finish);
+ }
+}
+
+var tests = [
+ function revert(next) {
+ loadTabInWindow(window, function (tab) {
+ gURLBar.handleRevert();
+ is(
+ gURLBar.value,
+ "example.com",
+ "URL bar had user/pass stripped after reverting"
+ );
+ gBrowser.removeTab(tab);
+ next();
+ });
+ },
+ function customize(next) {
+ // Need to wait for delayedStartup for the customization part of the test,
+ // since that's where BrowserToolboxCustomizeDone is set.
+ BrowserTestUtils.openNewBrowserWindow().then(function (win) {
+ loadTabInWindow(win, function () {
+ openToolbarCustomizationUI(function () {
+ closeToolbarCustomizationUI(function () {
+ is(
+ win.gURLBar.value,
+ "example.com",
+ "URL bar had user/pass stripped after customize"
+ );
+ win.close();
+ next();
+ }, win);
+ }, win);
+ });
+ });
+ },
+ function pageloaderror(next) {
+ loadTabInWindow(window, function (tab) {
+ // Load a new URL and then immediately stop it, to simulate a page load
+ // error.
+ BrowserTestUtils.loadURIString(
+ tab.linkedBrowser,
+ "http://test1.example.com"
+ );
+ tab.linkedBrowser.stop();
+ is(
+ gURLBar.value,
+ "example.com",
+ "URL bar had user/pass stripped after load error"
+ );
+ gBrowser.removeTab(tab);
+ next();
+ });
+ },
+];
+
+function loadTabInWindow(win, callback) {
+ info("Loading tab");
+ let url = "http://user:pass@example.com/";
+ let tab = (win.gBrowser.selectedTab = BrowserTestUtils.addTab(
+ win.gBrowser,
+ url
+ ));
+ BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, url).then(() => {
+ info("Tab loaded");
+ is(
+ win.gURLBar.value,
+ "example.com",
+ "URL bar had user/pass stripped initially"
+ );
+ callback(tab);
+ }, true);
+}
+
+function openToolbarCustomizationUI(aCallback, aBrowserWin) {
+ if (!aBrowserWin) {
+ aBrowserWin = window;
+ }
+
+ aBrowserWin.gCustomizeMode.enter();
+
+ aBrowserWin.gNavToolbox.addEventListener(
+ "customizationready",
+ function () {
+ executeSoon(function () {
+ aCallback(aBrowserWin);
+ });
+ },
+ { once: true }
+ );
+}
+
+function closeToolbarCustomizationUI(aCallback, aBrowserWin) {
+ aBrowserWin.gNavToolbox.addEventListener(
+ "aftercustomization",
+ function () {
+ executeSoon(aCallback);
+ },
+ { once: true }
+ );
+
+ aBrowserWin.gCustomizeMode.exit();
+}
diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_tooltip.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_tooltip.js
new file mode 100644
index 0000000000..5ecb5e7a90
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_tooltip.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function synthesizeMouseOver(element) {
+ info("synthesize mouseover");
+ let promise = BrowserTestUtils.waitForEvent(element, "mouseover");
+ EventUtils.synthesizeMouseAtCenter(document.documentElement, {
+ type: "mouseout",
+ });
+ EventUtils.synthesizeMouseAtCenter(element, { type: "mouseover" });
+ EventUtils.synthesizeMouseAtCenter(element, { type: "mousemove" });
+ return promise;
+}
+
+function synthesizeMouseOut(element) {
+ info("synthesize mouseout");
+ let promise = BrowserTestUtils.waitForEvent(element, "mouseout");
+ EventUtils.synthesizeMouseAtCenter(element, { type: "mouseover" });
+ EventUtils.synthesizeMouseAtCenter(element, { type: "mouseout" });
+ EventUtils.synthesizeMouseAtCenter(document.documentElement, {
+ type: "mousemove",
+ });
+ return promise;
+}
+
+async function expectTooltip(text) {
+ if (!gURLBar._overflowing && !gURLBar._inOverflow) {
+ info("waiting for overflow event");
+ await BrowserTestUtils.waitForEvent(gURLBar.inputField, "overflow");
+ }
+
+ let tooltip = document.getElementById("aHTMLTooltip");
+ let element = gURLBar.inputField;
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(tooltip, "popupshown");
+ await synthesizeMouseOver(element);
+ info("awaiting for tooltip popup");
+ await popupShownPromise;
+
+ is(element.getAttribute("title"), text, "title attribute has expected text");
+ is(tooltip.textContent, text, "tooltip shows expected text");
+
+ await synthesizeMouseOut(element);
+}
+
+async function expectNoTooltip() {
+ if (gURLBar._overflowing || gURLBar._inOverflow) {
+ info("waiting for underflow event");
+ await BrowserTestUtils.waitForEvent(gURLBar.inputField, "underflow");
+ }
+
+ let element = gURLBar.inputField;
+ await synthesizeMouseOver(element);
+
+ is(element.getAttribute("title"), null, "title attribute shouldn't be set");
+
+ await synthesizeMouseOut(element);
+}
+
+add_task(async function () {
+ window.windowUtils.disableNonTestMouseEvents(true);
+ registerCleanupFunction(() => {
+ window.windowUtils.disableNonTestMouseEvents(false);
+ });
+
+ // Ensure the URL bar is neither focused nor hovered before we start.
+ gBrowser.selectedBrowser.focus();
+ await synthesizeMouseOut(gURLBar.inputField);
+
+ gURLBar.value = "short string";
+ await expectNoTooltip();
+
+ let longURL = "http://longurl.com/" + "foobar/".repeat(30);
+ gURLBar.value = longURL;
+ is(
+ gURLBar.inputField.value,
+ longURL.replace(/^http:\/\//, ""),
+ "Urlbar value has http:// stripped"
+ );
+ await expectTooltip(longURL);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_trimURLs.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_trimURLs.js
new file mode 100644
index 0000000000..d1bd46f022
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_trimURLs.js
@@ -0,0 +1,127 @@
+add_task(async function () {
+ const PREF_TRIMURLS = "browser.urlbar.trimURLs";
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ registerCleanupFunction(async function () {
+ BrowserTestUtils.removeTab(tab);
+ Services.prefs.clearUserPref(PREF_TRIMURLS);
+ gURLBar.setURI();
+ });
+
+ // Avoid search service sync init warnings due to URIFixup, when running the
+ // test alone.
+ await Services.search.init();
+
+ Services.prefs.setBoolPref(PREF_TRIMURLS, true);
+
+ testVal("http://mozilla.org/", "mozilla.org");
+ testVal("https://mozilla.org/", "https://mozilla.org");
+ testVal("http://mözilla.org/", "mözilla.org");
+ // This isn't a valid public suffix, thus we should untrim it or it would
+ // end up doing a search.
+ testVal("http://mozilla.imaginatory/");
+ testVal("http://www.mozilla.org/", "www.mozilla.org");
+ testVal("http://sub.mozilla.org/", "sub.mozilla.org");
+ testVal("http://sub1.sub2.sub3.mozilla.org/", "sub1.sub2.sub3.mozilla.org");
+ testVal("http://mozilla.org/file.ext", "mozilla.org/file.ext");
+ testVal("http://mozilla.org/sub/", "mozilla.org/sub/");
+
+ testVal("http://ftp.mozilla.org/", "ftp.mozilla.org");
+ testVal("http://ftp1.mozilla.org/", "ftp1.mozilla.org");
+ testVal("http://ftp42.mozilla.org/", "ftp42.mozilla.org");
+ testVal("http://ftpx.mozilla.org/", "ftpx.mozilla.org");
+ testVal("ftp://ftp.mozilla.org/", "ftp://ftp.mozilla.org");
+ testVal("ftp://ftp1.mozilla.org/", "ftp://ftp1.mozilla.org");
+ testVal("ftp://ftp42.mozilla.org/", "ftp://ftp42.mozilla.org");
+ testVal("ftp://ftpx.mozilla.org/", "ftp://ftpx.mozilla.org");
+
+ testVal("https://user:pass@mozilla.org/", "https://user:pass@mozilla.org");
+ testVal("https://user@mozilla.org/", "https://user@mozilla.org");
+ testVal("http://user:pass@mozilla.org/", "user:pass@mozilla.org");
+ testVal("http://user@mozilla.org/", "user@mozilla.org");
+ testVal("http://sub.mozilla.org:666/", "sub.mozilla.org:666");
+
+ testVal("https://[fe80::222:19ff:fe11:8c76]/file.ext");
+ testVal("http://[fe80::222:19ff:fe11:8c76]/", "[fe80::222:19ff:fe11:8c76]");
+ testVal("https://user:pass@[fe80::222:19ff:fe11:8c76]:666/file.ext");
+ testVal(
+ "http://user:pass@[fe80::222:19ff:fe11:8c76]:666/file.ext",
+ "user:pass@[fe80::222:19ff:fe11:8c76]:666/file.ext"
+ );
+
+ testVal("mailto:admin@mozilla.org");
+ testVal("gopher://mozilla.org/");
+ testVal("about:config");
+ testVal("jar:http://mozilla.org/example.jar!/");
+ testVal("view-source:http://mozilla.org/");
+
+ // Behaviour for hosts with no dots depends on the whitelist:
+ let fixupWhitelistPref = "browser.fixup.domainwhitelist.localhost";
+ Services.prefs.setBoolPref(fixupWhitelistPref, false);
+ testVal("http://localhost");
+ Services.prefs.setBoolPref(fixupWhitelistPref, true);
+ testVal("http://localhost", "localhost");
+ Services.prefs.clearUserPref(fixupWhitelistPref);
+
+ testVal("http:// invalid url");
+
+ testVal("http://someotherhostwithnodots");
+
+ // This host is whitelisted, it can be trimmed.
+ testVal("http://localhost/ foo bar baz", "localhost/ foo bar baz");
+
+ // This is not trimmed because it's not in the domain whitelist.
+ testVal(
+ "http://localhost.localdomain/ foo bar baz",
+ "http://localhost.localdomain/ foo bar baz"
+ );
+
+ Services.prefs.setBoolPref(PREF_TRIMURLS, false);
+
+ testVal("http://mozilla.org/");
+
+ Services.prefs.setBoolPref(PREF_TRIMURLS, true);
+
+ let promiseLoaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ "http://example.com/"
+ );
+ BrowserTestUtils.loadURIString(gBrowser, "http://example.com/");
+ await promiseLoaded;
+
+ await testCopy("example.com", "http://example.com/");
+
+ gURLBar.setPageProxyState("invalid");
+ gURLBar.valueIsTyped = true;
+ await testCopy("example.com", "example.com");
+});
+
+function testVal(originalValue, targetValue) {
+ gURLBar.value = originalValue;
+ gURLBar.valueIsTyped = false;
+ let trimmedValue = UrlbarPrefs.get("trimURLs")
+ ? BrowserUIUtils.trimURL(originalValue)
+ : originalValue;
+ Assert.equal(gURLBar.value, trimmedValue, "url bar value set");
+ // Now focus the urlbar and check the inputField value is properly set.
+ gURLBar.focus();
+ Assert.equal(
+ gURLBar.value,
+ targetValue || originalValue,
+ "Check urlbar value on focus"
+ );
+ // On blur we should trim again.
+ gURLBar.blur();
+ Assert.equal(gURLBar.value, trimmedValue, "Check urlbar value on blur");
+}
+
+function testCopy(originalValue, targetValue) {
+ return SimpleTest.promiseClipboardChange(targetValue, () => {
+ Assert.equal(gURLBar.value, originalValue, "url bar copy value set");
+ gURLBar.focus();
+ gURLBar.select();
+ goDoCommand("cmd_copy");
+ });
+}
diff --git a/browser/components/urlbar/tests/browser/browser_aboutHomeLoading.js b/browser/components/urlbar/tests/browser/browser_aboutHomeLoading.js
new file mode 100644
index 0000000000..1bb65c0c42
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_aboutHomeLoading.js
@@ -0,0 +1,196 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This tests ensures the urlbar is cleared properly when about:home is visited.
+ */
+
+"use strict";
+
+const { SessionSaver } = ChromeUtils.importESModule(
+ "resource:///modules/sessionstore/SessionSaver.sys.mjs"
+);
+const { TabStateFlusher } = ChromeUtils.importESModule(
+ "resource:///modules/sessionstore/TabStateFlusher.sys.mjs"
+);
+
+add_task(function addHomeButton() {
+ CustomizableUI.addWidgetToArea("home-button", "nav-bar");
+ registerCleanupFunction(() =>
+ CustomizableUI.removeWidgetFromArea("home-button")
+ );
+});
+
+/**
+ * Test what happens if loading a URL that should clear the
+ * location bar after a parent process URL.
+ */
+add_task(async function clearURLBarAfterParentProcessURL() {
+ let tab = await new Promise(resolve => {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "about:preferences"
+ );
+ let newTabBrowser = gBrowser.getBrowserForTab(gBrowser.selectedTab);
+ newTabBrowser.addEventListener(
+ "Initialized",
+ async function () {
+ resolve(gBrowser.selectedTab);
+ },
+ { capture: true, once: true }
+ );
+ });
+ document.getElementById("home-button").click();
+ await BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ HomePage.get()
+ );
+ is(gURLBar.value, "", "URL bar should be empty");
+ is(
+ tab.linkedBrowser.userTypedValue,
+ null,
+ "The browser should have no recorded userTypedValue"
+ );
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * Same as above, but open the tab without passing the URL immediately
+ * which changes behaviour in tabbrowser.xml.
+ */
+add_task(async function clearURLBarAfterParentProcessURLInExistingTab() {
+ let tab = await new Promise(resolve => {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+ let newTabBrowser = gBrowser.getBrowserForTab(gBrowser.selectedTab);
+ newTabBrowser.addEventListener(
+ "Initialized",
+ async function () {
+ resolve(gBrowser.selectedTab);
+ },
+ { capture: true, once: true }
+ );
+ BrowserTestUtils.loadURIString(newTabBrowser, "about:preferences");
+ });
+ document.getElementById("home-button").click();
+ await BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ HomePage.get()
+ );
+ is(gURLBar.value, "", "URL bar should be empty");
+ is(
+ tab.linkedBrowser.userTypedValue,
+ null,
+ "The browser should have no recorded userTypedValue"
+ );
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * Load about:home directly from an about:newtab page. Because it is an
+ * 'initial' page, we need to treat this specially if the user actually
+ * loads a page like this from the URL bar.
+ */
+add_task(async function clearURLBarAfterManuallyLoadingAboutHome() {
+ let promiseTabOpenedAndSwitchedTo = BrowserTestUtils.switchTab(
+ gBrowser,
+ () => {}
+ );
+ // This opens about:newtab:
+ BrowserOpenTab();
+ let tab = await promiseTabOpenedAndSwitchedTo;
+ is(gURLBar.value, "", "URL bar should be empty");
+ is(tab.linkedBrowser.userTypedValue, null, "userTypedValue should be null");
+
+ gURLBar.value = "about:home";
+ gURLBar.select();
+ let aboutHomeLoaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ "about:home"
+ );
+ EventUtils.sendKey("return");
+ await aboutHomeLoaded;
+
+ is(gURLBar.value, "", "URL bar should be empty");
+ is(tab.linkedBrowser.userTypedValue, null, "userTypedValue should be null");
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * Ensure we don't show 'about:home' in the URL bar temporarily in new tabs
+ * while we're switching remoteness (when the URL we're loading and the
+ * default content principal are different).
+ */
+add_task(async function dontTemporarilyShowAboutHome() {
+ requestLongerTimeout(2);
+
+ await SpecialPowers.pushPrefEnv({ set: [["browser.startup.page", 1]] });
+ let windowOpenedPromise = BrowserTestUtils.waitForNewWindow();
+ let win = OpenBrowserWindow();
+ await windowOpenedPromise;
+ let promiseTabSwitch = BrowserTestUtils.switchTab(win.gBrowser, () => {});
+ win.BrowserOpenTab();
+ await promiseTabSwitch;
+ is(win.gBrowser.visibleTabs.length, 2, "2 tabs opened");
+ await TabStateFlusher.flush(win.gBrowser.selectedBrowser);
+ await BrowserTestUtils.closeWindow(win);
+ ok(SessionStore.getClosedWindowCount(), "Should have a closed window");
+
+ await SessionSaver.run();
+
+ windowOpenedPromise = BrowserTestUtils.waitForNewWindow();
+ win = SessionStore.undoCloseWindow(0);
+ await windowOpenedPromise;
+ let wpl = {
+ onLocationChange() {
+ is(win.gURLBar.value, "", "URL bar value should stay empty.");
+ },
+ };
+ win.gBrowser.addProgressListener(wpl);
+
+ if (win.gBrowser.visibleTabs.length < 2) {
+ await BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "TabOpen");
+ }
+ let otherTab = win.gBrowser.selectedTab.previousElementSibling;
+ let tabLoaded = BrowserTestUtils.browserLoaded(
+ otherTab.linkedBrowser,
+ false,
+ "about:home"
+ );
+ await BrowserTestUtils.switchTab(win.gBrowser, otherTab);
+ await tabLoaded;
+ win.gBrowser.removeProgressListener(wpl);
+ is(win.gURLBar.value, "", "URL bar value should be empty.");
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Test that if the Home Button is clicked after a user has typed
+ * some value into the URL bar, that the URL bar is cleared if
+ * the homepage is one of the initial pages set.
+ */
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "http://example.com",
+ gBrowser,
+ },
+ async browser => {
+ const TYPED_VALUE = "This string should get cleared";
+ gURLBar.value = TYPED_VALUE;
+ browser.userTypedValue = TYPED_VALUE;
+
+ document.getElementById("home-button").click();
+ await BrowserTestUtils.browserLoaded(browser, false, HomePage.get());
+ is(gURLBar.value, "", "URL bar should be empty");
+ is(
+ browser.userTypedValue,
+ null,
+ "The browser should have no recorded userTypedValue"
+ );
+ }
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_acknowledgeFeedbackAndDismissal.js b/browser/components/urlbar/tests/browser/browser_acknowledgeFeedbackAndDismissal.js
new file mode 100644
index 0000000000..cc1ed29ceb
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_acknowledgeFeedbackAndDismissal.js
@@ -0,0 +1,304 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests feedback and dismissal acknowledgments in the view.
+ */
+
+"use strict";
+
+// The name of this command must be one that's recognized as not ending the
+// urlbar session. See `isSessionOngoing` comments for details.
+const FEEDBACK_COMMAND = "show_less_frequently";
+
+let gTestProvider;
+
+add_setup(async function () {
+ gTestProvider = new TestProvider({
+ results: [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ url: "https://example.com/",
+ isBlockable: true,
+ blockL10n: {
+ id: "urlbar-result-menu-dismiss-firefox-suggest",
+ },
+ }
+ ),
+ ],
+ });
+
+ gTestProvider.commandCount = {};
+ UrlbarProvidersManager.registerProvider(gTestProvider);
+
+ // Add a visit so that there's one result above the test result (the
+ // heuristic) and one below (the visit) just to make sure removing the test
+ // result doesn't mess up adjacent results.
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await UrlbarTestUtils.formHistory.clear();
+ await PlacesTestUtils.addVisits("https://example.com/aaa");
+
+ registerCleanupFunction(() => {
+ UrlbarProvidersManager.unregisterProvider(gTestProvider);
+ });
+});
+
+// Tests dismissal acknowledgment when the dismissed row is not selected.
+add_task(async function acknowledgeDismissal_rowNotSelected() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+ await doDismissTest({ shouldBeSelected: false });
+});
+
+// Tests dismissal acknowledgment when the dismissed row is selected.
+add_task(async function acknowledgeDismissal_rowSelected() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+
+ // Select the row.
+ let resultIndex = await getTestResultIndex();
+ while (gURLBar.view.selectedRowIndex != resultIndex) {
+ this.EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+
+ await doDismissTest({ resultIndex, shouldBeSelected: true });
+});
+
+// Tests a feedback acknowledgment command immediately followed by a dismissal
+// acknowledgment command. This makes sure that both feedback acknowledgment
+// works and a subsequent dismissal command works while the urlbar session
+// remains ongoing.
+add_task(async function acknowledgeFeedbackAndDismissal() {
+ // Trigger the suggestion.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+
+ let resultIndex = await getTestResultIndex();
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex);
+
+ // Click the feedback command.
+ await UrlbarTestUtils.openResultMenuAndClickItem(window, FEEDBACK_COMMAND, {
+ resultIndex,
+ });
+
+ Assert.equal(
+ gTestProvider.commandCount[FEEDBACK_COMMAND],
+ 1,
+ "One feedback command should have happened"
+ );
+ gTestProvider.commandCount[FEEDBACK_COMMAND] = 0;
+
+ Assert.ok(
+ gURLBar.view.isOpen,
+ "The view should remain open clicking the command"
+ );
+ Assert.ok(
+ details.element.row.hasAttribute("feedback-acknowledgment"),
+ "Row should have feedback acknowledgment after clicking command"
+ );
+
+ info("Doing dismissal");
+ await doDismissTest({ resultIndex, shouldBeSelected: true });
+});
+
+/**
+ * Does a dismissal test:
+ *
+ * 1. Clicks the dismiss command in the test result
+ * 2. Verifies a dismissal acknowledgment tip replaces the result
+ * 3. Clicks the "Got it" button in the tip
+ * 4. Verifies the tip is dismissed
+ *
+ * @param {object} options
+ * Options object
+ * @param {boolean} options.shouldBeSelected
+ * True if the test result is expected to be selected initially. If true, this
+ * function verifies the "Got it" button in the dismissal acknowledgment tip
+ * also becomes selected.
+ * @param {number} options.resultIndex
+ * The index of the test result, if known beforehand. Leave -1 to find it
+ * automatically.
+ */
+async function doDismissTest({ shouldBeSelected, resultIndex = -1 }) {
+ if (resultIndex < 0) {
+ resultIndex = await getTestResultIndex();
+ }
+
+ let selectedElement = gURLBar.view.selectedElement;
+ Assert.ok(selectedElement, "There should be an initially selected element");
+
+ if (shouldBeSelected) {
+ Assert.equal(
+ gURLBar.view.selectedRowIndex,
+ resultIndex,
+ "The test result should be selected"
+ );
+ } else {
+ Assert.notEqual(
+ gURLBar.view.selectedRowIndex,
+ resultIndex,
+ "The test result should not be selected"
+ );
+ }
+
+ let resultCount = UrlbarTestUtils.getResultCount(window);
+
+ // Click the dismiss command.
+ await UrlbarTestUtils.openResultMenuAndClickItem(window, "dismiss", {
+ resultIndex,
+ openByMouse: true,
+ });
+
+ Assert.equal(
+ gTestProvider.commandCount.dismiss,
+ 1,
+ "One dismissal should have happened"
+ );
+ gTestProvider.commandCount.dismiss = 0;
+
+ // The row should be a tip now.
+ Assert.ok(gURLBar.view.isOpen, "The view should remain open after dismissal");
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ resultCount,
+ "The result count should not haved changed after dismissal"
+ );
+
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex);
+ Assert.equal(
+ details.type,
+ UrlbarUtils.RESULT_TYPE.TIP,
+ "Row should be a tip after dismissal"
+ );
+ Assert.equal(
+ details.result.payload.type,
+ "dismissalAcknowledgment",
+ "Tip type should be dismissalAcknowledgment"
+ );
+ Assert.ok(
+ !details.element.row.hasAttribute("selected"),
+ "Row should not have 'selected' attribute"
+ );
+ Assert.ok(
+ !details.element.row._content.hasAttribute("selected"),
+ "Row-inner should not have 'selected' attribute"
+ );
+ Assert.ok(
+ !details.element.row.hasAttribute("feedback-acknowledgment"),
+ "Row should not have feedback acknowledgment after dismissal"
+ );
+
+ // Get the dismissal acknowledgment's "Got it" button.
+ let gotItButton = UrlbarTestUtils.getButtonForResultIndex(
+ window,
+ "0",
+ resultIndex
+ );
+ Assert.ok(gotItButton, "Row should have a 'Got it' button");
+
+ if (shouldBeSelected) {
+ Assert.equal(
+ gURLBar.view.selectedElement,
+ gotItButton,
+ "The 'Got it' button should be selected"
+ );
+ } else {
+ Assert.notEqual(
+ gURLBar.view.selectedElement,
+ gotItButton,
+ "The 'Got it' button should not be selected"
+ );
+ Assert.equal(
+ gURLBar.view.selectedElement,
+ selectedElement,
+ "The initially selected element should remain selected"
+ );
+ }
+
+ // Click it.
+ EventUtils.synthesizeMouseAtCenter(gotItButton, {}, window);
+
+ // The view should remain open and the tip row should be gone.
+ Assert.ok(
+ gURLBar.view.isOpen,
+ "The view should remain open clicking the 'Got it' button"
+ );
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ resultCount - 1,
+ "The result count should be one less after clicking 'Got it' button"
+ );
+ for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) {
+ details = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ Assert.ok(
+ details.type != UrlbarUtils.RESULT_TYPE.TIP &&
+ details.result.providerName != gTestProvider.name,
+ "Tip result and test result should not be present"
+ );
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window);
+}
+
+/**
+ * A provider that acknowledges feedback and dismissals.
+ */
+class TestProvider extends UrlbarTestUtils.TestProvider {
+ getResultCommands(result) {
+ return [
+ {
+ name: FEEDBACK_COMMAND,
+ l10n: {
+ id: "firefox-suggest-weather-command-inaccurate-location",
+ },
+ },
+ {
+ name: "dismiss",
+ l10n: {
+ id: "firefox-suggest-weather-command-not-interested",
+ },
+ },
+ ];
+ }
+
+ onEngagement(isPrivate, state, queryContext, details) {
+ if (details.result?.providerName == this.name) {
+ let { selType } = details;
+
+ info(`onEngagement called, selType=` + selType);
+
+ if (!this.commandCount.hasOwnProperty(selType)) {
+ this.commandCount[selType] = 0;
+ }
+ this.commandCount[selType]++;
+
+ if (selType == FEEDBACK_COMMAND) {
+ queryContext.view.acknowledgeFeedback(details.result);
+ } else if (selType == "dismiss") {
+ queryContext.view.acknowledgeDismissal(details.result);
+ }
+ }
+ }
+}
+
+async function getTestResultIndex() {
+ let index = 0;
+ let resultCount = UrlbarTestUtils.getResultCount(window);
+ for (; index < resultCount; index++) {
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, index);
+ if (details.result.providerName == gTestProvider.name) {
+ break;
+ }
+ }
+ Assert.less(index, resultCount, "The test result should be present");
+ return index;
+}
diff --git a/browser/components/urlbar/tests/browser/browser_action_searchengine.js b/browser/components/urlbar/tests/browser/browser_action_searchengine.js
new file mode 100644
index 0000000000..2520315fa2
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_action_searchengine.js
@@ -0,0 +1,125 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that a search result has the correct attributes and visits the
+ * expected URL for the engine.
+ */
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.search.separatePrivateDefault.ui.enabled", true],
+ ["browser.search.separatePrivateDefault", false],
+ ],
+ });
+
+ await SearchTestUtils.installSearchExtension(
+ { name: "MozSearch" },
+ { setAsDefault: true }
+ );
+ await SearchTestUtils.installSearchExtension({
+ name: "MozSearchPrivate",
+ search_url: "https://example.com/private",
+ });
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.formHistory.clear();
+ });
+});
+
+async function testSearch(win, expectedName, expectedBaseUrl) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "open a search",
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(win, 0);
+
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "Should have type search"
+ );
+ Assert.deepEqual(
+ result.searchParams,
+ {
+ engine: expectedName,
+ keyword: undefined,
+ query: "open a search",
+ suggestion: undefined,
+ inPrivateWindow: undefined,
+ isPrivateEngine: undefined,
+ },
+ "Should have the correct result parameters."
+ );
+
+ Assert.equal(
+ result.image,
+ UrlbarUtils.ICON.SEARCH_GLASS,
+ "Should have the search icon image"
+ );
+
+ let tabPromise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ let element = await UrlbarTestUtils.waitForAutocompleteResultAt(win, 0);
+ EventUtils.synthesizeMouseAtCenter(element, {}, win);
+ await tabPromise;
+
+ Assert.equal(
+ win.gBrowser.selectedBrowser.currentURI.spec,
+ expectedBaseUrl + "?q=open+a+search",
+ "Should have loaded the correct page"
+ );
+}
+
+add_task(async function test_search_normal_window() {
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:mozilla"
+ );
+
+ registerCleanupFunction(async function () {
+ try {
+ BrowserTestUtils.removeTab(tab);
+ } catch (ex) {
+ /* tab may have already been closed in case of failure */
+ }
+ });
+
+ await testSearch(window, "MozSearch", "https://example.com/");
+});
+
+add_task(async function test_search_private_window_no_separate_default() {
+ const win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+
+ registerCleanupFunction(async function () {
+ await BrowserTestUtils.closeWindow(win);
+ });
+
+ await testSearch(win, "MozSearch", "https://example.com/");
+});
+
+add_task(async function test_search_private_window() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.separatePrivateDefault", true]],
+ });
+
+ let engine = Services.search.getEngineByName("MozSearchPrivate");
+ let originalEngine = await Services.search.getDefaultPrivate();
+ await Services.search.setDefaultPrivate(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+
+ registerCleanupFunction(async () => {
+ await BrowserTestUtils.closeWindow(win);
+ await Services.search.setDefaultPrivate(
+ originalEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ });
+
+ const win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+
+ await testSearch(win, "MozSearchPrivate", "https://example.com/private");
+});
diff --git a/browser/components/urlbar/tests/browser/browser_action_searchengine_alias.js b/browser/components/urlbar/tests/browser/browser_action_searchengine_alias.js
new file mode 100644
index 0000000000..b79c324a04
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_action_searchengine_alias.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that search result obtained using a search keyword gives an entry with
+ * the correct attributes and visits the expected URL for the engine.
+ */
+
+add_task(async function () {
+ await SearchTestUtils.installSearchExtension(
+ { keyword: "moz" },
+ { setAsDefault: true }
+ );
+ let engine = Services.search.getEngineByName("Example");
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:mozilla"
+ );
+
+ // Disable autofill so mozilla.org isn't autofilled below.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.autoFill", false]],
+ });
+
+ registerCleanupFunction(async function () {
+ try {
+ BrowserTestUtils.removeTab(tab);
+ } catch (ex) {
+ /* tab may have already been closed in case of failure */
+ }
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.formHistory.clear();
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "moz",
+ });
+ Assert.equal(gURLBar.value, "moz", "Value should be unchanged");
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "moz open a search",
+ });
+ // Wait for the second new search that starts when search mode is entered.
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: engine.name,
+ entry: "typed",
+ });
+ Assert.equal(gURLBar.value, "open a search", "value should be query");
+
+ let tabPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await tabPromise;
+
+ Assert.equal(
+ gBrowser.selectedBrowser.currentURI.spec,
+ "https://example.com/?q=open+a+search",
+ "Should have loaded the correct page"
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_add_search_engine.js b/browser/components/urlbar/tests/browser/browser_add_search_engine.js
new file mode 100644
index 0000000000..f016aab3e7
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_add_search_engine.js
@@ -0,0 +1,325 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test adding engines through the Address Bar context menu.
+
+const { PromptTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromptTestUtils.sys.mjs"
+);
+const BASE_URL =
+ "http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/";
+
+add_task(async function context_none() {
+ info("Checks the context menu with a page that doesn't offer any engines.");
+ let url = "http://mochi.test:8888/";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ await UrlbarTestUtils.withContextMenu(window, popup => {
+ info("The separator and the add engine item should not be present.");
+ let elt = popup.parentNode.getMenuItem("add-engine-separator");
+ Assert.ok(!!elt);
+ Assert.ok(!BrowserTestUtils.is_visible(elt));
+ Assert.ok(!popup.parentNode.getMenuItem("add-engine-menu"));
+ Assert.ok(!popup.parentNode.getMenuItem("add-engine-0"));
+ });
+ });
+});
+
+add_task(async function context_one() {
+ info("Checks the context menu with a page that offers one engine.");
+ let url = getRootDirectory(gTestPath) + "add_search_engine_one.html";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ await UrlbarTestUtils.withContextMenu(window, async popup => {
+ info("The separator and the add engine item should be present.");
+ let elt = popup.parentNode.getMenuItem("add-engine-separator");
+ Assert.ok(BrowserTestUtils.is_visible(elt));
+
+ Assert.ok(!popup.parentNode.getMenuItem("add-engine-menu"));
+ Assert.ok(!popup.parentNode.getMenuItem("add-engine-1"));
+
+ elt = popup.parentNode.getMenuItem("add-engine-0");
+ Assert.ok(BrowserTestUtils.is_visible(elt));
+ await document.l10n.translateElements([elt]);
+ Assert.ok(elt.label.includes("add_search_engine_0"));
+ Assert.ok(elt.hasAttribute("image"));
+ Assert.equal(
+ elt.getAttribute("uri"),
+ BASE_URL + "add_search_engine_0.xml"
+ );
+
+ info("Click on the menuitem");
+ let enginePromise = promiseEngine("engine-added", "add_search_engine_0");
+ popup.activateItem(elt);
+ await enginePromise;
+ Assert.equal(popup.state, "closed");
+ });
+
+ await UrlbarTestUtils.withContextMenu(window, popup => {
+ info("The separator and the add engine item should not be present.");
+ let elt = popup.parentNode.getMenuItem("add-engine-separator");
+ Assert.ok(!BrowserTestUtils.is_visible(elt));
+ Assert.ok(!popup.parentNode.getMenuItem("add-engine-menu"));
+ Assert.ok(!popup.parentNode.getMenuItem("add-engine-0"));
+ });
+
+ info("Remove the engine.");
+ let engine = await Services.search.getEngineByName("add_search_engine_0");
+ await Services.search.removeEngine(engine);
+
+ await UrlbarTestUtils.withContextMenu(window, async popup => {
+ info("The separator and the add engine item should be present again.");
+ let elt = popup.parentNode.getMenuItem("add-engine-separator");
+ Assert.ok(BrowserTestUtils.is_visible(elt));
+
+ Assert.ok(!popup.parentNode.getMenuItem("add-engine-menu"));
+ Assert.ok(!popup.parentNode.getMenuItem("add-engine-1"));
+
+ elt = popup.parentNode.getMenuItem("add-engine-0");
+ Assert.ok(BrowserTestUtils.is_visible(elt));
+ await document.l10n.translateElements([elt]);
+ Assert.ok(elt.label.includes("add_search_engine_0"));
+ });
+ });
+});
+
+add_task(async function context_invalid() {
+ info("Checks the context menu with a page that offers an invalid engine.");
+ await SpecialPowers.pushPrefEnv({
+ set: [["prompts.contentPromptSubDialog", false]],
+ });
+
+ let url = getRootDirectory(gTestPath) + "add_search_engine_invalid.html";
+ await BrowserTestUtils.withNewTab(url, async tab => {
+ await UrlbarTestUtils.withContextMenu(window, async popup => {
+ info("The separator and the add engine item should be present.");
+ Assert.ok(popup.parentNode.getMenuItem("add-engine-separator"));
+ Assert.ok(!popup.parentNode.getMenuItem("add-engine-menu"));
+ Assert.ok(!popup.parentNode.getMenuItem("add-engine-1"));
+
+ let elt = popup.parentNode.getMenuItem("add-engine-0");
+ Assert.ok(BrowserTestUtils.is_visible(elt));
+ await document.l10n.translateElements([elt]);
+ Assert.ok(elt.label.includes("add_search_engine_404"));
+ Assert.equal(
+ elt.getAttribute("uri"),
+ BASE_URL + "add_search_engine_404.xml"
+ );
+
+ info("Click on the menuitem");
+ let promptPromise = PromptTestUtils.waitForPrompt(tab.linkedBrowser, {
+ modalType: Ci.nsIPromptService.MODAL_TYPE_CONTENT,
+ promptType: "alert",
+ });
+
+ popup.activateItem(elt);
+
+ let prompt = await promptPromise;
+ Assert.ok(
+ prompt.ui.infoBody.textContent.includes(
+ BASE_URL + "add_search_engine_404.xml"
+ ),
+ "Should have included the url in the prompt body"
+ );
+ await PromptTestUtils.handlePrompt(prompt);
+ Assert.equal(popup.state, "closed");
+ });
+ });
+});
+
+add_task(async function context_same_name() {
+ info("Checks the context menu with a page that offers same named engines.");
+ let url = getRootDirectory(gTestPath) + "add_search_engine_same_names.html";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ await UrlbarTestUtils.withContextMenu(window, async popup => {
+ info("The separator and the add engine item should be present.");
+ let elt = popup.parentNode.getMenuItem("add-engine-separator");
+ Assert.ok(BrowserTestUtils.is_visible(elt));
+
+ Assert.ok(!popup.parentNode.getMenuItem("add-engine-menu"));
+ Assert.ok(!popup.parentNode.getMenuItem("add-engine-1"));
+
+ elt = popup.parentNode.getMenuItem("add-engine-0");
+ Assert.ok(BrowserTestUtils.is_visible(elt));
+ await document.l10n.translateElements([elt]);
+ Assert.ok(elt.label.includes("add_search_engine_0"));
+ });
+ });
+});
+
+add_task(async function context_two() {
+ info("Checks the context menu with a page that offers two engines.");
+ let url = getRootDirectory(gTestPath) + "add_search_engine_two.html";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ await UrlbarTestUtils.withContextMenu(window, async popup => {
+ info("The separator and the add engine item should be present.");
+ let elt = popup.parentNode.getMenuItem("add-engine-separator");
+ Assert.ok(BrowserTestUtils.is_visible(elt));
+
+ Assert.ok(!popup.parentNode.getMenuItem("add-engine-menu"));
+
+ elt = popup.parentNode.getMenuItem("add-engine-0");
+ Assert.ok(BrowserTestUtils.is_visible(elt));
+ await document.l10n.translateElements([elt]);
+ Assert.ok(elt.label.includes("add_search_engine_0"));
+ elt = popup.parentNode.getMenuItem("add-engine-1");
+ Assert.ok(BrowserTestUtils.is_visible(elt));
+ await document.l10n.translateElements([elt]);
+ Assert.ok(elt.label.includes("add_search_engine_1"));
+ });
+ });
+});
+
+add_task(async function context_many() {
+ info("Checks the context menu with a page that offers many engines.");
+ let url = getRootDirectory(gTestPath) + "add_search_engine_many.html";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ await UrlbarTestUtils.withContextMenu(window, async popup => {
+ info("The separator and the add engine menu should be present.");
+ let separator = popup.parentNode.getMenuItem("add-engine-separator");
+ Assert.ok(BrowserTestUtils.is_visible(separator));
+
+ info("Engines should appear in sub menu");
+ let menu = popup.parentNode.getMenuItem("add-engine-menu");
+ Assert.ok(BrowserTestUtils.is_visible(menu));
+ Assert.ok(
+ !menu.nextElementSibling
+ ?.getAttribute("anonid")
+ .startsWith("add-engine")
+ );
+ Assert.ok(menu.hasAttribute("image"), "Menu should have an icon");
+ Assert.ok(
+ !menu.label.includes("add-engine"),
+ "Menu should not contain an engine name"
+ );
+
+ info("Open the submenu");
+ let popupShown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ menu.openMenu(true);
+ await popupShown;
+ for (let i = 0; i < 4; ++i) {
+ let elt = popup.parentNode.getMenuItem(`add-engine-${i}`);
+ Assert.equal(elt.parentNode, menu.menupopup);
+ Assert.ok(BrowserTestUtils.is_visible(elt));
+ }
+
+ info("Click on the first engine to install it");
+ let enginePromise = promiseEngine("engine-added", "add_search_engine_0");
+ let elt = popup.parentNode.getMenuItem("add-engine-0");
+
+ elt.closest("menupopup").activateItem(elt);
+ await enginePromise;
+ Assert.equal(popup.state, "closed");
+ });
+
+ await UrlbarTestUtils.withContextMenu(window, async popup => {
+ info("Check the installed engine has been removed");
+ // We're below the limit of engines for the menu now.
+ Assert.ok(!!popup.parentNode.getMenuItem("add-engine-separator"));
+ Assert.ok(!popup.parentNode.getMenuItem("add-engine-menu"));
+
+ for (let i = 0; i < 3; ++i) {
+ let elt = popup.parentNode.getMenuItem(`add-engine-${i}`);
+ Assert.equal(elt.parentNode, popup);
+ Assert.ok(BrowserTestUtils.is_visible(elt));
+ await document.l10n.translateElements([elt]);
+ Assert.ok(elt.label.includes(`add_search_engine_${i + 1}`));
+ }
+ });
+
+ info("Remove the engine.");
+ let engine = await Services.search.getEngineByName("add_search_engine_0");
+ await Services.search.removeEngine(engine);
+
+ await UrlbarTestUtils.withContextMenu(window, async popup => {
+ info("The separator and the add engine menu should be present.");
+ let separator = popup.parentNode.getMenuItem("add-engine-separator");
+ Assert.ok(BrowserTestUtils.is_visible(separator));
+
+ info("Engines should appear in sub menu");
+ let menu = popup.parentNode.getMenuItem("add-engine-menu");
+ Assert.ok(BrowserTestUtils.is_visible(menu));
+ Assert.ok(
+ !menu.nextElementSibling
+ ?.getAttribute("anonid")
+ .startsWith("add-engine")
+ );
+
+ info("Open the submenu");
+ let popupShown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ menu.openMenu(true);
+ await popupShown;
+ for (let i = 0; i < 4; ++i) {
+ let elt = popup.parentNode.getMenuItem(`add-engine-${i}`);
+ Assert.equal(elt.parentNode, menu.menupopup);
+ if (
+ AppConstants.platform != "macosx" ||
+ !Services.prefs.getBoolPref(
+ "widget.macos.native-context-menus",
+ false
+ )
+ ) {
+ Assert.ok(BrowserTestUtils.is_visible(elt));
+ }
+ }
+ });
+ });
+});
+
+add_task(async function context_after_customize() {
+ info("Checks the context menu after customization.");
+ let url = getRootDirectory(gTestPath) + "add_search_engine_one.html";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ await UrlbarTestUtils.withContextMenu(window, async popup => {
+ info("The separator and the add engine item should be present.");
+ let elt = popup.parentNode.getMenuItem("add-engine-separator");
+ Assert.ok(BrowserTestUtils.is_visible(elt));
+
+ Assert.ok(!popup.parentNode.getMenuItem("add-engine-menu"));
+ Assert.ok(!popup.parentNode.getMenuItem("add-engine-1"));
+
+ elt = popup.parentNode.getMenuItem("add-engine-0");
+ Assert.ok(BrowserTestUtils.is_visible(elt));
+ await document.l10n.translateElements([elt]);
+ Assert.ok(elt.label.includes("add_search_engine_0"));
+ });
+
+ let promise = BrowserTestUtils.waitForEvent(
+ gNavToolbox,
+ "customizationready"
+ );
+ gCustomizeMode.enter();
+ await promise;
+ promise = BrowserTestUtils.waitForEvent(gNavToolbox, "aftercustomization");
+ gCustomizeMode.exit();
+ await promise;
+
+ await UrlbarTestUtils.withContextMenu(window, async popup => {
+ info("The separator and the add engine item should be present.");
+ let elt = popup.parentNode.getMenuItem("add-engine-separator");
+ Assert.ok(BrowserTestUtils.is_visible(elt));
+
+ Assert.ok(!popup.parentNode.getMenuItem("add-engine-menu"));
+ Assert.ok(!popup.parentNode.getMenuItem("add-engine-1"));
+
+ elt = popup.parentNode.getMenuItem("add-engine-0");
+ Assert.ok(BrowserTestUtils.is_visible(elt));
+ await document.l10n.translateElements([elt]);
+ Assert.ok(elt.label.includes("add_search_engine_0"));
+ });
+ });
+});
+
+function promiseEngine(expectedData, expectedEngineName) {
+ info(`Waiting for engine ${expectedData}`);
+ return TestUtils.topicObserved(
+ "browser-search-engine-modified",
+ (engine, data) => {
+ info(`Got engine ${engine.wrappedJSObject.name} ${data}`);
+ return (
+ expectedData == data &&
+ expectedEngineName == engine.wrappedJSObject.name
+ );
+ }
+ ).then(([engine, data]) => engine);
+}
diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_backspaced.js b/browser/components/urlbar/tests/browser/browser_autoFill_backspaced.js
new file mode 100644
index 0000000000..65f533c0fe
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_autoFill_backspaced.js
@@ -0,0 +1,268 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* This test ensures that backspacing autoFilled values still allows to
+ * confirm the remaining value.
+ */
+
+"use strict";
+
+async function test_autocomplete(data) {
+ let { desc, typed, autofilled, modified, keys, type, onAutoFill } = data;
+ info(desc);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: typed,
+ fireInputEvent: true,
+ });
+ Assert.equal(gURLBar.value, autofilled, "autofilled value is as expected");
+ if (onAutoFill) {
+ onAutoFill();
+ }
+
+ info("Synthesizing keys");
+ for (let key of keys) {
+ let args = Array.isArray(key) ? key : [key];
+ EventUtils.synthesizeKey(...args);
+ }
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ Assert.equal(gURLBar.value, modified, "backspaced value is as expected");
+
+ Assert.greater(
+ UrlbarTestUtils.getResultCount(window),
+ 0,
+ "Should get at least 1 result"
+ );
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+
+ Assert.equal(result.type, type, "Should have the correct result type");
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ gURLBar.blur();
+}
+
+add_task(async function () {
+ registerCleanupFunction(async function () {
+ Services.prefs.clearUserPref("browser.urlbar.autoFill");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions");
+ gURLBar.handleRevert();
+ await PlacesUtils.history.clear();
+ });
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false);
+
+ await PlacesTestUtils.addVisits([
+ "http://example.com/",
+ "http://example.com/foo",
+ ]);
+ // Bookmark the page so it ignores autofill threshold and doesn't risk to
+ // not be autofilled.
+ let bm = await PlacesUtils.bookmarks.insert({
+ url: "http://example.com/",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ });
+ registerCleanupFunction(async function () {
+ await PlacesUtils.bookmarks.remove(bm);
+ });
+
+ await test_autocomplete({
+ desc: "DELETE the autofilled part should search",
+ typed: "exam",
+ autofilled: "example.com/",
+ modified: "exam",
+ keys: ["KEY_Delete"],
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ });
+ await test_autocomplete({
+ desc: "DELETE the final slash should visit",
+ typed: "example.com",
+ autofilled: "example.com/",
+ modified: "example.com",
+ keys: ["KEY_Delete"],
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ });
+
+ await test_autocomplete({
+ desc: "BACK_SPACE the autofilled part should search",
+ typed: "exam",
+ autofilled: "example.com/",
+ modified: "exam",
+ keys: ["KEY_Backspace"],
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ });
+ await test_autocomplete({
+ desc: "BACK_SPACE the final slash should visit",
+ typed: "example.com",
+ autofilled: "example.com/",
+ modified: "example.com",
+ keys: ["KEY_Backspace"],
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ });
+
+ await test_autocomplete({
+ desc: "DELETE the autofilled part, then BACK_SPACE, should search",
+ typed: "exam",
+ autofilled: "example.com/",
+ modified: "exa",
+ keys: ["KEY_Delete", "KEY_Backspace"],
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ });
+ await test_autocomplete({
+ desc: "DELETE the final slash, then BACK_SPACE, should search",
+ typed: "example.com",
+ autofilled: "example.com/",
+ modified: "example.co",
+ keys: ["KEY_Delete", "KEY_Backspace"],
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ });
+
+ await test_autocomplete({
+ desc: "BACK_SPACE the autofilled part, then BACK_SPACE, should search",
+ typed: "exam",
+ autofilled: "example.com/",
+ modified: "exa",
+ keys: ["KEY_Backspace", "KEY_Backspace"],
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ });
+ await test_autocomplete({
+ desc: "BACK_SPACE the final slash, then BACK_SPACE, should search",
+ typed: "example.com",
+ autofilled: "example.com/",
+ modified: "example.co",
+ keys: ["KEY_Backspace", "KEY_Backspace"],
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ });
+
+ await test_autocomplete({
+ desc: "BACK_SPACE after blur should search",
+ typed: "ex",
+ autofilled: "example.com/",
+ modified: "e",
+ keys: ["KEY_Backspace"],
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ onAutoFill: () => {
+ gURLBar.blur();
+ gURLBar.focus();
+ Assert.equal(
+ gURLBar.selectionStart,
+ gURLBar.value.length,
+ "blur/focus should not change selection"
+ );
+ Assert.equal(
+ gURLBar.selectionEnd,
+ gURLBar.value.length,
+ "blur/focus should not change selection"
+ );
+ },
+ });
+ await test_autocomplete({
+ desc: "DELETE after blur should search",
+ typed: "ex",
+ autofilled: "example.com/",
+ modified: "e",
+ keys: ["KEY_ArrowLeft", "KEY_Delete"],
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ onAutoFill: () => {
+ gURLBar.blur();
+ gURLBar.focus();
+ Assert.equal(
+ gURLBar.selectionStart,
+ gURLBar.value.length,
+ "blur/focus should not change selection"
+ );
+ Assert.equal(
+ gURLBar.selectionEnd,
+ gURLBar.value.length,
+ "blur/focus should not change selection"
+ );
+ },
+ });
+ await test_autocomplete({
+ desc: "double BACK_SPACE after blur should search",
+ typed: "exa",
+ autofilled: "example.com/",
+ modified: "e",
+ keys: ["KEY_Backspace", "KEY_Backspace"],
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ onAutoFill: () => {
+ gURLBar.blur();
+ gURLBar.focus();
+ Assert.equal(
+ gURLBar.selectionStart,
+ gURLBar.value.length,
+ "blur/focus should not change selection"
+ );
+ Assert.equal(
+ gURLBar.selectionEnd,
+ gURLBar.value.length,
+ "blur/focus should not change selection"
+ );
+ },
+ });
+
+ await test_autocomplete({
+ desc: "Right arrow key and then backspace should delete the backslash and not re-trigger autofill",
+ typed: "ex",
+ autofilled: "example.com/",
+ modified: "example.com",
+ keys: ["KEY_ArrowRight", "KEY_Backspace"],
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ });
+
+ await test_autocomplete({
+ desc: "Right arrow key, selecting the last few characters using the keyboard, and then backspace should delete the characters and not re-trigger autofill",
+ typed: "ex",
+ autofilled: "example.com/",
+ modified: "example.c",
+ keys: [
+ "KEY_ArrowRight",
+ ["KEY_ArrowLeft", { shiftKey: true }],
+ ["KEY_ArrowLeft", { shiftKey: true }],
+ ["KEY_ArrowLeft", { shiftKey: true }],
+ "KEY_Backspace",
+ ],
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ });
+
+ await test_autocomplete({
+ desc: "End and then backspace should delete the backslash and not re-trigger autofill",
+ typed: "ex",
+ autofilled: "example.com/",
+ modified: "example.com",
+ keys: [
+ AppConstants.platform == "macosx"
+ ? ["KEY_ArrowRight", { metaKey: true }]
+ : "KEY_End",
+ "KEY_Backspace",
+ ],
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ });
+
+ await test_autocomplete({
+ desc: "Clicking in the input after the text and then backspace should delete the backslash and not re-trigger autofill",
+ typed: "ex",
+ autofilled: "example.com/",
+ modified: "example.com",
+ keys: ["KEY_Backspace"],
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ onAutoFill: () => {
+ // This assumes that the center of the input is to the right of the end
+ // of the text, so the caret is placed at the end of the text on click.
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ },
+ });
+
+ await test_autocomplete({
+ desc: "Selecting the next result and then backspace should delete the last character and not re-trigger autofill",
+ typed: "ex",
+ autofilled: "example.com/",
+ modified: "example.com/fo",
+ keys: ["KEY_ArrowDown", "KEY_Backspace"],
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ });
+
+ await PlacesUtils.history.clear();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_canonize.js b/browser/components/urlbar/tests/browser/browser_autoFill_canonize.js
new file mode 100644
index 0000000000..af6a2eb08b
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_autoFill_canonize.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* This test ensures that pressing ctrl+enter bypasses the autoFilled
+ * value, and only considers what the user typed (but not just enter).
+ */
+
+async function test_autocomplete(data) {
+ let { desc, typed, autofilled, modified, waitForUrl, keys } = data;
+ info(desc);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: typed,
+ });
+ Assert.equal(gURLBar.value, autofilled, "autofilled value is as expected");
+
+ let promiseLoad = BrowserTestUtils.waitForDocLoadAndStopIt(
+ waitForUrl,
+ gBrowser.selectedBrowser
+ );
+
+ keys.forEach(([key, mods]) => EventUtils.synthesizeKey(key, mods));
+
+ Assert.equal(gURLBar.value, modified, "value is as expected");
+
+ await promiseLoad;
+ gURLBar.blur();
+}
+
+add_task(async function () {
+ registerCleanupFunction(async function () {
+ Services.prefs.clearUserPref("browser.urlbar.autoFill");
+ gURLBar.handleRevert();
+ await PlacesUtils.history.clear();
+ });
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", true);
+
+ // Add a typed visit, so it will be autofilled.
+ await PlacesTestUtils.addVisits({
+ uri: "http://example.com/",
+ transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ });
+
+ await test_autocomplete({
+ desc: "CTRL+ENTER on the autofilled part should use autofill",
+ typed: "exam",
+ autofilled: "example.com/",
+ modified: "example.com",
+ waitForUrl: "http://example.com/",
+ keys: [["KEY_Enter"]],
+ });
+
+ await test_autocomplete({
+ desc: "CTRL+ENTER on the autofilled part should bypass autofill",
+ typed: "exam",
+ autofilled: "example.com/",
+ modified: "https://www.exam.com",
+ waitForUrl: "https://www.exam.com/",
+ keys: [["KEY_Enter", { ctrlKey: true }]],
+ });
+});
diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_caretNotAtEnd.js b/browser/components/urlbar/tests/browser/browser_autoFill_caretNotAtEnd.js
new file mode 100644
index 0000000000..570a1c2c8c
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_autoFill_caretNotAtEnd.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function noAutofillWhenCaretNotAtEnd() {
+ gURLBar.focus();
+
+ // Add a visit that can be autofilled.
+ await PlacesUtils.history.clear();
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com/",
+ },
+ ]);
+
+ // Fill the input with xample.
+ gURLBar.inputField.value = "xample";
+
+ // Move the caret to the beginning and type e.
+ gURLBar.selectionStart = 0;
+ gURLBar.selectionEnd = 0;
+ EventUtils.sendString("e");
+
+ // Check the first result and input.
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(!result.autofill, "The first result should not be autofill");
+
+ Assert.equal(gURLBar.value, "example");
+ Assert.equal(gURLBar.selectionStart, 1);
+ Assert.equal(gURLBar.selectionEnd, 1);
+
+ await PlacesUtils.history.clear();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_firstResult.js b/browser/components/urlbar/tests/browser/browser_autoFill_firstResult.js
new file mode 100644
index 0000000000..b3fb932c4c
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_autoFill_firstResult.js
@@ -0,0 +1,200 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test makes sure that autofilling the first result of a new search works
+// correctly: autofill happens when it should and doesn't when it shouldn't.
+
+"use strict";
+
+add_setup(async function () {
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+ await PlacesTestUtils.addVisits(["http://example.com/"]);
+
+ // Disable placeholder completion. The point of this test is to make sure the
+ // first result is autofilled (or not) correctly. Autofilling the placeholder
+ // before the search starts interferes with that.
+ gURLBar._enableAutofillPlaceholder = false;
+ registerCleanupFunction(async () => {
+ gURLBar._enableAutofillPlaceholder = true;
+ });
+});
+
+// The first result should be autofilled when all conditions are met. This also
+// does a sanity check to make sure that placeholder autofill is correctly
+// disabled, which is helpful for all tasks here and is why this one is first.
+add_task(async function successfulAutofill() {
+ // Do a simple search that should autofill. This will also set up the
+ // autofill placeholder string, which next we make sure is *not* autofilled.
+ await doInitialAutofillSearch();
+
+ // As a sanity check, do another search to make sure the placeholder is *not*
+ // autofilled. Make sure it's not autofilled by checking the input value and
+ // selection *before* the search completes. If placeholder autofill was not
+ // correctly disabled, then these assertions will fail.
+
+ gURLBar.value = "exa";
+ UrlbarTestUtils.fireInputEvent(window);
+
+ // before the search completes: no autofill
+ Assert.equal(gURLBar.value, "exa");
+ Assert.equal(gURLBar.selectionStart, "exa".length);
+ Assert.equal(gURLBar.selectionEnd, "exa".length);
+
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ // after the search completes: successful autofill
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(gURLBar.value, "example.com/");
+ Assert.equal(gURLBar.selectionStart, "exa".length);
+ Assert.equal(gURLBar.selectionEnd, "example.com/".length);
+});
+
+// The first result should not be autofilled when it's not an autofill result.
+add_task(async function firstResultNotAutofill() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ fireInputEvent: true,
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(!details.autofill);
+ Assert.equal(gURLBar.value, "foo");
+ Assert.equal(gURLBar.selectionStart, "foo".length);
+ Assert.equal(gURLBar.selectionEnd, "foo".length);
+});
+
+// The first result should *not* be autofilled when the placeholder is not
+// selected, the selection is empty, and the caret is *not* at the end of the
+// search string.
+add_task(async function caretNotAtEndOfSearchString() {
+ // To set up the placeholder, do an initial search that triggers autofill.
+ await doInitialAutofillSearch();
+
+ // Now do another search but set the caret to somewhere else besides the end
+ // of the new search string.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ selectionStart: "exa".length,
+ selectionEnd: "exa".length,
+ fireInputEvent: false,
+ });
+
+ // The first result should be an autofill result, but it should not have been
+ // autofilled.
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(gURLBar.value, "exam");
+ Assert.equal(gURLBar.selectionStart, "exa".length);
+ Assert.equal(gURLBar.selectionEnd, "exa".length);
+
+ await cleanUp();
+});
+
+// The first result should *not* be autofilled when the placeholder is not
+// selected, the selection is *not* empty, and the caret is at the end of the
+// search string.
+add_task(async function selectionNotEmpty() {
+ // To set up the placeholder, do an initial search that triggers autofill.
+ await doInitialAutofillSearch();
+
+ // Now do another search. Set the selection end at the end of the search
+ // string, but make the selection non-empty.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ selectionStart: "exa".length,
+ selectionEnd: "exam".length,
+ });
+
+ // The first result should be an autofill result, but it should not have been
+ // autofilled.
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(gURLBar.value, "exam");
+ Assert.equal(gURLBar.selectionStart, "exa".length);
+ Assert.equal(gURLBar.selectionEnd, "exam".length);
+
+ await cleanUp();
+});
+
+// The first result should be autofilled when the placeholder is not selected,
+// the selection is empty, and the caret is at the end of the search string.
+add_task(async function successfulAutofillAfterSettingPlaceholder() {
+ // To set up the placeholder, do an initial search that triggers autofill.
+ await doInitialAutofillSearch();
+
+ // Now do another search.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ selectionStart: "exam".length,
+ selectionEnd: "exam".length,
+ });
+
+ // It should be autofilled.
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(gURLBar.value, "example.com/");
+ Assert.equal(gURLBar.selectionStart, "exam".length);
+ Assert.equal(gURLBar.selectionEnd, "example.com/".length);
+
+ await cleanUp();
+});
+
+// The first result should be autofilled when the placeholder *is* selected --
+// more precisely, when the portion of the placeholder after the new search
+// string is selected.
+add_task(async function successfulAutofillPlaceholderSelected() {
+ // To set up the placeholder, do an initial search that triggers autofill.
+ await doInitialAutofillSearch();
+
+ // Now do another search and select the portion of the placeholder after the
+ // new search string.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ selectionStart: "exam".length,
+ selectionEnd: "example.com/".length,
+ });
+
+ // It should be autofilled.
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(gURLBar.value, "example.com/");
+ Assert.equal(gURLBar.selectionStart, "exam".length);
+ Assert.equal(gURLBar.selectionEnd, "example.com/".length);
+
+ await cleanUp();
+});
+
+async function doInitialAutofillSearch() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "ex",
+ fireInputEvent: true,
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(gURLBar.value, "example.com/");
+ Assert.equal(gURLBar.selectionStart, "ex".length);
+ Assert.equal(gURLBar.selectionEnd, "example.com/".length);
+}
+
+async function cleanUp() {
+ // In some cases above, a test task searches for "exam" at the end, and then
+ // the next task searches for "ex". Autofill results will not be allowed in
+ // the next task in that case since the old search string starts with the new
+ // search string. To prevent one task from interfering with the next, do a
+ // search that changes the search string. Also close the popup while we're
+ // here, although that's not really necessary.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "reset last search string",
+ });
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeKey("KEY_Escape");
+ });
+}
diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_paste.js b/browser/components/urlbar/tests/browser/browser_autoFill_paste.js
new file mode 100644
index 0000000000..47d92cb7d3
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_autoFill_paste.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test checks we don't autofill on paste.
+
+"use strict";
+
+add_task(async function test() {
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+ await PlacesTestUtils.addVisits(["http://example.com/"]);
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ });
+
+ // Search for "e". It should autofill to example.com/.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "e",
+ fireInputEvent: true,
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(gURLBar.value, "example.com/");
+ Assert.equal(gURLBar.selectionStart, "e".length);
+ Assert.equal(gURLBar.selectionEnd, "example.com/".length);
+
+ // Now paste.
+ await selectAndPaste("ex");
+
+ // Nothing should have been autofilled.
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(!details.autofill);
+ Assert.equal(gURLBar.value, "ex");
+ Assert.equal(gURLBar.selectionStart, "ex".length);
+ Assert.equal(gURLBar.selectionEnd, "ex".length);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_placeholder.js b/browser/components/urlbar/tests/browser/browser_autoFill_placeholder.js
new file mode 100644
index 0000000000..6fcd664de0
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_autoFill_placeholder.js
@@ -0,0 +1,1017 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test makes sure that the autofill placeholder value is autofilled
+// correctly. The placeholder is a string that we immediately autofill when a
+// search starts and before its first result arrives in order to prevent text
+// flicker in the input.
+//
+// Because this test specifically checks autofill *before* searches complete, we
+// can't use promiseAutocompleteResultPopup() or other helpers that wait for
+// searches to complete. Instead the test uses fireInputEvent() to trigger
+// placeholder autofill and then immediately checks autofill status.
+
+"use strict";
+
+// Allow more time for verify mode.
+requestLongerTimeout(5);
+
+add_setup(async function () {
+ await cleanUp();
+});
+
+// Basic origin autofill test.
+add_task(async function origin() {
+ await addVisits("http://example.com/");
+
+ await search({
+ searchString: "ex",
+ valueBefore: "ex",
+ valueAfter: "example.com/",
+ placeholderAfter: "example.com/",
+ });
+ await search({
+ searchString: "exa",
+ valueBefore: "example.com/",
+ valueAfter: "example.com/",
+ placeholderAfter: "example.com/",
+ });
+ await search({
+ searchString: "EXAM",
+ valueBefore: "EXAMple.com/",
+ valueAfter: "EXAMple.com/",
+ placeholderAfter: "EXAMple.com/",
+ });
+ await search({
+ searchString: "eXaMp",
+ valueBefore: "eXaMple.com/",
+ valueAfter: "eXaMple.com/",
+ placeholderAfter: "eXaMple.com/",
+ });
+ await search({
+ searchString: "exampL",
+ valueBefore: "exampLe.com/",
+ valueAfter: "exampLe.com/",
+ placeholderAfter: "exampLe.com/",
+ });
+ await search({
+ searchString: "example.com",
+ valueBefore: "example.com/",
+ valueAfter: "example.com/",
+ placeholderAfter: "example.com/",
+ });
+ await search({
+ searchString: "example.com/",
+ valueBefore: "example.com/",
+ valueAfter: "example.com/",
+ placeholderAfter: "example.com/",
+ });
+
+ await cleanUp();
+});
+
+// Basic URL autofill test.
+add_task(async function url() {
+ await addVisits("http://example.com/aaa/bbb/ccc");
+
+ await search({
+ searchString: "example.com/a",
+ valueBefore: "example.com/a",
+ valueAfter: "example.com/aaa/",
+ placeholderAfter: "example.com/aaa/",
+ });
+ await search({
+ searchString: "EXAmple.com/aA",
+ valueBefore: "EXAmple.com/aAa/",
+ valueAfter: "EXAmple.com/aAa/",
+ placeholderAfter: "EXAmple.com/aAa/",
+ });
+ await search({
+ searchString: "example.com/aAa",
+ valueBefore: "example.com/aAa/",
+ valueAfter: "example.com/aAa/",
+ placeholderAfter: "example.com/aAa/",
+ });
+ await search({
+ searchString: "example.com/aaa/",
+ valueBefore: "example.com/aaa/",
+ valueAfter: "example.com/aaa/",
+ placeholderAfter: "example.com/aaa/",
+ });
+ await search({
+ searchString: "example.com/aaa/b",
+ valueBefore: "example.com/aaa/b",
+ valueAfter: "example.com/aaa/bbb/",
+ placeholderAfter: "example.com/aaa/bbb/",
+ });
+ await search({
+ searchString: "example.com/aAa/bB",
+ valueBefore: "example.com/aAa/bBb/",
+ valueAfter: "example.com/aAa/bBb/",
+ placeholderAfter: "example.com/aAa/bBb/",
+ });
+ await search({
+ searchString: "example.com/aAa/bBb",
+ valueBefore: "example.com/aAa/bBb/",
+ valueAfter: "example.com/aAa/bBb/",
+ placeholderAfter: "example.com/aAa/bBb/",
+ });
+ await search({
+ searchString: "example.com/aaa/bbb/",
+ valueBefore: "example.com/aaa/bbb/",
+ valueAfter: "example.com/aaa/bbb/",
+ placeholderAfter: "example.com/aaa/bbb/",
+ });
+ await search({
+ searchString: "example.com/aaa/bbb/c",
+ valueBefore: "example.com/aaa/bbb/c",
+ valueAfter: "example.com/aaa/bbb/ccc",
+ placeholderAfter: "example.com/aaa/bbb/ccc",
+ });
+ await search({
+ searchString: "example.com/aAa/bBb/cC",
+ valueBefore: "example.com/aAa/bBb/cCc",
+ valueAfter: "example.com/aAa/bBb/cCc",
+ placeholderAfter: "example.com/aAa/bBb/cCc",
+ });
+ await search({
+ searchString: "example.com/aaa/bbb/ccc",
+ valueBefore: "example.com/aaa/bbb/ccc",
+ valueAfter: "example.com/aaa/bbb/ccc",
+ placeholderAfter: "example.com/aaa/bbb/ccc",
+ });
+
+ await cleanUp();
+});
+
+// Basic adaptive history autofill test.
+add_task(async function adaptiveHistory() {
+ UrlbarPrefs.set("autoFill.adaptiveHistory.enabled", true);
+
+ await addVisits("http://example.com/test");
+ await UrlbarUtils.addToInputHistory("http://example.com/test", "exa");
+
+ await search({
+ searchString: "exa",
+ valueBefore: "exa",
+ valueAfter: "example.com/test",
+ placeholderAfter: "example.com/test",
+ });
+ await search({
+ searchString: "EXAM",
+ valueBefore: "EXAMple.com/test",
+ valueAfter: "EXAMple.com/test",
+ placeholderAfter: "EXAMple.com/test",
+ });
+ await search({
+ searchString: "eXaMpLe",
+ valueBefore: "eXaMpLe.com/test",
+ valueAfter: "eXaMpLe.com/test",
+ placeholderAfter: "eXaMpLe.com/test",
+ });
+ await search({
+ searchString: "example.",
+ valueBefore: "example.com/test",
+ valueAfter: "example.com/test",
+ placeholderAfter: "example.com/test",
+ });
+ await search({
+ searchString: "example.c",
+ valueBefore: "example.com/test",
+ valueAfter: "example.com/test",
+ placeholderAfter: "example.com/test",
+ });
+ await search({
+ searchString: "example.com",
+ valueBefore: "example.com/test",
+ valueAfter: "example.com/test",
+ placeholderAfter: "example.com/test",
+ });
+ await search({
+ searchString: "example.com/",
+ valueBefore: "example.com/test",
+ valueAfter: "example.com/test",
+ placeholderAfter: "example.com/test",
+ });
+ await search({
+ searchString: "example.com/T",
+ valueBefore: "example.com/Test",
+ valueAfter: "example.com/Test",
+ placeholderAfter: "example.com/Test",
+ });
+ await search({
+ searchString: "eXaMple.com/tE",
+ valueBefore: "eXaMple.com/tEst",
+ valueAfter: "eXaMple.com/tEst",
+ placeholderAfter: "eXaMple.com/tEst",
+ });
+ await search({
+ searchString: "example.com/tes",
+ valueBefore: "example.com/test",
+ valueAfter: "example.com/test",
+ placeholderAfter: "example.com/test",
+ });
+ await search({
+ searchString: "example.com/test",
+ valueBefore: "example.com/test",
+ valueAfter: "example.com/test",
+ placeholderAfter: "example.com/test",
+ });
+
+ UrlbarPrefs.clear("autoFill.adaptiveHistory.enabled");
+ await cleanUp();
+});
+
+// Search engine token alias test (aliases that start with "@").
+add_task(async function tokenAlias() {
+ // We have built-in engine aliases that may conflict with the one we choose
+ // here in terms of autofill, so be careful and choose a weird alias.
+ await SearchTestUtils.installSearchExtension({ keyword: "@__example" });
+
+ await search({
+ searchString: "@__ex",
+ valueBefore: "@__ex",
+ valueAfter: "@__example ",
+ placeholderAfter: "@__example ",
+ });
+ await search({
+ searchString: "@__exa",
+ valueBefore: "@__example ",
+ valueAfter: "@__example ",
+ placeholderAfter: "@__example ",
+ });
+ await search({
+ searchString: "@__EXAM",
+ valueBefore: "@__EXAMple ",
+ valueAfter: "@__EXAMple ",
+ placeholderAfter: "@__EXAMple ",
+ });
+ await search({
+ searchString: "@__eXaMp",
+ valueBefore: "@__eXaMple ",
+ valueAfter: "@__eXaMple ",
+ placeholderAfter: "@__eXaMple ",
+ });
+ await search({
+ searchString: "@__exampl",
+ valueBefore: "@__example ",
+ valueAfter: "@__example ",
+ placeholderAfter: "@__example ",
+ });
+
+ await cleanUp();
+});
+
+// The placeholder should not be used for a search that does not autofill, and
+// it should be cleared after the search completes.
+add_task(async function noAutofill() {
+ await addVisits("http://example.com/");
+
+ // Do an initial search that triggers autofill so that the placeholder has an
+ // initial value.
+ await search({
+ searchString: "ex",
+ valueBefore: "ex",
+ valueAfter: "example.com/",
+ placeholderAfter: "example.com/",
+ });
+
+ // Search with a string that does not match the placeholder. Placeholder
+ // autofill shouldn't happen.
+ await search({
+ searchString: "moz",
+ valueBefore: "moz",
+ valueAfter: "moz",
+ placeholderAfter: "",
+ });
+
+ // Search for "ex" again. It should be autofilled when the search completes
+ // but the placeholder will not be autofilled.
+ await search({
+ searchString: "ex",
+ valueBefore: "ex",
+ valueAfter: "example.com/",
+ placeholderAfter: "example.com/",
+ });
+
+ // Continue with a series of searches that should all use the placeholder.
+ await search({
+ searchString: "exa",
+ valueBefore: "example.com/",
+ valueAfter: "example.com/",
+ placeholderAfter: "example.com/",
+ });
+ await search({
+ searchString: "EXAM",
+ valueBefore: "EXAMple.com/",
+ valueAfter: "EXAMple.com/",
+ placeholderAfter: "EXAMple.com/",
+ });
+ await search({
+ searchString: "eXaMp",
+ valueBefore: "eXaMple.com/",
+ valueAfter: "eXaMple.com/",
+ placeholderAfter: "eXaMple.com/",
+ });
+ await search({
+ searchString: "exampl",
+ valueBefore: "example.com/",
+ valueAfter: "example.com/",
+ placeholderAfter: "example.com/",
+ });
+
+ await cleanUp();
+});
+
+// The placeholder should not be used for a search that autofills a different
+// value.
+add_task(async function differentAutofill() {
+ await addVisits("http://mozilla.org/", "http://example.com/");
+
+ // Do an initial search that triggers autofill so that the placeholder has an
+ // initial value.
+ await search({
+ searchString: "moz",
+ valueBefore: "moz",
+ valueAfter: "mozilla.org/",
+ placeholderAfter: "mozilla.org/",
+ });
+
+ // Search with a string that does not match the placeholder but does trigger
+ // autofill. Placeholder autofill shouldn't happen.
+ await search({
+ searchString: "ex",
+ valueBefore: "ex",
+ valueAfter: "example.com/",
+ placeholderAfter: "example.com/",
+ });
+
+ // Continue with a series of searches that should all use the placeholder.
+ await search({
+ searchString: "exa",
+ valueBefore: "example.com/",
+ valueAfter: "example.com/",
+ placeholderAfter: "example.com/",
+ });
+ await search({
+ searchString: "EXAm",
+ valueBefore: "EXAmple.com/",
+ valueAfter: "EXAmple.com/",
+ placeholderAfter: "EXAmple.com/",
+ });
+
+ // Search for "moz" again. It should be autofilled. Placeholder autofill
+ // shouldn't happen.
+ await search({
+ searchString: "moz",
+ valueBefore: "moz",
+ valueAfter: "mozilla.org/",
+ placeholderAfter: "mozilla.org/",
+ });
+
+ // Continue with a series of searches that should all use the placeholder.
+ await search({
+ searchString: "mozi",
+ valueBefore: "mozilla.org/",
+ valueAfter: "mozilla.org/",
+ placeholderAfter: "mozilla.org/",
+ });
+ await search({
+ searchString: "MOZil",
+ valueBefore: "MOZilla.org/",
+ valueAfter: "MOZilla.org/",
+ placeholderAfter: "MOZilla.org/",
+ });
+
+ await cleanUp();
+});
+
+// The placeholder should not be used for a search that uses a bookmark keyword
+// even when the keyword matches the placeholder, and the placeholder should be
+// cleared after the search completes.
+add_task(async function bookmarkKeyword() {
+ // Add a visit to example.com.
+ await addVisits("https://example.com/");
+
+ // Add a bookmark keyword that is a prefix of example.com.
+ await PlacesUtils.keywords.insert({
+ keyword: "ex",
+ url: "https://somekeyword.com/",
+ });
+
+ // Do an initial search that triggers autofill for the visit so that the
+ // placeholder has an initial value of "example.com/".
+ await search({
+ searchString: "e",
+ valueBefore: "e",
+ valueAfter: "example.com/",
+ placeholderAfter: "example.com/",
+ });
+
+ // Do a search that matches the bookmark keyword. The placeholder from the
+ // search above should be autofilled since the autofill placeholder
+ // ("example.com/") starts with the keyword ("ex"), but then when the bookmark
+ // result arrives, the autofilled value and placeholder should be cleared.
+ await search({
+ searchString: "ex",
+ valueBefore: "example.com/",
+ valueAfter: "ex",
+ placeholderAfter: "",
+ });
+
+ // Do another search that simulates the user continuing to type "example". No
+ // placeholder should be autofilled, but once the autofill result arrives for
+ // the visit, "example.com/" should be autofilled.
+ await search({
+ searchString: "exa",
+ valueBefore: "exa",
+ valueAfter: "example.com/",
+ placeholderAfter: "example.com/",
+ });
+
+ await PlacesUtils.keywords.remove("ex");
+ await cleanUp();
+});
+
+// The placeholder should not be used for a search that doesn't match its URI
+// fragment. This task uses a URL whose path is "/".
+add_task(async function noURIFragmentMatch1() {
+ await addVisits("https://example.com/#TEST");
+
+ const testData = [
+ {
+ desc: "Autofill example.com/#TEST then search for example.com/#Te",
+ searches: [
+ {
+ searchString: "example.com/#T",
+ valueBefore: "example.com/#T",
+ valueAfter: "example.com/#TEST",
+ placeholderAfter: "example.com/#TEST",
+ },
+ {
+ searchString: "example.com/#Te",
+ valueBefore: "example.com/#Te",
+ valueAfter: "example.com/#Te",
+ placeholderAfter: "",
+ },
+ ],
+ },
+ {
+ desc: "Autofill https://example.com/#TEST then search for https://example.com/#Te",
+ searches: [
+ {
+ searchString: "https://example.com/#T",
+ valueBefore: "https://example.com/#T",
+ valueAfter: "https://example.com/#TEST",
+ placeholderAfter: "https://example.com/#TEST",
+ },
+ {
+ searchString: "https://example.com/#Te",
+ valueBefore: "https://example.com/#Te",
+ valueAfter: "https://example.com/#Te",
+ placeholderAfter: "",
+ },
+ ],
+ },
+ {
+ desc: "Autofill example.com/#TEST then search for example.com/",
+ searches: [
+ {
+ searchString: "example.com/#T",
+ valueBefore: "example.com/#T",
+ valueAfter: "example.com/#TEST",
+ placeholderAfter: "example.com/#TEST",
+ },
+ {
+ searchString: "example.com/",
+ valueBefore: "example.com/",
+ valueAfter: "example.com/",
+ placeholderAfter: "example.com/",
+ },
+ ],
+ },
+ ];
+
+ for (const { desc, searches } of testData) {
+ info("Running subtest: " + desc);
+
+ for (let i = 0; i < searches.length; i++) {
+ info("Doing search at index " + i);
+ await search(searches[i]);
+ }
+
+ // Clear the placeholder for the next subtest.
+ info("Doing extra search to clear placeholder");
+ await search({
+ searchString: "no match",
+ valueBefore: "no match",
+ valueAfter: "no match",
+ placeholderAfter: "",
+ });
+ }
+
+ await cleanUp();
+});
+
+// The placeholder should not be used for a search that doesn't match its URI
+// fragment. This task uses a URL whose path is "/foo".
+add_task(async function noURIFragmentMatch2() {
+ await addVisits("https://example.com/foo#TEST");
+
+ const testData = [
+ {
+ desc: "Autofill example.com/foo#TEST then search for example.com/foo#Te",
+ searches: [
+ {
+ searchString: "example.com/foo#T",
+ valueBefore: "example.com/foo#T",
+ valueAfter: "example.com/foo#TEST",
+ placeholderAfter: "example.com/foo#TEST",
+ },
+ {
+ searchString: "example.com/foo#Te",
+ valueBefore: "example.com/foo#Te",
+ valueAfter: "example.com/foo#Te",
+ placeholderAfter: "",
+ },
+ ],
+ },
+ {
+ desc: "Autofill https://example.com/foo#TEST then search for https://example.com/foo#Te",
+ searches: [
+ {
+ searchString: "https://example.com/foo#T",
+ valueBefore: "https://example.com/foo#T",
+ valueAfter: "https://example.com/foo#TEST",
+ placeholderAfter: "https://example.com/foo#TEST",
+ },
+ {
+ searchString: "https://example.com/foo#Te",
+ valueBefore: "https://example.com/foo#Te",
+ valueAfter: "https://example.com/foo#Te",
+ placeholderAfter: "",
+ },
+ ],
+ },
+ {
+ desc: "Autofill example.com/foo#TEST then search for example.com/",
+ searches: [
+ {
+ searchString: "example.com/foo#T",
+ valueBefore: "example.com/foo#T",
+ valueAfter: "example.com/foo#TEST",
+ placeholderAfter: "example.com/foo#TEST",
+ },
+ {
+ searchString: "example.com/",
+ valueBefore: "example.com/",
+ valueAfter: "example.com/",
+ placeholderAfter: "example.com/",
+ },
+ ],
+ },
+ ];
+
+ for (const { desc, searches } of testData) {
+ info("Running subtest: " + desc);
+
+ for (let i = 0; i < searches.length; i++) {
+ info("Doing search at index " + i);
+ await search(searches[i]);
+ }
+
+ // Clear the placeholder for the next subtest.
+ info("Doing extra search to clear placeholder");
+ await search({
+ searchString: "no match",
+ valueBefore: "no match",
+ valueAfter: "no match",
+ placeholderAfter: "",
+ });
+ }
+
+ await cleanUp();
+});
+
+// The placeholder should not be used for a search that does not autofill its
+// URL path.
+add_task(async function noPathMatch() {
+ await addVisits("http://example.com/shallow/deep/file");
+
+ const testData = [
+ {
+ desc: "Autofill example.com/shallow/ then search for exam",
+ searches: [
+ {
+ searchString: "example.com/s",
+ valueBefore: "example.com/s",
+ valueAfter: "example.com/shallow/",
+ placeholderAfter: "example.com/shallow/",
+ },
+ {
+ searchString: "exam",
+ valueBefore: "exam",
+ valueAfter: "example.com/",
+ placeholderAfter: "example.com/",
+ },
+ ],
+ },
+ {
+ desc: "Autofill example.com/shallow/ then search for example.com/",
+ searches: [
+ {
+ searchString: "example.com/s",
+ valueBefore: "example.com/s",
+ valueAfter: "example.com/shallow/",
+ placeholderAfter: "example.com/shallow/",
+ },
+ {
+ searchString: "example.com/",
+ valueBefore: "example.com/",
+ valueAfter: "example.com/",
+ placeholderAfter: "example.com/",
+ },
+ ],
+ },
+ {
+ desc: "Autofill example.com/shallow/deep/ then search for exam",
+ searches: [
+ {
+ searchString: "example.com/shallow/d",
+ valueBefore: "example.com/shallow/d",
+ valueAfter: "example.com/shallow/deep/",
+ placeholderAfter: "example.com/shallow/deep/",
+ },
+ {
+ searchString: "exam",
+ valueBefore: "exam",
+ valueAfter: "example.com/",
+ placeholderAfter: "example.com/",
+ },
+ ],
+ },
+ {
+ desc: "Autofill example.com/shallow/deep/ then search for example.com/",
+ searches: [
+ {
+ searchString: "example.com/shallow/d",
+ valueBefore: "example.com/shallow/d",
+ valueAfter: "example.com/shallow/deep/",
+ placeholderAfter: "example.com/shallow/deep/",
+ },
+ {
+ searchString: "example.com/",
+ valueBefore: "example.com/",
+ valueAfter: "example.com/",
+ placeholderAfter: "example.com/",
+ },
+ ],
+ },
+ {
+ desc: "Autofill example.com/shallow/deep/ then search for example.com/s",
+ searches: [
+ {
+ searchString: "example.com/shallow/d",
+ valueBefore: "example.com/shallow/d",
+ valueAfter: "example.com/shallow/deep/",
+ placeholderAfter: "example.com/shallow/deep/",
+ },
+ {
+ searchString: "example.com/s",
+ valueBefore: "example.com/s",
+ valueAfter: "example.com/shallow/",
+ placeholderAfter: "example.com/shallow/",
+ },
+ ],
+ },
+ {
+ desc: "Autofill example.com/shallow/deep/ then search for example.com/shallow/",
+ searches: [
+ {
+ searchString: "example.com/shallow/d",
+ valueBefore: "example.com/shallow/d",
+ valueAfter: "example.com/shallow/deep/",
+ placeholderAfter: "example.com/shallow/deep/",
+ },
+ {
+ searchString: "example.com/shallow/",
+ valueBefore: "example.com/shallow/",
+ valueAfter: "example.com/shallow/",
+ placeholderAfter: "example.com/shallow/",
+ },
+ ],
+ },
+ {
+ desc: "Autofill example.com/shallow/deep/file then search for exam",
+ searches: [
+ {
+ searchString: "example.com/shallow/deep/f",
+ valueBefore: "example.com/shallow/deep/f",
+ valueAfter: "example.com/shallow/deep/file",
+ placeholderAfter: "example.com/shallow/deep/file",
+ },
+ {
+ searchString: "exam",
+ valueBefore: "exam",
+ valueAfter: "example.com/",
+ placeholderAfter: "example.com/",
+ },
+ ],
+ },
+ {
+ desc: "Autofill example.com/shallow/deep/file then search for example.com/",
+ searches: [
+ {
+ searchString: "example.com/shallow/deep/f",
+ valueBefore: "example.com/shallow/deep/f",
+ valueAfter: "example.com/shallow/deep/file",
+ placeholderAfter: "example.com/shallow/deep/file",
+ },
+ {
+ searchString: "example.com/",
+ valueBefore: "example.com/",
+ valueAfter: "example.com/",
+ placeholderAfter: "example.com/",
+ },
+ ],
+ },
+ {
+ desc: "Autofill example.com/shallow/deep/file then search for example.com/s",
+ searches: [
+ {
+ searchString: "example.com/shallow/deep/f",
+ valueBefore: "example.com/shallow/deep/f",
+ valueAfter: "example.com/shallow/deep/file",
+ placeholderAfter: "example.com/shallow/deep/file",
+ },
+ {
+ searchString: "example.com/s",
+ valueBefore: "example.com/s",
+ valueAfter: "example.com/shallow/",
+ placeholderAfter: "example.com/shallow/",
+ },
+ ],
+ },
+ {
+ desc: "Autofill example.com/shallow/deep/file then search for example.com/shallow/",
+ searches: [
+ {
+ searchString: "example.com/shallow/deep/f",
+ valueBefore: "example.com/shallow/deep/f",
+ valueAfter: "example.com/shallow/deep/file",
+ placeholderAfter: "example.com/shallow/deep/file",
+ },
+ {
+ searchString: "example.com/shallow/",
+ valueBefore: "example.com/shallow/",
+ valueAfter: "example.com/shallow/",
+ placeholderAfter: "example.com/shallow/",
+ },
+ ],
+ },
+ {
+ desc: "Autofill example.com/shallow/deep/file then search for example.com/shallow/d",
+ searches: [
+ {
+ searchString: "example.com/shallow/deep/f",
+ valueBefore: "example.com/shallow/deep/f",
+ valueAfter: "example.com/shallow/deep/file",
+ placeholderAfter: "example.com/shallow/deep/file",
+ },
+ {
+ searchString: "example.com/shallow/d",
+ valueBefore: "example.com/shallow/d",
+ valueAfter: "example.com/shallow/deep/",
+ placeholderAfter: "example.com/shallow/deep/",
+ },
+ ],
+ },
+ {
+ desc: "Autofill example.com/shallow/deep/file then search for example.com/shallow/deep/",
+ searches: [
+ {
+ searchString: "example.com/shallow/deep/f",
+ valueBefore: "example.com/shallow/deep/f",
+ valueAfter: "example.com/shallow/deep/file",
+ placeholderAfter: "example.com/shallow/deep/file",
+ },
+ {
+ searchString: "example.com/shallow/deep/fi",
+ valueBefore: "example.com/shallow/deep/file",
+ valueAfter: "example.com/shallow/deep/file",
+ placeholderAfter: "example.com/shallow/deep/file",
+ },
+ {
+ searchString: "example.com/shallow/deep/",
+ valueBefore: "example.com/shallow/deep/",
+ valueAfter: "example.com/shallow/deep/",
+ placeholderAfter: "example.com/shallow/deep/",
+ },
+ ],
+ },
+ ];
+
+ for (const { desc, searches } of testData) {
+ info("Running subtest: " + desc);
+
+ for (let i = 0; i < searches.length; i++) {
+ info("Doing search at index " + i);
+ await search(searches[i]);
+ }
+
+ // Clear the placeholder for the next subtest.
+ info("Doing extra search to clear placeholder");
+ await search({
+ searchString: "no match",
+ valueBefore: "no match",
+ valueAfter: "no match",
+ placeholderAfter: "",
+ });
+ }
+
+ await cleanUp();
+});
+
+// An adaptive history placeholder should not be used for a search that does not
+// autofill it.
+add_task(async function noAdaptiveHistoryMatch() {
+ UrlbarPrefs.set("autoFill.adaptiveHistory.enabled", true);
+
+ await addVisits("http://example.com/test");
+ await UrlbarUtils.addToInputHistory("http://example.com/test", "exam");
+
+ // Search for a longer string than the adaptive history input. Adaptive
+ // history autofill should be triggered.
+ await search({
+ searchString: "example",
+ valueBefore: "example",
+ valueAfter: "example.com/test",
+ placeholderAfter: "example.com/test",
+ });
+
+ // Search for the same string as the adaptive history input. The placeholder
+ // from the previous search should be used and adaptive history autofill
+ // should be triggered.
+ await search({
+ searchString: "exam",
+ valueBefore: "example.com/test",
+ valueAfter: "example.com/test",
+ placeholderAfter: "example.com/test",
+ });
+
+ // Search for a shorter string than the adaptive history input. The
+ // placeholder from the previous search should not be used since the search
+ // string is shorter than the adaptive history input.
+ await search({
+ searchString: "ex",
+ valueBefore: "ex",
+ valueAfter: "example.com/",
+ placeholderAfter: "example.com/",
+ });
+
+ UrlbarPrefs.clear("autoFill.adaptiveHistory.enabled");
+ await cleanUp();
+});
+
+/**
+ * This function does the following:
+ *
+ * 1. Starts a search with `searchString` but doesn't wait for it to complete.
+ * 2. Compares the input value to `valueBefore`. If anything is autofilled at
+ * this point, it will be due to the placeholder.
+ * 3. Waits for the search to complete.
+ * 4. Compares the input value to `valueAfter`. If anything is autofilled at
+ * this point, it will be due to the autofill result fetched by the search.
+ * 5. Compares the placeholder to `placeholderAfter`.
+ *
+ * @param {object} options
+ * The options object.
+ * @param {string} options.searchString
+ * The search string.
+ * @param {string} options.valueBefore
+ * The expected input value before the search completes.
+ * @param {string} options.valueAfter
+ * The expected input value after the search completes.
+ * @param {string} options.placeholderAfter
+ * The expected placeholder value after the search completes.
+ * @returns {Promise}
+ */
+async function search({
+ searchString,
+ valueBefore,
+ valueAfter,
+ placeholderAfter,
+}) {
+ info(
+ "Searching: " +
+ JSON.stringify({
+ searchString,
+ valueBefore,
+ valueAfter,
+ placeholderAfter,
+ })
+ );
+
+ await SimpleTest.promiseFocus(window);
+ gURLBar.inputField.focus();
+
+ // Set the input value and move the caret to the end to simulate the user
+ // typing. It's important the caret is at the end because otherwise autofill
+ // won't happen.
+ gURLBar.value = searchString;
+ gURLBar.inputField.setSelectionRange(
+ searchString.length,
+ searchString.length
+ );
+
+ // Placeholder autofill is done on input, so fire an input event. We can't use
+ // `promiseAutocompleteResultPopup()` or other helpers that wait for the
+ // search to complete because we are specifically checking placeholder
+ // autofill before the search completes.
+ UrlbarTestUtils.fireInputEvent(window);
+
+ // Check the input value and selection immediately, before waiting on the
+ // search to complete.
+ Assert.equal(
+ gURLBar.value,
+ valueBefore,
+ "gURLBar.value before the search completes"
+ );
+ Assert.equal(
+ gURLBar.selectionStart,
+ searchString.length,
+ "gURLBar.selectionStart before the search completes"
+ );
+ Assert.equal(
+ gURLBar.selectionEnd,
+ valueBefore.length,
+ "gURLBar.selectionEnd before the search completes"
+ );
+
+ // Wait for the search to complete.
+ info("Waiting for the search to complete");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ // Check the final value after the results arrived.
+ Assert.equal(
+ gURLBar.value,
+ valueAfter,
+ "gURLBar.value after the search completes"
+ );
+ Assert.equal(
+ gURLBar.selectionStart,
+ searchString.length,
+ "gURLBar.selectionStart after the search completes"
+ );
+ Assert.equal(
+ gURLBar.selectionEnd,
+ valueAfter.length,
+ "gURLBar.selectionEnd after the search completes"
+ );
+
+ // Check the placeholder.
+ if (placeholderAfter) {
+ Assert.ok(
+ gURLBar._autofillPlaceholder,
+ "gURLBar._autofillPlaceholder exists after the search completes"
+ );
+ Assert.strictEqual(
+ gURLBar._autofillPlaceholder.value,
+ placeholderAfter,
+ "gURLBar._autofillPlaceholder.value after the search completes"
+ );
+ } else {
+ Assert.strictEqual(
+ gURLBar._autofillPlaceholder,
+ null,
+ "gURLBar._autofillPlaceholder does not exist after the search completes"
+ );
+ }
+
+ // Check the first result.
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ !!details.autofill,
+ !!placeholderAfter,
+ "First result is an autofill result iff a placeholder is expected"
+ );
+}
+
+/**
+ * Adds enough visits to URLs so their origins start autofilling.
+ *
+ * @param {...string} urls The URLs to add visits to.
+ */
+async function addVisits(...urls) {
+ for (let url of urls) {
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits(url);
+ }
+ }
+}
+
+async function cleanUp() {
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+}
diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_preserve.js b/browser/components/urlbar/tests/browser/browser_autoFill_preserve.js
new file mode 100644
index 0000000000..a197be8bf1
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_autoFill_preserve.js
@@ -0,0 +1,257 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test makes sure that a few of aspects of autofill are correctly
+// preserved:
+//
+// * Autofill should preserve the user's case. If you type ExA, it should be
+// autofilled to ExAmple.com/, not example.com/.
+// * When you key down and then back up to the autofill result, autofill should
+// be restored, with the text selection and the user's case both preserved.
+// * When you key down/up so that no result is selected, the value that the
+// user typed to trigger autofill should be restored in the input.
+
+"use strict";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ // The example.com engine can interfere with this test.
+ set: [["browser.urlbar.suggest.engines", false]],
+ });
+ await cleanUp();
+});
+
+add_task(async function origin() {
+ await PlacesTestUtils.addVisits([
+ "http://example.com/",
+ "http://mozilla.org/example",
+ ]);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "ExA",
+ fireInputEvent: true,
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(gURLBar.value, "ExAmple.com/");
+ Assert.equal(gURLBar.selectionStart, "ExA".length);
+ Assert.equal(gURLBar.selectionEnd, "ExAmple.com/".length);
+ checkKeys([
+ ["KEY_ArrowDown", "http://mozilla.org/example", 1],
+ ["KEY_ArrowDown", "ExA", -1],
+ ["KEY_ArrowUp", "http://mozilla.org/example", 1],
+ ["KEY_ArrowUp", "ExAmple.com/", 0],
+ ["KEY_ArrowUp", "ExA", -1],
+ ["KEY_ArrowDown", "ExAmple.com/", 0],
+ ]);
+ await cleanUp();
+});
+
+add_task(async function originPort() {
+ await PlacesTestUtils.addVisits([
+ "http://example.com:8888/",
+ "http://mozilla.org/example",
+ ]);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "ExA",
+ fireInputEvent: true,
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(gURLBar.value, "ExAmple.com:8888/");
+ Assert.equal(gURLBar.selectionStart, "ExA".length);
+ Assert.equal(gURLBar.selectionEnd, "ExAmple.com:8888/".length);
+ checkKeys([
+ ["KEY_ArrowDown", "http://mozilla.org/example", 1],
+ ["KEY_ArrowDown", "ExA", -1],
+ ["KEY_ArrowUp", "http://mozilla.org/example", 1],
+ ["KEY_ArrowUp", "ExAmple.com:8888/", 0],
+ ["KEY_ArrowUp", "ExA", -1],
+ ["KEY_ArrowDown", "ExAmple.com:8888/", 0],
+ ]);
+ await cleanUp();
+});
+
+add_task(async function originScheme() {
+ await PlacesTestUtils.addVisits([
+ "http://example.com/",
+ "http://mozilla.org/example",
+ ]);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "http://ExA",
+ fireInputEvent: true,
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(gURLBar.value, "http://ExAmple.com/");
+ Assert.equal(gURLBar.selectionStart, "http://ExA".length);
+ Assert.equal(gURLBar.selectionEnd, "http://ExAmple.com/".length);
+ checkKeys([
+ ["KEY_ArrowDown", "http://mozilla.org/example", 1],
+ ["KEY_ArrowDown", "http://ExA", -1],
+ ["KEY_ArrowUp", "http://mozilla.org/example", 1],
+ ["KEY_ArrowUp", "http://ExAmple.com/", 0],
+ ["KEY_ArrowUp", "http://ExA", -1],
+ ["KEY_ArrowDown", "http://ExAmple.com/", 0],
+ ]);
+ await cleanUp();
+});
+
+add_task(async function originPortScheme() {
+ await PlacesTestUtils.addVisits([
+ "http://example.com:8888/",
+ "http://mozilla.org/example",
+ ]);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "http://ExA",
+ fireInputEvents: true,
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(gURLBar.value, "http://ExAmple.com:8888/");
+ Assert.equal(gURLBar.selectionStart, "http://ExA".length);
+ Assert.equal(gURLBar.selectionEnd, "http://ExAmple.com:8888/".length);
+ checkKeys([
+ ["KEY_ArrowDown", "http://mozilla.org/example", 1],
+ ["KEY_ArrowDown", "http://ExA", -1],
+ ["KEY_ArrowUp", "http://mozilla.org/example", 1],
+ ["KEY_ArrowUp", "http://ExAmple.com:8888/", 0],
+ ["KEY_ArrowUp", "http://ExA", -1],
+ ["KEY_ArrowDown", "http://ExAmple.com:8888/", 0],
+ ]);
+ await cleanUp();
+});
+
+add_task(async function url() {
+ await PlacesTestUtils.addVisits([
+ "http://example.com/foo",
+ "http://example.com/foo",
+ "http://example.com/fff",
+ ]);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "ExAmple.com/f",
+ fireInputEvent: true,
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(gURLBar.value, "ExAmple.com/foo");
+ Assert.equal(gURLBar.selectionStart, "ExAmple.com/f".length);
+ Assert.equal(gURLBar.selectionEnd, "ExAmple.com/foo".length);
+ checkKeys([
+ ["KEY_ArrowDown", "http://example.com/fff", 1],
+ ["KEY_ArrowDown", "ExAmple.com/f", -1],
+ ["KEY_ArrowUp", "http://example.com/fff", 1],
+ ["KEY_ArrowUp", "ExAmple.com/foo", 0],
+ ["KEY_ArrowUp", "ExAmple.com/f", -1],
+ ["KEY_ArrowDown", "ExAmple.com/foo", 0],
+ ]);
+ await cleanUp();
+});
+
+add_task(async function urlPort() {
+ await PlacesTestUtils.addVisits([
+ "http://example.com:8888/foo",
+ "http://example.com:8888/foo",
+ "http://example.com:8888/fff",
+ ]);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "ExAmple.com:8888/f",
+ fireInputEvent: true,
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(gURLBar.value, "ExAmple.com:8888/foo");
+ Assert.equal(gURLBar.selectionStart, "ExAmple.com:8888/f".length);
+ Assert.equal(gURLBar.selectionEnd, "ExAmple.com:8888/foo".length);
+ checkKeys([
+ ["KEY_ArrowDown", "http://example.com:8888/fff", 1],
+ ["KEY_ArrowDown", "ExAmple.com:8888/f", -1],
+ ["KEY_ArrowUp", "http://example.com:8888/fff", 1],
+ ["KEY_ArrowUp", "ExAmple.com:8888/foo", 0],
+ ["KEY_ArrowUp", "ExAmple.com:8888/f", -1],
+ ["KEY_ArrowDown", "ExAmple.com:8888/foo", 0],
+ ]);
+ await cleanUp();
+});
+
+add_task(async function tokenAlias() {
+ await SearchTestUtils.installSearchExtension({ keyword: "@example" });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@ExA",
+ fireInputEvent: true,
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(gURLBar.value, "@ExAmple ");
+ Assert.equal(gURLBar.selectionStart, "@ExA".length);
+ Assert.equal(gURLBar.selectionEnd, "@ExAmple ".length);
+ // Token aliases (1) hide the one-off buttons and (2) show only a single
+ // result, the "Search with" result for the alias's engine, so there's no way
+ // to key up/down to change the selection, so this task doesn't check key
+ // presses like the others do.
+ await cleanUp();
+});
+
+// This test is a little different from the others. It backspaces over the
+// autofilled substring and checks that autofill is *not* preserved.
+add_task(async function backspaceNoAutofill() {
+ await PlacesTestUtils.addVisits([
+ "http://example.com/",
+ "http://example.com/",
+ "http://mozilla.org/example",
+ ]);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "ExA",
+ fireInputEvent: true,
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(gURLBar.value, "ExAmple.com/");
+ Assert.equal(gURLBar.selectionStart, "ExA".length);
+ Assert.equal(gURLBar.selectionEnd, "ExAmple.com/".length);
+
+ EventUtils.synthesizeKey("KEY_Backspace");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(!details.autofill);
+ Assert.equal(gURLBar.value, "ExA");
+ Assert.equal(gURLBar.selectionStart, "ExA".length);
+ Assert.equal(gURLBar.selectionEnd, "ExA".length);
+
+ let heuristicValue = "ExA";
+
+ checkKeys([
+ ["KEY_ArrowDown", "http://example.com/", 1],
+ ["KEY_ArrowDown", "http://mozilla.org/example", 2],
+ ["KEY_ArrowDown", "ExA", -1],
+ ["KEY_ArrowUp", "http://mozilla.org/example", 2],
+ ["KEY_ArrowUp", "http://example.com/", 1],
+ ["KEY_ArrowUp", heuristicValue, 0],
+ ["KEY_ArrowUp", "ExA", -1],
+ ["KEY_ArrowDown", heuristicValue, 0],
+ ]);
+
+ await cleanUp();
+});
+
+function checkKeys(testTuples) {
+ for (let [key, value, selectedIndex] of testTuples) {
+ EventUtils.synthesizeKey(key);
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), selectedIndex);
+ Assert.equal(gURLBar.untrimmedValue, value);
+ }
+}
+
+async function cleanUp() {
+ EventUtils.synthesizeKey("KEY_Escape");
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+}
diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_trimURLs.js b/browser/components/urlbar/tests/browser/browser_autoFill_trimURLs.js
new file mode 100644
index 0000000000..3e068d52a4
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_autoFill_trimURLs.js
@@ -0,0 +1,183 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test ensures that autoFilled values are not trimmed, unless the user
+// selects from the autocomplete popup.
+
+"use strict";
+
+add_setup(async function () {
+ const PREF_TRIMURL = "browser.urlbar.trimURLs";
+ const PREF_AUTOFILL = "browser.urlbar.autoFill";
+
+ registerCleanupFunction(async function () {
+ Services.prefs.clearUserPref(PREF_TRIMURL);
+ Services.prefs.clearUserPref(PREF_AUTOFILL);
+ await PlacesUtils.history.clear();
+ gURLBar.handleRevert();
+ });
+ Services.prefs.setBoolPref(PREF_TRIMURL, true);
+ Services.prefs.setBoolPref(PREF_AUTOFILL, true);
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+
+ // Adding a tab would hit switch-to-tab, so it's safer to just add a visit.
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://www.autofilltrimurl.com/whatever",
+ },
+ {
+ uri: "https://www.secureautofillurl.com/whatever",
+ },
+ ]);
+});
+
+async function promiseSearch(searchtext) {
+ gURLBar.focus();
+ gURLBar.inputField.value = searchtext.substr(0, searchtext.length - 1);
+ EventUtils.sendString(searchtext.substr(-1, 1));
+ await UrlbarTestUtils.promiseSearchComplete(window);
+}
+
+async function promiseTestResult(test) {
+ info(`Searching for '${test.search}'`);
+
+ await promiseSearch(test.search);
+
+ Assert.equal(
+ gURLBar.inputField.value,
+ test.autofilledValue,
+ `Autofilled value is as expected for search '${test.search}'`
+ );
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+
+ Assert.equal(
+ result.displayed.title,
+ test.resultListDisplayTitle,
+ `Autocomplete result should have displayed title as expected for search '${test.search}'`
+ );
+
+ Assert.equal(
+ result.displayed.action,
+ test.resultListActionText,
+ `Autocomplete action text should be as expected for search '${test.search}'`
+ );
+
+ Assert.equal(
+ result.type,
+ test.resultListType,
+ `Autocomplete result should have searchengine for the type for search '${test.search}'`
+ );
+
+ Assert.equal(
+ !!result.searchParams,
+ !!test.searchParams,
+ "Should have search params if expected"
+ );
+ if (test.searchParams) {
+ let definedParams = {};
+ for (let [k, v] of Object.entries(result.searchParams)) {
+ if (v !== undefined) {
+ definedParams[k] = v;
+ }
+ }
+ Assert.deepEqual(
+ definedParams,
+ test.searchParams,
+ "Shoud have the correct search params"
+ );
+ } else {
+ Assert.equal(
+ result.url,
+ test.finalCompleteValue,
+ "Should have the correct URL/finalCompleteValue"
+ );
+ }
+}
+
+const tests = [
+ {
+ search: "http://",
+ autofilledValue: "http://",
+ resultListDisplayTitle: "http://",
+ resultListActionText: "Search with Google",
+ resultListType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ searchParams: {
+ engine: "Google",
+ query: "http://",
+ },
+ },
+ {
+ search: "https://",
+ autofilledValue: "https://",
+ resultListDisplayTitle: "https://",
+ resultListActionText: "Search with Google",
+ resultListType: UrlbarUtils.RESULT_TYPE.SEARCH,
+ searchParams: {
+ engine: "Google",
+ query: "https://",
+ },
+ },
+ {
+ search: "au",
+ autofilledValue: "autofilltrimurl.com/",
+ resultListDisplayTitle: "www.autofilltrimurl.com",
+ resultListActionText: "Visit",
+ resultListType: UrlbarUtils.RESULT_TYPE.URL,
+ finalCompleteValue: "http://www.autofilltrimurl.com/",
+ },
+ {
+ search: "http://au",
+ autofilledValue: "http://autofilltrimurl.com/",
+ resultListDisplayTitle: "www.autofilltrimurl.com",
+ resultListActionText: "Visit",
+ resultListType: UrlbarUtils.RESULT_TYPE.URL,
+ finalCompleteValue: "http://www.autofilltrimurl.com/",
+ },
+ {
+ search: "sec",
+ autofilledValue: "secureautofillurl.com/",
+ resultListDisplayTitle: "https://www.secureautofillurl.com",
+ resultListActionText: "Visit",
+ resultListType: UrlbarUtils.RESULT_TYPE.URL,
+ finalCompleteValue: "https://www.secureautofillurl.com/",
+ },
+ {
+ search: "https://sec",
+ autofilledValue: "https://secureautofillurl.com/",
+ resultListDisplayTitle: "https://www.secureautofillurl.com",
+ resultListActionText: "Visit",
+ resultListType: UrlbarUtils.RESULT_TYPE.URL,
+ finalCompleteValue: "https://www.secureautofillurl.com/",
+ },
+];
+
+add_task(async function autofill_tests() {
+ for (let test of tests) {
+ await promiseTestResult(test);
+ }
+});
+
+add_task(async function autofill_complete_domain() {
+ await promiseSearch("http://www.autofilltrimurl.com");
+ Assert.equal(
+ gURLBar.inputField.value,
+ "http://www.autofilltrimurl.com/",
+ "Should have the correct autofill value"
+ );
+
+ // Now ensure selecting from the popup correctly trims.
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 2,
+ "Should have the correct matches"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.equal(
+ gURLBar.inputField.value,
+ "www.autofilltrimurl.com/whatever",
+ "Should have applied trim correctly"
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_typed.js b/browser/components/urlbar/tests/browser/browser_autoFill_typed.js
new file mode 100644
index 0000000000..371e73c400
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_autoFill_typed.js
@@ -0,0 +1,172 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test makes sure that autofill works as expected when typing, character
+// by character.
+
+"use strict";
+
+add_setup(async function () {
+ await cleanUp();
+});
+
+add_task(async function origin() {
+ await PlacesTestUtils.addVisits(["http://example.com/"]);
+ // all lowercase
+ await typeAndCheck([
+ ["e", "example.com/"],
+ ["x", "example.com/"],
+ ["a", "example.com/"],
+ ["m", "example.com/"],
+ ["p", "example.com/"],
+ ["l", "example.com/"],
+ ["e", "example.com/"],
+ [".", "example.com/"],
+ ["c", "example.com/"],
+ ["o", "example.com/"],
+ ["m", "example.com/"],
+ ["/", "example.com/"],
+ ]);
+ gURLBar.value = "";
+ // mixed case
+ await typeAndCheck([
+ ["E", "Example.com/"],
+ ["x", "Example.com/"],
+ ["A", "ExAmple.com/"],
+ ["m", "ExAmple.com/"],
+ ["P", "ExAmPle.com/"],
+ ["L", "ExAmPLe.com/"],
+ ["e", "ExAmPLe.com/"],
+ [".", "ExAmPLe.com/"],
+ ["C", "ExAmPLe.Com/"],
+ ["o", "ExAmPLe.Com/"],
+ ["M", "ExAmPLe.CoM/"],
+ ["/", "ExAmPLe.CoM/"],
+ ]);
+ await cleanUp();
+});
+
+add_task(async function url() {
+ await PlacesTestUtils.addVisits(["http://example.com/foo/bar"]);
+ // all lowercase
+ await typeAndCheck([
+ ["e", "example.com/"],
+ ["x", "example.com/"],
+ ["a", "example.com/"],
+ ["m", "example.com/"],
+ ["p", "example.com/"],
+ ["l", "example.com/"],
+ ["e", "example.com/"],
+ [".", "example.com/"],
+ ["c", "example.com/"],
+ ["o", "example.com/"],
+ ["m", "example.com/"],
+ ["/", "example.com/"],
+ ["f", "example.com/foo/"],
+ ["o", "example.com/foo/"],
+ ["o", "example.com/foo/"],
+ ["/", "example.com/foo/"],
+ ["b", "example.com/foo/bar"],
+ ["a", "example.com/foo/bar"],
+ ["r", "example.com/foo/bar"],
+ ]);
+ gURLBar.value = "";
+ // mixed case
+ await typeAndCheck([
+ ["E", "Example.com/"],
+ ["x", "Example.com/"],
+ ["A", "ExAmple.com/"],
+ ["m", "ExAmple.com/"],
+ ["P", "ExAmPle.com/"],
+ ["L", "ExAmPLe.com/"],
+ ["e", "ExAmPLe.com/"],
+ [".", "ExAmPLe.com/"],
+ ["C", "ExAmPLe.Com/"],
+ ["o", "ExAmPLe.Com/"],
+ ["M", "ExAmPLe.CoM/"],
+ ["/", "ExAmPLe.CoM/"],
+ ["f", "ExAmPLe.CoM/foo/"],
+ ["o", "ExAmPLe.CoM/foo/"],
+ ["o", "ExAmPLe.CoM/foo/"],
+ ["/", "ExAmPLe.CoM/foo/"],
+ ["b", "ExAmPLe.CoM/foo/bar"],
+ ["a", "ExAmPLe.CoM/foo/bar"],
+ ["r", "ExAmPLe.CoM/foo/bar"],
+ ]);
+ await cleanUp();
+});
+
+add_task(async function tokenAlias() {
+ // We have built-in engine aliases that may conflict with the one we choose
+ // here in terms of autofill, so be careful and choose a weird alias.
+ await SearchTestUtils.installSearchExtension({ keyword: "@__example" });
+ // all lowercase
+ await typeAndCheck([
+ ["@", "@"],
+ ["_", "@__example "],
+ ["_", "@__example "],
+ ["e", "@__example "],
+ ["x", "@__example "],
+ ["a", "@__example "],
+ ["m", "@__example "],
+ ["p", "@__example "],
+ ["l", "@__example "],
+ ["e", "@__example "],
+ ]);
+ gURLBar.value = "";
+ // mixed case
+ await typeAndCheck([
+ ["@", "@"],
+ ["_", "@__example "],
+ ["_", "@__example "],
+ ["E", "@__Example "],
+ ["x", "@__Example "],
+ ["A", "@__ExAmple "],
+ ["m", "@__ExAmple "],
+ ["P", "@__ExAmPle "],
+ ["L", "@__ExAmPLe "],
+ ["e", "@__ExAmPLe "],
+ ]);
+ await cleanUp();
+});
+
+async function typeAndCheck(values) {
+ gURLBar.focus();
+ for (let i = 0; i < values.length; i++) {
+ let [char, expectedInputValue] = values[i];
+ info(
+ `Typing: i=${i} char=${char} ` +
+ `substring="${expectedInputValue.substring(0, i + 1)}"`
+ );
+ EventUtils.synthesizeKey(char);
+ if (i == 0 && char == "@") {
+ // A single "@" doesn't trigger autofill, so skip the checks below. (It
+ // shows all the @ aliases.)
+ continue;
+ }
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ let restIsSpaces = !expectedInputValue.substring(i + 1).trim();
+ Assert.equal(gURLBar.value, expectedInputValue);
+ Assert.equal(gURLBar.selectionStart, i + 1);
+ Assert.equal(gURLBar.selectionEnd, expectedInputValue.length);
+ if (restIsSpaces) {
+ // Autofilled @ aliases have a trailing space. We should check that the
+ // space is autofilled when each preceding character is typed, but once
+ // the final non-space char is typed, autofill actually stops and the
+ // trailing space is not autofilled. (Which is maybe not the way it
+ // should work...) Skip the check below.
+ continue;
+ }
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ }
+}
+
+async function cleanUp() {
+ gURLBar.value = "";
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeKey("KEY_Escape");
+ });
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+}
diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_undo.js b/browser/components/urlbar/tests/browser/browser_autoFill_undo.js
new file mode 100644
index 0000000000..c233da80f2
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_autoFill_undo.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test checks the behavior of text undo (Ctrl-Z, cmd_undo) in regard to
+// autofill.
+
+"use strict";
+
+add_task(async function test() {
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+ await PlacesTestUtils.addVisits(["http://example.com/"]);
+
+ // Search for "ex". It should autofill to example.com/.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "ex",
+ fireInputEvent: true,
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(gURLBar.value, "example.com/");
+ Assert.equal(gURLBar.selectionStart, "ex".length);
+ Assert.equal(gURLBar.selectionEnd, "example.com/".length);
+
+ // Type an x.
+ EventUtils.synthesizeKey("x");
+
+ // Nothing should have been autofilled.
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(!details.autofill);
+ Assert.equal(gURLBar.value, "exx");
+ Assert.equal(gURLBar.selectionStart, "exx".length);
+ Assert.equal(gURLBar.selectionEnd, "exx".length);
+
+ // Undo the typed x.
+ goDoCommand("cmd_undo");
+
+ // The text should be restored to ex[ample.com/] (with the part in brackets
+ // autofilled and selected).
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(gURLBar.value, "example.com/");
+ Assert.ok(!details.autofill, "Autofill should not be set.");
+ Assert.equal(gURLBar.selectionStart, "ex".length);
+ Assert.equal(gURLBar.selectionEnd, "example.com/".length);
+
+ await PlacesUtils.history.clear();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_autoOpen.js b/browser/components/urlbar/tests/browser/browser_autoOpen.js
new file mode 100644
index 0000000000..8ed7e8e402
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_autoOpen.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+async function checkOpensOnFocus(win = window) {
+ // The view should not open when the input is focused programmatically.
+ win.gURLBar.blur();
+ win.gURLBar.focus();
+ Assert.ok(!win.gURLBar.view.isOpen, "check urlbar panel is not open");
+ win.gURLBar.blur();
+
+ // Check the keyboard shortcut.
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ win.document.getElementById("Browser:OpenLocation").doCommand();
+ });
+ await UrlbarTestUtils.promisePopupClose(win, () => {
+ win.gURLBar.blur();
+ });
+
+ // Focus with the mouse.
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ EventUtils.synthesizeMouseAtCenter(win.gURLBar.inputField, {}, win);
+ });
+ await UrlbarTestUtils.promisePopupClose(win, () => {
+ win.gURLBar.blur();
+ });
+}
+
+add_setup(async function () {
+ // Add some history for the empty panel.
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://mochi.test:8888/",
+ transition: PlacesUtils.history.TRANSITIONS.TYPED,
+ },
+ ]);
+ registerCleanupFunction(() => PlacesUtils.history.clear());
+});
+
+add_task(async function test() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async browser => {
+ await checkOpensOnFocus();
+ }
+ );
+});
+
+add_task(async function newtabAndHome() {
+ for (let url of ["about:newtab", "about:home"]) {
+ // withNewTab randomly hangs on these pages when waitForLoad = true (the
+ // default), so pass false.
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url, waitForLoad: false },
+ async browser => {
+ // We don't wait for load, but we must ensure to be on the expected url.
+ await TestUtils.waitForCondition(
+ () => gBrowser.currentURI.spec == url,
+ "Ensure we're on the expected page"
+ );
+ await checkOpensOnFocus();
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "http://example.com/" },
+ async otherBrowser => {
+ await checkOpensOnFocus();
+ // Switch back to about:newtab/home.
+ await BrowserTestUtils.switchTab(
+ gBrowser,
+ gBrowser.getTabForBrowser(browser)
+ );
+ await checkOpensOnFocus();
+ // Switch back to example.com.
+ await BrowserTestUtils.switchTab(
+ gBrowser,
+ gBrowser.getTabForBrowser(otherBrowser)
+ );
+ await checkOpensOnFocus();
+ }
+ );
+ // After example.com closes, about:newtab/home is selected again.
+ await checkOpensOnFocus();
+ // Load example.com in the same tab.
+ BrowserTestUtils.loadURIString(
+ gBrowser.selectedBrowser,
+ "http://example.com/"
+ );
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await checkOpensOnFocus();
+ }
+ );
+ }
+});
diff --git a/browser/components/urlbar/tests/browser/browser_autocomplete_a11y_label.js b/browser/components/urlbar/tests/browser/browser_autocomplete_a11y_label.js
new file mode 100644
index 0000000000..ead026244e
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_autocomplete_a11y_label.js
@@ -0,0 +1,185 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This test ensures that we produce good labels for a11y purposes.
+ */
+
+const { CommonUtils } = ChromeUtils.importESModule(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Common.sys.mjs"
+);
+
+const SUGGEST_ALL_PREF = "browser.search.suggest.enabled";
+const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches";
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+
+let accService;
+
+async function getResultText(element, expectedValue, description = "") {
+ await BrowserTestUtils.waitForCondition(
+ () => {
+ let accessible = accService.getAccessibleFor(element);
+ return accessible !== null && accessible.name === expectedValue;
+ },
+ description,
+ 200
+ );
+}
+
+/**
+ * Initializes the accessibility service and registers a cleanup function to
+ * shut it down. If it's not shut down properly, it can crash the current tab
+ * and cause the test to fail, especially in verify mode.
+ *
+ * This function is adapted from from tests in accessible/tests/browser and its
+ * helper functions are adapted or copied from functions of the same names in
+ * the same directory.
+ */
+async function initAccessibilityService() {
+ const [a11yInitObserver, a11yInit] = initAccService();
+ await a11yInitObserver;
+ accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+ await a11yInit;
+
+ registerCleanupFunction(async () => {
+ const [a11yShutdownObserver, a11yShutdownPromise] = shutdownAccService();
+ await a11yShutdownObserver;
+ accService = null;
+ forceGC();
+ await a11yShutdownPromise;
+ });
+}
+
+// Adapted from `initAccService()` in accessible/tests/browser/head.js
+function initAccService() {
+ return [
+ CommonUtils.addAccServiceInitializedObserver(),
+ CommonUtils.observeAccServiceInitialized(),
+ ];
+}
+
+// Adapted from `shutdownAccService()` in accessible/tests/browser/head.js
+function shutdownAccService() {
+ return [
+ CommonUtils.addAccServiceShutdownObserver(),
+ CommonUtils.observeAccServiceShutdown(),
+ ];
+}
+
+// Copied from accessible/tests/browser/shared-head.js
+function forceGC() {
+ SpecialPowers.gc();
+ SpecialPowers.forceShrinkingGC();
+ SpecialPowers.forceCC();
+ SpecialPowers.gc();
+ SpecialPowers.forceShrinkingGC();
+ SpecialPowers.forceCC();
+}
+
+add_setup(async function () {
+ await initAccessibilityService();
+});
+
+add_task(async function switchToTab() {
+ let tab = BrowserTestUtils.addTab(gBrowser, "about:robots");
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "% robots",
+ });
+
+ let index = 0;
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index);
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ "Should have a switch tab result"
+ );
+
+ let element = await UrlbarTestUtils.waitForAutocompleteResultAt(
+ window,
+ index
+ );
+ // The a11y text will include the "Firefox Suggest" pseudo-element label shown
+ // before the result.
+ await getResultText(
+ element._content,
+ "Firefox Suggest about:robots — Switch to Tab",
+ "Result a11y text is correct"
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ gURLBar.handleRevert();
+ gBrowser.removeTab(tab);
+});
+
+add_task(async function searchSuggestions() {
+ await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME,
+ setAsDefault: true,
+ });
+ Services.prefs.setBoolPref(SUGGEST_ALL_PREF, true);
+ let suggestionsEnabled = Services.prefs.getBoolPref(SUGGEST_URLBAR_PREF);
+ Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, true);
+ registerCleanupFunction(async function () {
+ Services.prefs.clearUserPref(SUGGEST_ALL_PREF);
+ Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, suggestionsEnabled);
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ });
+ let length = await UrlbarTestUtils.getResultCount(window);
+ // Don't assume that the search doesn't match history or bookmarks left around
+ // by earlier tests.
+ Assert.greaterOrEqual(
+ length,
+ 3,
+ "Should get at least heuristic result + two search suggestions"
+ );
+ // The first expected search is the search term itself since the heuristic
+ // result will come before the search suggestions.
+ let searchTerm = "foo";
+ let expectedSearches = [searchTerm, "foofoo", "foobar"];
+ for (let i = 0; i < length; i++) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ if (result.type === UrlbarUtils.RESULT_TYPE.SEARCH) {
+ Assert.greaterOrEqual(
+ expectedSearches.length,
+ 0,
+ "Should still have expected searches remaining"
+ );
+
+ let element = await UrlbarTestUtils.waitForAutocompleteResultAt(
+ window,
+ i
+ );
+
+ // Select the row so we see the expanded text.
+ gURLBar.view.selectedRowIndex = i;
+
+ if (result.searchParams.inPrivateWindow) {
+ await getResultText(
+ element._content,
+ searchTerm + " — Search in a Private Window",
+ "Check result label for search in private window"
+ );
+ } else {
+ let suggestion = expectedSearches.shift();
+ await getResultText(
+ element._content,
+ suggestion +
+ " — Search with browser_searchSuggestionEngine searchSuggestionEngine.xml",
+ "Check result label for non-private search"
+ );
+ }
+ }
+ }
+ Assert.ok(!expectedSearches.length);
+
+ await UrlbarTestUtils.promisePopupClose(window);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_autocomplete_autoselect.js b/browser/components/urlbar/tests/browser/browser_autocomplete_autoselect.js
new file mode 100644
index 0000000000..ef3da56ef0
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_autocomplete_autoselect.js
@@ -0,0 +1,122 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that the first item is correctly autoselected and some navigation
+ * around the results list.
+ */
+
+function repeat(limit, func) {
+ for (let i = 0; i < limit; i++) {
+ func(i);
+ }
+}
+
+function assertSelected(index) {
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ index,
+ "Should have selected the correct item"
+ );
+
+ // This is true because although both the listbox and the one-offs can have
+ // selections, the test doesn't check that.
+ Assert.equal(
+ UrlbarTestUtils.getOneOffSearchButtons(window).selectedButton,
+ null,
+ "A result is selected, so the one-offs should not have a selection"
+ );
+}
+
+function assertSelected_one_off(index) {
+ Assert.equal(
+ UrlbarTestUtils.getOneOffSearchButtons(window).selectedButtonIndex,
+ index,
+ "Expected one-off button should be selected"
+ );
+
+ // This is true because although both the listbox and the one-offs can have
+ // selections, the test doesn't check that.
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ -1,
+ "A one-off is selected, so the listbox should not have a selection"
+ );
+}
+
+add_task(async function () {
+ let maxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults");
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:mozilla"
+ );
+ registerCleanupFunction(async function () {
+ await PlacesUtils.history.clear();
+ BrowserTestUtils.removeTab(tab);
+ });
+
+ let visits = [];
+ repeat(maxResults, i => {
+ visits.push({
+ uri: makeURI("http://example.com/autocomplete/?" + i),
+ });
+ });
+ await PlacesTestUtils.addVisits(visits);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "example.com/autocomplete",
+ fireInputEvent: true,
+ });
+
+ let resultCount = await UrlbarTestUtils.getResultCount(window);
+
+ Assert.equal(
+ resultCount,
+ maxResults,
+ "Should get the expected amount of results"
+ );
+ assertSelected(0);
+
+ info("Key Down to select the next item");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ assertSelected(1);
+
+ info("Key Down maxResults-1 times should select the first one-off");
+ repeat(maxResults - 1, () => EventUtils.synthesizeKey("KEY_ArrowDown"));
+ assertSelected_one_off(0);
+
+ info("Key Down numButtons-1 should select the last one-off");
+ let numButtons =
+ UrlbarTestUtils.getOneOffSearchButtons(window).getSelectableButtons(
+ true
+ ).length;
+ repeat(numButtons - 1, () => EventUtils.synthesizeKey("KEY_ArrowDown"));
+ assertSelected_one_off(numButtons - 1);
+
+ info("Key Down twice more should select the second result");
+ repeat(2, () => EventUtils.synthesizeKey("KEY_ArrowDown"));
+ assertSelected(1);
+
+ info("Key Down maxResults + numButtons times should wrap around");
+ repeat(maxResults + numButtons, () =>
+ EventUtils.synthesizeKey("KEY_ArrowDown")
+ );
+ assertSelected(1);
+
+ info("Key Up maxResults + numButtons times should wrap around the other way");
+ repeat(maxResults + numButtons, () =>
+ EventUtils.synthesizeKey("KEY_ArrowUp")
+ );
+ assertSelected(1);
+
+ info("Page Up will go up the list, but not wrap");
+ EventUtils.synthesizeKey("KEY_PageUp");
+ assertSelected(0);
+
+ info("Page Up again will wrap around to the end of the list");
+ EventUtils.synthesizeKey("KEY_PageUp");
+ assertSelected(maxResults - 1);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_autocomplete_cursor.js b/browser/components/urlbar/tests/browser/browser_autocomplete_cursor.js
new file mode 100644
index 0000000000..5e0081a92c
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_autocomplete_cursor.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests the cursor remains in the right place when a new window is opened.
+ */
+
+add_task(async function test_windowSwitch() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:mozilla"
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "www.mozilla.org",
+ });
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
+
+ gURLBar.focus();
+ gURLBar.inputField.setSelectionRange(4, 4);
+
+ let newWindow = await BrowserTestUtils.openNewBrowserWindow();
+
+ await BrowserTestUtils.closeWindow(newWindow);
+
+ Assert.equal(
+ document.activeElement,
+ gURLBar.inputField,
+ "URL Bar should be focused"
+ );
+ Assert.equal(gURLBar.selectionStart, 4, "Should not have moved the cursor");
+ Assert.equal(gURLBar.selectionEnd, 4, "Should not have selected anything");
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_autocomplete_edit_completed.js b/browser/components/urlbar/tests/browser/browser_autocomplete_edit_completed.js
new file mode 100644
index 0000000000..c17949eb9e
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_autocomplete_edit_completed.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests selecting a result, and editing the value of that autocompleted result.
+ */
+
+add_task(async function () {
+ await PlacesUtils.history.clear();
+
+ await PlacesTestUtils.addVisits([
+ { uri: makeURI("http://example.com/foo") },
+ { uri: makeURI("http://example.com/foo/bar") },
+ ]);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+ registerCleanupFunction(async function () {
+ BrowserTestUtils.removeTab(tab);
+ await PlacesUtils.history.clear();
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "http://example.com",
+ });
+
+ const initialIndex = UrlbarTestUtils.getSelectedRowIndex(window);
+
+ info("Key Down to select the next item.");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+
+ let nextIndex = initialIndex + 1;
+ let nextResult = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ nextIndex
+ );
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ nextIndex,
+ "Should have selected the next item"
+ );
+ Assert.equal(
+ gURLBar.untrimmedValue,
+ nextResult.url,
+ "Should have completed the URL"
+ );
+
+ info("Press backspace");
+ EventUtils.synthesizeKey("KEY_Backspace");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ let editedValue = gURLBar.value;
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ initialIndex,
+ "Should have selected the initialIndex again"
+ );
+ Assert.notEqual(editedValue, nextResult.url, "The URL has changed.");
+
+ let docLoad = BrowserTestUtils.waitForDocLoadAndStopIt(
+ "http://" + editedValue,
+ gBrowser.selectedBrowser
+ );
+
+ info("Press return to load edited URL.");
+
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeKey("KEY_Enter");
+ });
+
+ await docLoad;
+});
diff --git a/browser/components/urlbar/tests/browser/browser_autocomplete_enter_race.js b/browser/components/urlbar/tests/browser/browser_autocomplete_enter_race.js
new file mode 100644
index 0000000000..a684c60e5b
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_autocomplete_enter_race.js
@@ -0,0 +1,193 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests what happens when the enter key is pressed quickly after entering text.
+ */
+
+// The order of these tests matters!
+
+add_setup(async function () {
+ let bm = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.com/?q=%s",
+ title: "test",
+ });
+ registerCleanupFunction(async function () {
+ await PlacesUtils.bookmarks.remove(bm);
+ await PlacesUtils.history.clear();
+ });
+ // Needs at least one success.
+ ok(true, "Setup complete");
+});
+
+add_task(
+ taskWithNewTab(async function test_loadSite() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.autofill", false]],
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "example.co",
+ });
+ gURLBar.focus();
+ EventUtils.sendString("m");
+ EventUtils.synthesizeKey("KEY_Enter");
+ info("wait for the page to load");
+ await BrowserTestUtils.browserLoaded(
+ gBrowser.selectedTab.linkedBrowser,
+ false,
+ "http://example.com/"
+ );
+ await SpecialPowers.popPrefEnv();
+ })
+);
+
+add_task(
+ taskWithNewTab(async function test_sametext() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "example.com",
+ fireInputEvent: true,
+ });
+
+ // Simulate re-entering the same text searched the last time. This may happen
+ // through a copy paste, but clipboard handling is not much reliable, so just
+ // fire an input event.
+ info("synthesize input event");
+ let event = document.createEvent("Events");
+ event.initEvent("input", true, true);
+ gURLBar.inputField.dispatchEvent(event);
+ EventUtils.synthesizeKey("KEY_Enter");
+
+ info("wait for the page to load");
+ await BrowserTestUtils.browserLoaded(
+ gBrowser.selectedTab.linkedBrowser,
+ false,
+ "http://example.com/"
+ );
+ })
+);
+
+add_task(
+ taskWithNewTab(async function test_after_empty_search() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ gURLBar.focus();
+ gURLBar.value = "e";
+ EventUtils.synthesizeKey("x");
+ EventUtils.synthesizeKey("KEY_Enter");
+
+ info("wait for the page to load");
+ await BrowserTestUtils.browserLoaded(
+ gBrowser.selectedTab.linkedBrowser,
+ false,
+ "http://example.com/"
+ );
+ })
+);
+
+add_task(
+ taskWithNewTab(async function test_disabled_ac() {
+ // Disable autocomplete.
+ let suggestHistory = Preferences.get("browser.urlbar.suggest.history");
+ Preferences.set("browser.urlbar.suggest.history", false);
+ let suggestBookmarks = Preferences.get("browser.urlbar.suggest.bookmark");
+ Preferences.set("browser.urlbar.suggest.bookmark", false);
+ let suggestOpenPages = Preferences.get("browser.urlbar.suggest.openpage");
+ Preferences.set("browser.urlbar.suggest.openpage", false);
+
+ await SearchTestUtils.installSearchExtension();
+
+ let engine = Services.search.getEngineByName("Example");
+ let originalEngine = await Services.search.getDefault();
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+
+ async function cleanup() {
+ Preferences.set("browser.urlbar.suggest.history", suggestHistory);
+ Preferences.set("browser.urlbar.suggest.bookmark", suggestBookmarks);
+ Preferences.set("browser.urlbar.suggest.openpage", suggestOpenPages);
+
+ await Services.search.setDefault(
+ originalEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ }
+ registerCleanupFunction(cleanup);
+
+ gURLBar.focus();
+ gURLBar.value = "e";
+ EventUtils.sendString("x");
+ EventUtils.synthesizeKey("KEY_Enter");
+
+ info("wait for the page to load");
+ await BrowserTestUtils.browserLoaded(
+ gBrowser.selectedTab.linkedBrowser,
+ false,
+ "https://example.com/?q=ex"
+ );
+ await cleanup();
+ })
+);
+
+// Tests that setting a high value for browser.urlbar.delay does not delay the
+// fetching of heuristic results.
+add_task(
+ taskWithNewTab(async function test_delay() {
+ // This is needed to clear the current value, otherwise autocomplete may think
+ // the user removed text from the end.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ // Set a large delay.
+ const TIMEOUT = 3000;
+ let delay = UrlbarPrefs.get("delay");
+ UrlbarPrefs.set("delay", TIMEOUT);
+ registerCleanupFunction(function () {
+ UrlbarPrefs.set("delay", delay);
+ });
+
+ gURLBar.focus();
+ gURLBar.value = "e";
+ let recievedResult = new Promise(resolve => {
+ gURLBar.controller.addQueryListener({
+ onQueryResults(queryContext) {
+ gURLBar.controller.removeQueryListener(this);
+ Assert.ok(
+ queryContext.heuristicResult,
+ "Recieved a heuristic result."
+ );
+ Assert.equal(
+ queryContext.searchString,
+ "ex",
+ "The heuristic result is based on the correct search string."
+ );
+ resolve();
+ },
+ });
+ });
+ let start = Cu.now();
+ EventUtils.sendString("x");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await recievedResult;
+ Assert.ok(Cu.now() - start < TIMEOUT);
+ })
+);
+
+// The main reason for running each test task in a new tab that's closed when
+// the task finishes is to avoid switch-to-tab results.
+function taskWithNewTab(fn) {
+ return async function () {
+ await BrowserTestUtils.withNewTab("about:blank", fn);
+ };
+}
diff --git a/browser/components/urlbar/tests/browser/browser_autocomplete_no_title.js b/browser/components/urlbar/tests/browser/browser_autocomplete_no_title.js
new file mode 100644
index 0000000000..fa30a7608a
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_autocomplete_no_title.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test ensures that we display just the domain name when a URL result doesn't
+ * have a title.
+ */
+
+add_task(async function () {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:mozilla"
+ );
+ await PlacesUtils.history.clear();
+ const uri = "http://bug1060642.example.com/beards/are/pretty/great";
+ await PlacesTestUtils.addVisits([{ uri, title: "" }]);
+ registerCleanupFunction(async function () {
+ await PlacesUtils.history.clear();
+ BrowserTestUtils.removeTab(tab);
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "bug1060642",
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(
+ result.displayed.title,
+ "bug1060642.example.com",
+ "Result title should be as expected"
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_autocomplete_readline_navigation.js b/browser/components/urlbar/tests/browser/browser_autocomplete_readline_navigation.js
new file mode 100644
index 0000000000..36f990503e
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_autocomplete_readline_navigation.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests navigation between results using ctrl-n/p.
+ */
+
+function repeat(limit, func) {
+ for (let i = 0; i < limit; i++) {
+ func(i);
+ }
+}
+
+function assertSelected(index) {
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ index,
+ "Should have the correct item selected"
+ );
+
+ // This is true because although both the listbox and the one-offs can have
+ // selections, the test doesn't check that.
+ Assert.equal(
+ UrlbarTestUtils.getOneOffSearchButtons(window).selectedButton,
+ null,
+ "A result is selected, so the one-offs should not have a selection"
+ );
+}
+
+add_task(async function () {
+ let maxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults");
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:mozilla"
+ );
+ registerCleanupFunction(async function () {
+ await PlacesUtils.history.clear();
+ BrowserTestUtils.removeTab(tab);
+ });
+
+ let visits = [];
+ repeat(maxResults, i => {
+ visits.push({
+ uri: makeURI("http://example.com/autocomplete/?" + i),
+ });
+ });
+ await PlacesTestUtils.addVisits(visits);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "example.com/autocomplete",
+ });
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, maxResults - 1);
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ maxResults,
+ "Should get maxResults=" + maxResults + " results"
+ );
+ assertSelected(0);
+
+ info("Ctrl-n to select the next item");
+ EventUtils.synthesizeKey("n", { ctrlKey: true });
+ assertSelected(1);
+
+ info("Ctrl-p to select the previous item");
+ EventUtils.synthesizeKey("p", { ctrlKey: true });
+ assertSelected(0);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_autocomplete_tag_star_visibility.js b/browser/components/urlbar/tests/browser/browser_autocomplete_tag_star_visibility.js
new file mode 100644
index 0000000000..10e26f6f71
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_autocomplete_tag_star_visibility.js
@@ -0,0 +1,167 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests for the bookmark star being correct displayed for results matching
+ * tags.
+ */
+
+add_task(async function () {
+ registerCleanupFunction(async function () {
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+
+ async function addTagItem(tagName) {
+ let url = `http://example.com/this/is/tagged/${tagName}`;
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url,
+ title: `test ${tagName}`,
+ });
+ PlacesUtils.tagging.tagURI(Services.io.newURI(url), [tagName]);
+ await PlacesTestUtils.addVisits({
+ uri: url,
+ title: `Test page with tag ${tagName}`,
+ });
+ }
+
+ // We use different tags for each part of the test, as otherwise the
+ // autocomplete code tries to be smart by using the previously cached element
+ // without updating it (since all parameters it knows about are the same).
+
+ let testcases = [
+ {
+ description: "Test with suggest.bookmark=true",
+ tagName: "tagtest1",
+ prefs: {
+ "suggest.bookmark": true,
+ },
+ input: "tagtest1",
+ expected: {
+ typeImageVisible: true,
+ },
+ },
+ {
+ description: "Test with suggest.bookmark=false",
+ tagName: "tagtest2",
+ prefs: {
+ "suggest.bookmark": false,
+ },
+ input: "tagtest2",
+ expected: {
+ typeImageVisible: false,
+ },
+ },
+ {
+ description: "Test with suggest.bookmark=true (again)",
+ tagName: "tagtest3",
+ prefs: {
+ "suggest.bookmark": true,
+ },
+ input: "tagtest3",
+ expected: {
+ typeImageVisible: true,
+ },
+ },
+ {
+ description: "Test with bookmark restriction token",
+ tagName: "tagtest4",
+ prefs: {
+ "suggest.bookmark": true,
+ },
+ input: "* tagtest4",
+ expected: {
+ typeImageVisible: true,
+ },
+ },
+ {
+ description: "Test with history restriction token",
+ tagName: "tagtest5",
+ prefs: {
+ "suggest.bookmark": true,
+ },
+ input: "^ tagtest5",
+ expected: {
+ typeImageVisible: false,
+ },
+ },
+ {
+ description: "Test partial tag and casing",
+ tagName: "tagtest6",
+ prefs: {
+ "suggest.bookmark": true,
+ },
+ input: "TeSt6",
+ expected: {
+ typeImageVisible: true,
+ },
+ },
+ ];
+
+ for (let testcase of testcases) {
+ info(`Test case: ${testcase.description}`);
+
+ await addTagItem(testcase.tagName);
+ for (let prefName of Object.keys(testcase.prefs)) {
+ Services.prefs.setBoolPref(
+ `browser.urlbar.${prefName}`,
+ testcase.prefs[prefName]
+ );
+ }
+
+ let context = await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: testcase.input,
+ });
+
+ // If testcase.input triggers local search mode, there won't be a heuristic.
+ let resultIndex =
+ context.searchMode && !context.searchMode.engineName ? 0 : 1;
+
+ Assert.greaterOrEqual(
+ UrlbarTestUtils.getResultCount(window),
+ resultIndex + 1,
+ `Should be at least ${resultIndex + 1} results`
+ );
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ resultIndex
+ );
+
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.URL,
+ "Should have a URL result type"
+ );
+ // The Quantum Bar differs from the legacy urlbar in the fact that, if
+ // bookmarks are filtered out, it won't show tags for history results.
+ let expected_tags = !testcase.expected.typeImageVisible
+ ? []
+ : [testcase.tagName];
+ Assert.deepEqual(
+ result.tags,
+ expected_tags,
+ "Should have the expected tag"
+ );
+
+ if (testcase.expected.typeImageVisible) {
+ Assert.equal(
+ result.displayed.typeIcon,
+ 'url("chrome://browser/skin/bookmark-12.svg")',
+ "Should have the star image displayed or not as expected"
+ );
+ } else {
+ Assert.equal(
+ result.displayed.typeIcon,
+ "none",
+ "Should have the star image displayed or not as expected"
+ );
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ gURLBar.handleRevert();
+ }
+});
diff --git a/browser/components/urlbar/tests/browser/browser_bestMatch.js b/browser/components/urlbar/tests/browser/browser_bestMatch.js
new file mode 100644
index 0000000000..1e384e389f
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_bestMatch.js
@@ -0,0 +1,229 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests best match rows in the view. See also:
+//
+// browser_quicksuggest_bestMatch.js
+// UI test for quick suggest best matches specifically
+// test_quicksuggest_bestMatch.js
+// Tests triggering quick suggest best matches and things that don't depend on
+// the view
+
+"use strict";
+
+// Tests a non-sponsored best match row.
+add_task(async function nonsponsored() {
+ let result = makeBestMatchResult();
+ await withProvider(result, async () => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+ await checkBestMatchRow({ result });
+ await UrlbarTestUtils.promisePopupClose(window);
+ });
+});
+
+// Tests a non-sponsored best match row with a help button.
+add_task(async function nonsponsoredHelpButton() {
+ let result = makeBestMatchResult({ helpUrl: "https://example.com/help" });
+ await withProvider(result, async () => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+ await checkBestMatchRow({ result, hasHelpUrl: true });
+ await UrlbarTestUtils.promisePopupClose(window);
+ });
+});
+
+// Tests a sponsored best match row.
+add_task(async function sponsored() {
+ let result = makeBestMatchResult({ isSponsored: true });
+ await withProvider(result, async () => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+ await checkBestMatchRow({ result, isSponsored: true });
+ await UrlbarTestUtils.promisePopupClose(window);
+ });
+});
+
+// Tests a sponsored best match row with a help button.
+add_task(async function sponsoredHelpButton() {
+ let result = makeBestMatchResult({
+ isSponsored: true,
+ helpUrl: "https://example.com/help",
+ });
+ await withProvider(result, async () => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+ await checkBestMatchRow({ result, isSponsored: true, hasHelpUrl: true });
+ await UrlbarTestUtils.promisePopupClose(window);
+ });
+});
+
+// Tests keyboard selection.
+add_task(async function keySelection() {
+ let result = makeBestMatchResult({
+ isSponsored: true,
+ helpUrl: "https://example.com/help",
+ });
+
+ await withProvider(result, async () => {
+ // Ordered list of class names of the elements that should be selected.
+ let expectedClassNames = [
+ "urlbarView-row-inner",
+ UrlbarPrefs.get("resultMenu")
+ ? "urlbarView-button-menu"
+ : "urlbarView-button-help",
+ ];
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+ await checkBestMatchRow({
+ result,
+ isSponsored: true,
+ hasHelpUrl: true,
+ });
+
+ // Test with the tab key in order vs. reverse order.
+ for (let reverse of [false, true]) {
+ info("Doing TAB key selection: " + JSON.stringify({ reverse }));
+
+ let classNames = [...expectedClassNames];
+ if (reverse) {
+ classNames.reverse();
+ }
+
+ let sendKey = () => {
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: reverse });
+ };
+
+ // Move selection through each expected element.
+ for (let className of classNames) {
+ info("Expecting selection: " + className);
+ sendKey();
+ Assert.ok(gURLBar.view.isOpen, "View remains open");
+ let { selectedElement } = gURLBar.view;
+ Assert.ok(selectedElement, "Selected element exists");
+ Assert.ok(
+ selectedElement.classList.contains(className),
+ "Expected element is selected"
+ );
+ }
+ sendKey();
+ Assert.ok(
+ gURLBar.view.isOpen,
+ "View remains open after keying through best match row"
+ );
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ });
+});
+
+async function checkBestMatchRow({
+ result,
+ isSponsored = false,
+ hasHelpUrl = false,
+}) {
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 1,
+ "One result is present"
+ );
+
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ let { row } = details.element;
+
+ Assert.equal(row.getAttribute("type"), "bestmatch", "row[type] is bestmatch");
+
+ let favicon = row._elements.get("favicon");
+ Assert.ok(favicon, "Row has a favicon");
+
+ let title = row._elements.get("title");
+ Assert.ok(title, "Row has a title");
+ Assert.ok(title.textContent, "Row title has non-empty textContext");
+ Assert.equal(title.textContent, result.payload.title, "Row title is correct");
+
+ let url = row._elements.get("url");
+ Assert.ok(url, "Row has a URL");
+ Assert.ok(url.textContent, "Row URL has non-empty textContext");
+ Assert.equal(
+ url.textContent,
+ result.payload.displayUrl,
+ "Row URL is correct"
+ );
+
+ let bottom = row._elements.get("bottom");
+ Assert.ok(bottom, "Row has a bottom");
+ Assert.equal(
+ !!result.payload.isSponsored,
+ isSponsored,
+ "Sanity check: Row's expected isSponsored matches result's"
+ );
+ if (isSponsored) {
+ Assert.equal(
+ bottom.textContent,
+ "Sponsored",
+ "Sponsored row bottom has Sponsored textContext"
+ );
+ } else {
+ Assert.equal(
+ bottom.textContent,
+ "",
+ "Non-sponsored row bottom has empty textContext"
+ );
+ }
+
+ let button = row._buttons.get(
+ UrlbarPrefs.get("resultMenu") ? "menu" : "help"
+ );
+ Assert.equal(
+ !!result.payload.helpUrl,
+ hasHelpUrl,
+ "Sanity check: Row's expected hasHelpUrl matches result"
+ );
+ if (hasHelpUrl) {
+ Assert.ok(button, "Row with helpUrl has a help or menu button");
+ } else {
+ Assert.ok(
+ !button,
+ "Row without helpUrl does not have a help or menu button"
+ );
+ }
+}
+
+async function withProvider(result, callback) {
+ let provider = new UrlbarTestUtils.TestProvider({
+ results: [result],
+ priority: Infinity,
+ });
+ UrlbarProvidersManager.registerProvider(provider);
+ try {
+ await callback();
+ } finally {
+ UrlbarProvidersManager.unregisterProvider(provider);
+ }
+}
+
+function makeBestMatchResult(payloadExtra = {}) {
+ return Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ ...UrlbarResult.payloadAndSimpleHighlights([], {
+ title: "Test best match",
+ url: "https://example.com/best-match",
+ ...payloadExtra,
+ })
+ ),
+ { isBestMatch: true }
+ );
+}
diff --git a/browser/components/urlbar/tests/browser/browser_blanking.js b/browser/components/urlbar/tests/browser/browser_blanking.js
new file mode 100644
index 0000000000..eabaa2575d
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_blanking.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URL = `${TEST_BASE_URL}file_blank_but_not_blank.html`;
+
+add_task(async function () {
+ for (let page of gInitialPages) {
+ if (page == "about:newtab") {
+ // New tab preloading makes this a pain to test, so skip
+ continue;
+ }
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, page);
+ ok(
+ !gURLBar.value,
+ "The URL bar should be empty if we load a plain " + page + " page."
+ );
+ BrowserTestUtils.removeTab(tab);
+ }
+});
+
+add_task(async function () {
+ // The test was originally to check that reloading of a javascript: URL could
+ // throw an error and empty the URL bar. This situation can no longer happen
+ // as in bug 836567 we set document.URL to active document's URL on navigation
+ // to a javascript: URL; reloading after that will simply reload the original
+ // active document rather than the javascript: URL itself. But we can still
+ // verify that the URL bar's value is correct.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+ is(gURLBar.value, TEST_URL, "The URL bar should match the URI");
+ let browserLoaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ SpecialPowers.spawn(tab.linkedBrowser, [], function () {
+ content.document.querySelector("a").click();
+ });
+ await browserLoaded;
+ is(
+ gURLBar.value,
+ TEST_URL,
+ "The URL bar should be the previous active document's URI."
+ );
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ // This is sync, so by the time we return we should have changed the URL bar.
+ content.location.reload();
+ }).catch(e => {
+ // Ignore expected exception.
+ });
+ is(
+ gURLBar.value,
+ TEST_URL,
+ "The URL bar should still be the previous active document's URI."
+ );
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_bufferer_onQueryResults.js b/browser/components/urlbar/tests/browser/browser_bufferer_onQueryResults.js
new file mode 100644
index 0000000000..7325d44b2c
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_bufferer_onQueryResults.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This test covers a race condition of input events followed by Enter.
+// The test is putting the event bufferer in a situation where a new query has
+// already results in the context object, but onQueryResults has not been
+// invoked yet. The EventBufferer should wait for onQueryResults to proceed,
+// otherwise the view cannot yet contain the updated query string and we may
+// end up searching for a partial string.
+
+add_setup(async function () {
+ sandbox = sinon.createSandbox();
+ await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + "searchSuggestionEngine.xml",
+ setAsDefault: true,
+ });
+ // To reproduce the race condition it's important to disable any provider
+ // having `deferUserSelection` == true;
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.engines", false]],
+ });
+ await PlacesUtils.history.clear();
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ sandbox.restore();
+ });
+});
+
+add_task(async function test() {
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "about:robots",
+ });
+
+ let defer = PromiseUtils.defer();
+ let waitFirstSearchResults = PromiseUtils.defer();
+ let count = 0;
+ let original = gURLBar.controller.notify;
+ sandbox.stub(gURLBar.controller, "notify").callsFake(async (msg, context) => {
+ if (context?.deferUserSelectionProviders.size) {
+ Assert.ok(false, "Any provider deferring selection should be disabled");
+ }
+ if (msg == "onQueryResults") {
+ waitFirstSearchResults.resolve();
+ count++;
+ }
+ // Delay any events after the second onQueryResults call.
+ if (count >= 2) {
+ await defer.promise;
+ }
+ return original.call(gURLBar.controller, msg, context);
+ });
+
+ gURLBar.focus();
+ gURLBar.select();
+ EventUtils.synthesizeKey("t", {});
+ await waitFirstSearchResults.promise;
+ EventUtils.synthesizeKey("e", {});
+
+ let promiseLoaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ EventUtils.synthesizeKey("KEY_Enter", {});
+
+ let context = await UrlbarTestUtils.promiseSearchComplete(window);
+ await TestUtils.waitForCondition(
+ () => context.results.length,
+ "Waiting for any result in the QueryContext"
+ );
+ info("Simulate a request to replay deferred events at this point");
+ gURLBar.eventBufferer.replayDeferredEvents(true);
+
+ defer.resolve();
+ await promiseLoaded;
+
+ let expectedURL = UrlbarPrefs.isPersistedSearchTermsEnabled()
+ ? "http://mochi.test:8888/?terms=" + gURLBar.value
+ : gURLBar.untrimmedValue;
+ Assert.equal(gBrowser.selectedBrowser.currentURI.spec, expectedURL);
+
+ BrowserTestUtils.removeTab(tab);
+ sandbox.restore();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_calculator.js b/browser/components/urlbar/tests/browser/browser_calculator.js
new file mode 100644
index 0000000000..899cbc6d5b
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_calculator.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const FORMULA = "8 * 8";
+const RESULT = "64";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.calculator", true]],
+ });
+});
+
+add_task(async function test_calculator() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: FORMULA,
+ });
+
+ let result = (await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1))
+ .result;
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.DYNAMIC);
+ Assert.equal(result.payload.input, FORMULA);
+ Assert.equal(result.payload.value, RESULT);
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+
+ // Ensure the RESULT get written to the clipboard when selected.
+ await SimpleTest.promiseClipboardChange(RESULT, () => {
+ EventUtils.synthesizeKey("KEY_Enter");
+ });
+});
diff --git a/browser/components/urlbar/tests/browser/browser_canonizeURL.js b/browser/components/urlbar/tests/browser/browser_canonizeURL.js
new file mode 100644
index 0000000000..04f153e7d2
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_canonizeURL.js
@@ -0,0 +1,277 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests turning non-url-looking values typed in the input field into proper URLs.
+ */
+
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+
+add_task(async function checkCtrlWorks() {
+ registerCleanupFunction(async function () {
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.formHistory.clear();
+ });
+
+ let defaultEngine = await Services.search.getDefault();
+ let testcases = [
+ ["example", "https://www.example.com/", { ctrlKey: true }],
+ // Check that a direct load is not overwritten by a previous canonization.
+ ["http://example.com/test/", "http://example.com/test/", {}],
+ ["ex-ample", "https://www.ex-ample.com/", { ctrlKey: true }],
+ [" example ", "https://www.example.com/", { ctrlKey: true }],
+ [" example/foo ", "https://www.example.com/foo", { ctrlKey: true }],
+ [
+ " example/foo bar ",
+ "https://www.example.com/foo%20bar",
+ { ctrlKey: true },
+ ],
+ ["example.net", "http://example.net/", { ctrlKey: true }],
+ ["http://example", "http://example/", { ctrlKey: true }],
+ ["example:8080", "http://example:8080/", { ctrlKey: true }],
+ ["ex-ample.foo", "http://ex-ample.foo/", { ctrlKey: true }],
+ ["example.foo/bar ", "http://example.foo/bar", { ctrlKey: true }],
+ ["1.1.1.1", "http://1.1.1.1/", { ctrlKey: true }],
+ ["ftp.example.bar", "http://ftp.example.bar/", { ctrlKey: true }],
+ [
+ "ex ample",
+ defaultEngine.getSubmission("ex ample", null, "keyword").uri.spec,
+ { ctrlKey: true },
+ ],
+ ];
+
+ // Disable autoFill for this test, since it could mess up the results.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.autoFill", false],
+ ["browser.urlbar.ctrlCanonizesURLs", true],
+ ],
+ });
+
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+
+ for (let [inputValue, expectedURL, options] of testcases) {
+ info(`Testing input string: "${inputValue}" - expected: "${expectedURL}"`);
+ let promiseLoad = BrowserTestUtils.waitForDocLoadAndStopIt(
+ expectedURL,
+ win.gBrowser.selectedBrowser
+ );
+ let promiseStopped = BrowserTestUtils.browserStopped(
+ win.gBrowser.selectedBrowser,
+ undefined,
+ true
+ );
+ win.gURLBar.focus();
+ win.gURLBar.inputField.value = inputValue.slice(0, -1);
+ EventUtils.sendString(inputValue.slice(-1), win);
+ EventUtils.synthesizeKey("KEY_Enter", options, win);
+ await Promise.all([promiseLoad, promiseStopped]);
+ }
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function checkPrefTurnsOffCanonize() {
+ // Add a dummy search engine to avoid hitting the network.
+ await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME,
+ setAsDefault: true,
+ });
+
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+
+ // Ensure we don't end up loading something in the current tab becuase it's empty:
+ let initialTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser: win.gBrowser,
+ opening: "about:mozilla",
+ });
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.ctrlCanonizesURLs", false]],
+ });
+
+ let newURL = "http://mochi.test:8888/?terms=example";
+ // On MacOS CTRL+Enter is not supposed to open in a new tab, because it uses
+ // CMD+Enter for that.
+ let promiseLoaded =
+ AppConstants.platform == "macosx"
+ ? BrowserTestUtils.browserLoaded(
+ win.gBrowser.selectedBrowser,
+ false,
+ newURL
+ )
+ : BrowserTestUtils.waitForNewTab(win.gBrowser);
+
+ win.gURLBar.focus();
+ win.gURLBar.selectionStart = win.gURLBar.selectionEnd =
+ win.gURLBar.inputField.value.length;
+ win.gURLBar.inputField.value = "exampl";
+ EventUtils.sendString("e", win);
+ EventUtils.synthesizeKey("KEY_Enter", { ctrlKey: true }, win);
+
+ await promiseLoaded;
+ if (AppConstants.platform == "macosx") {
+ Assert.equal(
+ initialTab.linkedBrowser.currentURI.spec,
+ newURL,
+ "Original tab should have navigated"
+ );
+ } else {
+ Assert.equal(
+ initialTab.linkedBrowser.currentURI.spec,
+ "about:mozilla",
+ "Original tab shouldn't have navigated"
+ );
+ Assert.equal(
+ win.gBrowser.selectedBrowser.currentURI.spec,
+ newURL,
+ "New tab should have navigated"
+ );
+ }
+ while (win.gBrowser.tabs.length > 1) {
+ win.gBrowser.removeTab(win.gBrowser.selectedTab, { animate: false });
+ }
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function autofill() {
+ // Re-enable autofill and canonization.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.autoFill", true],
+ ["browser.urlbar.ctrlCanonizesURLs", true],
+ ],
+ });
+
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+
+ // Quantumbar automatically disables autofill when the old search string
+ // starts with the new search string, so to make sure that doesn't happen and
+ // that earlier tests don't conflict with this one, start a new search for
+ // some other string.
+ win.gURLBar.select();
+ EventUtils.sendString("blah", win);
+
+ // Add a visit that will be autofilled.
+ await PlacesUtils.history.clear();
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com/",
+ transition: PlacesUtils.history.TRANSITIONS.TYPED,
+ },
+ ]);
+
+ let testcases = [
+ ["ex", "https://www.ex.com/", { ctrlKey: true }],
+ // Check that a direct load is not overwritten by a previous canonization.
+ ["ex", "http://example.com/", {}],
+ // search alias
+ ["@goo", "https://www.goo.com/", { ctrlKey: true }],
+ ];
+
+ function promiseAutofill() {
+ return BrowserTestUtils.waitForEvent(win.gURLBar.inputField, "select");
+ }
+
+ for (let [inputValue, expectedURL, options] of testcases) {
+ let promiseLoad = BrowserTestUtils.waitForDocLoadAndStopIt(
+ expectedURL,
+ win.gBrowser.selectedBrowser
+ );
+ win.gURLBar.select();
+ let autofillPromise = promiseAutofill();
+ EventUtils.sendString(inputValue, win);
+ await autofillPromise;
+ EventUtils.synthesizeKey("KEY_Enter", options, win);
+ await promiseLoad;
+
+ // Here again, make sure autofill isn't disabled for the next search. See
+ // the comment above.
+ win.gURLBar.select();
+ EventUtils.sendString("blah", win);
+ }
+
+ await PlacesUtils.history.clear();
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function () {
+ info(
+ "Test whether canonization is disabled until the ctrl key is releasing if the key was used to paste text into urlbar"
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.ctrlCanonizesURLs", true]],
+ });
+
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+
+ info("Paste the word to the urlbar");
+ const testWord = "example";
+ simulatePastingToUrlbar(testWord, win);
+ is(win.gURLBar.value, testWord, "Paste the test word correctly");
+
+ info("Send enter key while pressing the ctrl key");
+ EventUtils.synthesizeKey("VK_RETURN", { ctrlKey: true }, win);
+ await BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ is(
+ win.gBrowser.selectedBrowser.documentURI.spec,
+ `http://mochi.test:8888/?terms=${testWord}`,
+ "The loaded url is not canonized"
+ );
+ EventUtils.synthesizeKey("VK_CONTROL", { type: "keyup" }, win);
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function () {
+ info("Test whether canonization is enabled again after releasing the ctrl");
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.ctrlCanonizesURLs", true]],
+ });
+
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+
+ info("Paste the word to the urlbar");
+ const testWord = "example";
+ simulatePastingToUrlbar(testWord, win);
+ is(win.gURLBar.value, testWord, "Paste the test word correctly");
+
+ info("Release the ctrl key befoer typing Enter key");
+ EventUtils.synthesizeKey("VK_CONTROL", { type: "keyup" }, win);
+
+ info("Send enter key with the ctrl");
+ const onLoad = BrowserTestUtils.waitForDocLoadAndStopIt(
+ `https://www.${testWord}.com/`,
+ win.gBrowser.selectedBrowser
+ );
+ const onStop = BrowserTestUtils.browserStopped(
+ win.gBrowser.selectedBrowser,
+ undefined,
+ true
+ );
+ EventUtils.synthesizeKey("VK_RETURN", { ctrlKey: true }, win);
+ await Promise.all([onLoad, onStop]);
+ info("The loaded url is canonized");
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+function simulatePastingToUrlbar(text, win) {
+ win.gURLBar.focus();
+
+ const keyForPaste = win.document
+ .getElementById("key_paste")
+ .getAttribute("key")
+ .toLowerCase();
+ EventUtils.synthesizeKey(
+ keyForPaste,
+ { type: "keydown", ctrlKey: true },
+ win
+ );
+
+ win.gURLBar.select();
+ EventUtils.sendString(text, win);
+}
diff --git a/browser/components/urlbar/tests/browser/browser_caret_position.js b/browser/components/urlbar/tests/browser/browser_caret_position.js
new file mode 100644
index 0000000000..35eee55efe
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_caret_position.js
@@ -0,0 +1,359 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const LARGE_DATA_URL =
+ "data:text/plain," + [...Array(1000)].map(() => "0123456789").join("");
+
+// Tests for the caret position after gURLBar.setURI().
+add_task(async function setURI() {
+ const testData = [
+ {
+ firstURL: "https://example.com/test",
+ secondURL: "https://example.com/test",
+ initialSelectionStart: 0,
+ initialSelectionEnd: 0,
+ expectedSelectionStart: 0,
+ expectedSelectionEnd: 0,
+ },
+ {
+ firstURL: "https://example.com/test",
+ secondURL: "https://example.com/test",
+ initialSelectionStart: 20,
+ initialSelectionEnd: 20,
+ expectedSelectionStart: 20,
+ expectedSelectionEnd: 20,
+ },
+ {
+ firstURL: "https://example.com/test",
+ secondURL: "https://example.com/test",
+ initialSelectionStart: 1,
+ initialSelectionEnd: 20,
+ expectedSelectionStart: 1,
+ expectedSelectionEnd: 20,
+ },
+ {
+ firstURL: "https://example.com/test",
+ secondURL: "https://example.com/test",
+ initialSelectionStart: "https://example.com/test".length,
+ initialSelectionEnd: "https://example.com/test".length,
+ expectedSelectionStart: "https://example.com/test".length,
+ expectedSelectionEnd: "https://example.com/test".length,
+ },
+ {
+ firstURL: "https://example.com/test",
+ secondURL: "https://example.com/test",
+ initialSelectionStart: 0,
+ initialSelectionEnd: "https://example.com/test".length,
+ expectedSelectionStart: 0,
+ expectedSelectionEnd: "https://example.com/test".length,
+ },
+ {
+ firstURL: "https://example.com/test",
+ secondURL: "https://example.org/test",
+ initialSelectionStart: 0,
+ initialSelectionEnd: 0,
+ expectedSelectionStart: 0,
+ expectedSelectionEnd: 0,
+ },
+ {
+ firstURL: "https://example.com/test",
+ secondURL: "https://example.org/test",
+ initialSelectionStart: 20,
+ initialSelectionEnd: 20,
+ expectedSelectionStart: 20,
+ expectedSelectionEnd: 20,
+ },
+ {
+ firstURL: "https://example.com/test",
+ secondURL: "https://example.org/test",
+ initialSelectionStart: 1,
+ initialSelectionEnd: 10,
+ expectedSelectionStart: 1,
+ expectedSelectionEnd: 10,
+ },
+ {
+ firstURL: "https://example.com/test",
+ secondURL: "https://example.org/test",
+ initialSelectionStart: "https://example.".length,
+ initialSelectionEnd: "https://example.c".length,
+ expectedSelectionStart: "https://example.c".length,
+ expectedSelectionEnd: "https://example.c".length,
+ },
+ {
+ firstURL: "https://example.com/test",
+ secondURL: "https://example.org/test",
+ initialSelectionStart: "https://example.com/test".length,
+ initialSelectionEnd: "https://example.com/test".length,
+ expectedSelectionStart: "https://example.org/test".length,
+ expectedSelectionEnd: "https://example.org/test".length,
+ },
+ {
+ firstURL: "https://example.com/test",
+ secondURL: "https://example.org/test",
+ initialSelectionStart: 0,
+ initialSelectionEnd: "https://example.com/test".length,
+ expectedSelectionStart: "https://example.org/test".length,
+ expectedSelectionEnd: "https://example.org/test".length,
+ },
+ {
+ firstURL: "https://example.com/test",
+ secondURL: "https://example.com/longer",
+ initialSelectionStart: "https://example.com/test".length,
+ initialSelectionEnd: "https://example.com/test".length,
+ expectedSelectionStart: "https://example.com/longer".length,
+ expectedSelectionEnd: "https://example.com/longer".length,
+ },
+ {
+ firstURL: "https://example.com/test",
+ secondURL: "https://example.com/longer",
+ initialSelectionStart: 20,
+ initialSelectionEnd: 20,
+ expectedSelectionStart: 20,
+ expectedSelectionEnd: 20,
+ },
+ {
+ firstURL: "https://example.com/longer",
+ secondURL: "https://example.com/test",
+ initialSelectionStart: 0,
+ initialSelectionEnd: "https://example.com/longer".length,
+ expectedSelectionStart: "https://example.com/test".length,
+ expectedSelectionEnd: "https://example.com/test".length,
+ },
+ {
+ firstURL: "https://example.com/longer",
+ secondURL: "https://example.com/test",
+ initialSelectionStart: "https://example.com/longer".length,
+ initialSelectionEnd: "https://example.com/longer".length,
+ expectedSelectionStart: "https://example.com/test".length,
+ expectedSelectionEnd: "https://example.com/test".length,
+ },
+ {
+ firstURL: "https://example.com/longer",
+ secondURL: "https://example.com/test",
+ initialSelectionStart: "https://example.com/longer".length - 1,
+ initialSelectionEnd: "https://example.com/longer".length - 1,
+ expectedSelectionStart: "https://example.com/test".length,
+ expectedSelectionEnd: "https://example.com/test".length,
+ },
+ {
+ firstURL: "https://example.com/longer",
+ secondURL: "https://example.com/test",
+ initialSelectionStart: 0,
+ initialSelectionEnd: "https://example.com/longer".length - 1,
+ expectedSelectionStart: "https://example.com/test".length,
+ expectedSelectionEnd: "https://example.com/test".length,
+ },
+ {
+ firstURL: "https://example.com/test",
+ secondURL: "about:blank",
+ initialSelectionStart: 0,
+ initialSelectionEnd: 0,
+ expectedSelectionStart: 0,
+ expectedSelectionEnd: 0,
+ },
+ {
+ firstURL: "https://example.com/test",
+ secondURL: "about:blank",
+ initialSelectionStart: 0,
+ initialSelectionEnd: "https://example.com/test".length,
+ expectedSelectionStart: 0,
+ expectedSelectionEnd: 0,
+ },
+ {
+ firstURL: "https://example.com/test",
+ secondURL: "about:blank",
+ initialSelectionStart: 3,
+ initialSelectionEnd: 4,
+ expectedSelectionStart: 0,
+ expectedSelectionEnd: 0,
+ },
+ {
+ firstURL: "https://example.com/test",
+ secondURL: "about:blank",
+ initialSelectionStart: "https://example.com/test".length,
+ initialSelectionEnd: "https://example.com/test".length,
+ expectedSelectionStart: 0,
+ expectedSelectionEnd: 0,
+ },
+ {
+ firstURL: "about:blank",
+ secondURL: "https://example.com/test",
+ initialSelectionStart: 0,
+ initialSelectionEnd: 0,
+ expectedSelectionStart: 0,
+ expectedSelectionEnd: 0,
+ },
+ {
+ firstURL: "about:blank",
+ secondURL: LARGE_DATA_URL,
+ initialSelectionStart: 0,
+ initialSelectionEnd: 0,
+ expectedSelectionStart: 0,
+ expectedSelectionEnd: 0,
+ },
+ {
+ firstURL: "about:telemetry",
+ secondURL: LARGE_DATA_URL,
+ initialSelectionStart: "about:telemetry".length,
+ initialSelectionEnd: "about:telemetry".length,
+ expectedSelectionStart: LARGE_DATA_URL.length,
+ expectedSelectionEnd: LARGE_DATA_URL.length,
+ },
+ ];
+
+ for (const data of testData) {
+ info(
+ `Test for ${data.firstURL} -> ${data.secondURL} with initial selection: ${data.initialSelectionStart}, ${data.initialSelectionEnd}`
+ );
+ info("Check the caret position after setting second URL");
+ gURLBar.setURI(makeURI(data.firstURL));
+ gURLBar.selectionStart = data.initialSelectionStart;
+ gURLBar.selectionEnd = data.initialSelectionEnd;
+
+ // The change of the scroll amount dependent on the selection change will be
+ // ignored if the previous processing is unfinished yet. Therefore, make the
+ // processing finalize explicitly here.
+ await flushScrollStyle();
+
+ gURLBar.focus();
+ gURLBar.setURI(makeURI(data.secondURL));
+ await flushScrollStyle();
+
+ Assert.equal(gURLBar.selectionStart, data.expectedSelectionStart);
+ Assert.equal(gURLBar.selectionEnd, data.expectedSelectionEnd);
+ if (data.secondURL.length === data.expectedSelectionStart) {
+ // If the caret is at the end of url, the input field shows the end of
+ // text.
+ Assert.equal(
+ gURLBar.inputField.scrollLeft,
+ gURLBar.inputField.scrollLeftMax
+ );
+ }
+
+ info("Check the caret position while the input is not focused");
+ gURLBar.setURI(makeURI(data.firstURL));
+ gURLBar.selectionStart = data.initialSelectionStart;
+ gURLBar.selectionEnd = data.initialSelectionEnd;
+
+ await flushScrollStyle();
+
+ gURLBar.blur();
+ gURLBar.setURI(makeURI(data.secondURL));
+ await flushScrollStyle();
+
+ if (data.firstURL === data.secondURL) {
+ Assert.equal(gURLBar.selectionStart, data.initialSelectionStart);
+ Assert.equal(gURLBar.selectionEnd, data.initialSelectionEnd);
+ } else {
+ Assert.equal(gURLBar.selectionStart, gURLBar.value.length);
+ Assert.equal(gURLBar.selectionEnd, gURLBar.value.length);
+ }
+ Assert.equal(gURLBar.inputField.scrollLeft, 0);
+ }
+});
+
+// Tests that up and down keys move the caret on certain platforms, and that
+// opening the popup doesn't change the caret position.
+add_task(async function navigation() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "This is a generic sentence",
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ const INITIAL_SELECTION_START = 3;
+ const INITIAL_SELECTION_END = 10;
+ gURLBar.selectionStart = INITIAL_SELECTION_START;
+ gURLBar.selectionEnd = INITIAL_SELECTION_END;
+
+ if (AppConstants.platform == "macosx" || AppConstants.platform == "linux") {
+ await checkCaretMoves(
+ "KEY_ArrowDown",
+ gURLBar.value.length,
+ "Caret should have moved to the end",
+ window
+ );
+ await checkPopupOpens("KEY_ArrowDown", window);
+
+ await checkCaretMoves(
+ "KEY_ArrowUp",
+ 0,
+ "Caret should have moved to the start",
+ window
+ );
+ await checkPopupOpens("KEY_ArrowUp", window);
+ } else {
+ await checkPopupOpens("KEY_ArrowDown", window);
+ await checkPopupOpens("KEY_ArrowUp", window);
+ }
+});
+
+async function checkCaretMoves(key, pos, msg, win) {
+ checkIfKeyStartsQuery(key, false, win);
+ Assert.equal(
+ UrlbarTestUtils.isPopupOpen(win),
+ false,
+ `${key}: Popup shouldn't be open`
+ );
+ Assert.equal(
+ win.gURLBar.selectionStart,
+ win.gURLBar.selectionEnd,
+ `${key}: Input selection should be empty`
+ );
+ Assert.equal(win.gURLBar.selectionStart, pos, `${key}: ${msg}`);
+}
+
+async function checkPopupOpens(key, win) {
+ // Store current selection and check it doesn't change.
+ let selectionStart = win.gURLBar.selectionStart;
+ let selectionEnd = win.gURLBar.selectionEnd;
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ checkIfKeyStartsQuery(key, true, win);
+ });
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(win),
+ 0,
+ `${key}: Heuristic result should be selected`
+ );
+ Assert.equal(
+ win.gURLBar.selectionStart,
+ selectionStart,
+ `${key}: Input selection start should not change`
+ );
+ Assert.equal(
+ win.gURLBar.selectionEnd,
+ selectionEnd,
+ `${key}: Input selection end should not change`
+ );
+ await UrlbarTestUtils.promisePopupClose(win);
+}
+
+function checkIfKeyStartsQuery(key, shouldStartQuery, win) {
+ let queryStarted = false;
+ let queryListener = {
+ onQueryStarted() {
+ queryStarted = true;
+ },
+ };
+ win.gURLBar.controller.addQueryListener(queryListener);
+ EventUtils.synthesizeKey(key, {}, win);
+ win.gURLBar.eventBufferer.replayDeferredEvents(false);
+ win.gURLBar.controller.removeQueryListener(queryListener);
+ Assert.equal(
+ queryStarted,
+ shouldStartQuery,
+ `${key}: Should${shouldStartQuery ? "" : "n't"} have started a query`
+ );
+}
+
+async function flushScrollStyle() {
+ // Flush pending notifications for the style.
+ /* eslint-disable no-unused-expressions */
+ gURLBar.inputField.scrollLeft;
+ // Ensure to apply the style.
+ await new Promise(resolve =>
+ gURLBar.inputField.ownerGlobal.requestAnimationFrame(resolve)
+ );
+}
diff --git a/browser/components/urlbar/tests/browser/browser_click_row_border.js b/browser/components/urlbar/tests/browser/browser_click_row_border.js
new file mode 100644
index 0000000000..59915ed3b1
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_click_row_border.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URL = "https://example.com/autocomplete";
+
+add_setup(async function () {
+ await PlacesTestUtils.addVisits(TEST_URL);
+ registerCleanupFunction(async function () {
+ await PlacesUtils.history.clear();
+ });
+});
+
+add_task(async function test_click_row_border() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "example.com/autocomplete",
+ });
+ let resultRow = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
+ let loaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ TEST_URL
+ );
+ info("Clicking on the result's top pixel row");
+ EventUtils.synthesizeMouse(
+ resultRow,
+ parseInt(getComputedStyle(resultRow).borderTopLeftRadius) * 2,
+ 1,
+ {}
+ );
+ info("Waiting for page to load");
+ await loaded;
+ ok(true, "Page loaded");
+});
diff --git a/browser/components/urlbar/tests/browser/browser_closePanelOnClick.js b/browser/components/urlbar/tests/browser/browser_closePanelOnClick.js
new file mode 100644
index 0000000000..2bbe412acb
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_closePanelOnClick.js
@@ -0,0 +1,34 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This tests that the urlbar panel closes when clicking certain ui elements.
+ */
+
+"use strict";
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab("about:robots", async () => {
+ for (let elt of [
+ gBrowser.selectedBrowser,
+ gBrowser.tabContainer,
+ document.querySelector("#nav-bar toolbarspring"),
+ ]) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value: "dummy",
+ });
+ // Must have at least one test.
+ Assert.ok(!!elt, "Found a valid element: " + (elt.id || elt.localName));
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeNativeMouseEvent({
+ type: "click",
+ target: elt,
+ atCenter: true,
+ })
+ );
+ }
+ });
+});
diff --git a/browser/components/urlbar/tests/browser/browser_content_opener.js b/browser/components/urlbar/tests/browser/browser_content_opener.js
new file mode 100644
index 0000000000..0cf4865ad7
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_content_opener.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ TEST_BASE_URL + "dummy_page.html",
+ async function (browser) {
+ let windowOpenedPromise = BrowserTestUtils.waitForNewWindow();
+ await SpecialPowers.spawn(browser, [], function () {
+ content.window.open("", "_BLANK", "toolbar=no,height=300,width=500");
+ });
+ let newWin = await windowOpenedPromise;
+ is(
+ newWin.gURLBar.value,
+ "about:blank",
+ "Should be displaying about:blank for the opened window."
+ );
+ await BrowserTestUtils.closeWindow(newWin);
+ }
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_contextualsearch.js b/browser/components/urlbar/tests/browser/browser_contextualsearch.js
new file mode 100644
index 0000000000..5de1673e6a
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_contextualsearch.js
@@ -0,0 +1,119 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { UrlbarProviderContextualSearch } = ChromeUtils.importESModule(
+ "resource:///modules/UrlbarProviderContextualSearch.sys.mjs"
+);
+
+add_setup(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.contextualSearch.enabled", true]],
+ });
+});
+
+add_task(async function test_selectContextualSearchResult_already_installed() {
+ await SearchTestUtils.installSearchExtension({
+ name: "Contextual",
+ search_url: "https://example.com/browser",
+ });
+
+ const ENGINE_TEST_URL = "https://example.com/";
+ let onLoaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ ENGINE_TEST_URL
+ );
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, ENGINE_TEST_URL);
+ await onLoaded;
+
+ const query = "search";
+ let engine = Services.search.getEngineByName("Contextual");
+ const [expectedUrl] = UrlbarUtils.getSearchQueryUrl(engine, query);
+
+ Assert.ok(
+ expectedUrl.includes(`?q=${query}`),
+ "Expected URL should be a search URL"
+ );
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: query,
+ });
+ const resultIndex = UrlbarTestUtils.getResultCount(window) - 1;
+ const result = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ resultIndex
+ );
+
+ is(
+ result.dynamicType,
+ "contextualSearch",
+ "Second last result is a contextual search result"
+ );
+
+ info("Focus and select the contextual search result");
+ UrlbarTestUtils.setSelectedRowIndex(window, resultIndex);
+ let onLoad = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ expectedUrl
+ );
+ EventUtils.synthesizeKey("KEY_Enter");
+ await onLoad;
+
+ Assert.equal(
+ gBrowser.selectedBrowser.currentURI.spec,
+ expectedUrl,
+ "Selecting the contextual search result opens the search URL"
+ );
+});
+
+add_task(async function test_selectContextualSearchResult_not_installed() {
+ const ENGINE_TEST_URL =
+ "http://mochi.test:8888/browser/browser/components/search/test/browser/opensearch.html";
+ const EXPECTED_URL =
+ "http://mochi.test:8888/browser/browser/components/search/test/browser/?search&test=search";
+ let onLoaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ ENGINE_TEST_URL
+ );
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, ENGINE_TEST_URL);
+ await onLoaded;
+
+ const query = "search";
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: query,
+ });
+ const resultIndex = UrlbarTestUtils.getResultCount(window) - 1;
+ const result = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ resultIndex
+ );
+
+ Assert.equal(
+ result.dynamicType,
+ "contextualSearch",
+ "Second last result is a contextual search result"
+ );
+
+ info("Focus and select the contextual search result");
+ UrlbarTestUtils.setSelectedRowIndex(window, resultIndex);
+ let onLoad = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ EXPECTED_URL
+ );
+ EventUtils.synthesizeKey("KEY_Enter");
+ await onLoad;
+
+ Assert.equal(
+ gBrowser.selectedBrowser.currentURI.spec,
+ EXPECTED_URL,
+ "Selecting the contextual search result opens the search URL"
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_copy_during_load.js b/browser/components/urlbar/tests/browser/browser_copy_during_load.js
new file mode 100644
index 0000000000..4a81ff08be
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_copy_during_load.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that copying from the urlbar page works correctly after a result is
+// confirmed but takes a while to load.
+
+add_task(async function () {
+ const SLOW_PAGE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://www.example.com"
+ ) + "slow-page.sjs";
+
+ await BrowserTestUtils.withNewTab(gBrowser, async tab => {
+ gURLBar.focus();
+ gURLBar.value = SLOW_PAGE;
+ let promise = TestUtils.waitForCondition(
+ () => gURLBar.getAttribute("pageproxystate") == "invalid"
+ );
+ EventUtils.synthesizeKey("KEY_Enter");
+ info("wait for the initial conditions");
+ await promise;
+
+ info("Copy the whole url");
+ await SimpleTest.promiseClipboardChange(SLOW_PAGE, () => {
+ gURLBar.select();
+ goDoCommand("cmd_copy");
+ });
+
+ info("Copy the initial part of the url, as a different valid url");
+ await SimpleTest.promiseClipboardChange(
+ SLOW_PAGE.substring(0, SLOW_PAGE.indexOf("slow-page.sjs")),
+ () => {
+ gURLBar.selectionStart = 0;
+ gURLBar.selectionEnd = gURLBar.value.indexOf("slow-page.sjs");
+ goDoCommand("cmd_copy");
+ }
+ );
+
+ // This is apparently necessary to avoid a timeout on mochitest shutdown(!?)
+ let browserStoppedPromise = BrowserTestUtils.browserStopped(
+ gBrowser,
+ null,
+ true
+ );
+ BrowserStop();
+ await browserStoppedPromise;
+ });
+});
diff --git a/browser/components/urlbar/tests/browser/browser_copying.js b/browser/components/urlbar/tests/browser/browser_copying.js
new file mode 100644
index 0000000000..9c32115fb4
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_copying.js
@@ -0,0 +1,416 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function getUrl(hostname, file) {
+ return (
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ hostname
+ ) + file
+ );
+}
+
+add_task(async function () {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ registerCleanupFunction(function () {
+ gBrowser.removeTab(tab);
+ gURLBar.setURI();
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.trimURLs", true],
+ // avoid prompting about phishing
+ ["network.http.phishy-userpass-length", 32],
+ ],
+ });
+
+ for (let testCase of tests) {
+ if (testCase.setup) {
+ await testCase.setup();
+ }
+
+ if (testCase.loadURL) {
+ info(`Loading : ${testCase.loadURL}`);
+ let expectedLoad = testCase.expectedLoad || testCase.loadURL;
+ BrowserTestUtils.loadURIString(
+ gBrowser.selectedBrowser,
+ testCase.loadURL
+ );
+ await BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ expectedLoad
+ );
+ } else if (testCase.setURL) {
+ gURLBar.value = testCase.setURL;
+ }
+ if (testCase.setURL || testCase.loadURL) {
+ gURLBar.valueIsTyped = !!testCase.setURL;
+ is(
+ gURLBar.value,
+ testCase.expectedURL,
+ "url bar value set to " + gURLBar.value
+ );
+ }
+
+ gURLBar.focus();
+ if (testCase.expectedValueOnFocus) {
+ Assert.equal(
+ gURLBar.value,
+ testCase.expectedValueOnFocus,
+ "Check value on focus"
+ );
+ }
+ await testCopy(testCase.copyVal, testCase.copyExpected);
+ gURLBar.blur();
+
+ if (testCase.cleanup) {
+ await testCase.cleanup();
+ }
+ }
+});
+
+var tests = [
+ // pageproxystate="invalid"
+ {
+ setURL: "http://example.com/",
+ expectedURL: "example.com",
+ copyExpected: "example.com",
+ },
+ {
+ copyVal: "<e>xample.com",
+ copyExpected: "e",
+ },
+ {
+ copyVal: "<e>x<a>mple.com",
+ copyExpected: "ea",
+ },
+ {
+ copyVal: "<e><xa>mple.com",
+ copyExpected: "exa",
+ },
+ {
+ copyVal: "<e><xa>mple.co<m>",
+ copyExpected: "exam",
+ },
+ {
+ copyVal: "<e><xample.co><m>",
+ copyExpected: "example.com",
+ },
+
+ // pageproxystate="valid" from this point on (due to the load)
+ {
+ loadURL: "http://example.com/",
+ expectedURL: "example.com",
+ copyExpected: "http://example.com/",
+ },
+ {
+ copyVal: "<example.co>m",
+ copyExpected: "example.co",
+ },
+ {
+ copyVal: "e<x>ample.com",
+ copyExpected: "x",
+ },
+ {
+ copyVal: "<e>xample.com",
+ copyExpected: "e",
+ },
+ {
+ copyVal: "<e>xample.co<m>",
+ copyExpected: "em",
+ },
+ {
+ copyVal: "<exam><ple.com>",
+ copyExpected: "example.com",
+ },
+
+ {
+ loadURL: "http://example.com/foo",
+ expectedURL: "example.com/foo",
+ copyExpected: "http://example.com/foo",
+ },
+ {
+ copyVal: "<example.com>/foo",
+ copyExpected: "http://example.com",
+ },
+ {
+ copyVal: "<example>.com/foo",
+ copyExpected: "example",
+ },
+ // Test that 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: "<example.com/ space>/test",
+ copyExpected: "http://example.com/%20space",
+ },
+ {
+ copyVal: "<example.com/ space/test>",
+ 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: "<example.com/ foo bar> baz/",
+ copyExpected: "http://example.com/%20foo%20bar",
+ },
+ {
+ copyVal: "example.<com/ foo bar> 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: "<example.com/(>)()\xe9",
+ copyExpected: "http://example.com/(",
+ },
+ {
+ copyVal: "e<xample.com/(>)()\xe9",
+ copyExpected: "xample.com/(",
+ },
+
+ {
+ loadURL: "http://example.com/%C3%A9%C3%A9",
+ expectedURL: "example.com/\xe9\xe9",
+ copyExpected: "http://example.com/%C3%A9%C3%A9",
+ },
+ {
+ copyVal: "e<xample.com/\xe9>\xe9",
+ copyExpected: "xample.com/\xe9",
+ },
+ {
+ copyVal: "<example.com/\xe9>\xe9",
+ copyExpected: "http://example.com/%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: "s<ub2.ält.mochi.test:8888/f>oo",
+ copyExpected: "ub2.ält.mochi.test:8888/f",
+ },
+ {
+ copyVal: "<sub2.ält.mochi.test:8888/f>oo",
+ copyExpected: "http://sub2.%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<xample.com/?\xf7>\xf7",
+ copyExpected: "xample.com/?\xf7",
+ },
+ {
+ copyVal: "<example.com/?\xf7>\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: "<data:text/html,(>%C3%A9 %25P)",
+ copyExpected: "data:text/html,(",
+ },
+ {
+ copyVal: "<data:text/html,(%C3%A9 %25P>)",
+ copyExpected: "data:text/html,(%C3%A9 %25P",
+ },
+
+ {
+ async setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.decodeURLsOnCopy", true]],
+ });
+ },
+ async cleanup() {
+ await SpecialPowers.popPrefEnv();
+ },
+ loadURL:
+ "http://example.com/%D0%B1%D0%B8%D0%BE%D0%B3%D1%80%D0%B0%D1%84%D0%B8%D1%8F",
+ expectedURL: "example.com/биография",
+ copyExpected: "http://example.com/биография",
+ },
+ {
+ copyVal: "<example.com/би>ография",
+ copyExpected: "http://example.com/%D0%B1%D0%B8",
+ },
+
+ {
+ async setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.decodeURLsOnCopy", true]],
+ });
+ // Setup a valid intranet url that resolves but is not yet known.
+ const proxyService = Cc[
+ "@mozilla.org/network/protocol-proxy-service;1"
+ ].getService(Ci.nsIProtocolProxyService);
+ let proxyInfo = proxyService.newProxyInfo(
+ "http",
+ "localhost",
+ 8888,
+ "",
+ "",
+ 0,
+ 4096,
+ null
+ );
+ this._proxyFilter = {
+ applyFilter(channel, defaultProxyInfo, callback) {
+ callback.onProxyFilterResult(
+ channel.URI.host === "mytest" ? proxyInfo : defaultProxyInfo
+ );
+ },
+ };
+ proxyService.registerChannelFilter(this._proxyFilter, 0);
+ registerCleanupFunction(() => {
+ if (this._proxyFilter) {
+ proxyService.unregisterChannelFilter(this._proxyFilter);
+ }
+ });
+ },
+ async cleanup() {
+ await SpecialPowers.popPrefEnv();
+ const proxyService = Cc[
+ "@mozilla.org/network/protocol-proxy-service;1"
+ ].getService(Ci.nsIProtocolProxyService);
+ proxyService.unregisterChannelFilter(this._proxyFilter);
+ this._proxyFilter = null;
+ },
+ loadURL: "http://mytest/",
+ expectedURL: "mytest",
+ expectedValueOnFocus: "http://mytest/",
+ copyExpected: "http://mytest/",
+ },
+
+ {
+ async setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.decodeURLsOnCopy", true]],
+ });
+ },
+ async cleanup() {
+ await SpecialPowers.popPrefEnv();
+ },
+ loadURL: "https://example.com/",
+ expectedURL: "https://example.com",
+ copyExpected: "https://example.com",
+ },
+];
+
+function testCopy(copyVal, targetValue) {
+ info("Expecting copy of: " + targetValue);
+
+ if (copyVal) {
+ let offsets = [];
+ while (true) {
+ let startBracket = copyVal.indexOf("<");
+ let endBracket = copyVal.indexOf(">");
+ if (startBracket == -1 && endBracket == -1) {
+ break;
+ }
+ if (startBracket > endBracket || startBracket == -1) {
+ offsets = [];
+ break;
+ }
+ offsets.push([startBracket, endBracket - 1]);
+ copyVal = copyVal.replace("<", "").replace(">", "");
+ }
+ if (!offsets.length || copyVal != gURLBar.value) {
+ ok(false, "invalid copyVal: " + copyVal);
+ }
+ gURLBar.selectionStart = offsets[0][0];
+ gURLBar.selectionEnd = offsets[0][1];
+ if (offsets.length > 1) {
+ let sel = gURLBar.editor.selection;
+ let r0 = sel.getRangeAt(0);
+ let node0 = r0.startContainer;
+ sel.removeAllRanges();
+ offsets.map(function (startEnd) {
+ let range = r0.cloneRange();
+ range.setStart(node0, startEnd[0]);
+ range.setEnd(node0, startEnd[1]);
+ sel.addRange(range);
+ });
+ }
+ } else {
+ gURLBar.select();
+ }
+
+ return SimpleTest.promiseClipboardChange(targetValue, () =>
+ goDoCommand("cmd_copy")
+ );
+}
diff --git a/browser/components/urlbar/tests/browser/browser_customizeMode.js b/browser/components/urlbar/tests/browser/browser_customizeMode.js
new file mode 100644
index 0000000000..0ed26644cc
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_customizeMode.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test checks that the left/right arrow keys and home/end keys work in
+// the urlbar after customize mode starts and ends.
+
+"use strict";
+
+add_task(async function test() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await startCustomizing(win);
+ await endCustomizing(win);
+
+ let urlbar = win.gURLBar;
+
+ let value = "example";
+ urlbar.value = value;
+ urlbar.focus();
+ urlbar.selectionEnd = value.length;
+ urlbar.selectionStart = value.length;
+
+ // left
+ EventUtils.synthesizeKey("KEY_ArrowLeft", {}, win);
+ Assert.equal(urlbar.selectionStart, value.length - 1);
+ Assert.equal(urlbar.selectionEnd, value.length - 1);
+
+ // home
+ if (AppConstants.platform == "macosx") {
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { metaKey: true }, win);
+ } else {
+ EventUtils.synthesizeKey("KEY_Home", {}, win);
+ }
+ Assert.equal(urlbar.selectionStart, 0);
+ Assert.equal(urlbar.selectionEnd, 0);
+
+ // right
+ EventUtils.synthesizeKey("KEY_ArrowRight", {}, win);
+ Assert.equal(urlbar.selectionStart, 1);
+ Assert.equal(urlbar.selectionEnd, 1);
+
+ // end
+ if (AppConstants.platform == "macosx") {
+ EventUtils.synthesizeKey("KEY_ArrowRight", { metaKey: true }, win);
+ } else {
+ EventUtils.synthesizeKey("KEY_End", {}, win);
+ }
+ Assert.equal(urlbar.selectionStart, value.length);
+ Assert.equal(urlbar.selectionEnd, value.length);
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+async function startCustomizing(win = window) {
+ if (win.document.documentElement.getAttribute("customizing") != "true") {
+ let eventPromise = BrowserTestUtils.waitForEvent(
+ win.gNavToolbox,
+ "customizationready"
+ );
+ win.gCustomizeMode.enter();
+ await eventPromise;
+ }
+}
+
+async function endCustomizing(win = window) {
+ if (win.document.documentElement.getAttribute("customizing") == "true") {
+ let eventPromise = BrowserTestUtils.waitForEvent(
+ win.gNavToolbox,
+ "aftercustomization"
+ );
+ win.gCustomizeMode.exit();
+ await eventPromise;
+ }
+}
diff --git a/browser/components/urlbar/tests/browser/browser_cutting.js b/browser/components/urlbar/tests/browser/browser_cutting.js
new file mode 100644
index 0000000000..31b51751d9
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_cutting.js
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ gURLBar.focus();
+ gURLBar.inputField.value = "https://example.com/";
+ gURLBar.selectionStart = 4;
+ gURLBar.selectionEnd = 5;
+ goDoCommand("cmd_cut");
+ is(
+ gURLBar.inputField.value,
+ "http://example.com/",
+ "location bar value after cutting 's' from https"
+ );
+ gURLBar.handleRevert();
+}
diff --git a/browser/components/urlbar/tests/browser/browser_decode.js b/browser/components/urlbar/tests/browser/browser_decode.js
new file mode 100644
index 0000000000..ae0b4dfda1
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_decode.js
@@ -0,0 +1,144 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// This test makes sure (1) you can't break the urlbar by typing particular JSON
+// or JS fragments into it, (2) urlbar.textValue shows URLs unescaped, and (3)
+// the urlbar also shows the URLs embedded in action URIs unescaped. See bug
+// 1233672.
+
+add_task(async function injectJSON() {
+ let inputStrs = [
+ 'http://example.com/ ", "url": "bar',
+ "http://example.com/\\",
+ 'http://example.com/"',
+ 'http://example.com/","url":"evil.com',
+ "http://mozilla.org/\\u0020",
+ 'http://www.mozilla.org/","url":1e6,"some-key":"foo',
+ 'http://www.mozilla.org/","url":null,"some-key":"foo',
+ 'http://www.mozilla.org/","url":["foo","bar"],"some-key":"foo',
+ ];
+ for (let inputStr of inputStrs) {
+ await checkInput(inputStr);
+ }
+ gURLBar.value = "";
+ gURLBar.handleRevert();
+ gURLBar.blur();
+});
+
+add_task(function losslessDecode() {
+ let urlNoScheme = "example.com/\u30a2\u30a4\u30a6\u30a8\u30aa";
+ let url = "http://" + urlNoScheme;
+ const result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url }
+ );
+ gURLBar.setValueFromResult({ result });
+ // Since this is directly setting textValue, it is expected to be trimmed.
+ Assert.equal(
+ gURLBar.inputField.value,
+ urlNoScheme,
+ "The string displayed in the textbox should not be escaped"
+ );
+ gURLBar.value = "";
+ gURLBar.handleRevert();
+ gURLBar.blur();
+});
+
+add_task(async function actionURILosslessDecode() {
+ let urlNoScheme = "example.com/\u30a2\u30a4\u30a6\u30a8\u30aa";
+ let url = "http://" + urlNoScheme;
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: url,
+ });
+
+ // At this point the heuristic result is selected but the urlbar's value is
+ // simply `url`. Key down and back around until the heuristic result is
+ // selected again.
+ do {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ } while (UrlbarTestUtils.getSelectedRowIndex(window) != 0);
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.URL,
+ "Should have selected a result of URL type"
+ );
+
+ Assert.equal(
+ gURLBar.inputField.value,
+ urlNoScheme,
+ "The string displayed in the textbox should not be escaped"
+ );
+
+ gURLBar.value = "";
+ gURLBar.handleRevert();
+ gURLBar.blur();
+});
+
+add_task(async function test_resultsDisplayDecoded() {
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.formHistory.clear();
+
+ await PlacesTestUtils.addVisits("http://example.com/%E9%A1%B5");
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "example",
+ });
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(
+ result.displayed.url,
+ "http://example.com/\u9875",
+ "Should be displayed the correctly unescaped URL"
+ );
+});
+
+async function checkInput(inputStr) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: inputStr,
+ });
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+
+ // URL matches have their param.urls fixed up.
+ let fixupInfo = Services.uriFixup.getFixupURIInfo(
+ inputStr,
+ Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS |
+ Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP
+ );
+ let expectedVisitURL = fixupInfo.fixedURI.spec;
+
+ Assert.equal(result.url, expectedVisitURL, "Should have the correct URL");
+ Assert.equal(
+ result.title,
+ inputStr.replace("\\", "/"),
+ "Should have the correct title"
+ );
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.URL,
+ "Should have be a result of type URL"
+ );
+
+ Assert.equal(
+ result.displayed.title,
+ inputStr.replace("\\", "/"),
+ "Should be displaying the correct text"
+ );
+ let [action] = await document.l10n.formatValues([
+ { id: "urlbar-result-action-visit" },
+ ]);
+ Assert.equal(
+ result.displayed.action,
+ action,
+ "Should be displaying the correct action text"
+ );
+}
diff --git a/browser/components/urlbar/tests/browser/browser_delete.js b/browser/components/urlbar/tests/browser/browser_delete.js
new file mode 100644
index 0000000000..f4a883ea30
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_delete.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test deleting the start of urls works correctly.
+ */
+
+add_task(async function () {
+ let bm = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://bug1105244.example.com/",
+ title: "test",
+ });
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.bookmarks.remove(bm);
+ });
+
+ await BrowserTestUtils.withNewTab("about:blank", testDelete);
+});
+
+function sendHome() {
+ // unclear why VK_HOME doesn't work on Mac, but it doesn't...
+ if (AppConstants.platform == "macosx") {
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { metaKey: true });
+ } else {
+ EventUtils.synthesizeKey("KEY_Home");
+ }
+}
+
+function sendDelete() {
+ EventUtils.synthesizeKey("KEY_Delete");
+}
+
+async function testDelete() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "bug1105244",
+ });
+
+ // move to the start.
+ sendHome();
+
+ // delete the first few chars - each delete should operate on the input field.
+ await UrlbarTestUtils.promisePopupOpen(window, sendDelete);
+ Assert.equal(gURLBar.inputField.value, "ug1105244.example.com/");
+ sendDelete();
+ Assert.equal(gURLBar.inputField.value, "g1105244.example.com/");
+}
diff --git a/browser/components/urlbar/tests/browser/browser_deleteAllText.js b/browser/components/urlbar/tests/browser/browser_deleteAllText.js
new file mode 100644
index 0000000000..5b355fa477
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_deleteAllText.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test makes sure that deleting all text in the input doesn't mess up
+// subsequent searches.
+
+"use strict";
+
+add_task(async function test() {
+ await runTest();
+ // Setting suggest.topsites to false disables the view's autoOpen behavior,
+ // which changes this test's outcomes.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.topsites", false]],
+ });
+ info("Running the test with autoOpen disabled.");
+ await runTest();
+ await SpecialPowers.popPrefEnv();
+});
+
+async function runTest() {
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+ await PlacesTestUtils.addVisits([
+ "http://example.com/",
+ "http://mozilla.org/",
+ ]);
+
+ // Do an initial search for "x".
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "x",
+ fireInputEvent: true,
+ });
+ await checkResults();
+
+ await deleteInput();
+
+ // Type "x". A new search should start. Don't use
+ // promiseAutocompleteResultPopup, which has some logic that starts the search
+ // manually in certain conditions. We want to specifically check that the
+ // input event causes UrlbarInput to start a new search on its own. If it
+ // doesn't, then the test will hang here on promiseSearchComplete.
+ EventUtils.synthesizeKey("x");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await checkResults();
+
+ // Now repeat the backspace + x two more times. Same thing should happen.
+ for (let i = 0; i < 2; i++) {
+ await deleteInput();
+ EventUtils.synthesizeKey("x");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await checkResults();
+ }
+
+ await deleteInput();
+ // autoOpen opened the panel, so we need to close it.
+ gURLBar.view.close();
+}
+
+async function checkResults() {
+ Assert.equal(await UrlbarTestUtils.getResultCount(window), 2);
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(details.type, UrlbarUtils.RESULT_TYPE.SEARCH);
+ Assert.equal(details.searchParams.query, "x");
+ details = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(details.type, UrlbarUtils.RESULT_TYPE.URL);
+ Assert.equal(details.url, "http://example.com/");
+}
+
+async function deleteInput() {
+ if (UrlbarPrefs.get("suggest.topsites")) {
+ // The popup should remain open and show top sites.
+ while (gURLBar.value.length) {
+ EventUtils.synthesizeKey("KEY_Backspace");
+ }
+ Assert.ok(
+ gURLBar.view.isOpen,
+ "View should remain open when deleting all input text"
+ );
+ let queryContext = await UrlbarTestUtils.promiseSearchComplete(window);
+ Assert.notEqual(
+ queryContext.results.length,
+ 0,
+ "View should show results when deleting all input text"
+ );
+ Assert.equal(
+ queryContext.searchString,
+ "",
+ "Results should be for the empty search string (i.e. top sites) when deleting all input text"
+ );
+ } else {
+ // Deleting all text should close the view.
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ while (gURLBar.value.length) {
+ EventUtils.synthesizeKey("KEY_Backspace");
+ }
+ });
+ }
+}
diff --git a/browser/components/urlbar/tests/browser/browser_display_selectedAction_Extensions.js b/browser/components/urlbar/tests/browser/browser_display_selectedAction_Extensions.js
new file mode 100644
index 0000000000..d3a51ede76
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_display_selectedAction_Extensions.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests for the presence of selected action text "Extensions:" in the URL bar.
+ */
+
+add_task(async function testSwitchToTabTextDisplay() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ omnibox: {
+ keyword: "omniboxtest",
+ },
+
+ background() {
+ /* global browser */
+ browser.omnibox.setDefaultSuggestion({
+ description: "doit",
+ });
+ // Just do nothing for this test.
+ },
+ },
+ });
+
+ await extension.startup();
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "omniboxtest ",
+ fireInputEvent: true,
+ });
+
+ // The "Extension:" label appears after a key down followed by a key up
+ // back to the extension result.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+
+ // Checks to see if "Extension:" text in URL bar is visible
+ const extensionText = document.getElementById("urlbar-label-extension");
+ Assert.ok(BrowserTestUtils.is_visible(extensionText));
+ Assert.equal(extensionText.value, "Extension:");
+
+ // Check to see if all other labels are hidden
+ const allLabels = document.getElementById("urlbar-label-box").children;
+ for (let label of allLabels) {
+ if (label != extensionText) {
+ Assert.ok(BrowserTestUtils.is_hidden(label));
+ }
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+ await extension.unload();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_dns_first_for_single_words.js b/browser/components/urlbar/tests/browser/browser_dns_first_for_single_words.js
new file mode 100644
index 0000000000..a0aacc83d2
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_dns_first_for_single_words.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Checks that if browser.fixup.dns_first_for_single_words pref is set, we pass
+// the original search string to the docshell and not a search url.
+
+add_task(async function test() {
+ const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+ );
+ const sandbox = sinon.createSandbox();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.fixup.dns_first_for_single_words", true]],
+ });
+
+ registerCleanupFunction(sandbox.restore);
+
+ /**
+ * Tests the given search string.
+ *
+ * @param {string} str The search string
+ * @param {boolean} passthrough whether the value should be passed unchanged
+ * to the docshell that will first execute a DNS request.
+ */
+ async function testVal(str, passthrough) {
+ sandbox.stub(gURLBar, "_loadURL").callsFake(url => {
+ if (passthrough) {
+ Assert.equal(url, str, "Should pass the unmodified search string");
+ } else {
+ Assert.ok(url.startsWith("http"), "Should pass an url");
+ }
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: str,
+ });
+ EventUtils.synthesizeKey("KEY_Enter");
+ sandbox.restore();
+ }
+
+ await testVal("test", true);
+ await testVal("te-st", true);
+ await testVal("test ", true);
+ await testVal(" test", true);
+ await testVal(" test", true);
+ await testVal("test.test", true);
+ await testVal("test test", false);
+ // This is not a single word host, though it contains one. At a certain point
+ // we may evaluate to increase coverage of the feature to also ask for this.
+ await testVal("test/test", false);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_downArrowKeySearch.js b/browser/components/urlbar/tests/browser/browser_downArrowKeySearch.js
new file mode 100644
index 0000000000..2f9cc19983
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_downArrowKeySearch.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Checks that pressing the down arrow key starts the proper searches, depending
+// on the input value/state.
+
+"use strict";
+
+add_setup(async function () {
+ await PlacesUtils.history.clear();
+ // Enough vists to get this site into Top Sites.
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits("http://example.com/");
+ }
+
+ await updateTopSites(
+ sites => sites && sites[0] && sites[0].url == "http://example.com/"
+ );
+ registerCleanupFunction(async function () {
+ await PlacesUtils.history.clear();
+ });
+});
+
+add_task(async function url() {
+ await BrowserTestUtils.withNewTab("http://example.com/", async () => {
+ gURLBar.focus();
+ gURLBar.selectionEnd = gURLBar.untrimmedValue.length;
+ gURLBar.selectionStart = gURLBar.untrimmedValue.length;
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), 0);
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill);
+ Assert.equal(details.url, "http://example.com/");
+ Assert.equal(gURLBar.value, "example.com/");
+ await UrlbarTestUtils.promisePopupClose(window);
+ });
+});
+
+add_task(async function userTyping() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), 0);
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(details.type, UrlbarUtils.RESULT_TYPE.SEARCH);
+ Assert.ok(details.searchParams);
+ Assert.equal(details.searchParams.query, "foo");
+ Assert.equal(gURLBar.value, "foo");
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+add_task(async function empty() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), -1);
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(details.url, "http://example.com/");
+ Assert.equal(gURLBar.value, "");
+});
+
+add_task(async function new_window() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ win.gURLBar.focus();
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(win), -1);
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(win, 0);
+ Assert.equal(details.url, "http://example.com/");
+ Assert.equal(win.gURLBar.value, "");
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_dragdropURL.js b/browser/components/urlbar/tests/browser/browser_dragdropURL.js
new file mode 100644
index 0000000000..52c19e8965
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_dragdropURL.js
@@ -0,0 +1,106 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests for draging and dropping to the Urlbar.
+ */
+
+const TEST_URL = "data:text/html,a test page";
+
+add_task(async function test_setup() {
+ // Stop search-engine loads from hitting the network.
+ await SearchTestUtils.installSearchExtension({}, { setAsDefault: true });
+
+ registerCleanupFunction(async function cleanup() {
+ while (gBrowser.tabs.length > 1) {
+ BrowserTestUtils.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]);
+ }
+ });
+
+ CustomizableUI.addWidgetToArea("home-button", "nav-bar");
+ registerCleanupFunction(() =>
+ CustomizableUI.removeWidgetFromArea("home-button")
+ );
+});
+
+/**
+ * Simulates a drop on the URL bar input field.
+ * The drag source must be something different from the URL bar, so we pick the
+ * home button somewhat arbitrarily.
+ *
+ * @param {object} content a {type, data} object representing the DND content.
+ */
+function simulateURLBarDrop(content) {
+ EventUtils.synthesizeDrop(
+ document.getElementById("home-button"), // Dragstart element.
+ gURLBar.inputField, // Drop element.
+ [[content]], // Drag data.
+ "copy",
+ window
+ );
+}
+
+add_task(async function checkDragURL() {
+ await BrowserTestUtils.withNewTab(TEST_URL, function (browser) {
+ info("Check dragging a normal url to the urlbar");
+ const DRAG_URL = "http://www.example.com/";
+ simulateURLBarDrop({ type: "text/plain", data: DRAG_URL });
+ Assert.equal(
+ gURLBar.value,
+ TEST_URL,
+ "URL bar value should not have changed"
+ );
+ Assert.equal(
+ gBrowser.selectedBrowser.userTypedValue,
+ null,
+ "Stored URL bar value should not have changed"
+ );
+ });
+});
+
+add_task(async function checkDragForbiddenURL() {
+ await BrowserTestUtils.withNewTab(TEST_URL, function (browser) {
+ // See also browser_removeUnsafeProtocolsFromURLBarPaste.js for other
+ // examples. In general we trust that function, we pick some testcases to
+ // ensure we disallow dropping trimmed text.
+ for (let url of [
+ "chrome://browser/content/aboutDialog.xhtml",
+ "file:///",
+ "javascript:",
+ "javascript:void(0)",
+ "java\r\ns\ncript:void(0)",
+ " javascript:void(0)",
+ "\u00A0java\nscript:void(0)",
+ "javascript:document.domain",
+ "javascript:javascript:alert('hi!')",
+ ]) {
+ info(`Check dragging "{$url}" to the URL bar`);
+ simulateURLBarDrop({ type: "text/plain", data: url });
+ Assert.notEqual(
+ gURLBar.value,
+ url,
+ `Shouldn't be allowed to drop ${url} on URL bar`
+ );
+ }
+ });
+});
+
+add_task(async function checkDragText() {
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ info("Check dragging multi word text to the urlbar");
+ const TEXT = "Firefox is awesome";
+ const TEXT_URL = "https://example.com/?q=Firefox+is+awesome";
+ let promiseLoad = BrowserTestUtils.browserLoaded(browser, false, TEXT_URL);
+ simulateURLBarDrop({ type: "text/plain", data: TEXT });
+ await promiseLoad;
+
+ info("Check dragging single word text to the urlbar");
+ const WORD = "Firefox";
+ const WORD_URL = "https://example.com/?q=Firefox";
+ promiseLoad = BrowserTestUtils.browserLoaded(browser, false, WORD_URL);
+ simulateURLBarDrop({ type: "text/plain", data: WORD });
+ await promiseLoad;
+ });
+});
diff --git a/browser/components/urlbar/tests/browser/browser_dynamicResults.js b/browser/components/urlbar/tests/browser/browser_dynamicResults.js
new file mode 100644
index 0000000000..a4e9013be5
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_dynamicResults.js
@@ -0,0 +1,799 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This tests dynamic results.
+ */
+
+"use strict";
+
+const DYNAMIC_TYPE_NAME = "test";
+
+const DYNAMIC_TYPE_VIEW_TEMPLATE = {
+ stylesheet: getRootDirectory(gTestPath) + "dynamicResult0.css",
+ children: [
+ {
+ name: "selectable",
+ tag: "span",
+ attributes: {
+ selectable: "true",
+ },
+ },
+ {
+ name: "text",
+ tag: "span",
+ },
+ {
+ name: "buttonBox",
+ tag: "span",
+ children: [
+ {
+ name: "button1",
+ tag: "span",
+ attributes: {
+ role: "button",
+ attribute_to_remove: "value",
+ },
+ },
+ {
+ name: "button2",
+ tag: "span",
+ attributes: {
+ role: "button",
+ },
+ },
+ ],
+ },
+ ],
+};
+
+const DUMMY_PAGE =
+ "http://example.com/browser/browser/base/content/test/general/dummy_page.html";
+
+// Tests the dynamic type registration functions and stylesheet loading.
+add_task(async function registration() {
+ // Get our test stylesheet URIs.
+ let stylesheetURIs = [];
+ for (let i = 0; i < 2; i++) {
+ stylesheetURIs.push(
+ Services.io.newURI(getRootDirectory(gTestPath) + `dynamicResult${i}.css`)
+ );
+ }
+
+ // Maps from dynamic type names to their type.
+ let viewTemplatesByName = {
+ foo: {
+ stylesheet: stylesheetURIs[0].spec,
+ children: [
+ {
+ name: "text",
+ tag: "span",
+ },
+ ],
+ },
+ bar: {
+ stylesheet: stylesheetURIs[1].spec,
+ children: [
+ {
+ name: "icon",
+ tag: "span",
+ },
+ {
+ name: "button",
+ tag: "span",
+ attributes: {
+ role: "button",
+ },
+ },
+ ],
+ },
+ };
+
+ // First, open another window so that multiple windows are open when we add
+ // the types so we can verify below that the stylesheets are added to all open
+ // windows.
+ let newWindows = [];
+ newWindows.push(await BrowserTestUtils.openNewBrowserWindow());
+
+ // Add the test dynamic types.
+ for (let [name, viewTemplate] of Object.entries(viewTemplatesByName)) {
+ UrlbarResult.addDynamicResultType(name);
+ UrlbarView.addDynamicViewTemplate(name, viewTemplate);
+ }
+
+ // Get them back to make sure they were added.
+ for (let name of Object.keys(viewTemplatesByName)) {
+ let actualType = UrlbarResult.getDynamicResultType(name);
+ // Types are currently just empty objects.
+ Assert.deepEqual(actualType, {}, "Types should match");
+ }
+
+ // Their stylesheets should have been applied to all open windows. There's no
+ // good way to check this because:
+ //
+ // * nsIStyleSheetService has a function that returns whether a stylesheet has
+ // been loaded, but it's global and not per window.
+ // * nsIDOMWindowUtils has functions to load stylesheets but not one to check
+ // whether a stylesheet has been loaded.
+ // * document.stylesheets only contains stylesheets in the DOM.
+ //
+ // So instead we set a CSS variable on #urlbar in each of our stylesheets and
+ // check that it's present.
+ function getCSSVariables(windows) {
+ let valuesByWindow = new Map();
+ for (let win of windows) {
+ let values = [];
+ valuesByWindow.set(window, values);
+ for (let i = 0; i < stylesheetURIs.length; i++) {
+ let value = win
+ .getComputedStyle(gURLBar.panel)
+ .getPropertyValue(`--testDynamicResult${i}`);
+ values.push((value || "").trim());
+ }
+ }
+ return valuesByWindow;
+ }
+ function checkCSSVariables(windows) {
+ for (let values of getCSSVariables(windows).values()) {
+ for (let i = 0; i < stylesheetURIs.length; i++) {
+ if (values[i].trim() !== `ok${i}`) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ // The stylesheets are loaded asyncly, so we need to poll for it.
+ await TestUtils.waitForCondition(() =>
+ checkCSSVariables(BrowserWindowTracker.orderedWindows)
+ );
+ Assert.ok(true, "Stylesheets loaded in all open windows");
+
+ // Open another window to make sure the stylesheets are loaded in it after we
+ // added the new dynamic types.
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+ newWindows.push(newWin);
+ await TestUtils.waitForCondition(() => checkCSSVariables([newWin]));
+ Assert.ok(true, "Stylesheets loaded in new window");
+
+ // Remove the dynamic types.
+ for (let name of Object.keys(viewTemplatesByName)) {
+ UrlbarView.removeDynamicViewTemplate(name);
+ UrlbarResult.removeDynamicResultType(name);
+ let actualType = UrlbarResult.getDynamicResultType(name);
+ Assert.equal(actualType, null, "Type should be unregistered");
+ }
+
+ // The stylesheets should be removed from all windows.
+ let valuesByWindow = getCSSVariables(BrowserWindowTracker.orderedWindows);
+ for (let values of valuesByWindow.values()) {
+ for (let i = 0; i < stylesheetURIs.length; i++) {
+ Assert.ok(!values[i], "Stylesheet should be removed");
+ }
+ }
+
+ // Close the new windows.
+ for (let win of newWindows) {
+ await BrowserTestUtils.closeWindow(win);
+ }
+});
+
+// Tests that the view is created correctly from the view template.
+add_task(async function viewCreated() {
+ await withDynamicTypeProvider(async () => {
+ // Do a search.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ waitForFocus: SimpleTest.waitForFocus,
+ });
+
+ // Get the row.
+ let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1);
+ Assert.equal(
+ row.result.type,
+ UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ "row.result.type"
+ );
+ Assert.equal(
+ row.getAttribute("dynamicType"),
+ DYNAMIC_TYPE_NAME,
+ "row[dynamicType]"
+ );
+ let inner = row.querySelector(".urlbarView-row-inner");
+ Assert.ok(inner, ".urlbarView-row-inner should exist");
+
+ // Check the DOM.
+ checkDOM(inner, DYNAMIC_TYPE_VIEW_TEMPLATE.children);
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ });
+});
+
+// Tests that the view is updated correctly.
+async function checkViewUpdated(provider) {
+ await withDynamicTypeProvider(async () => {
+ // Test a few different search strings. The dynamic result view will be
+ // updated to reflect the current string.
+ for (let searchString of ["test", "some other string", "and another"]) {
+ // Do a search.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: searchString,
+ waitForFocus: SimpleTest.waitForFocus,
+ });
+
+ let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1);
+ let text = row.querySelector(
+ `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-text`
+ );
+
+ // The view's call to provider.getViewUpdate is async, so we need to make
+ // sure the update has been applied before continuing to avoid
+ // intermittent failures.
+ await TestUtils.waitForCondition(
+ () => text.getAttribute("searchString") == searchString
+ );
+
+ // The "searchString" attribute of these elements should be updated.
+ let elementNames = ["selectable", "text", "button1", "button2"];
+ for (let name of elementNames) {
+ let element = row.querySelector(
+ `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-${name}`
+ );
+ Assert.equal(
+ element.getAttribute("searchString"),
+ searchString,
+ 'element.getAttribute("searchString")'
+ );
+ }
+
+ let button1 = row.querySelector(
+ `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-button1`
+ );
+
+ Assert.equal(
+ button1.hasAttribute("attribute_to_remove"),
+ false,
+ "Attribute should be removed"
+ );
+
+ // text.textContent should be updated.
+ Assert.equal(
+ text.textContent,
+ `result.payload.searchString is: ${searchString}`,
+ "text.textContent"
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ }
+ }, provider);
+}
+
+add_task(async function checkViewUpdatedPlain() {
+ await checkViewUpdated(new TestProvider());
+});
+
+add_task(async function checkViewUpdatedWDynamicViewTemplate() {
+ /**
+ * A dummy provider that provides the viewTemplate dynamically.
+ */
+ class TestShouldCallGetViewTemplateProvider extends TestProvider {
+ getViewTemplateWasCalled = false;
+
+ getViewTemplate() {
+ this.getViewTemplateWasCalled = true;
+ return DYNAMIC_TYPE_VIEW_TEMPLATE;
+ }
+ }
+
+ let provider = new TestShouldCallGetViewTemplateProvider();
+ Assert.ok(
+ !provider.getViewTemplateWasCalled,
+ "getViewTemplate has not yet been called for the provider"
+ );
+ Assert.ok(
+ !UrlbarView.dynamicViewTemplatesByName.get(DYNAMIC_TYPE_NAME),
+ "No template has been registered"
+ );
+ await checkViewUpdated(provider);
+ Assert.ok(
+ provider.getViewTemplateWasCalled,
+ "getViewTemplate was called for the provider"
+ );
+});
+
+// Tests that selection correctly moves through buttons and selectables in a
+// dynamic result.
+add_task(async function selection() {
+ await withDynamicTypeProvider(async () => {
+ // Add a visit so we have at least one result after the dynamic result.
+ await PlacesUtils.history.clear();
+ await PlacesTestUtils.addVisits("http://example.com/test");
+
+ // Do a search.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ waitForFocus: SimpleTest.waitForFocus,
+ });
+
+ // Sanity check that the dynamic result is at index 1.
+ let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1);
+ Assert.equal(
+ row.result.type,
+ UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ "row.result.type"
+ );
+
+ // The heuristic result will be selected. TAB from the heuristic through
+ // all the selectable elements in the dynamic result.
+ let selectables = ["selectable", "button1", "button2"];
+ for (let name of selectables) {
+ let element = row.querySelector(
+ `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-${name}`
+ );
+ Assert.ok(element, "Sanity check element");
+ EventUtils.synthesizeKey("KEY_Tab");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElement(window),
+ element,
+ `Selected element: ${name}`
+ );
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 1,
+ "Row at index 1 selected"
+ );
+ Assert.equal(UrlbarTestUtils.getSelectedRow(window), row, "Row selected");
+ }
+
+ // TAB again to select the result after the dynamic result.
+ EventUtils.synthesizeKey("KEY_Tab");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 2,
+ "Row at index 2 selected"
+ );
+ Assert.notEqual(
+ UrlbarTestUtils.getSelectedRow(window),
+ row,
+ "Row is not selected"
+ );
+
+ // SHIFT+TAB back through the dynamic result.
+ for (let name of selectables.reverse()) {
+ let element = row.querySelector(
+ `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-${name}`
+ );
+ Assert.ok(element, "Sanity check element");
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElement(window),
+ element,
+ `Selected element: ${name}`
+ );
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 1,
+ "Row at index 1 selected"
+ );
+ Assert.equal(UrlbarTestUtils.getSelectedRow(window), row, "Row selected");
+ }
+
+ // SHIFT+TAB again to select the heuristic result.
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 0,
+ "Row at index 0 selected"
+ );
+ Assert.notEqual(
+ UrlbarTestUtils.getSelectedRow(window),
+ row,
+ "Row is not selected"
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ await PlacesUtils.history.clear();
+ });
+});
+
+// Tests picking elements in a dynamic result.
+add_task(async function pick() {
+ await withDynamicTypeProvider(async provider => {
+ let selectables = ["selectable", "button1", "button2"];
+ for (let i = 0; i < selectables.length; i++) {
+ let selectable = selectables[i];
+
+ // Do a search.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ waitForFocus: SimpleTest.waitForFocus,
+ });
+
+ // Sanity check that the dynamic result is at index 1.
+ let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1);
+ Assert.equal(
+ row.result.type,
+ UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ "row.result.type"
+ );
+
+ // The heuristic result will be selected. TAB from the heuristic
+ // to the selectable element.
+ let element = row.querySelector(
+ `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-${selectable}`
+ );
+ Assert.ok(element, "Sanity check element");
+ EventUtils.synthesizeKey("KEY_Tab", { repeat: i + 1 });
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElement(window),
+ element,
+ `Selected element: ${name}`
+ );
+
+ // Pick the element.
+ let pickPromise = provider.promisePick();
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Enter")
+ );
+ let [result, pickedElement] = await pickPromise;
+ Assert.equal(result, row.result, "Picked result");
+ Assert.equal(pickedElement, element, "Picked element");
+ }
+ });
+});
+
+// Tests picking elements in a dynamic result.
+add_task(async function shouldNavigate() {
+ /**
+ * A dummy provider that providers results with a `shouldNavigate` property.
+ */
+ class TestShouldNavigateProvider extends TestProvider {
+ /**
+ * @param {object} context - Data regarding the context of the query.
+ * @param {Function} addCallback - Function to add a result to the query.
+ */
+ async startQuery(context, addCallback) {
+ for (let result of this._results) {
+ result.payload.searchString = context.searchString;
+ result.payload.shouldNavigate = true;
+ result.payload.url = DUMMY_PAGE;
+ addCallback(this, result);
+ }
+ }
+ }
+
+ await withDynamicTypeProvider(async provider => {
+ // Do a search.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ waitForFocus: SimpleTest.waitForFocus,
+ });
+
+ // Sanity check that the dynamic result is at index 1.
+ let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1);
+ Assert.equal(
+ row.result.type,
+ UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ "row.result.type"
+ );
+
+ // The heuristic result will be selected. TAB from the heuristic
+ // to the selectable element.
+ let element = row.querySelector(
+ `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-selectable`
+ );
+ Assert.ok(element, "Sanity check element");
+ EventUtils.synthesizeKey("KEY_Tab", { repeat: 1 });
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElement(window),
+ element,
+ `Selected element: ${name}`
+ );
+
+ // Pick the element.
+ let pickPromise = provider.promisePick();
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Enter")
+ );
+ // Verify that onEngagement was still called.
+ let [result, pickedElement] = await pickPromise;
+ Assert.equal(result, row.result, "Picked result");
+ Assert.equal(pickedElement, element, "Picked element");
+
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ is(
+ gBrowser.currentURI.spec,
+ DUMMY_PAGE,
+ "We navigated to payload.url when result selected"
+ );
+
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, "about:home");
+ await BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ "about:home"
+ );
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ waitForFocus: SimpleTest.waitForFocus,
+ });
+
+ row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1);
+ element = row.querySelector(
+ `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-selectable`
+ );
+
+ pickPromise = provider.promisePick();
+ EventUtils.synthesizeMouseAtCenter(element, {});
+ [result, pickedElement] = await pickPromise;
+ Assert.equal(result, row.result, "Picked result");
+ Assert.equal(pickedElement, element, "Picked element");
+
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ is(
+ gBrowser.currentURI.spec,
+ DUMMY_PAGE,
+ "We navigated to payload.url when result is clicked"
+ );
+ }, new TestShouldNavigateProvider());
+});
+
+// Tests applying highlighting to a dynamic result.
+add_task(async function highlighting() {
+ /**
+ * Provides a dynamic result with highlighted text.
+ */
+ class TestHighlightProvider extends TestProvider {
+ startQuery(context, addCallback) {
+ let result = Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ ...UrlbarResult.payloadAndSimpleHighlights(context.tokens, {
+ dynamicType: DYNAMIC_TYPE_NAME,
+ text: ["Test title", UrlbarUtils.HIGHLIGHT.SUGGESTED],
+ })
+ ),
+ { suggestedIndex: 1 }
+ );
+ addCallback(this, result);
+ }
+
+ getViewUpdate(result, idsByName) {
+ return {};
+ }
+ }
+
+ // Test that highlighting is applied.
+ await withDynamicTypeProvider(async () => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ waitForFocus: SimpleTest.waitForFocus,
+ });
+
+ let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1);
+ Assert.equal(
+ row.result.type,
+ UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ "row.result.type"
+ );
+ let parentTextNode = row.querySelector(
+ `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-text`
+ );
+ let highlightedTextNode = row.querySelector(
+ `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-text > strong`
+ );
+ Assert.equal(parentTextNode.firstChild.textContent, "Test");
+ Assert.equal(
+ highlightedTextNode.textContent,
+ " title",
+ "The highlighting was applied successfully."
+ );
+ }, new TestHighlightProvider());
+
+ /**
+ * Provides a dynamic result with highlighted text that is then overridden.
+ */
+ class TestHighlightProviderOveridden extends TestHighlightProvider {
+ getViewUpdate(result, idsByName) {
+ return {
+ text: {
+ textContent: "Test title",
+ },
+ };
+ }
+ }
+
+ // Test that highlighting is not applied when overridden from getViewUpdate.
+ await withDynamicTypeProvider(async () => {
+ // Do a search.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ waitForFocus: SimpleTest.waitForFocus,
+ });
+
+ let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1);
+ Assert.equal(
+ row.result.type,
+ UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ "row.result.type"
+ );
+ let parentTextNode = row.querySelector(
+ `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-text`
+ );
+ let highlightedTextNode = row.querySelector(
+ `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-text > strong`
+ );
+ Assert.equal(
+ parentTextNode.firstChild.textContent,
+ "Test title",
+ "No highlighting was applied"
+ );
+ Assert.ok(!highlightedTextNode, "The <strong> child node was deleted.");
+ }, new TestHighlightProviderOveridden());
+});
+
+/**
+ * Provides a dynamic result.
+ */
+class TestProvider extends UrlbarTestUtils.TestProvider {
+ constructor() {
+ super({
+ results: [
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ dynamicType: DYNAMIC_TYPE_NAME,
+ }
+ ),
+ { suggestedIndex: 1 }
+ ),
+ ],
+ });
+ }
+
+ async startQuery(context, addCallback) {
+ for (let result of this._results) {
+ result.payload.searchString = context.searchString;
+ addCallback(this, result);
+ }
+ }
+
+ getViewUpdate(result, idsByName) {
+ for (let child of DYNAMIC_TYPE_VIEW_TEMPLATE.children) {
+ Assert.ok(idsByName.get(child.name), `idsByName contains ${child.name}`);
+ }
+
+ return {
+ selectable: {
+ textContent: "Selectable",
+ attributes: {
+ searchString: result.payload.searchString,
+ },
+ },
+ text: {
+ textContent: `result.payload.searchString is: ${result.payload.searchString}`,
+ attributes: {
+ searchString: result.payload.searchString,
+ },
+ },
+ button1: {
+ textContent: "Button 1",
+ attributes: {
+ searchString: result.payload.searchString,
+ attribute_to_remove: null,
+ },
+ },
+ button2: {
+ textContent: "Button 2",
+ attributes: {
+ searchString: result.payload.searchString,
+ },
+ },
+ };
+ }
+
+ onEngagement(isPrivate, state, queryContext, details) {
+ if (this._pickPromiseResolve) {
+ let { result, element } = details;
+ this._pickPromiseResolve([result, element]);
+ delete this._pickPromiseResolve;
+ delete this._pickPromise;
+ }
+ }
+
+ promisePick() {
+ this._pickPromise = new Promise(resolve => {
+ this._pickPromiseResolve = resolve;
+ });
+ return this._pickPromise;
+ }
+}
+
+/**
+ * Provides a dynamic result.
+ *
+ * @param {object} callback - Function that runs the body of the test.
+ * @param {object} provider - The dummy provider to use.
+ */
+async function withDynamicTypeProvider(
+ callback,
+ provider = new TestProvider()
+) {
+ // Add a dynamic result type.
+ UrlbarResult.addDynamicResultType(DYNAMIC_TYPE_NAME);
+ if (!provider.getViewTemplate) {
+ UrlbarView.addDynamicViewTemplate(
+ DYNAMIC_TYPE_NAME,
+ DYNAMIC_TYPE_VIEW_TEMPLATE
+ );
+ }
+
+ // Add a provider of the dynamic type.
+ UrlbarProvidersManager.registerProvider(provider);
+
+ await callback(provider);
+
+ // Clean up.
+ UrlbarProvidersManager.unregisterProvider(provider);
+ if (!provider.getViewTemplate) {
+ UrlbarView.removeDynamicViewTemplate(DYNAMIC_TYPE_NAME);
+ }
+ UrlbarResult.removeDynamicResultType(DYNAMIC_TYPE_NAME);
+}
+
+function checkDOM(parentNode, expectedChildren) {
+ info(
+ `checkDOM: Checking parentNode id=${parentNode.id} className=${parentNode.className}`
+ );
+ for (let i = 0; i < expectedChildren.length; i++) {
+ let child = expectedChildren[i];
+ let actualChild = parentNode.children[i];
+ info(`checkDOM: Checking expected child: ${JSON.stringify(child)}`);
+ Assert.ok(actualChild, "actualChild should exist");
+ Assert.equal(actualChild.tagName, child.tag, "child.tag");
+ Assert.equal(actualChild.getAttribute("name"), child.name, "child.name");
+ Assert.ok(
+ actualChild.classList.contains(
+ `urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-${child.name}`
+ ),
+ "child.name should be in classList"
+ );
+ // We have to use startsWith/endsWith since the middle of the ID is a random
+ // number.
+ Assert.ok(actualChild.id.startsWith("urlbarView-row-"));
+ Assert.ok(
+ actualChild.id.endsWith(child.name),
+ "The child was assigned the correct ID."
+ );
+ for (let [name, value] of Object.entries(child.attributes || {})) {
+ if (name == "attribute_to_remove") {
+ Assert.equal(
+ actualChild.hasAttribute(name),
+ false,
+ `attribute: ${name}`
+ );
+ continue;
+ }
+ Assert.equal(actualChild.getAttribute(name), value, `attribute: ${name}`);
+ }
+ for (let name of child.classList || []) {
+ Assert.ok(actualChild.classList.contains(name), `classList: ${name}`);
+ }
+ if (child.children) {
+ checkDOM(actualChild, child.children);
+ }
+ }
+}
diff --git a/browser/components/urlbar/tests/browser/browser_edit_invalid_url.js b/browser/components/urlbar/tests/browser/browser_edit_invalid_url.js
new file mode 100644
index 0000000000..5a710c1285
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_edit_invalid_url.js
@@ -0,0 +1,91 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Checks that we trim invalid urls when they are selected, so that if the user
+// modifies the selected url, or just closes the results pane, we do a visit
+// rather than searching for the trimmed string.
+
+const url = BrowserUIUtils.trimURLProtocol + "invalid.somehost/mytest";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.trimURLs", true]],
+ });
+ await PlacesTestUtils.addVisits(url);
+ registerCleanupFunction(PlacesUtils.history.clear);
+});
+
+add_task(async function test_escape() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "invalid",
+ });
+ // Look for our result.
+ let resultCount = UrlbarTestUtils.getResultCount(window);
+ Assert.greater(resultCount, 1, "There should be at least two results");
+ for (let i = 0; i < resultCount; i++) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ info(`Result at ${i} has url ${result.url}`);
+ if (result.url.startsWith(url)) {
+ break;
+ }
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+ Assert.equal(
+ gURLBar.value,
+ url,
+ "The string displayed in the textbox should be the untrimmed url"
+ );
+ // Close the results pane by ESC.
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeKey("KEY_Escape");
+ });
+ // Confirm the result and check the loaded page.
+ let promise = waitforLoadURL();
+ EventUtils.synthesizeKey("KEY_Enter");
+ let loadedUrl = await promise;
+ Assert.equal(loadedUrl, url, "Should try to load a url");
+});
+
+add_task(async function test_edit_url() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "invalid",
+ });
+ // Look for our result.
+ let resultCount = UrlbarTestUtils.getResultCount(window);
+ Assert.greater(resultCount, 1, "There should be at least two results");
+ for (let i = 1; i < resultCount; i++) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ info(`Result at ${i} has url ${result.url}`);
+ if (result.url.startsWith(url)) {
+ break;
+ }
+ }
+ Assert.equal(
+ gURLBar.value,
+ url,
+ "The string displayed in the textbox should be the untrimmed url"
+ );
+ // Modify the url.
+ EventUtils.synthesizeKey("2");
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.URL, "Should visit a url");
+ Assert.equal(result.url, url + "2", "Should visit the modified url");
+
+ // Confirm the result and check the loaded page.
+ let promise = waitforLoadURL();
+ EventUtils.synthesizeKey("KEY_Enter");
+ let loadedUrl = await promise;
+ Assert.equal(loadedUrl, url + "2", "Should try to load the modified url");
+});
+
+async function waitforLoadURL() {
+ let sandbox = sinon.createSandbox();
+ let loadedUrl = await new Promise(resolve =>
+ sandbox.stub(gURLBar, "_loadURL").callsFake(resolve)
+ );
+ sandbox.restore();
+ return loadedUrl;
+}
diff --git a/browser/components/urlbar/tests/browser/browser_engagement.js b/browser/components/urlbar/tests/browser/browser_engagement.js
new file mode 100644
index 0000000000..b964a61a75
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_engagement.js
@@ -0,0 +1,206 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests the UrlbarProvider.onEngagement() method.
+
+"use strict";
+
+add_task(async function abandonment() {
+ await doTest({
+ expectedEndState: "abandonment",
+ endEngagement: async () => {
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+ },
+ });
+});
+
+add_task(async function engagement() {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await doTest({
+ expectedEndState: "engagement",
+ endEngagement: async () => {
+ let result, element;
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ result = gURLBar.view.selectedResult;
+ element = gURLBar.view.selectedElement;
+ EventUtils.synthesizeKey("KEY_Enter");
+ });
+ return { result, element };
+ },
+ expectedEndDetails: {
+ selIndex: 0,
+ selType: "history",
+ provider: "",
+ searchSource: "urlbar",
+ isSessionOngoing: false,
+ },
+ });
+ });
+});
+
+add_task(async function privateWindow_abandonment() {
+ let win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+ await doTest({
+ win,
+ expectedEndState: "abandonment",
+ expectedIsPrivate: true,
+ endEngagement: async () => {
+ await UrlbarTestUtils.promisePopupClose(win, () => win.gURLBar.blur());
+ },
+ });
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function privateWindow_engagement() {
+ let win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+ await doTest({
+ win,
+ expectedEndState: "engagement",
+ expectedIsPrivate: true,
+ endEngagement: async () => {
+ let result, element;
+ await UrlbarTestUtils.promisePopupClose(win, () => {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ result = win.gURLBar.view.selectedResult;
+ element = win.gURLBar.view.selectedElement;
+ EventUtils.synthesizeKey("KEY_Enter", {}, win);
+ });
+ return { result, element };
+ },
+ expectedEndDetails: {
+ selIndex: 0,
+ selType: "history",
+ provider: "",
+ searchSource: "urlbar",
+ isSessionOngoing: false,
+ },
+ });
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Performs an engagement test.
+ *
+ * @param {object} options
+ * Options object.
+ * @param {string} options.expectedEndState
+ * The expected state at the end of the engagement.
+ * @param {Function} options.endEngagement
+ * A function that should end the engagement. If the expected end state is
+ * "engagement", the function should return `{ result, element }` with the
+ * expected engaged result and element.
+ * @param {window} [options.win]
+ * The window to perform the test in.
+ * @param {boolean} [options.expectedIsPrivate]
+ * Whether the engagement and query context are expected to be private.
+ * @param {object} [options.expectedEndDetails]
+ * The expected `details` at the end of the engagement. `searchString` is
+ * automatically included since it's always present. If `provider` is
+ * expected, then include it and set it to any value; this function will
+ * replace it with the name of the test provider.
+ */
+async function doTest({
+ expectedEndState,
+ endEngagement,
+ win = window,
+ expectedIsPrivate = false,
+ expectedEndDetails = {},
+}) {
+ let provider = new TestProvider();
+ UrlbarProvidersManager.registerProvider(provider);
+
+ let startPromise = provider.promiseEngagement();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "test",
+ fireInputEvent: true,
+ });
+
+ let [isPrivate, state, queryContext, details] = await startPromise;
+ Assert.equal(isPrivate, expectedIsPrivate, "Start isPrivate");
+ Assert.equal(state, "start", "Start state");
+
+ // `queryContext` isn't always defined for `start`, and `onEngagement`
+ // shouldn't rely on it being defined on start, but there's no good reason to
+ // assert that it's not defined here.
+
+ // Similarly, `details` is never defined for `start`, but there's no good
+ // reason to assert that it's not defined.
+
+ let endPromise = provider.promiseEngagement();
+ let { result, element } = (await endEngagement()) ?? {};
+
+ [isPrivate, state, queryContext, details] = await endPromise;
+ Assert.equal(isPrivate, expectedIsPrivate, "End isPrivate");
+ Assert.equal(state, expectedEndState, "End state");
+ Assert.ok(queryContext, "End queryContext");
+ Assert.equal(
+ queryContext.isPrivate,
+ expectedIsPrivate,
+ "End queryContext.isPrivate"
+ );
+
+ let detailsDefaults = {
+ searchString: "test",
+ searchSource: "urlbar",
+ provider: undefined,
+ selIndex: -1,
+ };
+ if ("provider" in expectedEndDetails) {
+ detailsDefaults.provider = provider.name;
+ delete expectedEndDetails.provider;
+ }
+
+ if (expectedEndState == "engagement") {
+ Assert.ok(
+ result,
+ "endEngagement() should have returned the expected engaged result"
+ );
+ Assert.ok(
+ element,
+ "endEngagement() should have returned the expected engaged element"
+ );
+ expectedEndDetails.result = result;
+ expectedEndDetails.element = element;
+ }
+
+ Assert.deepEqual(
+ details,
+ Object.assign(detailsDefaults, expectedEndDetails),
+ "End details"
+ );
+
+ UrlbarProvidersManager.unregisterProvider(provider);
+}
+
+/**
+ * Test provider that resolves promises when onEngagement is called.
+ */
+class TestProvider extends UrlbarTestUtils.TestProvider {
+ _resolves = [];
+
+ constructor() {
+ super({
+ priority: Infinity,
+ results: [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://example.com/" }
+ ),
+ ],
+ });
+ }
+
+ onEngagement(...args) {
+ let resolve = this._resolves.shift();
+ if (resolve) {
+ resolve(args);
+ }
+ }
+
+ promiseEngagement() {
+ return new Promise(resolve => this._resolves.push(resolve));
+ }
+}
diff --git a/browser/components/urlbar/tests/browser/browser_enter.js b/browser/components/urlbar/tests/browser/browser_enter.js
new file mode 100644
index 0000000000..de1cda7cc1
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_enter.js
@@ -0,0 +1,331 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_VALUE = "example.com/\xF7?\xF7";
+const START_VALUE = "example.com/%C3%B7?%C3%B7";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.quickactions", false]],
+ });
+ const engine = await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + "searchSuggestionEngine.xml",
+ setAsDefault: true,
+ });
+ engine.alias = "@default";
+});
+
+add_task(async function returnKeypress() {
+ info("Simple return keypress");
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, START_VALUE);
+
+ gURLBar.focus();
+ EventUtils.synthesizeKey("KEY_Enter");
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ // Check url bar and selected tab.
+ is(
+ gURLBar.value,
+ TEST_VALUE,
+ "Urlbar should preserve the value on return keypress"
+ );
+ is(gBrowser.selectedTab, tab, "New URL was loaded in the current tab");
+
+ // Cleanup.
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function altReturnKeypress() {
+ info("Alt+Return keypress");
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, START_VALUE);
+
+ let tabOpenPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabOpen"
+ );
+ gURLBar.focus();
+ EventUtils.synthesizeKey("KEY_Enter", { altKey: true });
+
+ // wait for the new tab to appear.
+ await tabOpenPromise;
+
+ // Check url bar and selected tab.
+ is(
+ gURLBar.value,
+ TEST_VALUE,
+ "Urlbar should preserve the value on return keypress"
+ );
+ isnot(gBrowser.selectedTab, tab, "New URL was loaded in a new tab");
+
+ // Cleanup.
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function altGrReturnKeypress() {
+ info("AltGr+Return keypress");
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, START_VALUE);
+
+ let tabOpenPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabOpen"
+ );
+ gURLBar.focus();
+ EventUtils.synthesizeKey("KEY_Enter", { altGraphKey: true });
+
+ // wait for the new tab to appear.
+ await tabOpenPromise;
+
+ // Check url bar and selected tab.
+ is(
+ gURLBar.value,
+ TEST_VALUE,
+ "Urlbar should preserve the value on return keypress"
+ );
+ isnot(gBrowser.selectedTab, tab, "New URL was loaded in a new tab");
+
+ // Cleanup.
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function searchOnEnterNoPick() {
+ info("Search on Enter without picking a urlbar result");
+ await SpecialPowers.pushPrefEnv({
+ // The test checks that the untrimmed value is equal to the spec.
+ // When using showSearchTerms, the untrimmed value becomes
+ // the search terms.
+ set: [["browser.urlbar.showSearchTerms.featureGate", false]],
+ });
+
+ // Why is BrowserTestUtils.openNewForegroundTab not causing the bug?
+ let promiseTabOpened = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabOpen"
+ );
+ EventUtils.synthesizeMouseAtCenter(gBrowser.tabContainer.newTabButton, {});
+ let openEvent = await promiseTabOpened;
+ let tab = openEvent.target;
+
+ let loadPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ null,
+ true
+ );
+ gURLBar.focus();
+ gURLBar.value = "test test";
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+
+ Assert.ok(
+ gBrowser.selectedBrowser.currentURI.spec.endsWith("test+test"),
+ "Should have loaded the correct page"
+ );
+ Assert.equal(
+ gBrowser.selectedBrowser.currentURI.spec,
+ gURLBar.untrimmedValue,
+ "The location should have changed"
+ );
+
+ // Cleanup.
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function searchOnEnterSoon() {
+ info("Search on Enter as soon as typing a char");
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ START_VALUE
+ );
+
+ const onLoad = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ const onPageHide = SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ return new Promise(resolve => {
+ content.window.addEventListener("pagehide", () => {
+ resolve();
+ });
+ });
+ });
+ const onResult = SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ return new Promise(resolve => {
+ content.window.addEventListener("keyup", () => {
+ resolve("keyup");
+ });
+ content.window.addEventListener("unload", () => {
+ resolve("unload");
+ });
+ });
+ });
+
+ // Focus on the input field in urlbar.
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ const ownerDocument = gBrowser.selectedBrowser.ownerDocument;
+ is(
+ ownerDocument.activeElement,
+ gURLBar.inputField,
+ "The input field in urlbar has focus"
+ );
+
+ info("Keydown a char and Enter");
+ EventUtils.synthesizeKey("x", { type: "keydown" });
+ EventUtils.synthesizeKey("KEY_Enter", { type: "keydown" });
+
+ // Wait for pagehide event in the content.
+ await onPageHide;
+ is(
+ ownerDocument.activeElement,
+ gURLBar.inputField,
+ "The input field in urlbar still has focus"
+ );
+
+ // Check the caret position.
+ Assert.equal(
+ gURLBar.selectionStart,
+ gURLBar.value.length,
+ "The selectionStart indicates at ending of the value"
+ );
+ Assert.equal(
+ gURLBar.selectionEnd,
+ gURLBar.value.length,
+ "The selectionEnd indicates at ending of the value"
+ );
+
+ // Keyup both key as soon as pagehide event happens.
+ EventUtils.synthesizeKey("x", { type: "keyup" });
+ EventUtils.synthesizeKey("KEY_Enter", { type: "keyup" });
+
+ // Wait for moving the focus.
+ await TestUtils.waitForCondition(
+ () => ownerDocument.activeElement === gBrowser.selectedBrowser
+ );
+ info("The focus is moved to the browser");
+
+ // Check whether keyup event is not captured before unload event happens.
+ const result = await onResult;
+ is(result, "unload", "Keyup event is not captured.");
+
+ // Check the caret position again.
+ Assert.equal(
+ gURLBar.selectionStart,
+ 0,
+ "The selectionStart indicates at beginning of the value"
+ );
+ Assert.equal(
+ gURLBar.selectionEnd,
+ 0,
+ "The selectionEnd indicates at beginning of the value"
+ );
+
+ // Cleanup.
+ await onLoad;
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function searchByMultipleEnters() {
+ info("Search on Enter after selecting the search engine by Enter");
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ START_VALUE
+ );
+
+ info("Select a search engine by Enter key");
+ gURLBar.focus();
+ gURLBar.select();
+ EventUtils.sendString("@default");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await TestUtils.waitForCondition(
+ () => gURLBar.searchMode,
+ "Wait until entering search mode"
+ );
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: "browser_searchSuggestionEngine searchSuggestionEngine.xml",
+ entry: "keywordoffer",
+ });
+ const ownerDocument = gBrowser.selectedBrowser.ownerDocument;
+ is(
+ ownerDocument.activeElement,
+ gURLBar.inputField,
+ "The input field in urlbar has focus"
+ );
+
+ info("Search by Enter key");
+ const onLoad = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ EventUtils.sendString("mozilla");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await onLoad;
+ is(
+ ownerDocument.activeElement,
+ gBrowser.selectedBrowser,
+ "The focus is moved to the browser"
+ );
+
+ // Cleanup.
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function typeCharWhileProcessingEnter() {
+ info("Typing a char while processing enter key");
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ START_VALUE
+ );
+
+ const onLoad = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ `http://${START_VALUE}`
+ );
+ gURLBar.focus();
+
+ info("Keydown Enter");
+ EventUtils.synthesizeKey("KEY_Enter", { type: "keydown" });
+ await TestUtils.waitForCondition(
+ () => gURLBar._keyDownEnterDeferred,
+ "Wait for starting process for the enter key"
+ );
+
+ info("Keydown a char");
+ EventUtils.synthesizeKey("x", { type: "keydown" });
+
+ info("Keyup both");
+ EventUtils.synthesizeKey("x", { type: "keyup" });
+ EventUtils.synthesizeKey("KEY_Enter", { type: "keyup" });
+
+ Assert.equal(
+ gURLBar.inputField.value,
+ TEST_VALUE,
+ "The value of urlbar is correct"
+ );
+
+ await onLoad;
+ Assert.ok("Browser loaded the correct url");
+
+ // Cleanup.
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function keyupEnterWhilePressingMeta() {
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ info("Keydown Meta+Enter");
+ gURLBar.focus();
+ gURLBar.value = "";
+ EventUtils.synthesizeKey("KEY_Enter", { type: "keydown", metaKey: true });
+
+ // Pressing Enter key while pressing Meta key, and next, even when releasing
+ // Enter key before releasing Meta key, the keyup event is not fired.
+ // Therefor, we fire Meta keyup event only.
+ info("Keyup Meta");
+ EventUtils.synthesizeKey("KEY_Meta", { type: "keyup" });
+
+ // Check whether we can input on URL bar.
+ EventUtils.synthesizeKey("a");
+ is(gURLBar.value, "a", "Can input a char");
+
+ // Cleanup.
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_enterAfterMouseOver.js b/browser/components/urlbar/tests/browser/browser_enterAfterMouseOver.js
new file mode 100644
index 0000000000..e102fda09c
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_enterAfterMouseOver.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that enter works correctly after a mouse over.
+ */
+
+function repeat(limit, func) {
+ for (let i = 0; i < limit; i++) {
+ func(i);
+ }
+}
+
+async function promiseAutoComplete(inputText) {
+ gURLBar.focus();
+ gURLBar.value = inputText.slice(0, -1);
+ EventUtils.sendString(inputText.slice(-1));
+ await UrlbarTestUtils.promiseSearchComplete(window);
+}
+
+function assertSelected(index) {
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ index,
+ "Should have the correct index selected"
+ );
+}
+
+let gMaxResults;
+
+add_task(async function () {
+ registerCleanupFunction(async function () {
+ await PlacesUtils.history.clear();
+ });
+
+ await PlacesUtils.history.clear();
+
+ gMaxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults");
+
+ let visits = [];
+ repeat(gMaxResults, i => {
+ visits.push({
+ uri: makeURI("http://example.com/autocomplete/?" + i),
+ });
+ });
+ await PlacesTestUtils.addVisits(visits);
+
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank");
+ await promiseAutoComplete("http://example.com/autocomplete/");
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ gMaxResults,
+ "Should have got the correct amount of results"
+ );
+
+ let initiallySelected = UrlbarTestUtils.getSelectedRowIndex(window);
+
+ info("Key Down to select the next item");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ assertSelected(initiallySelected + 1);
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ initiallySelected + 1
+ );
+ let expectedURL = result.url;
+
+ Assert.equal(
+ gURLBar.untrimmedValue,
+ expectedURL,
+ "Value in the URL bar should be updated by keyboard selection"
+ );
+
+ // Verify that what we're about to do changes the selectedIndex:
+ Assert.notEqual(
+ initiallySelected + 1,
+ 3,
+ "Shouldn't be changing the selectedIndex to the same index we keyboard-selected."
+ );
+
+ let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 3);
+ EventUtils.synthesizeMouseAtCenter(element, { type: "mousemove" });
+
+ await UrlbarTestUtils.promisePopupClose(window, async () => {
+ let openedExpectedPage = BrowserTestUtils.waitForDocLoadAndStopIt(
+ expectedURL,
+ gBrowser.selectedBrowser
+ );
+ EventUtils.synthesizeKey("KEY_Enter");
+ await openedExpectedPage;
+ });
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_focusedCmdK.js b/browser/components/urlbar/tests/browser/browser_focusedCmdK.js
new file mode 100644
index 0000000000..fc32c2c13c
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_focusedCmdK.js
@@ -0,0 +1,15 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function () {
+ // Test that Ctrl/Cmd + K will focus the url bar
+ let focusPromise = BrowserTestUtils.waitForEvent(gURLBar, "focus");
+ document.documentElement.focus();
+ EventUtils.synthesizeKey("k", { accelKey: true });
+ await focusPromise;
+ Assert.equal(
+ document.activeElement,
+ gURLBar.inputField,
+ "URL Bar should be focused"
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_groupLabels.js b/browser/components/urlbar/tests/browser/browser_groupLabels.js
new file mode 100644
index 0000000000..2b43990b77
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_groupLabels.js
@@ -0,0 +1,629 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests group labels in the view.
+
+"use strict";
+
+const SUGGESTIONS_FIRST_PREF = "browser.urlbar.showSearchSuggestionsFirst";
+const SUGGESTIONS_PREF = "browser.urlbar.suggest.searches";
+
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+const TEST_ENGINE_2_BASENAME = "searchSuggestionEngine2.xml";
+const MAX_RESULTS = UrlbarPrefs.get("maxRichResults");
+
+const TOP_SITES = [
+ "http://example-1.com/",
+ "http://example-2.com/",
+ "http://example-3.com/",
+];
+
+const FIREFOX_SUGGEST_LABEL = "Firefox Suggest";
+
+// %s is replaced with the engine name.
+const ENGINE_SUGGESTIONS_LABEL = "%s suggestions";
+
+// Allow more time for Mac machines so they don't time out in verify mode.
+if (AppConstants.platform == "macosx") {
+ requestLongerTimeout(3);
+}
+
+add_setup(async function () {
+ Assert.ok(
+ UrlbarPrefs.get("showSearchSuggestionsFirst"),
+ "Precondition: Search suggestions shown first by default"
+ );
+
+ // Add some history.
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await UrlbarTestUtils.formHistory.clear();
+ await addHistory();
+
+ // Make sure we have some top sites.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.topsites", true],
+ ["browser.newtabpage.activity-stream.default.sites", TOP_SITES.join(",")],
+ ],
+ });
+ // Waiting for all top sites to be added intermittently times out, so just
+ // wait for any to be added. We're not testing top sites here; we only need
+ // the view to open in top-sites mode.
+ await updateTopSites(sites => sites && sites.length);
+
+ // Add a mock engine so we don't hit the network.
+ await SearchTestUtils.installSearchExtension({}, { setAsDefault: true });
+
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ });
+});
+
+// The Firefox Suggest label should not appear when the labels pref is disabled.
+add_task(async function prefDisabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.groupLabels.enabled", false]],
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+ await checkLabels(MAX_RESULTS, {});
+ await UrlbarTestUtils.promisePopupClose(window);
+ await SpecialPowers.popPrefEnv();
+});
+
+// The Firefox Suggest label should not appear when the view shows top sites.
+add_task(async function topSites() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ await checkLabels(-1, {});
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+// The Firefox Suggest label should appear when the search string is non-empty
+// and there are only general results.
+add_task(async function general() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+ await checkLabels(MAX_RESULTS, {
+ 1: FIREFOX_SUGGEST_LABEL,
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+// The Firefox Suggest label should appear when the search string is non-empty
+// and there are suggestions followed by general results.
+add_task(async function suggestionsBeforeGeneral() {
+ await withSuggestions(async () => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+ await checkLabels(MAX_RESULTS, {
+ 3: FIREFOX_SUGGEST_LABEL,
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+ });
+});
+
+// Both the Firefox Suggest and Suggestions labels should appear when the search
+// string is non-empty, general results are shown before suggestions, and there
+// are general and suggestion results.
+add_task(async function generalBeforeSuggestions() {
+ await withSuggestions(async engine => {
+ Assert.ok(engine.name, "Engine name is non-empty");
+ await SpecialPowers.pushPrefEnv({
+ set: [[SUGGESTIONS_FIRST_PREF, false]],
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+ await checkLabels(MAX_RESULTS, {
+ 1: FIREFOX_SUGGEST_LABEL,
+ [MAX_RESULTS - 2]: engineSuggestionsLabel(engine.name),
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+ });
+});
+
+// Neither the Firefox Suggest nor Suggestions label should appear when the
+// search string is non-empty, general results are shown before suggestions, and
+// there are only suggestion results.
+add_task(async function generalBeforeSuggestions_suggestionsOnly() {
+ await PlacesUtils.history.clear();
+
+ await withSuggestions(async engine => {
+ await SpecialPowers.pushPrefEnv({
+ set: [[SUGGESTIONS_FIRST_PREF, false]],
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+ await checkLabels(3, {});
+ await UrlbarTestUtils.promisePopupClose(window);
+ });
+
+ // Add back history so subsequent tasks run with this test's initial state.
+ await addHistory();
+});
+
+// The Suggestions label should be updated when the default engine changes.
+add_task(async function generalBeforeSuggestions_defaultChanged() {
+ // Install both test engines, one after the other. Engine 2 will be the final
+ // default engine.
+ await withSuggestions(async engine1 => {
+ await withSuggestions(async engine2 => {
+ Assert.ok(engine2.name, "Engine 2 name is non-empty");
+ Assert.notEqual(engine1.name, engine2.name, "Engine names are different");
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ engine2.name,
+ "Engine 2 is default"
+ );
+ await SpecialPowers.pushPrefEnv({
+ set: [[SUGGESTIONS_FIRST_PREF, false]],
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+ await checkLabels(MAX_RESULTS, {
+ 1: FIREFOX_SUGGEST_LABEL,
+ [MAX_RESULTS - 2]: engineSuggestionsLabel(engine2.name),
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+ }, TEST_ENGINE_2_BASENAME);
+ });
+});
+
+// The Firefox Suggest label should appear above a suggested-index result when
+// the result is the only result with that label.
+add_task(async function suggestedIndex_only() {
+ // Clear history, add a provider that returns a result with suggestedIndex =
+ // -1, set up an engine with suggestions, and start a query. The suggested-
+ // index result will be the only result with a label.
+ await PlacesUtils.history.clear();
+
+ let index = -1;
+ let provider = new SuggestedIndexProvider(index);
+ UrlbarProvidersManager.registerProvider(provider);
+
+ await withSuggestions(async engine => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 3);
+ Assert.equal(
+ result.element.row.result.suggestedIndex,
+ index,
+ "Sanity check: Our suggested-index result is present"
+ );
+ await checkLabels(4, {
+ 3: FIREFOX_SUGGEST_LABEL,
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+ });
+
+ UrlbarProvidersManager.unregisterProvider(provider);
+
+ // Add back history so subsequent tasks run with this test's initial state.
+ await addHistory();
+});
+
+// The Firefox Suggest label should appear above a suggested-index result when
+// the result is the first but not the only result with that label.
+add_task(async function suggestedIndex_first() {
+ let index = 1;
+ let provider = new SuggestedIndexProvider(index);
+ UrlbarProvidersManager.registerProvider(provider);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index);
+ Assert.equal(
+ result.element.row.result.suggestedIndex,
+ index,
+ "Sanity check: Our suggested-index result is present"
+ );
+ await checkLabels(MAX_RESULTS, {
+ [index]: FIREFOX_SUGGEST_LABEL,
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ UrlbarProvidersManager.unregisterProvider(provider);
+});
+
+// The Firefox Suggest label should not appear above a suggested-index result
+// when the result is not the first with that label.
+add_task(async function suggestedIndex_notFirst() {
+ let index = -1;
+ let provider = new SuggestedIndexProvider(index);
+ UrlbarProvidersManager.registerProvider(provider);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ MAX_RESULTS + index
+ );
+ Assert.equal(
+ result.element.row.result.suggestedIndex,
+ index,
+ "Sanity check: Our suggested-index result is present"
+ );
+ await checkLabels(MAX_RESULTS, {
+ 1: FIREFOX_SUGGEST_LABEL,
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ UrlbarProvidersManager.unregisterProvider(provider);
+});
+
+// Labels that appear multiple times but not consecutively should be shown.
+add_task(async function repeatLabels() {
+ let engineName = Services.search.defaultEngine.name;
+ let results = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ { url: "http://example.com/1" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ { suggestion: "test1", engine: engineName }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ { url: "http://example.com/2" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ { suggestion: "test2", engine: engineName }
+ ),
+ ];
+
+ for (let i = 0; i < results.length; i++) {
+ results[i].suggestedIndex = i;
+ }
+
+ let provider = new UrlbarTestUtils.TestProvider({
+ results,
+ priority: Infinity,
+ });
+ UrlbarProvidersManager.registerProvider(provider);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+ await checkLabels(results.length, {
+ 0: FIREFOX_SUGGEST_LABEL,
+ 1: engineSuggestionsLabel(engineName),
+ 2: FIREFOX_SUGGEST_LABEL,
+ 3: engineSuggestionsLabel(engineName),
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ UrlbarProvidersManager.unregisterProvider(provider);
+});
+
+// Clicking a row label shouldn't do anything.
+add_task(async function clickLabel() {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ // Do a search. The mock history added in init() should appear with the
+ // Firefox Suggest label at index 1.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+ await checkLabels(MAX_RESULTS, {
+ 1: FIREFOX_SUGGEST_LABEL,
+ });
+
+ // Check the result at index 2.
+ let result2 = await UrlbarTestUtils.getDetailsOfResultAt(window, 2);
+ Assert.ok(result2.url, "Result at index 2 has a URL");
+ let url2 = result2.url;
+ Assert.ok(
+ url2.startsWith("http://example.com/"),
+ "Result at index 2 is one of our mock history results"
+ );
+
+ // Get the row at index 3 and click above it. The click should hit the row
+ // at index 2 and load its URL. We do this to make sure our click code
+ // here in the test works properly and that performing a similar click
+ // relative to index 1 (see below) would hit the row at index 0 if not for
+ // the label at index 1.
+ let result3 = await UrlbarTestUtils.getDetailsOfResultAt(window, 3);
+ let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ info("Performing click relative to index 3");
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ click(result3.element.row, { y: -2 })
+ );
+ info("Waiting for load after performing click relative to index 3");
+ await loadPromise;
+ Assert.equal(gBrowser.currentURI.spec, url2, "Loaded URL at index 2");
+ // Now do the search again.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+
+ await checkLabels(MAX_RESULTS, {
+ 1: FIREFOX_SUGGEST_LABEL,
+ });
+
+ // Check the result at index 1, the one with the label.
+ let result1 = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.ok(result1.url, "Result at index 1 has a URL");
+ let url1 = result1.url;
+ Assert.ok(
+ url1.startsWith("http://example.com/"),
+ "Result at index 1 is one of our mock history results"
+ );
+ Assert.notEqual(url1, url2, "URLs at indexes 1 and 2 are different");
+
+ // Do a click on the row at index 1 in the same way as before. This time
+ // nothing should happen because the click should hit the label, not the
+ // row at index 0.
+ info("Clicking row label at index 1");
+ click(result1.element.row, { y: -2 });
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 500));
+ Assert.ok(UrlbarTestUtils.isPopupOpen(window), "View remains open");
+ Assert.equal(
+ gBrowser.currentURI.spec,
+ url2,
+ "Current URL is still URL from index 2"
+ );
+
+ // Now click the main part of the row at index 1. Its URL should load.
+ loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ let { height } = result1.element.row.getBoundingClientRect();
+ info(`Clicking main part of the row at index 1, height=${height}`);
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ click(result1.element.row)
+ );
+ info("Waiting for load after clicking row at index 1");
+ await loadPromise;
+ Assert.equal(gBrowser.currentURI.spec, url1, "Loaded URL at index 1");
+ });
+});
+
+add_task(async function ariaLabel() {
+ const helpUrl = "http://example.com/help";
+ const results = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ { url: "http://example.com/1", helpUrl }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ { url: "http://example.com/2", helpUrl }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ { url: "http://example.com/3" }
+ ),
+ ];
+
+ for (let i = 0; i < results.length; i++) {
+ results[i].suggestedIndex = i;
+ }
+
+ const provider = new UrlbarTestUtils.TestProvider({
+ results,
+ priority: Infinity,
+ });
+ UrlbarProvidersManager.registerProvider(provider);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+ await checkLabels(results.length, {
+ 0: FIREFOX_SUGGEST_LABEL,
+ });
+
+ const expectedRows = [
+ { hasGroupAriaLabel: true, ariaLabel: FIREFOX_SUGGEST_LABEL },
+ { hasGroupAriaLabel: false },
+ { hasGroupAriaLabel: false },
+ ];
+ await checkGroupAriaLabels(expectedRows);
+
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ UrlbarProvidersManager.unregisterProvider(provider);
+});
+
+/**
+ * Provider that returns a suggested-index result.
+ */
+class SuggestedIndexProvider extends UrlbarTestUtils.TestProvider {
+ constructor(suggestedIndex) {
+ super({
+ results: [
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ { url: "http://example.com/" }
+ ),
+ { suggestedIndex }
+ ),
+ ],
+ });
+ }
+}
+
+async function addHistory() {
+ for (let i = 0; i < MAX_RESULTS; i++) {
+ await PlacesTestUtils.addVisits("http://example.com/" + i);
+ }
+}
+
+/**
+ * Asserts that each result in the view does or doesn't have a label, depending
+ * on `labelsByIndex`.
+ *
+ * @param {number} resultCount
+ * The expected number of results. Pass -1 to use the max index in
+ * `labelsByIndex` or the actual result count if `labelsByIndex` is empty.
+ * @param {object} labelsByIndex
+ * A mapping from indexes to expected labels.
+ */
+async function checkLabels(resultCount, labelsByIndex) {
+ if (resultCount >= 0) {
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ resultCount,
+ "Expected result count"
+ );
+ } else {
+ // This `else` branch is only necessary because waiting for all top sites to
+ // be added intermittently times out. Don't let the test fail for such a
+ // dumb reason.
+ let indexes = Object.keys(labelsByIndex);
+ if (indexes.length) {
+ resultCount = indexes.sort((a, b) => b - a)[0] + 1;
+ } else {
+ resultCount = UrlbarTestUtils.getResultCount(window);
+ Assert.greater(resultCount, 0, "Actual result count is > 0");
+ }
+ }
+ for (let i = 0; i < resultCount; i++) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ let { row } = result.element;
+ let before = getComputedStyle(row, "::before");
+ if (labelsByIndex.hasOwnProperty(i)) {
+ Assert.equal(
+ before.content,
+ "attr(label)",
+ `::before.content is correct at index ${i}`
+ );
+ Assert.equal(
+ row.getAttribute("label"),
+ labelsByIndex[i],
+ `Row has correct label at index ${i}`
+ );
+ } else {
+ Assert.equal(
+ before.content,
+ "none",
+ `::before.content is 'none' at index ${i}`
+ );
+ Assert.ok(
+ !row.hasAttribute("label"),
+ `Row does not have label attribute at index ${i}`
+ );
+ }
+ }
+}
+
+/**
+ * Asserts that an element for group aria label.
+ *
+ * @param {Array} expectedRows The expected rows.
+ */
+async function checkGroupAriaLabels(expectedRows) {
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ expectedRows.length,
+ "Expected result count"
+ );
+
+ for (let i = 0; i < expectedRows.length; i++) {
+ const result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ const { row } = result.element;
+ const groupAriaLabel = row.querySelector(".urlbarView-group-aria-label");
+
+ const expected = expectedRows[i];
+
+ Assert.equal(
+ !!groupAriaLabel,
+ expected.hasGroupAriaLabel,
+ `Group aria label exists as expected in the results[${i}]`
+ );
+
+ if (expected.hasGroupAriaLabel) {
+ Assert.equal(
+ groupAriaLabel.getAttribute("aria-label"),
+ expected.ariaLabel,
+ `Content of aria-label attribute in the element for group aria label in the results[${i}] is correct`
+ );
+ }
+ }
+}
+
+function engineSuggestionsLabel(engineName) {
+ return ENGINE_SUGGESTIONS_LABEL.replace("%s", engineName);
+}
+
+/**
+ * Adds a search engine that provides suggestions, calls your callback, and then
+ * remove the engine.
+ *
+ * @param {Function} callback
+ * Your callback function.
+ * @param {string} [engineBasename]
+ * The basename of the engine file.
+ */
+async function withSuggestions(
+ callback,
+ engineBasename = TEST_ENGINE_BASENAME
+) {
+ await SpecialPowers.pushPrefEnv({
+ set: [[SUGGESTIONS_PREF, true]],
+ });
+ let engine = await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + engineBasename,
+ });
+ let oldDefaultEngine = await Services.search.getDefault();
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ try {
+ await callback(engine);
+ } finally {
+ await Services.search.setDefault(
+ oldDefaultEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ await Services.search.removeEngine(engine);
+ await SpecialPowers.popPrefEnv();
+ }
+}
+
+function click(element, { x = undefined, y = undefined } = {}) {
+ let { width, height } = element.getBoundingClientRect();
+ if (typeof x != "number") {
+ x = width / 2;
+ }
+ if (typeof y != "number") {
+ y = height / 2;
+ }
+ EventUtils.synthesizeMouse(element, x, y, { type: "mousedown" });
+ EventUtils.synthesizeMouse(element, x, y, { type: "mouseup" });
+}
diff --git a/browser/components/urlbar/tests/browser/browser_handleCommand_fallback.js b/browser/components/urlbar/tests/browser/browser_handleCommand_fallback.js
new file mode 100644
index 0000000000..9d8ac8754c
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_handleCommand_fallback.js
@@ -0,0 +1,142 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the fallback paths of handleCommand (no view and no previous
+ * result) work consistently against the normal case of picking the heuristic
+ * result.
+ */
+
+const TEST_STRINGS = [
+ "test",
+ "test/",
+ "test.com",
+ "test.invalid",
+ "moz",
+ "moz test",
+ "@moz test",
+ "keyword",
+ "keyword test",
+ "test/test/",
+ "test /test/",
+];
+
+add_task(async function () {
+ // Disable autofill so mozilla.org isn't autofilled below.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.autoFill", false]],
+ });
+
+ sandbox = sinon.createSandbox();
+ await SearchTestUtils.installSearchExtension();
+ await SearchTestUtils.installSearchExtension({ name: "Example2" });
+
+ let bm = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "https://example.com/?q=%s",
+ title: "test",
+ });
+ await PlacesUtils.keywords.insert({
+ keyword: "keyword",
+ url: "https://example.com/?q=%s",
+ });
+ registerCleanupFunction(async () => {
+ sandbox.restore();
+ await PlacesUtils.bookmarks.remove(bm);
+ await UrlbarTestUtils.formHistory.clear();
+ });
+
+ async function promiseLoadURL() {
+ return new Promise(resolve => {
+ sandbox.stub(gURLBar, "_loadURL").callsFake(function () {
+ sandbox.restore();
+ // The last arguments are optional and apply only to some cases, so we
+ // could not use deepEqual with them.
+ resolve(Array.from(arguments).slice(0, 3));
+ });
+ });
+ }
+
+ // Run the string through a normal search where the user types the string
+ // and confirms the heuristic result, store the arguments to _loadURL, then
+ // confirm the same string without a view and without an input event, and
+ // compare the arguments.
+ for (let value of TEST_STRINGS) {
+ info(`Input the value normally and Enter. Value: ${value}`);
+ let promise = promiseLoadURL();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value,
+ });
+ EventUtils.synthesizeKey("KEY_Enter");
+ let args = await promise;
+ Assert.ok(args.length, "Sanity check");
+ info("Close the panel and confirm again.");
+ promise = promiseLoadURL();
+ await UrlbarTestUtils.promisePopupClose(window);
+ EventUtils.synthesizeKey("KEY_Enter");
+ Assert.deepEqual(await promise, args, "Check arguments are coherent");
+
+ info("Set the value directly and Enter.");
+ // To properly testing the original value we must be out of search mode.
+ if (gURLBar.searchMode) {
+ await UrlbarTestUtils.exitSearchMode(window);
+ // Exiting search mode may reopen the panel.
+ await UrlbarTestUtils.promisePopupClose(window);
+ }
+ promise = promiseLoadURL();
+ gURLBar.value = value;
+ let spy = sinon.spy(UrlbarUtils, "getHeuristicResultFor");
+ EventUtils.synthesizeKey("KEY_Enter");
+ spy.restore();
+ Assert.ok(spy.called, "invoked getHeuristicResultFor");
+ Assert.deepEqual(await promise, args, "Check arguments are coherent");
+ gURLBar.handleRevert();
+ }
+});
+
+// This is testing the final fallback case that may happen when we can't
+// get a heuristic result, maybe because the Places database is corrupt.
+add_task(async function no_heuristic_test() {
+ sandbox = sinon.createSandbox();
+
+ let stub = sandbox
+ .stub(UrlbarUtils, "getHeuristicResultFor")
+ .callsFake(async function () {
+ throw new Error("I failed!");
+ });
+
+ registerCleanupFunction(async () => {
+ sandbox.restore();
+ await UrlbarTestUtils.formHistory.clear();
+ });
+
+ async function promiseLoadURL() {
+ return new Promise(resolve => {
+ sandbox.stub(gURLBar, "_loadURL").callsFake(function () {
+ sandbox.restore();
+ // The last arguments are optional and apply only to some cases, so we
+ // could not use deepEqual with them.
+ resolve(Array.from(arguments).slice(0, 3));
+ });
+ });
+ }
+
+ // Run the string through a normal search where the user types the string
+ // and confirms the heuristic result, store the arguments to _loadURL, then
+ // confirm the same string without a view and without an input event, and
+ // compare the arguments.
+ for (let value of TEST_STRINGS) {
+ // To properly testing the original value we must be out of search mode.
+ if (gURLBar.searchMode) {
+ await UrlbarTestUtils.exitSearchMode(window);
+ }
+ let promise = promiseLoadURL();
+ gURLBar.value = value;
+ EventUtils.synthesizeKey("KEY_Enter");
+ Assert.ok(stub.called, "invoked getHeuristicResultFor");
+ // The first argument to _loadURL should always be a valid url, so this
+ // should never throw.
+ new URL((await promise)[0]);
+ }
+});
diff --git a/browser/components/urlbar/tests/browser/browser_hashChangeProxyState.js b/browser/components/urlbar/tests/browser/browser_hashChangeProxyState.js
new file mode 100644
index 0000000000..d0e236fe7e
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_hashChangeProxyState.js
@@ -0,0 +1,151 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that navigating through both the URL bar and using in-page hash- or ref-
+ * based links and back or forward navigation updates the URL bar and identity block correctly.
+ */
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.navigation.requireUserInteraction", false]],
+ });
+ let baseURL = `${TEST_BASE_URL}dummy_page.html`;
+ let url = baseURL + "#foo";
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url },
+ async function (browser) {
+ let identityBox = document.getElementById("identity-box");
+ let expectedURL = url;
+
+ let verifyURLBarState = testType => {
+ is(
+ gURLBar.value,
+ expectedURL,
+ "URL bar visible value should be correct " + testType
+ );
+ is(
+ gURLBar.untrimmedValue,
+ expectedURL,
+ "URL bar value should be correct " + testType
+ );
+ ok(
+ identityBox.classList.contains("verifiedDomain"),
+ "Identity box should know we're doing SSL " + testType
+ );
+ is(
+ gURLBar.getAttribute("pageproxystate"),
+ "valid",
+ "URL bar is in valid page proxy state"
+ );
+ };
+
+ verifyURLBarState("at the beginning");
+
+ let locationChangePromise;
+ let resolveLocationChangePromise;
+ let expectURL = urlTemp => {
+ expectedURL = urlTemp;
+ locationChangePromise = new Promise(
+ r => (resolveLocationChangePromise = r)
+ );
+ };
+ let wpl = {
+ onLocationChange(unused, unused2, location) {
+ is(location.spec, expectedURL, "Got the expected URL");
+ resolveLocationChangePromise();
+ },
+ };
+ gBrowser.addProgressListener(wpl);
+
+ expectURL(baseURL + "#foo");
+ gURLBar.select();
+ EventUtils.sendKey("return");
+
+ await locationChangePromise;
+ verifyURLBarState("after hitting enter on the same URL a second time");
+
+ expectURL(baseURL + "#bar");
+ gURLBar.value = expectedURL;
+ gURLBar.select();
+ EventUtils.sendKey("return");
+
+ await locationChangePromise;
+ verifyURLBarState("after a URL bar hash navigation");
+
+ expectURL(baseURL + "#foo");
+ await SpecialPowers.spawn(browser, [], function () {
+ let a = content.document.createElement("a");
+ a.href = "#foo";
+ a.textContent = "Foo Link";
+ content.document.body.appendChild(a);
+ a.click();
+ });
+
+ await locationChangePromise;
+ verifyURLBarState("after a page link hash navigation");
+
+ expectURL(baseURL + "#bar");
+ gBrowser.goBack();
+
+ await locationChangePromise;
+ verifyURLBarState("after going back");
+
+ expectURL(baseURL + "#foo");
+ gBrowser.goForward();
+
+ await locationChangePromise;
+ verifyURLBarState("after going forward");
+
+ expectURL(baseURL + "#foo");
+ gURLBar.select();
+ EventUtils.sendKey("return");
+
+ await locationChangePromise;
+ verifyURLBarState("after hitting enter on the same URL");
+
+ gBrowser.removeProgressListener(wpl);
+ }
+ );
+});
+
+/**
+ * Check that initial secure loads that swap remoteness
+ * get the correct page icon when finished.
+ */
+add_task(async function () {
+ // Ensure there's no preloaded newtab browser, since that'll not fire a load event.
+ NewTabPagePreloading.removePreloadedBrowser(window);
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:newtab"
+ );
+ let url = `${TEST_BASE_URL}dummy_page.html#foo`;
+ gURLBar.value = url;
+ gURLBar.select();
+ EventUtils.sendKey("return");
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ is(
+ gURLBar.value,
+ url,
+ "URL bar visible value should be correct when the page loads from about:newtab"
+ );
+ is(
+ gURLBar.untrimmedValue,
+ url,
+ "URL bar value should be correct when the page loads from about:newtab"
+ );
+ let identityBox = document.getElementById("identity-box");
+ ok(
+ identityBox.classList.contains("verifiedDomain"),
+ "Identity box should know we're doing SSL when the page loads from about:newtab"
+ );
+ is(
+ gURLBar.getAttribute("pageproxystate"),
+ "valid",
+ "URL bar is in valid page proxy state when SSL page with hash loads from about:newtab"
+ );
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_helpUrl.js b/browser/components/urlbar/tests/browser/browser_helpUrl.js
new file mode 100644
index 0000000000..5182a8ddb0
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_helpUrl.js
@@ -0,0 +1,428 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests the help/info button that appears for results whose payloads have a
+// `helpUrl` property.
+
+"use strict";
+
+const MAX_RESULTS = UrlbarPrefs.get("maxRichResults");
+const RESULT_URL = "http://example.com/test";
+const RESULT_HELP_URL = "http://example.com/help";
+
+add_setup(async function () {
+ // Add enough results to fill up the view.
+ await PlacesUtils.history.clear();
+ for (let i = 0; i < MAX_RESULTS; i++) {
+ await PlacesTestUtils.addVisits("http://example.com/" + i);
+ }
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ });
+});
+
+// Sets `helpL10n` on the result payload and makes sure the help button ends
+// up with a corresponding l10n attribute.
+add_task(async function title_helpL10n() {
+ if (UrlbarPrefs.get("resultMenu")) {
+ return;
+ }
+ let provider = registerTestProvider(1);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ value: "example",
+ window,
+ });
+
+ await assertIsTestResult(1);
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ let helpButton = result.element.row._buttons.get("help");
+ Assert.ok(helpButton, "Sanity check: help button should exist");
+
+ let l10nAttrs = document.l10n.getAttributes(helpButton);
+ Assert.deepEqual(
+ l10nAttrs,
+ { id: "urlbar-tip-help-icon", args: null },
+ "The l10n ID attribute was correctly set"
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ UrlbarProvidersManager.unregisterProvider(provider);
+});
+
+// (SHIFT+)TABs through a result with a help button. The result is the
+// second result and has other results after it.
+add_task(async function keyboardSelection_secondResult() {
+ let provider = registerTestProvider(1);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ value: "example",
+ window,
+ });
+
+ // Sanity-check initial state.
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ MAX_RESULTS,
+ "There should be MAX_RESULTS results in the view"
+ );
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElementIndex(window),
+ 0,
+ "The heuristic result should be selected"
+ );
+ await assertIsTestResult(1);
+
+ info("Arrow down to the main part of the result.");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ assertMainPartSelected(1);
+
+ info("TAB to the button.");
+ EventUtils.synthesizeKey("KEY_Tab");
+ assertButtonSelected(2);
+
+ info("TAB to the next (third) result.");
+ EventUtils.synthesizeKey("KEY_Tab");
+ assertOtherResultSelected(3, "next result");
+
+ info("SHIFT+TAB to the help button.");
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
+ assertButtonSelected(2);
+
+ info("SHIFT+TAB to the main part of the result.");
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
+ assertMainPartSelected(1);
+
+ info("Arrow up to the previous (first) result.");
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ assertOtherResultSelected(0, "previous result");
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ UrlbarProvidersManager.unregisterProvider(provider);
+});
+
+// (SHIFT+)TABs through a result with a help button. The result is the
+// last result.
+add_task(async function keyboardSelection_lastResult() {
+ let provider = registerTestProvider(MAX_RESULTS - 1);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ value: "example",
+ window,
+ });
+
+ // Sanity-check initial state.
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ MAX_RESULTS,
+ "There should be MAX_RESULTS results in the view"
+ );
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElementIndex(window),
+ 0,
+ "The heuristic result should be selected"
+ );
+ await assertIsTestResult(MAX_RESULTS - 1);
+
+ let numSelectable = UrlbarPrefs.get("resultMenu")
+ ? MAX_RESULTS * 2 - 2
+ : MAX_RESULTS;
+
+ // Arrow down to the main part of the result.
+ EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: MAX_RESULTS - 1 });
+ assertMainPartSelected(numSelectable - 1);
+
+ // TAB to the help button.
+ EventUtils.synthesizeKey("KEY_Tab");
+ assertButtonSelected(numSelectable);
+
+ // Arrow down to the first one-off. If this test is running alone, the
+ // one-offs will rebuild themselves when the view is opened above, and they
+ // may not be visible yet. Wait for the first one to become visible before
+ // trying to select it.
+ await TestUtils.waitForCondition(() => {
+ return (
+ gURLBar.view.oneOffSearchButtons.buttons.firstElementChild &&
+ BrowserTestUtils.is_visible(
+ gURLBar.view.oneOffSearchButtons.buttons.firstElementChild
+ )
+ );
+ }, "Waiting for first one-off to become visible.");
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await TestUtils.waitForCondition(() => {
+ return gURLBar.view.oneOffSearchButtons.selectedButton;
+ }, "Waiting for one-off to become selected.");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElementIndex(window),
+ -1,
+ "No results should be selected."
+ );
+
+ // SHIFT+TAB to the help button.
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
+ assertButtonSelected(numSelectable);
+
+ // SHIFT+TAB to the main part of the result.
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
+ assertMainPartSelected(numSelectable - 1);
+
+ // Arrow up to the previous result.
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ assertOtherResultSelected(
+ numSelectable - (UrlbarPrefs.get("resultMenu") ? 3 : 2),
+ "previous result"
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ UrlbarProvidersManager.unregisterProvider(provider);
+});
+
+// Picks the main part of the test result -- the non-help-button part -- with
+// the keyboard.
+add_task(async function pick_mainPart_keyboard() {
+ await doPickTest({ pickButton: false, useKeyboard: true });
+});
+
+// Picks the help button with the keyboard.
+add_task(async function pick_helpButton_keyboard() {
+ await doPickTest({ pickButton: true, useKeyboard: true });
+});
+
+// Picks the main part of the test result -- the non-help-button part -- with
+// the mouse.
+add_task(async function pick_mainPart_mouse() {
+ await doPickTest({ pickButton: false, useKeyboard: false });
+});
+
+// Picks the help button with the mouse.
+add_task(async function pick_helpButton_mouse() {
+ await doPickTest({ pickButton: true, useKeyboard: false });
+});
+
+async function doPickTest({ pickButton, useKeyboard }) {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ let index = 1;
+ let provider = registerTestProvider(index);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ value: "example",
+ window,
+ });
+
+ // Sanity-check initial state.
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElementIndex(window),
+ 0,
+ "The heuristic result should be selected"
+ );
+ await assertIsTestResult(index);
+
+ if (useKeyboard) {
+ // Arrow down to the result.
+ EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: index });
+ assertMainPartSelected(
+ UrlbarPrefs.get("resultMenu") ? index * 2 - 1 : index
+ );
+ }
+
+ // Pick the result. The appropriate URL should load.
+ let loadPromise = pickButton
+ ? BrowserTestUtils.waitForNewTab(gBrowser)
+ : BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await Promise.all([
+ loadPromise,
+ UrlbarTestUtils.promisePopupClose(window, async () => {
+ if (pickButton && UrlbarPrefs.get("resultMenu")) {
+ await UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "h", {
+ openByMouse: !useKeyboard,
+ resultIndex: index,
+ });
+ } else if (useKeyboard) {
+ if (pickButton) {
+ // TAB to the button.
+ EventUtils.synthesizeKey("KEY_Tab");
+ assertButtonSelected(index + 1);
+ }
+ EventUtils.synthesizeKey("KEY_Enter");
+ } else {
+ // Get the click target.
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ index
+ );
+ let clickTarget = pickButton
+ ? result.element.row._buttons.get("help")
+ : result.element.row._content;
+ Assert.ok(
+ clickTarget,
+ "Click target found, pickButton=" + pickButton
+ );
+ EventUtils.synthesizeMouseAtCenter(clickTarget, {});
+ }
+ }),
+ ]);
+ Assert.equal(
+ gBrowser.selectedBrowser.currentURI.spec,
+ pickButton ? RESULT_HELP_URL : RESULT_URL,
+ "Expected URL should have loaded"
+ );
+
+ if (pickButton) {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+ UrlbarProvidersManager.unregisterProvider(provider);
+
+ // Avoid showing adaptive history autofill.
+ await PlacesTestUtils.clearInputHistory();
+ });
+}
+
+/**
+ * Registers a provider that creates a result with a help button.
+ *
+ * @param {number} suggestedIndex
+ * The result's suggestedIndex.
+ * @returns {UrlbarProvider}
+ * The new provider.
+ */
+function registerTestProvider(suggestedIndex) {
+ let results = [
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ url: RESULT_URL,
+ helpUrl: RESULT_HELP_URL,
+ helpL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-tip-get-help"
+ : "urlbar-tip-help-icon",
+ },
+ }
+ ),
+ { suggestedIndex }
+ ),
+ ];
+ let provider = new UrlbarTestUtils.TestProvider({ results });
+ UrlbarProvidersManager.registerProvider(provider);
+ return provider;
+}
+
+/**
+ * Asserts that the result at the given index is our test result with a help
+ * button.
+ *
+ * @param {number} index
+ * The expected index of the test result.
+ */
+async function assertIsTestResult(index) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index);
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.URL,
+ "The second result should be a URL"
+ );
+ Assert.equal(
+ result.url,
+ RESULT_URL,
+ "The result's URL should be the expected URL"
+ );
+
+ let { row } = result.element;
+ if (UrlbarPrefs.get("resultMenu")) {
+ Assert.ok(row._buttons.get("menu"), "The result should have a menu button");
+ } else {
+ let helpButton = row._buttons.get("help");
+ Assert.ok(helpButton, "The result should have a help button");
+ Assert.ok(helpButton.id, "Help button has an ID");
+ }
+ Assert.ok(row._content.id, "Row-inner has an ID");
+ Assert.equal(
+ row.getAttribute("role"),
+ "presentation",
+ "Row should have role=presentation"
+ );
+ Assert.equal(
+ row._content.getAttribute("role"),
+ "option",
+ "Row-inner should have role=option"
+ );
+}
+
+/**
+ * Asserts that a particular element is selected.
+ *
+ * @param {number} expectedSelectedElementIndex
+ * The expected selected element index.
+ * @param {string} expectedClassName
+ * A class name of the expected selected element.
+ * @param {string} msg
+ * A string to include in the assertion message.
+ */
+function assertSelection(expectedSelectedElementIndex, expectedClassName, msg) {
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElementIndex(window),
+ expectedSelectedElementIndex,
+ "Expected selected element index: " + msg
+ );
+ Assert.ok(
+ UrlbarTestUtils.getSelectedElement(window).classList.contains(
+ expectedClassName
+ ),
+ `Expected selected element: ${msg} (${
+ UrlbarTestUtils.getSelectedElement(window).classList
+ } == ${expectedClassName})`
+ );
+}
+
+/**
+ * Asserts that the main part of our test resut -- the non-help-button part --
+ * is selected.
+ *
+ * @param {number} expectedSelectedElementIndex
+ * The expected selected element index.
+ */
+function assertMainPartSelected(expectedSelectedElementIndex) {
+ assertSelection(
+ expectedSelectedElementIndex,
+ "urlbarView-row-inner",
+ "main part of test result"
+ );
+}
+
+/**
+ * Asserts that the help button part of our test result is selected.
+ *
+ * @param {number} expectedSelectedElementIndex
+ * The expected selected element index.
+ */
+function assertButtonSelected(expectedSelectedElementIndex) {
+ if (UrlbarPrefs.get("resultMenu")) {
+ assertSelection(
+ expectedSelectedElementIndex,
+ "urlbarView-button-menu",
+ "menu button"
+ );
+ } else {
+ assertSelection(
+ expectedSelectedElementIndex,
+ "urlbarView-button-help",
+ "help button"
+ );
+ }
+}
+
+/**
+ * Asserts that a result other than our test result is selected.
+ *
+ * @param {number} expectedSelectedElementIndex
+ * The expected selected element index.
+ * @param {string} msg
+ * A string to include in the assertion message.
+ */
+function assertOtherResultSelected(expectedSelectedElementIndex, msg) {
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElementIndex(window),
+ expectedSelectedElementIndex,
+ "Expected other selected element index: " + msg
+ );
+}
diff --git a/browser/components/urlbar/tests/browser/browser_heuristicNotAddedFirst.js b/browser/components/urlbar/tests/browser/browser_heuristicNotAddedFirst.js
new file mode 100644
index 0000000000..fa7c65b378
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_heuristicNotAddedFirst.js
@@ -0,0 +1,159 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// When the heuristic result is not the first result added, it should still be
+// selected.
+
+"use strict";
+
+// When the heuristic result is not the first result added, it should still be
+// selected.
+add_task(async function slowHeuristicSelected() {
+ // First, add a provider that adds a heuristic result on a delay. Both this
+ // provider and the one below have a high priority so that only they are used
+ // during the test.
+ let engine = await Services.search.getDefault();
+ let heuristicResult = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ {
+ suggestion: "test",
+ engine: engine.name,
+ }
+ );
+ heuristicResult.heuristic = true;
+ let heuristicProvider = new UrlbarTestUtils.TestProvider({
+ results: [heuristicResult],
+ name: "heuristicProvider",
+ priority: Infinity,
+ addTimeout: 500,
+ });
+ UrlbarProvidersManager.registerProvider(heuristicProvider);
+
+ // Second, add another provider that adds a non-heuristic result immediately
+ // with suggestedIndex = 1.
+ let nonHeuristicResult = makeTipResult();
+ nonHeuristicResult.suggestedIndex = 1;
+ let nonHeuristicProvider = new UrlbarTestUtils.TestProvider({
+ results: [nonHeuristicResult],
+ name: "nonHeuristicProvider",
+ priority: Infinity,
+ });
+ UrlbarProvidersManager.registerProvider(nonHeuristicProvider);
+
+ // Do a search.
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ value: "test",
+ window: win,
+ });
+
+ // The first result should be the heuristic and it should be selected.
+ let actualHeuristic = await UrlbarTestUtils.getDetailsOfResultAt(win, 0);
+ Assert.equal(actualHeuristic.type, UrlbarUtils.RESULT_TYPE.SEARCH);
+ Assert.equal(UrlbarTestUtils.getSelectedElementIndex(win), 0);
+
+ // Check the second result for good measure.
+ let actualNonHeuristic = await UrlbarTestUtils.getDetailsOfResultAt(win, 1);
+ Assert.equal(actualNonHeuristic.type, UrlbarUtils.RESULT_TYPE.TIP);
+
+ await UrlbarTestUtils.promisePopupClose(win);
+ UrlbarProvidersManager.unregisterProvider(heuristicProvider);
+ UrlbarProvidersManager.unregisterProvider(nonHeuristicProvider);
+ await BrowserTestUtils.closeWindow(win);
+});
+
+// When the heuristic result is not the first result added but a one-off search
+// button is already selected, the heuristic result should not steal the
+// selection from the one-off button.
+add_task(async function oneOffRemainsSelected() {
+ // First, add a provider that adds a heuristic result on a delay. Both this
+ // provider and the one below have a high priority so that only they are used
+ // during the test.
+ let engine = await Services.search.getDefault();
+ let heuristicResult = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ {
+ suggestion: "test",
+ engine: engine.name,
+ }
+ );
+ heuristicResult.heuristic = true;
+ let heuristicProvider = new UrlbarTestUtils.TestProvider({
+ results: [heuristicResult],
+ name: "heuristicProvider",
+ priority: Infinity,
+ addTimeout: 500,
+ });
+ UrlbarProvidersManager.registerProvider(heuristicProvider);
+
+ // Second, add another provider that adds a non-heuristic result immediately
+ // with suggestedIndex = 1.
+ let nonHeuristicResult = makeTipResult();
+ nonHeuristicResult.suggestedIndex = 1;
+ let nonHeuristicProvider = new UrlbarTestUtils.TestProvider({
+ results: [nonHeuristicResult],
+ name: "nonHeuristicProvider",
+ priority: Infinity,
+ });
+ UrlbarProvidersManager.registerProvider(nonHeuristicProvider);
+
+ // Do a search but don't wait for it to finish.
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+ let searchPromise = UrlbarTestUtils.promiseAutocompleteResultPopup({
+ value: "test",
+ window: win,
+ });
+
+ // When the view opens, press the up arrow key to select the one-off search
+ // settings button. There's no point in selecting instead the non-heuristic
+ // result because once we do that, the search is canceled, and the heuristic
+ // result will never be added.
+ await UrlbarTestUtils.promisePopupOpen(win, () => {});
+ EventUtils.synthesizeKey("KEY_ArrowUp", {}, win);
+
+ // Wait for the search to finish.
+ await searchPromise;
+
+ // The first result should be the heuristic.
+ let actualHeuristic = await UrlbarTestUtils.getDetailsOfResultAt(win, 0);
+ Assert.equal(actualHeuristic.type, UrlbarUtils.RESULT_TYPE.SEARCH);
+
+ // Check the second result for good measure.
+ let actualNonHeuristic = await UrlbarTestUtils.getDetailsOfResultAt(win, 1);
+ Assert.equal(actualNonHeuristic.type, UrlbarUtils.RESULT_TYPE.TIP);
+
+ // No result should be selected.
+ Assert.equal(UrlbarTestUtils.getSelectedElement(win), null);
+ Assert.equal(UrlbarTestUtils.getSelectedElementIndex(win), -1);
+
+ // The one-off settings button should be selected.
+ Assert.equal(
+ win.gURLBar.view.oneOffSearchButtons.selectedButton,
+ win.gURLBar.view.oneOffSearchButtons.settingsButton
+ );
+
+ await UrlbarTestUtils.promisePopupClose(win);
+ UrlbarProvidersManager.unregisterProvider(heuristicProvider);
+ UrlbarProvidersManager.unregisterProvider(nonHeuristicProvider);
+ await BrowserTestUtils.closeWindow(win);
+});
+
+function makeTipResult() {
+ return new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TIP,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ helpUrl: "http://example.com/",
+ type: "test",
+ titleL10n: { id: "urlbar-search-tips-confirm" },
+ buttons: [
+ {
+ url: "http://example.com/",
+ l10n: { id: "urlbar-search-tips-confirm" },
+ },
+ ],
+ }
+ );
+}
diff --git a/browser/components/urlbar/tests/browser/browser_hideHeuristic.js b/browser/components/urlbar/tests/browser/browser_hideHeuristic.js
new file mode 100644
index 0000000000..e8f8774e01
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_hideHeuristic.js
@@ -0,0 +1,513 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Basic smoke tests for the `browser.urlbar.experimental.hideHeuristic` pref,
+// which hides the heuristic result. Each task performs a search that triggers a
+// specific heuristic, verifies that it's hidden or shown as appropriate, and
+// verifies that it's picked when enter is pressed.
+//
+// If/when it becomes the default, we should update existing tests as necessary
+// and remove this one.
+
+"use strict";
+
+// Allow more time for Mac machines so they don't time out in verify mode.
+if (AppConstants.platform == "macosx") {
+ requestLongerTimeout(3);
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.experimental.hideHeuristic", true],
+ ["browser.urlbar.suggest.quickactions", false],
+ ],
+ });
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+});
+
+// UrlbarUtils.RESULT_GROUP.HEURISTIC_EXTENSION should be hidden.
+add_task(async function extension() {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await withVisits(async visitURLs => {
+ // Add an extension provider that returns a heuristic.
+ let url = "http://example.com/extension-test";
+ let provider = new UrlbarTestUtils.TestProvider({
+ name: "ExtensionTest",
+ type: UrlbarUtils.PROVIDER_TYPE.EXTENSION,
+ results: [
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ url,
+ title: "Test",
+ }
+ ),
+ { heuristic: true }
+ ),
+ ],
+ });
+ UrlbarProvidersManager.registerProvider(provider);
+
+ // Do a search that fetches the provider's result and check it.
+ let heuristic = await search({
+ value: "test",
+ expectedGroup: UrlbarUtils.RESULT_GROUP.HEURISTIC_EXTENSION,
+ });
+ Assert.equal(heuristic.payload.url, url, "Heuristic URL is correct");
+
+ // Check the other visit results.
+ await checkVisitResults(visitURLs);
+
+ // Press enter to verify the heuristic result is loaded.
+ await synthesizeEnterAndAwaitLoad(url);
+
+ UrlbarProvidersManager.unregisterProvider(provider);
+ });
+ });
+});
+
+// UrlbarUtils.RESULT_GROUP.HEURISTIC_OMNIBOX should be hidden.
+add_task(async function omnibox() {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ // Load an extension.
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ omnibox: {
+ keyword: "omniboxtest",
+ },
+ },
+ background() {
+ /* global browser */
+ browser.omnibox.onInputEntered.addListener(() => {
+ browser.test.sendMessage("onInputEntered");
+ });
+ },
+ });
+ await extension.startup();
+
+ // Do a search using the omnibox keyword and check the hidden heuristic
+ // result.
+ let heuristic = await search({
+ value: "omniboxtest foo",
+ expectedGroup: UrlbarUtils.RESULT_GROUP.HEURISTIC_OMNIBOX,
+ });
+ Assert.equal(
+ heuristic.payload.keyword,
+ "omniboxtest",
+ "Heuristic keyword is correct"
+ );
+
+ // Press enter to verify the heuristic result is picked.
+ let messagePromise = extension.awaitMessage("onInputEntered");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await messagePromise;
+
+ await extension.unload();
+ });
+});
+
+// UrlbarUtils.RESULT_GROUP.HEURISTIC_SEARCH_TIP should be shown.
+add_task(async function searchTip() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.searchTips.test.ignoreShowLimits", true]],
+ });
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser: window.gBrowser,
+ url: "about:newtab",
+ // `withNewTab` hangs waiting for about:newtab to load without this.
+ waitForLoad: false,
+ },
+ async () => {
+ await UrlbarTestUtils.promisePopupOpen(window, () => {});
+ Assert.ok(true, "View opened");
+ Assert.equal(UrlbarTestUtils.getResultCount(window), 1);
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.TIP);
+ Assert.ok(result.heuristic);
+ Assert.ok(UrlbarTestUtils.getSelectedElement(window), "Selection exists");
+ }
+ );
+ await SpecialPowers.popPrefEnv();
+});
+
+// UrlbarUtils.RESULT_GROUP.HEURISTIC_ENGINE_ALIAS should be hidden.
+add_task(async function engineAlias() {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await withVisits(async visitURLs => {
+ // Add an engine with an alias.
+ await withEngine({ keyword: "test" }, async () => {
+ // Do a search using the alias and check the hidden heuristic result.
+ // The heuristic will be HEURISTIC_FALLBACK, not HEURISTIC_ENGINE_ALIAS,
+ // because two searches are performed and
+ // `UrlbarTestUtils.promiseAutocompleteResultPopup` waits for both. The
+ // first returns a HEURISTIC_ENGINE_ALIAS that triggers search mode and
+ // then an immediate second search, which returns HEURISTIC_FALLBACK.
+ let heuristic = await search({
+ value: "test foo",
+ expectedGroup: UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK,
+ });
+ Assert.equal(
+ heuristic.payload.engine,
+ "Example",
+ "Heuristic engine is correct"
+ );
+ Assert.equal(
+ heuristic.payload.query,
+ "foo",
+ "Heuristic query is correct"
+ );
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: "Example",
+ entry: "typed",
+ });
+
+ // Check the other visit results.
+ await checkVisitResults(visitURLs);
+
+ // Press enter to verify the heuristic result is loaded.
+ await synthesizeEnterAndAwaitLoad("https://example.com/?q=foo");
+ });
+ });
+ });
+});
+
+// UrlbarUtils.RESULT_GROUP.HEURISTIC_BOOKMARK_KEYWORD should be hidden.
+add_task(async function bookmarkKeyword() {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await withVisits(async visitURLs => {
+ // Add a bookmark with a keyword.
+ let keyword = "bm";
+ let bm = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.com/?q=%s",
+ title: "test",
+ });
+ await PlacesUtils.keywords.insert({ keyword, url: bm.url });
+
+ // Do a search using the keyword and check the hidden heuristic result.
+ let heuristic = await search({
+ value: "bm foo",
+ expectedGroup: UrlbarUtils.RESULT_GROUP.HEURISTIC_BOOKMARK_KEYWORD,
+ });
+ Assert.equal(
+ heuristic.payload.keyword,
+ keyword,
+ "Heuristic keyword is correct"
+ );
+ let heuristicURL = "http://example.com/?q=foo";
+ Assert.equal(
+ heuristic.payload.url,
+ heuristicURL,
+ "Heuristic URL is correct"
+ );
+
+ // Check the other visit results.
+ await checkVisitResults(visitURLs);
+
+ // Press enter to verify the heuristic result is loaded.
+ await synthesizeEnterAndAwaitLoad(heuristicURL);
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await UrlbarTestUtils.formHistory.clear(window);
+ });
+ });
+});
+
+// UrlbarUtils.RESULT_GROUP.HEURISTIC_AUTOFILL should be hidden.
+add_task(async function autofill() {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await withVisits(async visitURLs => {
+ // Do a search that triggers autofill and check the hidden heuristic
+ // result.
+ let heuristic = await search({
+ value: "ex",
+ expectedGroup: UrlbarUtils.RESULT_GROUP.HEURISTIC_AUTOFILL,
+ });
+ Assert.ok(heuristic.autofill, "Heuristic is autofill");
+ let heuristicURL = "http://example.com/";
+ Assert.equal(
+ heuristic.payload.url,
+ heuristicURL,
+ "Heuristic URL is correct"
+ );
+ Assert.equal(gURLBar.value, "example.com/", "Input has been autofilled");
+
+ // Check the other visit results.
+ await checkVisitResults(visitURLs);
+
+ // Press enter to verify the heuristic result is loaded.
+ await synthesizeEnterAndAwaitLoad(heuristicURL);
+ });
+ });
+});
+
+// UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK with an unknown URL should be
+// hidden.
+add_task(async function fallback_unknownURL() {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ // Do a search for an unknown URL and check the hidden heuristic result.
+ let url = "http://example.com/unknown-url";
+ let heuristic = await search({
+ value: url,
+ expectedGroup: UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK,
+ });
+ Assert.equal(heuristic.payload.url, url, "Heuristic URL is correct");
+
+ // Press enter to verify the heuristic result is loaded.
+ await synthesizeEnterAndAwaitLoad(url);
+ });
+});
+
+// UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK with the search restriction token
+// should be hidden.
+add_task(async function fallback_searchRestrictionToken() {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await withVisits(async visitURLs => {
+ // Add a mock default engine so we don't hit the network.
+ await withEngine({ makeDefault: true }, async () => {
+ // Do a search with `?` and check the hidden heuristic result.
+ let heuristic = await search({
+ value: UrlbarTokenizer.RESTRICT.SEARCH + " foo",
+ expectedGroup: UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK,
+ });
+ Assert.equal(
+ heuristic.payload.engine,
+ "Example",
+ "Heuristic engine is correct"
+ );
+ Assert.equal(
+ heuristic.payload.query,
+ "foo",
+ "Heuristic query is correct"
+ );
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: "Example",
+ entry: "typed",
+ });
+
+ // Check the other visit results.
+ await checkVisitResults(visitURLs);
+
+ // Press enter to verify the heuristic result is loaded.
+ await synthesizeEnterAndAwaitLoad("https://example.com/?q=foo");
+
+ await UrlbarTestUtils.formHistory.clear(window);
+ });
+ });
+ });
+});
+
+// UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK with a search string that falls
+// back to a search result should be hidden.
+add_task(async function fallback_search() {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await withVisits(async visitURLs => {
+ // Add a mock default engine so we don't hit the network.
+ await withEngine({ makeDefault: true }, async () => {
+ // Do a search and check the hidden heuristic result.
+ let heuristic = await search({
+ value: "foo",
+ expectedGroup: UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK,
+ });
+ Assert.equal(
+ heuristic.payload.engine,
+ "Example",
+ "Heuristic engine is correct"
+ );
+ Assert.equal(
+ heuristic.payload.query,
+ "foo",
+ "Heuristic query is correct"
+ );
+
+ // Check the other visit results.
+ await checkVisitResults(visitURLs);
+
+ // Press enter to verify the heuristic result is loaded.
+ await synthesizeEnterAndAwaitLoad("https://example.com/?q=foo");
+
+ await UrlbarTestUtils.formHistory.clear(window);
+ });
+ });
+ });
+});
+
+// Picking a non-heuristic result should work correctly (and not pick the
+// heuristic).
+add_task(async function pickNonHeuristic() {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await withVisits(async visitURLs => {
+ // Do a search that triggers autofill and check the hidden heuristic
+ // result.
+ let heuristic = await search({
+ value: "ex",
+ expectedGroup: UrlbarUtils.RESULT_GROUP.HEURISTIC_AUTOFILL,
+ });
+ Assert.ok(heuristic.autofill, "Heuristic is autofill");
+ Assert.equal(
+ heuristic.payload.url,
+ "http://example.com/",
+ "Heuristic URL is correct"
+ );
+
+ // Pick the first visit result.
+ Assert.notEqual(
+ heuristic.payload.url,
+ visitURLs[0],
+ "Sanity check: Heuristic and first results have different URLs"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await synthesizeEnterAndAwaitLoad(visitURLs[0]);
+ });
+ });
+});
+
+/**
+ * Adds `maxRichResults` visits, calls your callback, and clears history. We add
+ * `maxRichResults` visits to verify that the view correctly contains the
+ * maximum number of results when the heuristic is hidden.
+ *
+ * @param {Function} callback
+ * The callback to call after adding visits. Can be async
+ */
+async function withVisits(callback) {
+ let urls = [];
+ for (let i = 0; i < UrlbarPrefs.get("maxRichResults"); i++) {
+ urls.push("http://example.com/foo/" + i);
+ }
+ await PlacesTestUtils.addVisits(urls);
+
+ // The URLs will appear in the view in reverse order so that newer visits are
+ // first. Reverse the array now so callers to `checkVisitResults` or
+ // `checkVisitResults` itself doesn't need to do it.
+ urls.reverse();
+
+ await callback(urls);
+ await PlacesUtils.history.clear();
+}
+
+/**
+ * Adds a search engine, calls your callback, and removes the engine.
+ *
+ * @param {object} options
+ * Options object
+ * @param {string} [options.keyword]
+ * The keyword/alias for the engine.
+ * @param {boolean} [options.makeDefault]
+ * Whether to make the engine default.
+ * @param {Function} callback
+ * The callback to call after changing the default search engine. Can be async
+ */
+async function withEngine(
+ { keyword = undefined, makeDefault = false },
+ callback
+) {
+ await SearchTestUtils.installSearchExtension({ keyword });
+ let engine = Services.search.getEngineByName("Example");
+ let originalEngine;
+ if (makeDefault) {
+ originalEngine = await Services.search.getDefault();
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ }
+ await callback();
+ if (originalEngine) {
+ await Services.search.setDefault(
+ originalEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ }
+ await Services.search.removeEngine(engine);
+}
+
+/**
+ * Asserts the view contains visit results with the given URLs.
+ *
+ * @param {Array} expectedURLs
+ * The expected urls.
+ */
+async function checkVisitResults(expectedURLs) {
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ expectedURLs.length,
+ "The view has other results"
+ );
+ for (let i = 0; i < expectedURLs.length; i++) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.URL,
+ "Other result type is correct at index " + i
+ );
+ Assert.equal(
+ result.source,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ "Other result source is correct at index " + i
+ );
+ Assert.equal(
+ result.url,
+ expectedURLs[i],
+ "Other result URL is correct at index " + i
+ );
+ }
+}
+
+/**
+ * Performs a search and makes some basic assertions under the assumption that
+ * the heuristic should be hidden.
+ *
+ * @param {object} options
+ * Options object
+ * @param {string} options.value
+ * The search string.
+ * @param {UrlbarUtils.RESULT_GROUP} options.expectedGroup
+ * The expected result group of the hidden heuristic.
+ * @returns {UrlbarResult}
+ * The hidden heuristic result.
+ */
+async function search({ value, expectedGroup }) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value,
+ fireInputEvent: true,
+ });
+
+ // _resultForCurrentValue should be the hidden heuristic result.
+ let { _resultForCurrentValue: result } = gURLBar;
+ Assert.ok(result, "_resultForCurrentValue is defined");
+ Assert.ok(result.heuristic, "_resultForCurrentValue.heuristic is true");
+ Assert.equal(
+ UrlbarUtils.getResultGroup(result),
+ expectedGroup,
+ "_resultForCurrentValue has expected group"
+ );
+
+ Assert.ok(!UrlbarTestUtils.getSelectedElement(window), "No selection exists");
+
+ return result;
+}
+
+/**
+ * Synthesizes the enter key and waits for a load in the current tab.
+ *
+ * @param {string} expectedURL
+ * The URL that should load.
+ */
+async function synthesizeEnterAndAwaitLoad(expectedURL) {
+ let loadPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ expectedURL
+ );
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+ await PlacesUtils.history.clear();
+}
diff --git a/browser/components/urlbar/tests/browser/browser_ime_composition.js b/browser/components/urlbar/tests/browser/browser_ime_composition.js
new file mode 100644
index 0000000000..13a0cf0584
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_ime_composition.js
@@ -0,0 +1,327 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests ime composition handling.
+
+function composeAndCheckPanel(string, isPopupOpen) {
+ EventUtils.synthesizeCompositionChange({
+ composition: {
+ string,
+ clauses: [
+ {
+ length: string.length,
+ attr: Ci.nsITextInputProcessor.ATTR_RAW_CLAUSE,
+ },
+ ],
+ },
+ caret: { start: string.length, length: 0 },
+ key: { key: string ? string[string.length - 1] : "KEY_Backspace" },
+ });
+ Assert.equal(
+ UrlbarTestUtils.isPopupOpen(window),
+ isPopupOpen,
+ "Check panel open state"
+ );
+}
+
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0],
+ ["browser.urlbar.suggest.quickactions", false],
+ ],
+ });
+
+ await PlacesUtils.history.clear();
+ // Add at least one typed entry for the empty results set. Also clear history
+ // so that this can be over the autofill threshold.
+ await PlacesTestUtils.addVisits({
+ uri: "http://mozilla.org/",
+ transition: PlacesUtils.history.TRANSITIONS.TYPED,
+ });
+ // Add a bookmark to ensure we autofill the engine domain for tab-to-search.
+ let bm = await PlacesUtils.bookmarks.insert({
+ url: "http://example.com/",
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ });
+
+ await SearchTestUtils.installSearchExtension(
+ {
+ name: "Test",
+ keyword: "@test",
+ },
+ { setAsDefault: true }
+ );
+
+ registerCleanupFunction(async () => {
+ await PlacesUtils.bookmarks.remove(bm);
+ await PlacesUtils.history.clear();
+ });
+
+ // Test both pref values.
+ for (let val of [true, false]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.keepPanelOpenDuringImeComposition", val]],
+ });
+ await test_composition(val);
+ await test_composition_searchMode_preview(val);
+ await test_composition_tabToSearch(val);
+ await test_composition_autofill(val);
+ }
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+});
+
+async function test_composition(keepPanelOpenDuringImeComposition) {
+ gURLBar.focus();
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ info("Check the panel state during composition");
+ composeAndCheckPanel("I", false);
+ Assert.equal(gURLBar.value, "I", "Check urlbar value");
+ composeAndCheckPanel("In", false);
+ Assert.equal(gURLBar.value, "In", "Check urlbar value");
+
+ info("Committing composition should open the panel.");
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeComposition({
+ type: "compositioncommitasis",
+ key: { key: "KEY_Enter" },
+ });
+ });
+ Assert.equal(gURLBar.value, "In", "Check urlbar value");
+
+ info("Check the panel state starting from an open panel.");
+ Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Popup should be open");
+ composeAndCheckPanel("t", keepPanelOpenDuringImeComposition);
+ Assert.equal(gURLBar.value, "Int", "Check urlbar value");
+ composeAndCheckPanel("te", keepPanelOpenDuringImeComposition);
+ Assert.equal(gURLBar.value, "Inte", "Check urlbar value");
+
+ // Committing composition should open the popup.
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeComposition({
+ type: "compositioncommitasis",
+ key: { key: "KEY_Enter" },
+ });
+ });
+ Assert.equal(gURLBar.value, "Inte", "Check urlbar value");
+
+ info("If composition is cancelled, the value shouldn't be changed.");
+ Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Popup should be open");
+ composeAndCheckPanel("r", keepPanelOpenDuringImeComposition);
+ Assert.equal(gURLBar.value, "Inter", "Check urlbar value");
+ composeAndCheckPanel("", keepPanelOpenDuringImeComposition);
+ Assert.equal(gURLBar.value, "Inte", "Check urlbar value");
+ // Canceled compositionend should reopen the popup.
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeComposition({
+ type: "compositioncommit",
+ data: "",
+ key: { key: "KEY_Escape" },
+ });
+ });
+ Assert.equal(gURLBar.value, "Inte", "Check urlbar value");
+
+ info(
+ "If composition replaces some characters and canceled, the search string should be the latest value."
+ );
+ Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Popup should be open");
+ EventUtils.synthesizeKey("VK_LEFT", { shiftKey: true });
+ EventUtils.synthesizeKey("VK_LEFT", { shiftKey: true });
+ composeAndCheckPanel("t", keepPanelOpenDuringImeComposition);
+ Assert.equal(gURLBar.value, "Int", "Check urlbar value");
+ composeAndCheckPanel("te", keepPanelOpenDuringImeComposition);
+ Assert.equal(gURLBar.value, "Inte", "Check urlbar value");
+ composeAndCheckPanel("", keepPanelOpenDuringImeComposition);
+ Assert.equal(gURLBar.value, "In", "Check urlbar value");
+
+ // Canceled compositionend should search the result with the latest value.
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeComposition({
+ type: "compositioncommitasis",
+ key: { key: "KEY_Escape" },
+ });
+ });
+ Assert.equal(gURLBar.value, "In", "Check urlbar value");
+
+ Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Popup should be open");
+ info(
+ "Removing all characters should leave the popup open, Esc should then close it."
+ );
+ EventUtils.synthesizeKey("KEY_Backspace", {});
+ EventUtils.synthesizeKey("KEY_Backspace", {});
+ Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Popup should be open");
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeKey("KEY_Escape", {});
+ });
+ Assert.equal(gURLBar.value, "", "Check urlbar value");
+
+ info("Composition which is canceled shouldn't cause opening the popup.");
+ Assert.ok(!UrlbarTestUtils.isPopupOpen(window), "Popup should be closed");
+ composeAndCheckPanel("I", false);
+ Assert.equal(gURLBar.value, "I", "Check urlbar value");
+ composeAndCheckPanel("In", false);
+ Assert.equal(gURLBar.value, "In", "Check urlbar value");
+ composeAndCheckPanel("", false);
+ Assert.equal(gURLBar.value, "", "Check urlbar value");
+
+ info("Canceled compositionend shouldn't open the popup if it was closed.");
+ await UrlbarTestUtils.promisePopupClose(window);
+ EventUtils.synthesizeComposition({
+ type: "compositioncommitasis",
+ key: { key: "KEY_Escape" },
+ });
+ Assert.ok(!UrlbarTestUtils.isPopupOpen(window), "Popup should be closed");
+ Assert.equal(gURLBar.value, "", "Check urlbar value");
+
+ info("Down key should open the popup even if the editor is empty.");
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {});
+ });
+ Assert.equal(gURLBar.value, "", "Check urlbar value");
+
+ info(
+ "If popup is open at starting composition, the popup should be reopened after composition anyway."
+ );
+ Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Popup should be open");
+ composeAndCheckPanel("I", keepPanelOpenDuringImeComposition);
+ Assert.equal(gURLBar.value, "I", "Check urlbar value");
+ composeAndCheckPanel("In", keepPanelOpenDuringImeComposition);
+ Assert.equal(gURLBar.value, "In", "Check urlbar value");
+ composeAndCheckPanel("", keepPanelOpenDuringImeComposition);
+ Assert.equal(gURLBar.value, "", "Check urlbar value");
+ // A canceled compositionend should open the popup if it was open.
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeComposition({
+ type: "compositioncommitasis",
+ key: { key: "KEY_Escape" },
+ });
+ });
+ Assert.equal(gURLBar.value, "", "Check urlbar value");
+
+ info("Type normally, and hit escape, the popup should be closed.");
+ Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Popup should be open");
+ EventUtils.synthesizeKey("I", {});
+ EventUtils.synthesizeKey("n", {});
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeKey("KEY_Escape", {});
+ });
+ Assert.equal(gURLBar.value, "In", "Check urlbar value");
+ // Clear typed chars.
+ EventUtils.synthesizeKey("KEY_Backspace", {});
+ EventUtils.synthesizeKey("KEY_Backspace", {});
+ Assert.equal(gURLBar.value, "", "Check urlbar value");
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeKey("KEY_Escape", {});
+ });
+
+ info("With autofill, compositionstart shouldn't open the popup");
+ Assert.ok(!UrlbarTestUtils.isPopupOpen(window), "Popup should be closed");
+ composeAndCheckPanel("M", false);
+ Assert.equal(gURLBar.value, "M", "Check urlbar value");
+ composeAndCheckPanel("Mo", false);
+ Assert.equal(gURLBar.value, "Mo", "Check urlbar value");
+ // Committing composition should open the popup.
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeComposition({
+ type: "compositioncommitasis",
+ key: { key: "KEY_Enter" },
+ });
+ });
+ Assert.equal(gURLBar.value, "Mozilla.org/", "Check urlbar value");
+}
+
+async function test_composition_searchMode_preview(
+ keepPanelOpenDuringImeComposition
+) {
+ info("Check Search Mode preview is retained by composition");
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@",
+ });
+
+ while (gURLBar.searchMode?.engineName != "Test") {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
+ }
+ let expectedSearchMode = {
+ engineName: "Test",
+ isPreview: true,
+ entry: "keywordoffer",
+ };
+ await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode);
+ composeAndCheckPanel("I", keepPanelOpenDuringImeComposition);
+ Assert.equal(gURLBar.value, "I", "Check urlbar value");
+ if (keepPanelOpenDuringImeComposition) {
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ }
+ // Test that we are in confirmed search mode.
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: "Test",
+ entry: "keywordoffer",
+ });
+ await UrlbarTestUtils.exitSearchMode(window);
+}
+
+async function test_composition_tabToSearch(keepPanelOpenDuringImeComposition) {
+ info("Check Tab-to-Search is retained by composition");
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exa",
+ fireInputEvent: true,
+ });
+
+ while (gURLBar.searchMode?.engineName != "Test") {
+ EventUtils.synthesizeKey("KEY_Tab", {}, window);
+ }
+ let expectedSearchMode = {
+ engineName: "Test",
+ isPreview: true,
+ entry: "tabtosearch",
+ };
+ await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode);
+ composeAndCheckPanel("I", keepPanelOpenDuringImeComposition);
+ Assert.equal(gURLBar.value, "I", "Check urlbar value");
+ if (keepPanelOpenDuringImeComposition) {
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ }
+ // Test that we are in confirmed search mode.
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: "Test",
+ entry: "tabtosearch",
+ });
+ await UrlbarTestUtils.exitSearchMode(window);
+}
+
+async function test_composition_autofill(keepPanelOpenDuringImeComposition) {
+ info("Check whether autofills or not");
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ info("Check the urlbar value during composition");
+ composeAndCheckPanel("m", false);
+
+ if (keepPanelOpenDuringImeComposition) {
+ info("Wait for search suggestions");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ }
+
+ Assert.equal(
+ gURLBar.value,
+ "m",
+ "The urlbar value is not autofilled while turning IME on"
+ );
+
+ info("Check the urlbar value after committing composition");
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeComposition({
+ type: "compositioncommitasis",
+ key: { key: "KEY_Enter" },
+ });
+ });
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ Assert.equal(gURLBar.value, "mozilla.org/", "The urlbar value is autofilled");
+
+ // Clean-up.
+ gURLBar.value = "";
+}
diff --git a/browser/components/urlbar/tests/browser/browser_inputHistory.js b/browser/components/urlbar/tests/browser/browser_inputHistory.js
new file mode 100644
index 0000000000..7fb93ca35d
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_inputHistory.js
@@ -0,0 +1,548 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This tests the urlbar adaptive behavior powered by input history.
+ */
+
+"use strict";
+
+async function bumpScore(
+ uri,
+ searchString,
+ counts,
+ useMouseClick = false,
+ needToLoad = false
+) {
+ if (counts.visits) {
+ let visits = new Array(counts.visits).fill(uri);
+ await PlacesTestUtils.addVisits(visits);
+ }
+ if (counts.picks) {
+ for (let i = 0; i < counts.picks; ++i) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: searchString,
+ });
+ let promise = needToLoad
+ ? BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser)
+ : BrowserTestUtils.waitForDocLoadAndStopIt(
+ uri,
+ gBrowser.selectedBrowser
+ );
+ // Look for the expected uri.
+ while (gURLBar.untrimmedValue != uri) {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {});
+ }
+ if (useMouseClick) {
+ let element = UrlbarTestUtils.getSelectedRow(window);
+ EventUtils.synthesizeMouseAtCenter(element, {});
+ } else {
+ EventUtils.synthesizeKey("KEY_Enter", {});
+ }
+ await promise;
+ }
+ }
+ await PlacesTestUtils.promiseAsyncUpdates();
+}
+
+async function decayInputHistory() {
+ await Cc["@mozilla.org/places/frecency-recalculator;1"]
+ .getService(Ci.nsIObserver)
+ .wrappedJSObject.decay();
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // We don't want autofill to influence this test.
+ ["browser.urlbar.autoFill", false],
+ ],
+ });
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+});
+
+add_task(async function test_adaptive_with_search_terms() {
+ let url1 = "http://site.tld/1";
+ let url2 = "http://site.tld/2";
+
+ info("Same visit count, same picks, one partial match, one exact match");
+ await PlacesUtils.history.clear();
+ await bumpScore(url1, "si", { visits: 3, picks: 3 });
+ await bumpScore(url2, "site", { visits: 3, picks: 3 });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "si",
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.url, url1, "Check first result");
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2);
+ Assert.equal(result.url, url2, "Check second result");
+
+ info(
+ "Same visit count, same picks, one partial match, one exact match, invert"
+ );
+ await PlacesUtils.history.clear();
+ await bumpScore(url1, "site", { visits: 3, picks: 3 });
+ await bumpScore(url2, "si", { visits: 3, picks: 3 });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "si",
+ });
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.url, url2, "Check first result");
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2);
+ Assert.equal(result.url, url1, "Check second result");
+
+ info("Same visit count, different picks, both exact match");
+ await PlacesUtils.history.clear();
+ await bumpScore(url1, "si", { visits: 3, picks: 3 });
+ await bumpScore(url2, "si", { visits: 3, picks: 1 });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "si",
+ });
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.url, url1, "Check first result");
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2);
+ Assert.equal(result.url, url2, "Check second result");
+
+ info("Same visit count, different picks, both exact match, invert");
+ await PlacesUtils.history.clear();
+ await bumpScore(url1, "si", { visits: 3, picks: 1 });
+ await bumpScore(url2, "si", { visits: 3, picks: 3 });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "si",
+ });
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.url, url2, "Check first result");
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2);
+ Assert.equal(result.url, url1, "Check second result");
+
+ info("Same visit count, different picks, both partial match");
+ await PlacesUtils.history.clear();
+ await bumpScore(url1, "site", { visits: 3, picks: 3 });
+ await bumpScore(url2, "site", { visits: 3, picks: 1 });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "si",
+ });
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.url, url1, "Check first result");
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2);
+ Assert.equal(result.url, url2, "Check second result");
+
+ info("Same visit count, different picks, both partial match, invert");
+ await PlacesUtils.history.clear();
+ await bumpScore(url1, "site", { visits: 3, picks: 1 });
+ await bumpScore(url2, "site", { visits: 3, picks: 3 });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "si",
+ });
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.url, url2, "Check first result");
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2);
+ Assert.equal(result.url, url1, "Check second result");
+});
+
+add_task(async function test_adaptive_with_decay() {
+ let url1 = "http://site.tld/1";
+ let url2 = "http://site.tld/2";
+
+ info("Same visit count, same picks, both exact match, decay first");
+ await PlacesUtils.history.clear();
+ await bumpScore(url1, "si", { visits: 3, picks: 3 });
+ await decayInputHistory();
+ await bumpScore(url2, "si", { visits: 3, picks: 3 });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "si",
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.url, url2, "Check first result");
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2);
+ Assert.equal(result.url, url1, "Check second result");
+
+ info("Same visit count, same picks, both exact match, decay second");
+ await PlacesUtils.history.clear();
+ await bumpScore(url2, "si", { visits: 3, picks: 3 });
+ await decayInputHistory();
+ await bumpScore(url1, "si", { visits: 3, picks: 3 });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "si",
+ });
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.url, url1, "Check first result");
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2);
+ Assert.equal(result.url, url2, "Check second result");
+});
+
+add_task(async function test_adaptive_limited() {
+ let url1 = "http://site.tld/1";
+ let url2 = "http://site.tld/2";
+
+ info("Same visit count, same picks, both exact match, decay first");
+ await PlacesUtils.history.clear();
+ await bumpScore(url1, "si", { visits: 3, picks: 3 });
+ await decayInputHistory();
+ await bumpScore(url2, "si", { visits: 3, picks: 3 });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "si",
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.url, url2, "Check first result");
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2);
+ Assert.equal(result.url, url1, "Check second result");
+
+ info("Same visit count, same picks, both exact match, decay second");
+ await PlacesUtils.history.clear();
+ await bumpScore(url2, "si", { visits: 3, picks: 3 });
+ await decayInputHistory();
+ await bumpScore(url1, "si", { visits: 3, picks: 3 });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "si",
+ });
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.url, url1, "Check first result");
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2);
+ Assert.equal(result.url, url2, "Check second result");
+});
+
+add_task(async function test_adaptive_limited() {
+ info("Up to 3 adaptive results should be added at the top, then enqueued");
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ // Add as many adaptive results as maxRichResults.
+ let n = UrlbarPrefs.get("maxRichResults");
+ let urls = Array(n)
+ .fill(0)
+ .map((e, i) => "http://site.tld/" + i);
+ for (let url of urls) {
+ await bumpScore(url, "site", { visits: 1, picks: 1 });
+ }
+
+ // Add a matching bookmark with an higher frecency.
+ let url = "http://site.bookmark.tld/";
+ await PlacesTestUtils.addVisits(url);
+ let bm = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "test_site_book",
+ url,
+ });
+
+ // After 1 heuristic and 3 input history results.
+ let expectedBookmarkIndex = 4;
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "site",
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ expectedBookmarkIndex
+ );
+ Assert.equal(result.url, url, "Check bookmarked result");
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, n - 1);
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ n,
+ "Check all the results are filled"
+ );
+ Assert.ok(
+ result.url.startsWith("http://site.tld"),
+ "Check last adaptive result"
+ );
+
+ await PlacesUtils.bookmarks.remove(bm);
+});
+
+add_task(async function test_adaptive_behaviors() {
+ info(
+ "Check adaptive results are not provided regardless of the requested behavior"
+ );
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ // Add an adaptive entry.
+ let historyUrl = "http://site.tld/1";
+ await bumpScore(historyUrl, "site", { visits: 1, picks: 1 });
+
+ let bookmarkURL = "http://bookmarked.site.tld/1";
+ let bm = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "test_book",
+ url: bookmarkURL,
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Search only bookmarks.
+ ["browser.urlbar.suggest.bookmark", true],
+ ["browser.urlbar.suggest.history", false],
+ ],
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "site",
+ });
+ let result = (await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1))
+ .result;
+ Assert.equal(result.payload.url, bookmarkURL, "Check bookmarked result");
+ Assert.notEqual(
+ result.providerName,
+ "InputHistory",
+ "The bookmarked result is not from InputHistory."
+ );
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 2,
+ "Check there are no unexpected results"
+ );
+ await PlacesUtils.bookmarks.remove(bm);
+
+ // Repeat the previous case but now the bookmark has the same URL as the
+ // history result. We expect the returned result comes from InputHistory.
+ bm = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "test_book",
+ url: historyUrl,
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "sit",
+ });
+ result = (await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1))
+ .result;
+ Assert.equal(result.payload.url, historyUrl, "Check bookmarked result");
+ Assert.equal(
+ result.providerName,
+ "InputHistory",
+ "The bookmarked result is from InputHistory."
+ );
+ Assert.equal(
+ result.source,
+ UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ "The input history result is a bookmark."
+ );
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 2,
+ "Check there are no unexpected results"
+ );
+
+ await SpecialPowers.popPrefEnv();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Search only open pages. We don't provide an open page, but we want to
+ // enable at least one of these prefs so that UrlbarProviderInputHistory
+ // is active.
+ ["browser.urlbar.suggest.bookmark", false],
+ ["browser.urlbar.suggest.history", false],
+ ["browser.urlbar.suggest.openpage", true],
+ ],
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "site",
+ });
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 1,
+ "There is no adaptive history result because it is not an open page."
+ );
+ await SpecialPowers.popPrefEnv();
+
+ // Clearing history but not deleting the bookmark. This simulates the case
+ // where the user has cleared their history or is using permanent private
+ // browsing mode.
+ await PlacesUtils.history.clear();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.bookmark", true],
+ ["browser.urlbar.suggest.history", false],
+ ["browser.urlbar.suggest.openpage", false],
+ ],
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "sit",
+ });
+ result = (await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1))
+ .result;
+ Assert.equal(result.payload.url, historyUrl, "Check bookmarked result");
+ Assert.equal(
+ result.providerName,
+ "InputHistory",
+ "The bookmarked result is from InputHistory."
+ );
+ Assert.equal(
+ result.source,
+ UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ "The input history result is a bookmark."
+ );
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 2,
+ "Check there are no unexpected results"
+ );
+
+ await PlacesUtils.bookmarks.remove(bm);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_adaptive_mouse() {
+ info("Check adaptive results are updated on mouse picks");
+ let url1 = "http://site.tld/1";
+ let url2 = "http://site.tld/2";
+
+ info("Same visit count, different picks");
+ await PlacesUtils.history.clear();
+ await bumpScore(url1, "site", { visits: 3, picks: 3 }, true);
+ await bumpScore(url2, "site", { visits: 3, picks: 1 }, true);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "si",
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.url, url1, "Check first result");
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2);
+ Assert.equal(result.url, url2, "Check second result");
+
+ info("Same visit count, different picks, invert");
+ await PlacesUtils.history.clear();
+ await bumpScore(url1, "site", { visits: 3, picks: 1 }, true);
+ await bumpScore(url2, "site", { visits: 3, picks: 3 }, true);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "si",
+ });
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.url, url2, "Check first result");
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2);
+ Assert.equal(result.url, url1, "Check second result");
+});
+
+add_task(async function test_adaptive_searchmode() {
+ info("Check adaptive history is not shown in search mode.");
+
+ let suggestionsEngine = await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + "searchSuggestionEngine.xml",
+ });
+
+ let url1 = "http://site.tld/1";
+ let url2 = "http://site.tld/2";
+
+ info("Sanity check: adaptive history is shown for a normal search.");
+ await PlacesUtils.history.clear();
+ await bumpScore(url1, "site", { visits: 3, picks: 3 }, true);
+ await bumpScore(url2, "site", { visits: 3, picks: 1 }, true);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "si",
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.url, url1, "Check first result");
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2);
+ Assert.equal(result.url, url2, "Check second result");
+
+ info("Entering search mode.");
+ // enterSearchMode checks internally that our site.tld URLs are not shown.
+ await UrlbarTestUtils.enterSearchMode(window, {
+ engineName: suggestionsEngine.name,
+ });
+
+ await Services.search.removeEngine(suggestionsEngine);
+});
+
+add_task(async function test_ignore_case() {
+ const url1 = "http://example.com/yes";
+ const url2 = "http://example.com/no";
+ await PlacesUtils.history.clear();
+ await PlacesTestUtils.addVisits([url1, url2]);
+ await UrlbarUtils.addToInputHistory(url1, "SampLE");
+ await UrlbarUtils.addToInputHistory(url1, "SaMpLE");
+ await UrlbarUtils.addToInputHistory(url1, "SAMPLE");
+ await UrlbarUtils.addToInputHistory(url1, "sample");
+ await UrlbarUtils.addToInputHistory(url2, "sample");
+ await UrlbarUtils.addToInputHistory(url2, "sample");
+ await UrlbarUtils.addToInputHistory(url2, "sample");
+ await UrlbarUtils.addToInputHistory(url2, "sample");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "sAM",
+ });
+ const result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(
+ result.url,
+ url1,
+ "Seaching for input history is case-insensitive"
+ );
+});
+
+add_task(async function test_adaptive_history_in_privatewindow() {
+ info(
+ "Check adaptive history is not shown in private window as tab switching candidate."
+ );
+
+ await PlacesUtils.history.clear();
+
+ info("Add a test url as an input history.");
+ const url = "http://example.com/";
+ // We need to wait for loading the page in order to register the url into
+ // moz_openpages_temp table.
+ await bumpScore(url, "exa", { visits: 1, picks: 1 }, false, true);
+
+ info("Check the url could be registered properly.");
+ const connection = await PlacesUtils.promiseLargeCacheDBConnection();
+ const rows = await connection.executeCached(
+ "SELECT userContextId FROM moz_openpages_temp WHERE url = :url",
+ { url }
+ );
+ Assert.equal(rows.length, 1, "Length of rows for the url is 1.");
+ Assert.greaterOrEqual(
+ rows[0].getResultByName("userContextId"),
+ 0,
+ "The url is registered as non-private-browsing context."
+ );
+
+ info("Open popup in private window.");
+ const privateWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: privateWindow,
+ value: "ex",
+ });
+
+ info("Check the popup results.");
+ let hasResult = false;
+ for (let i = 0; i < UrlbarTestUtils.getResultCount(privateWindow); i++) {
+ const result = await UrlbarTestUtils.getDetailsOfResultAt(privateWindow, i);
+
+ if (result.url !== url) {
+ continue;
+ }
+
+ Assert.notEqual(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ "Result type of the url is not for tab switch."
+ );
+
+ hasResult = true;
+ }
+ Assert.ok(hasResult, "Popup has a result for the url.");
+
+ await BrowserTestUtils.closeWindow(privateWindow);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_inputHistory_autofill.js b/browser/components/urlbar/tests/browser/browser_inputHistory_autofill.js
new file mode 100644
index 0000000000..19457884b6
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_inputHistory_autofill.js
@@ -0,0 +1,207 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests for input history related to autofill.
+
+"use strict";
+
+let addToInputHistorySpy;
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.autoFill.adaptiveHistory.enabled", true]],
+ });
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+
+ let sandbox = sinon.createSandbox();
+ addToInputHistorySpy = sandbox.spy(UrlbarUtils, "addToInputHistory");
+
+ registerCleanupFunction(async () => {
+ sandbox.restore();
+ });
+});
+
+// Input history use count should be bumped when an adaptive history autofill
+// result is triggered and picked.
+add_task(async function bumped() {
+ let input = "exam";
+ let tests = [
+ // Basic test where the search string = the adaptive history input.
+ {
+ url: "http://example.com/test",
+ searchString: "exam",
+ },
+ // The history with input "exam" should be bumped, not "example", even
+ // though the search string is "example".
+ {
+ url: "http://example.com/test",
+ searchString: "example",
+ },
+ // The history with URL "http://www.example.com/test" should be bumped, not
+ // "http://example.com/test", even though the search string starts with
+ // "example".
+ {
+ url: "http://www.example.com/test",
+ searchString: "exam",
+ },
+ ];
+
+ for (let { url, searchString } of tests) {
+ info("Running subtest: " + JSON.stringify({ url, searchString }));
+
+ await PlacesTestUtils.addVisits(url);
+ await UrlbarUtils.addToInputHistory(url, input);
+ addToInputHistorySpy.resetHistory();
+
+ let initialUseCount = await getUseCount({ url, input });
+ info("Got initial use count: " + initialUseCount);
+
+ await triggerAutofillAndPickResult(searchString, "example.com/test");
+
+ let calls = addToInputHistorySpy.getCalls();
+ Assert.equal(
+ calls.length,
+ 1,
+ "UrlbarUtils.addToInputHistory() called once"
+ );
+ Assert.deepEqual(
+ calls[0].args,
+ [url, input],
+ "UrlbarUtils.addToInputHistory() called with expected args"
+ );
+
+ Assert.greater(
+ await getUseCount({ url, input }),
+ initialUseCount,
+ "New use count > initial use count"
+ );
+
+ if (searchString != input) {
+ Assert.strictEqual(
+ await getUseCount({ input: searchString }),
+ undefined,
+ "Search string not present in input history: " + searchString
+ );
+ }
+
+ await PlacesUtils.history.clear();
+ await PlacesTestUtils.clearInputHistory();
+ addToInputHistorySpy.resetHistory();
+ }
+});
+
+// Input history use count should not be bumped when an origin autofill result
+// is triggered and picked.
+add_task(async function notBumped_origin() {
+ // Add enough visits to trigger origin autofill.
+ let url = "http://example.com/test";
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits(url);
+ }
+
+ await triggerAutofillAndPickResult("exam", "example.com/");
+
+ let calls = addToInputHistorySpy.getCalls();
+ Assert.equal(calls.length, 0, "UrlbarUtils.addToInputHistory() not called");
+
+ Assert.strictEqual(
+ await getUseCount({ url }),
+ undefined,
+ "URL not present in input history: " + url
+ );
+
+ await PlacesUtils.history.clear();
+});
+
+// Input history use count should not be bumped when a URL autofill result is
+// triggered and picked.
+add_task(async function notBumped_url() {
+ let url = "http://example.com/test";
+ await PlacesTestUtils.addVisits(url);
+
+ await triggerAutofillAndPickResult("example.com/t", "example.com/test");
+
+ let calls = addToInputHistorySpy.getCalls();
+ Assert.equal(calls.length, 0, "UrlbarUtils.addToInputHistory() not called");
+
+ Assert.strictEqual(
+ await getUseCount({ url }),
+ undefined,
+ "URL not present in input history: " + url
+ );
+
+ await PlacesUtils.history.clear();
+});
+
+/**
+ * Performs a search and picks the first result.
+ *
+ * @param {string} searchString
+ * The search string. Assumed to trigger an autofill result.
+ * @param {string} autofilledValue
+ * The input's expected value after autofill occurs.
+ */
+async function triggerAutofillAndPickResult(searchString, autofilledValue) {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: searchString,
+ fireInputEvent: true,
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill, "Result is autofill");
+ Assert.equal(gURLBar.value, autofilledValue, "gURLBar.value");
+ Assert.equal(gURLBar.selectionStart, searchString.length, "selectionStart");
+ Assert.equal(gURLBar.selectionEnd, autofilledValue.length, "selectionEnd");
+
+ let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+ });
+}
+
+/**
+ * Gets the use count of an input history record.
+ *
+ * @param {object} options
+ * Options object.
+ * @param {string} [options.url]
+ * The URL of the `moz_places` row corresponding to the record.
+ * @param {string} [options.input]
+ * The `input` value in the record.
+ * @returns {number}
+ * The use count. If no record exists with the URL and/or input, undefined is
+ * returned.
+ */
+async function getUseCount({ url = undefined, input = undefined }) {
+ return PlacesUtils.withConnectionWrapper("test::getUseCount", async db => {
+ let rows;
+ if (input && url) {
+ rows = await db.executeCached(
+ `SELECT i.use_count
+ FROM moz_inputhistory i
+ JOIN moz_places h ON h.id = i.place_id
+ WHERE h.url = :url AND i.input = :input`,
+ { url, input }
+ );
+ } else if (url) {
+ rows = await db.executeCached(
+ `SELECT i.use_count
+ FROM moz_inputhistory i
+ JOIN moz_places h ON h.id = i.place_id
+ WHERE h.url = :url`,
+ { url }
+ );
+ } else if (input) {
+ rows = await db.executeCached(
+ `SELECT use_count
+ FROM moz_inputhistory i
+ WHERE input = :input`,
+ { input }
+ );
+ }
+ return rows[0]?.getResultByIndex(0);
+ });
+}
diff --git a/browser/components/urlbar/tests/browser/browser_inputHistory_emptystring.js b/browser/components/urlbar/tests/browser/browser_inputHistory_emptystring.js
new file mode 100644
index 0000000000..28c967a851
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_inputHistory_emptystring.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This tests input history in cases where the search string is empty.
+ * In the future we may want to not account for these, but for now they are
+ * stored with an empty input field.
+ */
+
+"use strict";
+
+async function checkInputHistory(len = 0) {
+ await PlacesUtils.withConnectionWrapper(
+ "test::checkInputHistory",
+ async db => {
+ let rows = await db.executeCached(`SELECT input FROM moz_inputhistory`);
+ Assert.equal(rows.length, len, "There should only be 1 entry");
+ if (len) {
+ Assert.equal(rows[0].getResultByIndex(0), "", "Input should be empty");
+ }
+ }
+ );
+}
+
+const TEST_URL = "http://example.com/";
+
+async function do_test(openFn, pickMethod) {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:blank",
+ },
+ async function (browser) {
+ await PlacesTestUtils.clearInputHistory();
+ await openFn();
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ let promise = BrowserTestUtils.waitForDocLoadAndStopIt(TEST_URL, browser);
+ if (pickMethod == "keyboard") {
+ info(`Test pressing Enter`);
+ EventUtils.sendKey("down");
+ EventUtils.sendKey("return");
+ } else {
+ info("Test with click");
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ EventUtils.synthesizeMouseAtCenter(result.element.row, {});
+ }
+ await promise;
+ await checkInputHistory(1);
+ }
+ );
+}
+
+add_setup(async function () {
+ await PlacesUtils.history.clear();
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits(TEST_URL);
+ }
+
+ await updateTopSites(sites => sites && sites[0] && sites[0].url == TEST_URL);
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ });
+});
+
+add_task(async function test_history_no_search_terms() {
+ for (let pickMethod of ["keyboard", "mouse"]) {
+ // If a testFn returns false, it will be skipped.
+ for (let openFn of [
+ () => {
+ info("Test opening panel with down key");
+ gURLBar.focus();
+ EventUtils.sendKey("down");
+ },
+ async () => {
+ info("Test opening panel on focus");
+ gURLBar.blur();
+ EventUtils.synthesizeMouseAtCenter(gURLBar.textbox, {});
+ },
+ async () => {
+ info("Test opening panel on focus on a page");
+ let selectedBrowser = gBrowser.selectedBrowser;
+ // A page other than TEST_URL must be loaded, or the first Top Site
+ // result will be a switch-to-tab result and page won't be reloaded when
+ // the result is selected.
+ BrowserTestUtils.loadURIString(selectedBrowser, "http://example.org/");
+ await BrowserTestUtils.browserLoaded(selectedBrowser);
+ gURLBar.blur();
+ EventUtils.synthesizeMouseAtCenter(gURLBar.textbox, {});
+ },
+ ]) {
+ await do_test(openFn, pickMethod);
+ }
+ }
+});
diff --git a/browser/components/urlbar/tests/browser/browser_keepStateAcrossTabSwitches.js b/browser/components/urlbar/tests/browser/browser_keepStateAcrossTabSwitches.js
new file mode 100644
index 0000000000..03ba6a6473
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_keepStateAcrossTabSwitches.js
@@ -0,0 +1,224 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Verify user typed text remains in the URL bar when tab switching, even when
+ * loads fail.
+ */
+add_task(async function validURL() {
+ let input = "i-definitely-dont-exist.example.com";
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/"
+ );
+ let browser = tab.linkedBrowser;
+ // Note: Waiting on content document not being hidden because new tab pages can be preloaded,
+ // in which case no load events fire.
+ await SpecialPowers.spawn(browser, [], async () => {
+ await ContentTaskUtils.waitForCondition(() => {
+ return content.document && !content.document.hidden;
+ });
+ });
+ let errorPageLoaded = BrowserTestUtils.waitForErrorPage(browser);
+ gURLBar.value = input;
+ gURLBar.select();
+ EventUtils.sendKey("return");
+ await errorPageLoaded;
+ is(gURLBar.value, input, "Text is still in URL bar");
+ await BrowserTestUtils.switchTab(gBrowser, tab.previousElementSibling);
+ await BrowserTestUtils.switchTab(gBrowser, tab);
+ is(gURLBar.value, input, "Text is still in URL bar after tab switch");
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * Invalid URIs fail differently (that is, immediately, in the loadURI call)
+ * if keyword searches are turned off. Test that this works, too.
+ */
+add_task(async function invalidURL() {
+ let input = "To be or not to be-that is the question";
+ await SpecialPowers.pushPrefEnv({ set: [["keyword.enabled", false]] });
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank",
+ false
+ );
+ let browser = tab.linkedBrowser;
+ // Note: Waiting on content document not being hidden because new tab pages can be preloaded,
+ // in which case no load events fire.
+ await SpecialPowers.spawn(browser, [], async () => {
+ await ContentTaskUtils.waitForCondition(() => {
+ return content.document && !content.document.hidden;
+ });
+ });
+ let errorPageLoaded = BrowserTestUtils.waitForErrorPage(tab.linkedBrowser);
+ gURLBar.value = input;
+ gURLBar.select();
+ EventUtils.sendKey("return");
+ await errorPageLoaded;
+ is(gURLBar.value, input, "Text is still in URL bar");
+ is(tab.linkedBrowser.userTypedValue, input, "Text still stored on browser");
+ await BrowserTestUtils.switchTab(gBrowser, tab.previousElementSibling);
+ await BrowserTestUtils.switchTab(gBrowser, tab);
+ is(gURLBar.value, input, "Text is still in URL bar after tab switch");
+ is(tab.linkedBrowser.userTypedValue, input, "Text still stored on browser");
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * Test the urlbar status of text selection and focusing by tab switching.
+ */
+add_task(async function selectAndFocus() {
+ // Create a tab with normal web page.
+ const webpageTabURL = "https://example.com";
+ const webpageTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: webpageTabURL,
+ });
+
+ // Create a tab with userTypedValue.
+ const userTypedTabText = "test";
+ const userTypedTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ });
+ await UrlbarTestUtils.inputIntoURLBar(window, userTypedTabText);
+
+ // Create an empty tab.
+ const emptyTab = await BrowserTestUtils.openNewForegroundTab({ gBrowser });
+
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ BrowserTestUtils.removeTab(webpageTab);
+ BrowserTestUtils.removeTab(userTypedTab);
+ BrowserTestUtils.removeTab(emptyTab);
+ });
+
+ await doSelectAndFocusTest({
+ targetTab: webpageTab,
+ targetSelectionStart: 0,
+ targetSelectionEnd: 0,
+ anotherTab: userTypedTab,
+ });
+ await doSelectAndFocusTest({
+ targetTab: webpageTab,
+ targetSelectionStart: 2,
+ targetSelectionEnd: 5,
+ anotherTab: userTypedTab,
+ });
+ await doSelectAndFocusTest({
+ targetTab: webpageTab,
+ targetSelectionStart: webpageTabURL.length,
+ targetSelectionEnd: webpageTabURL.length,
+ anotherTab: userTypedTab,
+ });
+ await doSelectAndFocusTest({
+ targetTab: webpageTab,
+ targetSelectionStart: 0,
+ targetSelectionEnd: 0,
+ anotherTab: emptyTab,
+ });
+ await doSelectAndFocusTest({
+ targetTab: userTypedTab,
+ targetSelectionStart: 0,
+ targetSelectionEnd: 0,
+ anotherTab: webpageTab,
+ });
+ await doSelectAndFocusTest({
+ targetTab: userTypedTab,
+ targetSelectionStart: 0,
+ targetSelectionEnd: 0,
+ anotherTab: emptyTab,
+ });
+ await doSelectAndFocusTest({
+ targetTab: userTypedTab,
+ targetSelectionStart: 1,
+ targetSelectionEnd: 2,
+ anotherTab: emptyTab,
+ });
+ await doSelectAndFocusTest({
+ targetTab: userTypedTab,
+ targetSelectionStart: userTypedTabText.length,
+ targetSelectionEnd: userTypedTabText.length,
+ anotherTab: emptyTab,
+ });
+ await doSelectAndFocusTest({
+ targetTab: emptyTab,
+ targetSelectionStart: 0,
+ targetSelectionEnd: 0,
+ anotherTab: webpageTab,
+ });
+ await doSelectAndFocusTest({
+ targetTab: emptyTab,
+ targetSelectionStart: 0,
+ targetSelectionEnd: 0,
+ anotherTab: userTypedTab,
+ });
+});
+
+async function doSelectAndFocusTest({
+ targetTab,
+ targetSelectionStart,
+ targetSelectionEnd,
+ anotherTab,
+}) {
+ const testCases = [
+ {
+ targetFocus: false,
+ anotherFocus: false,
+ },
+ {
+ targetFocus: true,
+ anotherFocus: false,
+ },
+ {
+ targetFocus: true,
+ anotherFocus: true,
+ },
+ ];
+
+ for (const { targetFocus, anotherFocus } of testCases) {
+ // Setup the target tab.
+ await switchTab(targetTab);
+ setURLBarFocus(targetFocus);
+ gURLBar.inputField.setSelectionRange(
+ targetSelectionStart,
+ targetSelectionEnd
+ );
+ const targetValue = gURLBar.value;
+
+ // Switch to another tab.
+ await switchTab(anotherTab);
+ setURLBarFocus(anotherFocus);
+
+ // Switch back to the target tab.
+ await switchTab(targetTab);
+
+ // Check whether the value, selection and focusing status are reverted.
+ Assert.equal(gURLBar.value, targetValue);
+ Assert.equal(gURLBar.focused, targetFocus);
+ if (gURLBar.focused) {
+ Assert.equal(gURLBar.selectionStart, targetSelectionStart);
+ Assert.equal(gURLBar.selectionEnd, targetSelectionEnd);
+ } else {
+ Assert.equal(gURLBar.selectionStart, gURLBar.value.length);
+ Assert.equal(gURLBar.selectionEnd, gURLBar.value.length);
+ }
+ }
+}
+
+function setURLBarFocus(focus) {
+ if (focus) {
+ gURLBar.focus();
+ } else {
+ gURLBar.blur();
+ }
+}
+
+async function switchTab(tab) {
+ if (gBrowser.selectedTab !== tab) {
+ EventUtils.synthesizeMouseAtCenter(tab, {});
+ await BrowserTestUtils.waitForCondition(() => gBrowser.selectedTab === tab);
+ }
+}
diff --git a/browser/components/urlbar/tests/browser/browser_keyword.js b/browser/components/urlbar/tests/browser/browser_keyword.js
new file mode 100644
index 0000000000..618bcad3c7
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_keyword.js
@@ -0,0 +1,238 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This tests that keywords are displayed and handled correctly.
+ */
+
+async function promise_first_result(inputText) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: inputText,
+ });
+
+ return UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+}
+
+function assertURL(result, expectedUrl, keyword, input, postData) {
+ Assert.equal(result.url, expectedUrl, "Should have the correct URL");
+ if (postData) {
+ Assert.equal(
+ NetUtil.readInputStreamToString(
+ result.postData,
+ result.postData.available()
+ ),
+ postData,
+ "Should have the correct postData"
+ );
+ }
+}
+
+const TEST_URL = `${TEST_BASE_URL}print_postdata.sjs`;
+
+add_setup(async function () {
+ await PlacesUtils.keywords.insert({
+ keyword: "get",
+ url: TEST_URL + "?q=%s",
+ });
+ await PlacesUtils.keywords.insert({
+ keyword: "post",
+ url: TEST_URL,
+ postData: "q=%s",
+ });
+ await PlacesUtils.keywords.insert({
+ keyword: "question?",
+ url: TEST_URL + "?q2=%s",
+ });
+ await PlacesUtils.keywords.insert({
+ keyword: "?question",
+ url: TEST_URL + "?q3=%s",
+ });
+ // Avoid fetching search suggestions.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.searches", false]],
+ });
+ await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + "searchSuggestionEngine.xml",
+ setAsDefault: true,
+ });
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.keywords.remove("get");
+ await PlacesUtils.keywords.remove("post");
+ await PlacesUtils.keywords.remove("question?");
+ await PlacesUtils.keywords.remove("?question");
+ while (gBrowser.tabs.length > 1) {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+ });
+});
+
+add_task(async function test_display_keyword_without_query() {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla");
+
+ // Test a keyword that also has blank spaces to ensure they are ignored as well.
+ let result = await promise_first_result("get ");
+
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.KEYWORD,
+ "Should have a keyword result"
+ );
+ Assert.equal(
+ result.displayed.title,
+ "https://example.com/browser/browser/components/urlbar/tests/browser/print_postdata.sjs?q=",
+ "Node should contain the url of the bookmark"
+ );
+ let [action] = await document.l10n.formatValues([
+ { id: "urlbar-result-action-visit" },
+ ]);
+ Assert.equal(result.displayed.action, action, "Should have visit indicated");
+});
+
+add_task(async function test_keyword_using_get() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:mozilla"
+ );
+
+ let result = await promise_first_result("get something");
+
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.KEYWORD,
+ "Should have a keyword result"
+ );
+ Assert.equal(
+ result.displayed.title,
+ "example.com: something",
+ "Node should contain the name of the bookmark and query"
+ );
+ Assert.ok(!result.displayed.action, "Should have an empty action");
+
+ assertURL(result, TEST_URL + "?q=something", "get", "get something");
+
+ let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
+
+ // Click on the result
+ info("Normal click on result");
+ let tabPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ EventUtils.synthesizeMouseAtCenter(element, {});
+ await tabPromise;
+ Assert.equal(
+ tab.linkedBrowser.currentURI.spec,
+ TEST_URL + "?q=something",
+ "Tab should have loaded from clicking on result"
+ );
+
+ // Middle-click on the result
+ info("Middle-click on result");
+ result = await promise_first_result("get somethingmore");
+
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.KEYWORD,
+ "Should have a keyword result"
+ );
+
+ assertURL(result, TEST_URL + "?q=somethingmore", "get", "get somethingmore");
+
+ tabPromise = BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "TabOpen");
+ element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
+ EventUtils.synthesizeMouseAtCenter(element, { button: 1 });
+ let tabOpenEvent = await tabPromise;
+ let newTab = tabOpenEvent.target;
+ await BrowserTestUtils.browserLoaded(newTab.linkedBrowser);
+ Assert.equal(
+ newTab.linkedBrowser.currentURI.spec,
+ TEST_URL + "?q=somethingmore",
+ "Tab should have loaded from middle-clicking on result"
+ );
+});
+
+add_task(async function test_keyword_using_post() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:mozilla"
+ );
+
+ let result = await promise_first_result("post something");
+
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.KEYWORD,
+ "Should have a keyword result"
+ );
+ Assert.equal(
+ result.displayed.title,
+ "example.com: something",
+ "Node should contain the name of the bookmark and query"
+ );
+ Assert.ok(!result.displayed.action, "Should have an empty action");
+
+ assertURL(result, TEST_URL, "post", "post something", "q=something");
+
+ // Click on the result
+ info("Normal click on result");
+ let tabPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
+ EventUtils.synthesizeMouseAtCenter(element, {});
+ info("waiting for tab");
+ await tabPromise;
+ Assert.equal(
+ tab.linkedBrowser.currentURI.spec,
+ TEST_URL,
+ "Tab should have loaded from clicking on result"
+ );
+
+ let postData = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ async function () {
+ return content.document.body.textContent;
+ }
+ );
+ Assert.equal(postData, "q=something", "post data was submitted correctly");
+});
+
+add_task(async function test_keyword_with_question_mark() {
+ // TODO Bug 1517140: keywords containing restriction chars should not be
+ // allowed, or properly supported.
+ let result = await promise_first_result("question?");
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "Result should be a search"
+ );
+ Assert.equal(result.searchParams.query, "question?", "Check search query");
+
+ result = await promise_first_result("question? something");
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.KEYWORD,
+ "Result should be a keyword"
+ );
+ Assert.equal(result.keyword, "question?", "Check search query");
+
+ result = await promise_first_result("?question");
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "Result should be a search"
+ );
+ Assert.equal(result.searchParams.query, "question", "Check search query");
+
+ result = await promise_first_result("?question something");
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "Result should be a search"
+ );
+ Assert.equal(
+ result.searchParams.query,
+ "question something",
+ "Check search query"
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_keywordBookmarklets.js b/browser/components/urlbar/tests/browser/browser_keywordBookmarklets.js
new file mode 100644
index 0000000000..c10fcdd9c3
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_keywordBookmarklets.js
@@ -0,0 +1,133 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ let bm = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "bookmarklet",
+ url: "javascript:'%sx'%20",
+ });
+ await PlacesUtils.keywords.insert({ keyword: "bm", url: bm.url });
+ registerCleanupFunction(async function () {
+ await PlacesUtils.bookmarks.remove(bm);
+ });
+
+ let testFns = [
+ function () {
+ info("Type keyword and immediately press enter");
+ gURLBar.value = "bm";
+ gURLBar.focus();
+ EventUtils.synthesizeKey("KEY_Enter");
+ return "x";
+ },
+ function () {
+ info("Type keyword with searchstring and immediately press enter");
+ gURLBar.value = "bm a";
+ gURLBar.focus();
+ EventUtils.synthesizeKey("KEY_Enter");
+ return "ax";
+ },
+ async function () {
+ info("Search keyword, then press enter");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "bm",
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(result.title, "javascript:'x' ", "Check title");
+ EventUtils.synthesizeKey("KEY_Enter");
+ return "x";
+ },
+ async function () {
+ info("Search keyword with searchstring, then press enter");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "bm a",
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(result.title, "javascript:'ax' ", "Check title");
+ EventUtils.synthesizeKey("KEY_Enter");
+ return "ax";
+ },
+ async function () {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "bm",
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(result.title, "javascript:'x' ", "Check title");
+ let element = UrlbarTestUtils.getSelectedRow(window);
+ EventUtils.synthesizeMouseAtCenter(element, {});
+ return "x";
+ },
+ async function () {
+ info("Search keyword with searchstring, then click");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "bm a",
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(result.title, "javascript:'ax' ", "Check title");
+ let element = UrlbarTestUtils.getSelectedRow(window);
+ EventUtils.synthesizeMouseAtCenter(element, {});
+ return "ax";
+ },
+ ];
+ for (let testFn of testFns) {
+ await do_test(testFn);
+ }
+});
+
+async function do_test(loadFn) {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ },
+ async browser => {
+ let originalPrincipal = gBrowser.contentPrincipal;
+ let originalPrincipalURI = await getPrincipalURI(browser);
+
+ let promise = BrowserTestUtils.waitForContentEvent(browser, "pageshow");
+ const expectedTextContent = await loadFn();
+ info("Awaiting pageshow event");
+ await promise;
+ // URI should not change when we run a javascript: URL.
+ Assert.equal(gBrowser.currentURI.spec, "about:blank");
+ const textContent = await ContentTask.spawn(browser, [], function () {
+ return content.document.documentElement.textContent;
+ });
+ Assert.equal(textContent, expectedTextContent);
+
+ let newPrincipalURI = await getPrincipalURI(browser);
+ Assert.equal(
+ newPrincipalURI,
+ originalPrincipalURI,
+ "content has the same principal"
+ );
+
+ // In e10s, null principals don't round-trip so the same null principal sent
+ // from the child will be a new null principal. Verify that this is the
+ // case.
+ if (browser.isRemoteBrowser) {
+ Assert.ok(
+ originalPrincipal.isNullPrincipal &&
+ gBrowser.contentPrincipal.isNullPrincipal,
+ "both principals should be null principals in the parent"
+ );
+ } else {
+ Assert.ok(
+ gBrowser.contentPrincipal.equals(originalPrincipal),
+ "javascript bookmarklet should inherit principal"
+ );
+ }
+ }
+ );
+}
+
+function getPrincipalURI(browser) {
+ return SpecialPowers.spawn(browser, [], function () {
+ return content.document.nodePrincipal.spec;
+ });
+}
diff --git a/browser/components/urlbar/tests/browser/browser_keywordSearch.js b/browser/components/urlbar/tests/browser/browser_keywordSearch.js
new file mode 100644
index 0000000000..b8402a4e90
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_keywordSearch.js
@@ -0,0 +1,57 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var gTests = [
+ {
+ name: "normal search (search service)",
+ text: "test search",
+ expectText: "test+search",
+ },
+ {
+ name: "?-prefixed search (search service)",
+ text: "? foo ",
+ expectText: "foo",
+ },
+];
+
+add_setup(async function () {
+ await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + "searchSuggestionEngine.xml",
+ setAsDefault: true,
+ });
+});
+
+add_task(async function () {
+ // Test both directly setting a value and pressing enter, or setting the
+ // value through input events, like the user would do.
+ const setValueFns = [
+ value => {
+ gURLBar.value = value;
+ },
+ value => {
+ return UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value,
+ });
+ },
+ ];
+
+ for (let test of gTests) {
+ info("Testing: " + test.name);
+ await BrowserTestUtils.withNewTab({ gBrowser }, async browser => {
+ for (let setValueFn of setValueFns) {
+ gURLBar.select();
+ await setValueFn(test.text);
+ EventUtils.synthesizeKey("KEY_Enter");
+
+ let expectedUrl = "http://mochi.test:8888/?terms=" + test.expectText;
+ info("Waiting for load: " + expectedUrl);
+ await BrowserTestUtils.browserLoaded(browser, false, expectedUrl);
+ // At least one test.
+ Assert.equal(browser.currentURI.spec, expectedUrl);
+ }
+ });
+ }
+});
diff --git a/browser/components/urlbar/tests/browser/browser_keywordSearch_postData.js b/browser/components/urlbar/tests/browser/browser_keywordSearch_postData.js
new file mode 100644
index 0000000000..d2b3aa253a
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_keywordSearch_postData.js
@@ -0,0 +1,74 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var gTests = [
+ {
+ name: "single word search (search service)",
+ text: "pizza",
+ expectText: "pizza",
+ },
+ {
+ name: "multi word search (search service)",
+ text: "test search",
+ expectText: "test+search",
+ },
+ {
+ name: "?-prefixed search (search service)",
+ text: "? foo ",
+ expectText: "foo",
+ },
+];
+
+add_setup(async function () {
+ await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + "POSTSearchEngine.xml",
+ setAsDefault: true,
+ });
+});
+
+add_task(async function () {
+ // Test both directly setting a value and pressing enter, or setting the
+ // value through input events, like the user would do.
+ const setValueFns = [
+ value => {
+ gURLBar.value = value;
+ },
+ value => {
+ return UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value,
+ });
+ },
+ ];
+
+ for (let test of gTests) {
+ info("Testing: " + test.name);
+ await BrowserTestUtils.withNewTab({ gBrowser }, async browser => {
+ for (let setValueFn of setValueFns) {
+ gURLBar.select();
+ await setValueFn(test.text);
+ EventUtils.synthesizeKey("KEY_Enter");
+
+ await BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ "http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/print_postdata.sjs"
+ );
+
+ let textContent = await SpecialPowers.spawn(browser, [], async () => {
+ return content.document.body.textContent;
+ });
+
+ Assert.ok(textContent, "search page loaded");
+ let needle = "searchterms=" + test.expectText;
+ Assert.equal(
+ textContent,
+ needle,
+ "The query POST data should be returned in the response"
+ );
+ }
+ });
+ }
+});
diff --git a/browser/components/urlbar/tests/browser/browser_keyword_override.js b/browser/components/urlbar/tests/browser/browser_keyword_override.js
new file mode 100644
index 0000000000..b358f3a4ac
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_keyword_override.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This tests that the display of keyword results are not changed when the user
+ * presses the override button.
+ */
+
+add_task(async function () {
+ let bm = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.com/?q=%s",
+ title: "test",
+ });
+ await PlacesUtils.keywords.insert({
+ keyword: "keyword",
+ url: "http://example.com/?q=%s",
+ });
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.bookmarks.remove(bm);
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "keyword search",
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+
+ info("Before override");
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.KEYWORD,
+ "Should have a keyword result"
+ );
+ Assert.equal(
+ result.displayed.title,
+ "example.com: search",
+ "Node should contain the name of the bookmark and query"
+ );
+ Assert.ok(!result.displayed.action, "Should have an empty action");
+
+ info("During override");
+ EventUtils.synthesizeKey("VK_SHIFT", { type: "keydown" });
+
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.KEYWORD,
+ "Should have a keyword result"
+ );
+ Assert.equal(
+ result.displayed.title,
+ "example.com: search",
+ "Node should contain the name of the bookmark and query"
+ );
+ Assert.ok(!result.displayed.action, "Should have an empty action");
+
+ EventUtils.synthesizeKey("VK_SHIFT", { type: "keyup" });
+});
diff --git a/browser/components/urlbar/tests/browser/browser_keyword_select_and_type.js b/browser/components/urlbar/tests/browser/browser_keyword_select_and_type.js
new file mode 100644
index 0000000000..a3222c293f
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_keyword_select_and_type.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This tests that changing away from a keyword result and back again, still
+ * operates correctly.
+ */
+
+add_task(async function () {
+ let bookmarks = [];
+ bookmarks.push(
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.com/?q=%s",
+ title: "test",
+ })
+ );
+ await PlacesUtils.keywords.insert({
+ keyword: "keyword",
+ url: "http://example.com/?q=%s",
+ });
+
+ // This item is only needed so we can select the keyword item, select something
+ // else, then select the keyword item again.
+ bookmarks.push(
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.com/keyword",
+ title: "keyword abc",
+ })
+ );
+
+ registerCleanupFunction(async function () {
+ for (let bm of bookmarks) {
+ await PlacesUtils.bookmarks.remove(bm);
+ }
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:mozilla"
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "keyword a",
+ });
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1);
+
+ // First item should already be selected
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 0,
+ "Should have the first item selected"
+ );
+
+ // Select next one (important!)
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 1,
+ "Should have the second item selected"
+ );
+
+ // Re-select keyword item
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 0,
+ "Should have the first item selected"
+ );
+
+ EventUtils.sendString("b");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ Assert.equal(
+ gURLBar.value,
+ "keyword ab",
+ "urlbar should have expected input"
+ );
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.KEYWORD,
+ "Should have a result of type keyword"
+ );
+ Assert.equal(
+ result.url,
+ "http://example.com/?q=ab",
+ "Should have the correct url"
+ );
+
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_loadRace.js b/browser/components/urlbar/tests/browser/browser_loadRace.js
new file mode 100644
index 0000000000..b257625f30
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_loadRace.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This test is for testing races of loading the Urlbar when loading shortcuts.
+// For example, ensuring that if a search query is entered, but something causes
+// a page load whilst we're getting the search url, then we don't handle the
+// original search query.
+
+add_setup(async function () {
+ sandbox = sinon.createSandbox();
+
+ registerCleanupFunction(async () => {
+ sandbox.restore();
+ });
+});
+
+async function checkShortcutLoading(modifierKeys) {
+ let deferred = PromiseUtils.defer();
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "about:robots",
+ });
+
+ // We stub getHeuristicResultFor to guarentee it doesn't resolve until after
+ // we've loaded a new page.
+ let original = UrlbarUtils.getHeuristicResultFor;
+ sandbox
+ .stub(UrlbarUtils, "getHeuristicResultFor")
+ .callsFake(async searchString => {
+ await deferred.promise;
+ return original.call(this, searchString);
+ });
+
+ // This load will be blocked until the deferred is resolved.
+ // Use a string that will be interepreted as a local URL to avoid hitting the
+ // network.
+ gURLBar.focus();
+ gURLBar.value = "example.com";
+ gURLBar.userTypedValue = true;
+ EventUtils.synthesizeKey("KEY_Enter", modifierKeys);
+
+ Assert.ok(
+ UrlbarUtils.getHeuristicResultFor.calledOnce,
+ "should have called getHeuristicResultFor"
+ );
+
+ // Now load a different page.
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, "about:license");
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ Assert.equal(gBrowser.visibleTabs.length, 2, "Should have 2 tabs");
+
+ // Now that the new page has loaded, unblock the previous urlbar load.
+ deferred.resolve();
+ if (modifierKeys) {
+ let openedTab = await new Promise(resolve => {
+ window.addEventListener(
+ "TabOpen",
+ event => {
+ resolve(event.target);
+ },
+ { once: true }
+ );
+ });
+ await BrowserTestUtils.browserLoaded(openedTab.linkedBrowser);
+ Assert.ok(
+ openedTab.linkedBrowser.currentURI.spec.includes("example.com"),
+ "Should have attempted to open the shortcut page"
+ );
+ BrowserTestUtils.removeTab(openedTab);
+ }
+
+ Assert.equal(
+ tab.linkedBrowser.currentURI.spec,
+ "about:license",
+ "Tab url should not have changed"
+ );
+ Assert.equal(gBrowser.visibleTabs.length, 2, "Should still have 2 tabs");
+
+ BrowserTestUtils.removeTab(tab);
+ sandbox.restore();
+}
+
+add_task(async function test_location_change_stops_load() {
+ await checkShortcutLoading();
+});
+
+add_task(async function test_opening_different_tab_with_location_change() {
+ await checkShortcutLoading({ altKey: true });
+});
diff --git a/browser/components/urlbar/tests/browser/browser_locationBarCommand.js b/browser/components/urlbar/tests/browser/browser_locationBarCommand.js
new file mode 100644
index 0000000000..f0077c3334
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_locationBarCommand.js
@@ -0,0 +1,291 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This test is designed to ensure that the correct command/operation happens
+ * when pressing enter with various key combinations in the urlbar.
+ */
+
+const TEST_VALUE = "example.com";
+const START_VALUE = "example.org";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.altClickSave", true],
+ ["browser.urlbar.autoFill", false],
+ ],
+ });
+});
+
+add_task(async function alt_left_click_test() {
+ info("Running test: Alt left click");
+
+ // Monkey patch saveURL() to avoid dealing with file save code paths.
+ let oldSaveURL = saveURL;
+ let saveURLPromise = new Promise(resolve => {
+ saveURL = () => {
+ // Restore old saveURL() value.
+ saveURL = oldSaveURL;
+ resolve();
+ };
+ });
+
+ await triggerCommand("click", { altKey: true });
+
+ await saveURLPromise;
+ ok(true, "SaveURL was called");
+ is(gURLBar.value, "", "Urlbar reverted to original value");
+});
+
+add_task(async function shift_left_click_test() {
+ info("Running test: Shift left click");
+
+ let destinationURL = "http://" + TEST_VALUE + "/";
+ let newWindowPromise = BrowserTestUtils.waitForNewWindow({
+ url: destinationURL,
+ });
+ await triggerCommand("click", { shiftKey: true });
+ let win = await newWindowPromise;
+
+ info("URL should be loaded in a new window");
+ is(gURLBar.value, "", "Urlbar reverted to original value");
+ await promiseCheckChildNoFocusedElement(gBrowser.selectedBrowser);
+ is(
+ document.activeElement,
+ gBrowser.selectedBrowser,
+ "Content window should be focused"
+ );
+ is(win.gURLBar.value, TEST_VALUE, "New URL is loaded in new window");
+
+ // Cleanup.
+ let ourWindowRefocusedPromise = Promise.all([
+ BrowserTestUtils.waitForEvent(window, "activate"),
+ BrowserTestUtils.waitForEvent(window, "focus", true),
+ ]);
+ await BrowserTestUtils.closeWindow(win);
+ await ourWindowRefocusedPromise;
+});
+
+add_task(async function right_click_test() {
+ info("Running test: Right click on go button");
+
+ // Add a new tab.
+ await promiseOpenNewTab();
+
+ await triggerCommand("click", { button: 2 });
+
+ // Right click should do nothing (context menu will be shown).
+ is(gURLBar.value, TEST_VALUE, "Urlbar still has the value we entered");
+
+ // Cleanup.
+ gBrowser.removeCurrentTab();
+});
+
+add_task(async function shift_accel_left_click_test() {
+ info("Running test: Shift+Ctrl/Cmd left click on go button");
+
+ // Add a new tab.
+ let tab = await promiseOpenNewTab();
+
+ let loadStartedPromise = promiseLoadStarted();
+ await triggerCommand("click", { accelKey: true, shiftKey: true });
+ await loadStartedPromise;
+
+ // Check the load occurred in a new background tab.
+ info("URL should be loaded in a new background tab");
+ is(gURLBar.value, "", "Urlbar reverted to original value");
+ ok(!gURLBar.focused, "Urlbar is no longer focused after urlbar command");
+ is(gBrowser.selectedTab, tab, "Focus did not change to the new tab");
+
+ // Select the new background tab
+ gBrowser.selectedTab = gBrowser.selectedTab.nextElementSibling;
+ is(gURLBar.value, TEST_VALUE, "New URL is loaded in new tab");
+
+ // Cleanup.
+ gBrowser.removeCurrentTab();
+ gBrowser.removeCurrentTab();
+});
+
+add_task(async function load_in_current_tab_test() {
+ let tests = [
+ {
+ desc: "Simple return keypress",
+ type: "keypress",
+ },
+ {
+ desc: "Left click on go button",
+ type: "click",
+ },
+ {
+ desc: "Ctrl/Cmd+Return keypress",
+ type: "keypress",
+ details: { accelKey: true },
+ },
+ {
+ desc: "Alt+Return keypress in a blank tab",
+ type: "keypress",
+ details: { altKey: true },
+ },
+ {
+ desc: "AltGr+Return keypress in a blank tab",
+ type: "keypress",
+ details: { altGraphKey: true },
+ },
+ ];
+
+ for (let { desc, type, details } of tests) {
+ info(`Running test: ${desc}`);
+
+ // Add a new tab.
+ let tab = await promiseOpenNewTab();
+
+ // Trigger a load and check it occurs in the current tab.
+ let loadStartedPromise = promiseLoadStarted();
+ await triggerCommand(type, details);
+ await loadStartedPromise;
+
+ info("URL should be loaded in the current tab");
+ is(gURLBar.value, TEST_VALUE, "Urlbar still has the value we entered");
+ await promiseCheckChildNoFocusedElement(gBrowser.selectedBrowser);
+ is(
+ document.activeElement,
+ gBrowser.selectedBrowser,
+ "Content window should be focused"
+ );
+ is(gBrowser.selectedTab, tab, "New URL was loaded in the current tab");
+
+ // Cleanup.
+ gBrowser.removeCurrentTab();
+ }
+});
+
+add_task(async function load_in_new_tab_test() {
+ let tests = [
+ {
+ desc: "Ctrl/Cmd left click on go button",
+ type: "click",
+ details: { accelKey: true },
+ url: "about:blank",
+ },
+ {
+ desc: "Alt+Return keypress in a dirty tab",
+ type: "keypress",
+ details: { altKey: true },
+ url: START_VALUE,
+ },
+ {
+ desc: "AltGr+Return keypress in a dirty tab",
+ type: "keypress",
+ details: { altGraphKey: true },
+ url: START_VALUE,
+ },
+ ];
+
+ for (let { desc, type, details, url } of tests) {
+ info(`Running test: ${desc}`);
+
+ // Add a new tab.
+ let tab = await promiseOpenNewTab(url);
+
+ // Trigger a load and check it occurs in a new tab.
+ let tabSwitchedPromise = promiseNewTabSwitched();
+ await triggerCommand(type, details);
+ await tabSwitchedPromise;
+
+ // Check the load occurred in a new tab.
+ info("URL should be loaded in a new focused tab");
+ is(gURLBar.value, TEST_VALUE, "Urlbar still has the value we entered");
+ await promiseCheckChildNoFocusedElement(gBrowser.selectedBrowser);
+ is(
+ document.activeElement,
+ gBrowser.selectedBrowser,
+ "Content window should be focused"
+ );
+ isnot(gBrowser.selectedTab, tab, "New URL was loaded in a new tab");
+
+ // Cleanup.
+ gBrowser.removeCurrentTab();
+ gBrowser.removeCurrentTab();
+ }
+});
+
+async function triggerCommand(type, details = {}) {
+ gURLBar.focus();
+ gURLBar.value = "";
+ EventUtils.sendString(TEST_VALUE);
+
+ Assert.equal(
+ await UrlbarTestUtils.promiseUserContextId(window),
+ gBrowser.selectedTab.getAttribute("usercontextid"),
+ "userContextId must be the same as the originating tab"
+ );
+
+ if (type == "click") {
+ ok(
+ gURLBar.hasAttribute("usertyping"),
+ "usertyping attribute must be set for the go button to be visible"
+ );
+ EventUtils.synthesizeMouseAtCenter(gURLBar.goButton, details);
+ } else if (type == "keypress") {
+ EventUtils.synthesizeKey("KEY_Enter", details);
+ } else {
+ throw new Error("Unsupported event type");
+ }
+}
+
+function promiseLoadStarted() {
+ return new Promise(resolve => {
+ gBrowser.addTabsProgressListener({
+ onStateChange(browser, webProgress, req, flags, status) {
+ if (flags & Ci.nsIWebProgressListener.STATE_START) {
+ gBrowser.removeTabsProgressListener(this);
+ resolve();
+ }
+ },
+ });
+ });
+}
+
+let gUserContextIdSerial = 1;
+async function promiseOpenNewTab(url = "about:blank") {
+ let tab = BrowserTestUtils.addTab(gBrowser, url, {
+ userContextId: gUserContextIdSerial++,
+ });
+ let tabSwitchPromise = promiseNewTabSwitched(tab);
+ gBrowser.selectedTab = tab;
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await tabSwitchPromise;
+ return tab;
+}
+
+function promiseNewTabSwitched() {
+ return new Promise(resolve => {
+ gBrowser.addEventListener(
+ "TabSwitchDone",
+ function () {
+ executeSoon(resolve);
+ },
+ { once: true }
+ );
+ });
+}
+
+function promiseCheckChildNoFocusedElement(browser) {
+ if (!gMultiProcessBrowser) {
+ Assert.equal(
+ Services.focus.focusedElement,
+ null,
+ "There should be no focused element"
+ );
+ return null;
+ }
+
+ return ContentTask.spawn(browser, null, async function () {
+ Assert.equal(
+ Services.focus.focusedElement,
+ null,
+ "There should be no focused element"
+ );
+ });
+}
diff --git a/browser/components/urlbar/tests/browser/browser_locationBarExternalLoad.js b/browser/components/urlbar/tests/browser/browser_locationBarExternalLoad.js
new file mode 100644
index 0000000000..5a44db54ce
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_locationBarExternalLoad.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.autoFill", false]],
+ });
+ const url = "data:text/html,<body>hi";
+ await testURL(url, urlEnter);
+ await testURL(url, urlClick);
+});
+
+function urlEnter(url) {
+ gURLBar.value = url;
+ gURLBar.focus();
+ EventUtils.synthesizeKey("KEY_Enter");
+}
+
+function urlClick(url) {
+ gURLBar.focus();
+ gURLBar.value = "";
+ EventUtils.sendString(url);
+ EventUtils.synthesizeMouseAtCenter(gURLBar.goButton, {});
+}
+
+function promiseNewTabSwitched() {
+ return new Promise(resolve => {
+ gBrowser.addEventListener(
+ "TabSwitchDone",
+ function () {
+ executeSoon(resolve);
+ },
+ { once: true }
+ );
+ });
+}
+
+function promiseLoaded(browser) {
+ return SpecialPowers.spawn(browser, [undefined], async () => {
+ if (!["interactive", "complete"].includes(content.document.readyState)) {
+ await new Promise(resolve =>
+ docShell.chromeEventHandler.addEventListener(
+ "DOMContentLoaded",
+ resolve,
+ {
+ once: true,
+ capture: true,
+ }
+ )
+ );
+ }
+ });
+}
+
+async function testURL(url, loadFunc, endFunc) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ let browser = tab.linkedBrowser;
+
+ let pagePrincipal = gBrowser.contentPrincipal;
+ // We need to ensure that we set the pageshow event listener before running
+ // loadFunc, otherwise there's a chance that the content process will finish
+ // loading the page and fire pageshow before the event listener gets set.
+ let pageShowPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "pageshow"
+ );
+ loadFunc(url);
+ await pageShowPromise;
+
+ await SpecialPowers.spawn(
+ browser,
+ [{ isRemote: gMultiProcessBrowser }],
+ async function (arg) {
+ Assert.equal(
+ Services.focus.focusedElement,
+ null,
+ "focusedElement not null"
+ );
+ }
+ );
+
+ is(document.activeElement, browser, "content window should be focused");
+
+ ok(
+ !gBrowser.contentPrincipal.equals(pagePrincipal),
+ "load of " +
+ url +
+ " by " +
+ loadFunc.name +
+ " should produce a page with a different principal"
+ );
+
+ await BrowserTestUtils.removeTab(tab);
+}
diff --git a/browser/components/urlbar/tests/browser/browser_locationchange_urlbar_edit_dos.js b/browser/components/urlbar/tests/browser/browser_locationchange_urlbar_edit_dos.js
new file mode 100644
index 0000000000..b50446a4c9
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_locationchange_urlbar_edit_dos.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URL = `${TEST_BASE_URL}file_urlbar_edit_dos.html`;
+
+async function checkURLBarValueStays(browser) {
+ gURLBar.select();
+ EventUtils.sendString("a");
+ is(gURLBar.value, "a", "URL bar value should match after sending a key");
+ await new Promise(resolve => {
+ let listener = {
+ onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
+ ok(
+ aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT,
+ "Should only get a same document location change"
+ );
+ gBrowser.selectedBrowser.removeProgressListener(filter);
+ filter = null;
+ // Wait an extra tick before resolving. We want to make sure that other
+ // web progress listeners queued after this one are called before we
+ // continue the test, in case the remainder of the test depends on those
+ // listeners. That should happen anyway since promises are resolved on
+ // the next tick, but do this to be a little safer. In particular we
+ // want to avoid having the test pass when it should fail.
+ executeSoon(resolve);
+ },
+ };
+ let filter = Cc[
+ "@mozilla.org/appshell/component/browser-status-filter;1"
+ ].createInstance(Ci.nsIWebProgress);
+ filter.addProgressListener(listener, Ci.nsIWebProgress.NOTIFY_ALL);
+ gBrowser.selectedBrowser.addProgressListener(filter);
+ });
+ is(
+ gURLBar.value,
+ "a",
+ "URL bar should not have been changed by location changes."
+ );
+}
+
+add_task(async function () {
+ // Disable autofill so that when checkURLBarValueStays types "a", it's not
+ // autofilled to addons.mozilla.org (or anything else).
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.autoFill", false]],
+ });
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_URL,
+ },
+ async function (browser) {
+ let promise1 = checkURLBarValueStays(browser);
+ SpecialPowers.spawn(browser, [""], function () {
+ content.wrappedJSObject.dos_hash();
+ });
+ await promise1;
+ let promise2 = checkURLBarValueStays(browser);
+ SpecialPowers.spawn(browser, [""], function () {
+ content.wrappedJSObject.dos_pushState();
+ });
+ await promise2;
+ }
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_middleClick.js b/browser/components/urlbar/tests/browser/browser_middleClick.js
new file mode 100644
index 0000000000..1a5088f827
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_middleClick.js
@@ -0,0 +1,255 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test for middle click behavior.
+ */
+
+add_task(async function test_setup() {
+ CustomizableUI.addWidgetToArea("home-button", "nav-bar");
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.tabs.searchclipboardfor.middleclick", false]],
+ });
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("middlemouse.paste");
+ Services.prefs.clearUserPref("middlemouse.openNewWindow");
+ Services.prefs.clearUserPref("browser.tabs.opentabfor.middleclick");
+ Services.prefs.clearUserPref("browser.startup.homepage");
+ Services.prefs.clearUserPref("browser.tabs.loadBookmarksInBackground");
+ SpecialPowers.clipboardCopyString("");
+
+ CustomizableUI.removeWidgetFromArea("home-button");
+ });
+});
+
+add_task(async function test_middleClickOnTab() {
+ await testMiddleClickOnTab(false);
+ await testMiddleClickOnTab(true);
+});
+
+add_task(async function test_middleClickToOpenNewTab() {
+ await testMiddleClickToOpenNewTab(false, "#tabs-newtab-button");
+ await testMiddleClickToOpenNewTab(true, "#tabs-newtab-button");
+ await testMiddleClickToOpenNewTab(false, "#TabsToolbar");
+ await testMiddleClickToOpenNewTab(true, "#TabsToolbar");
+});
+
+add_task(async function test_middleClickOnURLBar() {
+ await testMiddleClickOnURLBar(false);
+ await testMiddleClickOnURLBar(true);
+});
+
+add_task(async function test_middleClickOnHomeButton() {
+ const TEST_DATA = [
+ {
+ isMiddleMousePastePrefOn: false,
+ isLoadInBackground: false,
+ startPagePref: "about:home",
+ expectedURLBarFocus: true,
+ expectedURLBarValue: "",
+ },
+ {
+ isMiddleMousePastePrefOn: false,
+ isLoadInBackground: false,
+ startPagePref: "about:blank",
+ expectedURLBarFocus: true,
+ expectedURLBarValue: "",
+ },
+ {
+ isMiddleMousePastePrefOn: false,
+ isLoadInBackground: false,
+ startPagePref: "https://example.com",
+ expectedURLBarFocus: false,
+ expectedURLBarValue: "https://example.com",
+ },
+ {
+ isMiddleMousePastePrefOn: true,
+ isLoadInBackground: false,
+ startPagePref: "about:home",
+ expectedURLBarFocus: true,
+ expectedURLBarValue: "",
+ },
+ {
+ isMiddleMousePastePrefOn: true,
+ isLoadInBackground: false,
+ startPagePref: "https://example.com",
+ expectedURLBarFocus: false,
+ expectedURLBarValue: "https://example.com",
+ },
+ {
+ isMiddleMousePastePrefOn: false,
+ isLoadInBackground: true,
+ startPagePref: "about:home",
+ expectedURLBarFocus: true,
+ expectedURLBarValue: "",
+ },
+ {
+ isMiddleMousePastePrefOn: false,
+ isLoadInBackground: true,
+ startPagePref: "https://example.com",
+ expectedURLBarFocus: true,
+ expectedURLBarValue: "",
+ },
+ {
+ isMiddleMousePastePrefOn: true,
+ isLoadInBackground: true,
+ startPagePref: "about:home",
+ expectedURLBarFocus: true,
+ expectedURLBarValue: "",
+ },
+ {
+ isMiddleMousePastePrefOn: true,
+ isLoadInBackground: true,
+ startPagePref: "https://example.com",
+ expectedURLBarFocus: true,
+ expectedURLBarValue: "",
+ },
+ ];
+
+ for (const testData of TEST_DATA) {
+ await testMiddleClickOnHomeButton(testData);
+ }
+});
+
+add_task(async function test_middleClickOnHomeButtonWithNewWindow() {
+ await testMiddleClickOnHomeButtonWithNewWindow(false);
+ await testMiddleClickOnHomeButtonWithNewWindow(true);
+});
+
+async function testMiddleClickOnTab(isMiddleMousePastePrefOn) {
+ info(`Set middlemouse.paste [${isMiddleMousePastePrefOn}]`);
+ Services.prefs.setBoolPref("middlemouse.paste", isMiddleMousePastePrefOn);
+
+ info("Set initial value");
+ SpecialPowers.clipboardCopyString("test\nsample");
+ gURLBar.value = "";
+ gURLBar.focus();
+
+ info("Open two tabs");
+ const tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ const tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ info("Middle click on tab2 to remove it");
+ EventUtils.synthesizeMouseAtCenter(tab2, { button: 1 });
+
+ info("Wait until the tab1 is selected");
+ await TestUtils.waitForCondition(() => gBrowser.selectedTab === tab1);
+
+ Assert.equal(gURLBar.value, "", "URLBar has no pasted value");
+
+ BrowserTestUtils.removeTab(tab1);
+}
+
+async function testMiddleClickToOpenNewTab(isMiddleMousePastePrefOn, selector) {
+ info(`Set middlemouse.paste [${isMiddleMousePastePrefOn}]`);
+ Services.prefs.setBoolPref("middlemouse.paste", isMiddleMousePastePrefOn);
+
+ info("Set initial value");
+ SpecialPowers.clipboardCopyString("test\nsample");
+ gURLBar.value = "";
+ gURLBar.focus();
+
+ info(`Click on ${selector}`);
+ const originalTab = gBrowser.selectedTab;
+ const element = document.querySelector(selector);
+ EventUtils.synthesizeMouseAtCenter(element, { button: 1 });
+
+ info("Wait until the new tab is opened");
+ await TestUtils.waitForCondition(() => gBrowser.selectedTab !== originalTab);
+
+ Assert.equal(gURLBar.value, "", "URLBar has no pasted value");
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+}
+
+async function testMiddleClickOnURLBar(isMiddleMousePastePrefOn) {
+ info(`Set middlemouse.paste [${isMiddleMousePastePrefOn}]`);
+ Services.prefs.setBoolPref("middlemouse.paste", isMiddleMousePastePrefOn);
+
+ info("Set initial value");
+ SpecialPowers.clipboardCopyString("test\nsample");
+ gURLBar.value = "";
+ gURLBar.focus();
+
+ info("Middle click on the urlbar");
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, { button: 1 });
+
+ if (isMiddleMousePastePrefOn) {
+ Assert.equal(gURLBar.value, "test sample", "URLBar has pasted value");
+ } else {
+ Assert.equal(gURLBar.value, "", "URLBar has no pasted value");
+ }
+}
+
+async function testMiddleClickOnHomeButton({
+ isMiddleMousePastePrefOn,
+ isLoadInBackground,
+ startPagePref,
+ expectedURLBarFocus,
+ expectedURLBarValue,
+}) {
+ info(`middlemouse.paste [${isMiddleMousePastePrefOn}]`);
+ info(`browser.startup.homepage [${startPagePref}]`);
+ info(`browser.tabs.loadBookmarksInBackground [${isLoadInBackground}]`);
+
+ info("Set initial value");
+ Services.prefs.setCharPref("browser.startup.homepage", startPagePref);
+ Services.prefs.setBoolPref(
+ "browser.tabs.loadBookmarksInBackground",
+ isLoadInBackground
+ );
+ Services.prefs.setBoolPref("middlemouse.paste", isMiddleMousePastePrefOn);
+ SpecialPowers.clipboardCopyString("test\nsample");
+ gURLBar.value = "";
+ gURLBar.focus();
+
+ info("Middle click on the home button");
+ const currentTab = gBrowser.selectedTab;
+ const homeButton = document.getElementById("home-button");
+ EventUtils.synthesizeMouseAtCenter(homeButton, { button: 1 });
+
+ if (!isLoadInBackground) {
+ info("Wait until the a new tab is selected");
+ await TestUtils.waitForCondition(() => gBrowser.selectedTab !== currentTab);
+ }
+
+ info("Wait until the focus moves");
+ await TestUtils.waitForCondition(
+ () =>
+ (document.activeElement === gURLBar.inputField) === expectedURLBarFocus
+ );
+
+ Assert.ok(true, "The focus is correct");
+ Assert.equal(gURLBar.value, expectedURLBarValue, "URLBar value is correct");
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+}
+
+async function testMiddleClickOnHomeButtonWithNewWindow(
+ isMiddleMousePastePrefOn
+) {
+ info(`Set middlemouse.paste [${isMiddleMousePastePrefOn}]`);
+ Services.prefs.setBoolPref("middlemouse.paste", isMiddleMousePastePrefOn);
+
+ info("Set prefs to open in a new window");
+ Services.prefs.setBoolPref("middlemouse.openNewWindow", true);
+ Services.prefs.setBoolPref("browser.tabs.opentabfor.middleclick", false);
+
+ info("Set initial value");
+ SpecialPowers.clipboardCopyString("test\nsample");
+ gURLBar.value = "";
+ gURLBar.focus();
+
+ info("Middle click on the home button");
+ const homeButton = document.getElementById("home-button");
+ const onNewWindowOpened = BrowserTestUtils.waitForNewWindow();
+ EventUtils.synthesizeMouseAtCenter(homeButton, { button: 1 });
+
+ const newWindow = await onNewWindowOpened;
+ Assert.equal(newWindow.gURLBar.value, "", "URLBar value is correct");
+
+ await BrowserTestUtils.closeWindow(newWindow);
+}
diff --git a/browser/components/urlbar/tests/browser/browser_new_tab_urlbar_reset.js b/browser/components/urlbar/tests/browser/browser_new_tab_urlbar_reset.js
new file mode 100644
index 0000000000..b2bce4b22e
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_new_tab_urlbar_reset.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Verify that urlbar state is reset when opening a new tab, so searching for the
+ * same text will reopen the results popup.
+ */
+add_task(async function () {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank",
+ false
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "m",
+ });
+ assertOpen();
+
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank",
+ false
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "m",
+ });
+ assertOpen();
+
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(tab2);
+});
+
+function assertOpen() {
+ Assert.equal(gURLBar.view.isOpen, true, "Should be showing the popup");
+}
diff --git a/browser/components/urlbar/tests/browser/browser_oneOffs.js b/browser/components/urlbar/tests/browser/browser_oneOffs.js
new file mode 100644
index 0000000000..d9a6a8d416
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_oneOffs.js
@@ -0,0 +1,980 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the one-off search buttons in the urlbar.
+ */
+
+"use strict";
+
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+
+let gMaxResults;
+let engine;
+
+XPCOMUtils.defineLazyGetter(this, "oneOffSearchButtons", () => {
+ return UrlbarTestUtils.getOneOffSearchButtons(window);
+});
+
+add_setup(async function () {
+ gMaxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults");
+
+ // Add a search suggestion engine and move it to the front so that it appears
+ // as the first one-off.
+ engine = await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME,
+ });
+ await Services.search.moveEngine(engine, 0);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.search.separatePrivateDefault.ui.enabled", false],
+ ["browser.urlbar.suggest.quickactions", false],
+ ["browser.urlbar.shortcuts.quickactions", true],
+ ],
+ });
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.formHistory.clear();
+ });
+
+ // Initialize history with enough visits to fill up the view.
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.formHistory.clear();
+ for (let i = 0; i < gMaxResults; i++) {
+ await PlacesTestUtils.addVisits(
+ "http://example.com/browser_urlbarOneOffs.js/?" + i
+ );
+ }
+
+ // Add some more visits to the last URL added above so that the top-sites view
+ // will be non-empty.
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits(
+ "http://example.com/browser_urlbarOneOffs.js/?" + (gMaxResults - 1)
+ );
+ }
+ await updateTopSites(sites => {
+ return sites && sites[0] && sites[0].url.startsWith("http://example.com/");
+ });
+
+ // Move the mouse away from the view so that a result or one-off isn't
+ // inadvertently highlighted. See bug 1659011.
+ EventUtils.synthesizeMouse(
+ gURLBar.inputField,
+ 0,
+ 0,
+ { type: "mousemove" },
+ window
+ );
+});
+
+// Opens the view without showing the one-offs. They should be hidden and arrow
+// key selection should work properly.
+add_task(async function noOneOffs() {
+ // Do a search for "@" since we hide the one-offs in that case.
+ let value = "@";
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value,
+ fireInputEvent: true,
+ });
+ await TestUtils.waitForCondition(
+ () => !oneOffSearchButtons._rebuilding,
+ "Waiting for one-offs to finish rebuilding"
+ );
+
+ Assert.equal(
+ UrlbarTestUtils.getOneOffSearchButtonsVisible(window),
+ false,
+ "One-offs should be hidden"
+ );
+ assertState(-1, -1, value);
+
+ // Get the result count. We don't particularly care what the results are,
+ // just what the count is so that we can key through them all.
+ let resultCount = UrlbarTestUtils.getResultCount(window);
+
+ // Key down through all results.
+ for (let i = 0; i < resultCount; i++) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ assertState(i, -1);
+ }
+
+ // Key down again. Nothing should be selected.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ assertState(-1, -1, value);
+
+ // Key down again. The first result should be selected.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ assertState(0, -1);
+
+ // Key up. Nothing should be selected.
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ assertState(-1, -1, value);
+
+ // Key up through all the results.
+ for (let i = resultCount - 1; i >= 0; i--) {
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ assertState(i, -1);
+ }
+
+ // Key up again. Nothing should be selected.
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ assertState(-1, -1, value);
+
+ await hidePopup();
+});
+
+// Opens the top-sites view. The one-offs should be shown.
+add_task(async function topSites() {
+ // Do a search that shows top sites.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ fireInputEvent: true,
+ });
+ await TestUtils.waitForCondition(
+ () => !oneOffSearchButtons._rebuilding,
+ "Waiting for one-offs to finish rebuilding"
+ );
+
+ // There's one top sites result, the page with a lot of visits from init.
+ let resultURL = "example.com/browser_urlbarOneOffs.js/?" + (gMaxResults - 1);
+ Assert.equal(UrlbarTestUtils.getResultCount(window), 1, "Result count");
+
+ Assert.equal(
+ UrlbarTestUtils.getOneOffSearchButtonsVisible(window),
+ true,
+ "One-offs are visible"
+ );
+
+ assertState(-1, -1, "");
+
+ // Key down into the result.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ assertState(0, -1, resultURL);
+
+ // Key down through each one-off.
+ let numButtons = oneOffSearchButtons.getSelectableButtons(true).length;
+ for (let i = 0; i < numButtons; i++) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ assertState(-1, i, "");
+ }
+
+ // Key down again. The selection should go away.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ assertState(-1, -1, "");
+
+ // Key down again. The result should be selected.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ assertState(0, -1, resultURL);
+
+ // Key back up. The selection should go away.
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ assertState(-1, -1, "");
+
+ // Key up again. The selection should wrap back around to the one-offs. Key
+ // up through all the one-offs.
+ for (let i = numButtons - 1; i >= 0; i--) {
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ assertState(-1, i, "");
+ }
+
+ // Key up. The result should be selected.
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ assertState(0, -1, resultURL);
+
+ // Key up again. The selection should go away.
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ assertState(-1, -1, "");
+
+ await hidePopup();
+});
+
+// Keys up and down through the non-top-sites view, i.e., the view that's shown
+// when the input has been edited.
+add_task(async function editedView() {
+ // Use a typed value that returns the visits added above but that doesn't
+ // trigger autofill since that would complicate the test.
+ let typedValue = "browser_urlbarOneOffs";
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: typedValue,
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, gMaxResults - 1);
+ let heuristicResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ assertState(0, -1, typedValue);
+
+ // Key down through each result. The first result is already selected, which
+ // is why gMaxResults - 1 is the correct number of times to do this.
+ for (let i = 0; i < gMaxResults - 1; i++) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ // i starts at zero so that the textValue passed to assertState is correct.
+ // But that means that i + 1 is the expected selected index, since initially
+ // (when this loop starts) the first result is selected.
+ assertState(
+ i + 1,
+ -1,
+ "example.com/browser_urlbarOneOffs.js/?" + (gMaxResults - i - 1)
+ );
+ Assert.ok(
+ !BrowserTestUtils.is_visible(heuristicResult.element.action),
+ "The heuristic action should not be visible"
+ );
+ }
+
+ // Key down through each one-off.
+ let numButtons = oneOffSearchButtons.getSelectableButtons(true).length;
+ for (let i = 0; i < numButtons; i++) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ assertState(-1, i, typedValue);
+ Assert.equal(
+ BrowserTestUtils.is_visible(heuristicResult.element.action),
+ !oneOffSearchButtons.selectedButton.classList.contains(
+ "search-setting-button"
+ ),
+ "The heuristic action should be visible when a one-off button is selected"
+ );
+ }
+
+ // Key down once more. The selection should wrap around to the first result.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ assertState(0, -1, typedValue);
+ Assert.ok(
+ BrowserTestUtils.is_visible(heuristicResult.element.action),
+ "The heuristic action should be visible"
+ );
+
+ // Now key up. The selection should wrap back around to the one-offs. Key
+ // up through all the one-offs.
+ for (let i = numButtons - 1; i >= 0; i--) {
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ assertState(-1, i, typedValue);
+ Assert.equal(
+ BrowserTestUtils.is_visible(heuristicResult.element.action),
+ !oneOffSearchButtons.selectedButton.classList.contains(
+ "search-setting-button"
+ ),
+ "The heuristic action should be visible when a one-off button is selected"
+ );
+ }
+
+ // Key up through each non-heuristic result.
+ for (let i = gMaxResults - 2; i >= 0; i--) {
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ assertState(
+ i + 1,
+ -1,
+ "example.com/browser_urlbarOneOffs.js/?" + (gMaxResults - i - 1)
+ );
+ Assert.ok(
+ !BrowserTestUtils.is_visible(heuristicResult.element.action),
+ "The heuristic action should not be visible"
+ );
+ }
+
+ // Key up once more. The heuristic result should be selected.
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ assertState(0, -1, typedValue);
+ Assert.ok(
+ BrowserTestUtils.is_visible(heuristicResult.element.action),
+ "The heuristic action should be visible"
+ );
+
+ await hidePopup();
+});
+
+// Checks that "Search with Current Search Engine" items are updated to "Search
+// with One-Off Engine" when a one-off is selected.
+add_task(async function searchWith() {
+ // Enable suggestions for this subtest so we can check non-heuristic results.
+ let oldDefaultEngine = await Services.search.getDefault();
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.searches", true]],
+ });
+
+ let typedValue = "foo";
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: typedValue,
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ assertState(0, -1, typedValue);
+
+ Assert.equal(
+ result.displayed.action,
+ "Search with " + (await Services.search.getDefault()).name,
+ "Sanity check: first result's action text"
+ );
+
+ // Alt+Down to the second one-off. Now the first result and the second
+ // one-off should both be selected.
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true, repeat: 2 });
+ assertState(0, 1, typedValue);
+
+ let engineName = oneOffSearchButtons.selectedButton.engine.name;
+ Assert.notEqual(
+ engineName,
+ (await Services.search.getDefault()).name,
+ "Sanity check: Second one-off engine should not be the current engine"
+ );
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ result.displayed.action,
+ "Search with " + engineName,
+ "First result's action text should be updated"
+ );
+
+ // Check non-heuristic results.
+ await hidePopup();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: typedValue,
+ });
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ assertState(1, -1, typedValue + "foo");
+ Assert.equal(
+ result.displayed.action,
+ "Search with " + engine.name,
+ "Sanity check: second result's action text"
+ );
+ Assert.ok(!result.heuristic, "The second result is not heuristic.");
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true, repeat: 2 });
+ assertState(1, 1, typedValue + "foo");
+
+ engineName = oneOffSearchButtons.selectedButton.engine.name;
+ Assert.notEqual(
+ engineName,
+ (await Services.search.getDefault()).name,
+ "Sanity check: Second one-off engine should not be the current engine"
+ );
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+
+ Assert.equal(
+ result.displayed.action,
+ "Search with " + engineName,
+ "Second result's action text should be updated"
+ );
+
+ await SpecialPowers.popPrefEnv();
+ await Services.search.setDefault(
+ oldDefaultEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ await hidePopup();
+});
+
+// Clicks a one-off with an engine.
+add_task(async function oneOffClick() {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+
+ // We are explicitly using something that looks like a url, to make the test
+ // stricter. Even if it looks like a url, we should search.
+ let typedValue = "foo.bar";
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: typedValue,
+ });
+ await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ assertState(0, -1, typedValue);
+ let oneOffs = oneOffSearchButtons.getSelectableButtons(true);
+
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeMouseAtCenter(oneOffs[0], {});
+ await searchPromise;
+ Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Urlbar view is still open.");
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: oneOffs[0].engine.name,
+ entry: "oneoff",
+ });
+ await UrlbarTestUtils.exitSearchMode(window, { backspace: true });
+
+ gBrowser.removeTab(gBrowser.selectedTab);
+ await UrlbarTestUtils.formHistory.clear();
+});
+
+// Presses the Return key when a one-off with an engine is selected.
+add_task(async function oneOffReturn() {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+
+ // We are explicitly using something that looks like a url, to make the test
+ // stricter. Even if it looks like a url, we should search.
+ let typedValue = "foo.bar";
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: typedValue,
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ assertState(0, -1, typedValue);
+ let oneOffs = oneOffSearchButtons.getSelectableButtons(true);
+
+ // Alt+Down to select the first one-off.
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ assertState(0, 0, typedValue);
+
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await searchPromise;
+ Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Urlbar view is still open.");
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: oneOffs[0].engine.name,
+ entry: "oneoff",
+ });
+ await UrlbarTestUtils.exitSearchMode(window, { backspace: true });
+
+ gBrowser.removeTab(gBrowser.selectedTab);
+ await UrlbarTestUtils.formHistory.clear();
+ await hidePopup();
+});
+
+// When all engines and local shortcuts are hidden except for the current
+// engine, the one-offs container should be hidden.
+add_task(async function allOneOffsHiddenExceptCurrentEngine() {
+ // Disable all the engines but the current one, check the oneoffs are
+ // hidden and that moving up selects the last match.
+ let defaultEngine = await Services.search.getDefault();
+ let engines = (await Services.search.getVisibleEngines()).filter(
+ e => e.name != defaultEngine.name
+ );
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.search.hiddenOneOffs", engines.map(e => e.name).join(",")],
+ ...UrlbarUtils.LOCAL_SEARCH_MODES.map(m => [
+ `browser.urlbar.${m.pref}`,
+ false,
+ ]),
+ ],
+ });
+
+ let typedValue = "foo";
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: typedValue,
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ assertState(0, -1);
+ Assert.equal(
+ UrlbarTestUtils.getOneOffSearchButtonsVisible(window),
+ false,
+ "The one-off buttons should be hidden"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ assertState(0, -1);
+ await hidePopup();
+ await SpecialPowers.popPrefEnv();
+});
+
+// The one-offs should be hidden when searching with an "@engine" search engine
+// alias.
+add_task(async function hiddenWhenUsingSearchAlias() {
+ let typedValue = "@example";
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: typedValue,
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ UrlbarTestUtils.getOneOffSearchButtonsVisible(window),
+ false,
+ "Should not be showing the one-off buttons"
+ );
+ await hidePopup();
+
+ typedValue = "not an engine alias";
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: typedValue,
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ UrlbarTestUtils.getOneOffSearchButtonsVisible(window),
+ true,
+ "Should be showing the one-off buttons"
+ );
+ await hidePopup();
+});
+
+// Makes sure the local shortcuts exist.
+add_task(async function localShortcuts() {
+ oneOffSearchButtons.invalidateCache();
+ await doLocalShortcutsShownTest();
+});
+
+// Clicks a local shortcut button.
+add_task(async function localShortcutClick() {
+ // We are explicitly using something that looks like a url, to make the test
+ // stricter. Even if it looks like a url, we should search.
+ let typedValue = "foo.bar";
+
+ oneOffSearchButtons.invalidateCache();
+ let rebuildPromise = BrowserTestUtils.waitForEvent(
+ oneOffSearchButtons,
+ "rebuild"
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: typedValue,
+ });
+ await rebuildPromise;
+
+ let buttons = oneOffSearchButtons.localButtons;
+ Assert.ok(buttons.length, "Sanity check: Local shortcuts exist");
+
+ for (let button of buttons) {
+ Assert.ok(button.source, "Sanity check: Button has a source");
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ await searchPromise;
+ Assert.ok(
+ UrlbarTestUtils.isPopupOpen(window),
+ "Urlbar view is still open."
+ );
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: button.source,
+ entry: "oneoff",
+ });
+ }
+
+ await UrlbarTestUtils.exitSearchMode(window, { backspace: true });
+ await hidePopup();
+});
+
+// Presses the Return key when a local shortcut is selected.
+add_task(async function localShortcutReturn() {
+ // We are explicitly using something that looks like a url, to make the test
+ // stricter. Even if it looks like a url, we should search.
+ let typedValue = "foo.bar";
+
+ oneOffSearchButtons.invalidateCache();
+ let rebuildPromise = BrowserTestUtils.waitForEvent(
+ oneOffSearchButtons,
+ "rebuild"
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: typedValue,
+ });
+ await rebuildPromise;
+
+ let buttons = oneOffSearchButtons.localButtons;
+ Assert.ok(buttons.length, "Sanity check: Local shortcuts exist");
+
+ let allButtons = oneOffSearchButtons.getSelectableButtons(false);
+ let firstLocalIndex = allButtons.length - buttons.length;
+
+ for (let i = 0; i < buttons.length; i++) {
+ let button = buttons[i];
+
+ // Alt+Down enough times to select the button.
+ let index = firstLocalIndex + i;
+ EventUtils.synthesizeKey("KEY_ArrowDown", {
+ altKey: true,
+ repeat: index + 1,
+ });
+ await TestUtils.waitForCondition(
+ () => oneOffSearchButtons.selectedButtonIndex == index,
+ "Waiting for local shortcut to become selected"
+ );
+
+ let expectedSelectedResultIndex = -1;
+ let count = UrlbarTestUtils.getResultCount(window);
+ if (count > 0) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ if (result.heuristic) {
+ expectedSelectedResultIndex = 0;
+ }
+ }
+ assertState(expectedSelectedResultIndex, index, typedValue);
+
+ Assert.ok(button.source, "Sanity check: Button has a source");
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await searchPromise;
+ Assert.ok(
+ UrlbarTestUtils.isPopupOpen(window),
+ "Urlbar view is still open."
+ );
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: button.source,
+ entry: "oneoff",
+ });
+ }
+
+ await UrlbarTestUtils.exitSearchMode(window, { backspace: true });
+ await hidePopup();
+});
+
+// With an empty search string, clicking a local shortcut should result in no
+// heuristic result.
+add_task(async function localShortcutEmptySearchString() {
+ oneOffSearchButtons.invalidateCache();
+ let rebuildPromise = BrowserTestUtils.waitForEvent(
+ oneOffSearchButtons,
+ "rebuild"
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ await rebuildPromise;
+
+ Assert.equal(
+ UrlbarTestUtils.getOneOffSearchButtonsVisible(window),
+ true,
+ "One-offs are visible"
+ );
+
+ let buttons = oneOffSearchButtons.localButtons;
+ Assert.ok(buttons.length, "Sanity check: Local shortcuts exist");
+
+ for (let button of buttons) {
+ Assert.ok(button.source, "Sanity check: Button has a source");
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ await searchPromise;
+ Assert.ok(
+ UrlbarTestUtils.isPopupOpen(window),
+ "Urlbar view is still open."
+ );
+ Assert.equal(
+ UrlbarTestUtils.getOneOffSearchButtonsVisible(window),
+ true,
+ "One-offs are visible"
+ );
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: button.source,
+ entry: "oneoff",
+ });
+
+ let resultCount = UrlbarTestUtils.getResultCount(window);
+ if (!resultCount) {
+ Assert.equal(
+ gURLBar.panel.getAttribute("noresults"),
+ "true",
+ "Panel has no results, therefore should have noresults attribute"
+ );
+ continue;
+ }
+ Assert.ok(
+ !gURLBar.panel.hasAttribute("noresults"),
+ "Panel has results, therefore should not have noresults attribute"
+ );
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(!result.heuristic, "The first result should not be heuristic");
+ }
+
+ await UrlbarTestUtils.exitSearchMode(window, { backspace: true });
+
+ await hidePopup();
+});
+
+// Trigger SearchOneOffs.willHide() outside of SearchOneOffs.__rebuild(). Ensure
+// that we always show the correct engines in the one-offs. This effectively
+// tests SearchOneOffs._engineInfo.domWasUpdated.
+add_task(async function avoidWillHideRace() {
+ // We set maxHistoricalSearchSuggestions to 0 since this test depends on
+ // UrlbarView calling SearchOneOffs.willHide(). That only happens when the
+ // Urlbar is in search mode after a query that returned no results.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.maxHistoricalSearchSuggestions", 0]],
+ });
+
+ oneOffSearchButtons.invalidateCache();
+
+ // Accel+K triggers SearchOneOffs.willHide() from UrlbarView instead of from
+ // SearchOneOffs.__rebuild.
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("k", { accelKey: true });
+ await searchPromise;
+ Assert.ok(
+ UrlbarTestUtils.getOneOffSearchButtonsVisible(window),
+ "One-offs should be visible"
+ );
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ info("Hide all engines but the test engine.");
+ let oldDefaultEngine = await Services.search.getDefault();
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ let engines = (await Services.search.getVisibleEngines()).filter(
+ e => e.name != engine.name
+ );
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.search.hiddenOneOffs", engines.map(e => e.name).join(",")],
+ ...UrlbarUtils.LOCAL_SEARCH_MODES.map(m => [
+ `browser.urlbar.${m.pref}`,
+ false,
+ ]),
+ ],
+ });
+ Assert.ok(
+ !oneOffSearchButtons._engineInfo,
+ "_engineInfo should be nulled out."
+ );
+
+ // This call to SearchOneOffs.willHide() should repopulate _engineInfo but not
+ // rebuild the one-offs. _engineInfo.willHide will be true and thus UrlbarView
+ // will not open.
+ EventUtils.synthesizeKey("k", { accelKey: true });
+ // We can't wait for UrlbarTestUtils.promiseSearchComplete here since we
+ // expect the popup will not open. We wait for _engineInfo to be populated
+ // instead.
+ await BrowserTestUtils.waitForCondition(
+ () => !!oneOffSearchButtons._engineInfo,
+ "_engineInfo is set."
+ );
+ Assert.ok(!UrlbarTestUtils.isPopupOpen(window), "The UrlbarView is closed.");
+ Assert.equal(
+ oneOffSearchButtons._engineInfo.willHide,
+ true,
+ "_engineInfo should be repopulated and willHide should be true."
+ );
+ Assert.equal(
+ oneOffSearchButtons._engineInfo.domWasUpdated,
+ undefined,
+ "domWasUpdated should not be populated since we haven't yet tried to rebuild the one-offs."
+ );
+
+ // Now search. The view will open and the one-offs will rebuild, although
+ // the one-offs will not be shown since there is only one engine.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+ Assert.equal(
+ oneOffSearchButtons._engineInfo.domWasUpdated,
+ true,
+ "domWasUpdated should be true"
+ );
+ Assert.ok(
+ !UrlbarTestUtils.getOneOffSearchButtonsVisible(window),
+ "One-offs should be hidden since there is only one engine."
+ );
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ await SpecialPowers.popPrefEnv();
+ await Services.search.setDefault(
+ oldDefaultEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ await SpecialPowers.popPrefEnv();
+});
+
+// Hides each of the local shortcuts one at a time. The search buttons should
+// automatically rebuild themselves.
+add_task(async function individualLocalShortcutsHidden() {
+ for (let { pref, source } of UrlbarUtils.LOCAL_SEARCH_MODES) {
+ await SpecialPowers.pushPrefEnv({
+ set: [[`browser.urlbar.${pref}`, false]],
+ });
+
+ let rebuildPromise = BrowserTestUtils.waitForEvent(
+ oneOffSearchButtons,
+ "rebuild"
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ await rebuildPromise;
+
+ Assert.equal(
+ UrlbarTestUtils.getOneOffSearchButtonsVisible(window),
+ true,
+ "One-offs are visible"
+ );
+
+ let buttons = oneOffSearchButtons.localButtons;
+ Assert.ok(buttons.length, "Sanity check: Local shortcuts exist");
+
+ let otherModes = UrlbarUtils.LOCAL_SEARCH_MODES.filter(
+ m => m.source != source
+ );
+ Assert.equal(
+ buttons.length,
+ otherModes.length,
+ "Expected number of enabled local shortcut buttons"
+ );
+
+ for (let i = 0; i < buttons.length; i++) {
+ Assert.equal(
+ buttons[i].source,
+ otherModes[i].source,
+ "Button has the expected source"
+ );
+ }
+
+ await hidePopup();
+ await SpecialPowers.popPrefEnv();
+ }
+});
+
+// Hides all the local shortcuts at once.
+add_task(async function allLocalShortcutsHidden() {
+ await SpecialPowers.pushPrefEnv({
+ set: UrlbarUtils.LOCAL_SEARCH_MODES.map(m => [
+ `browser.urlbar.${m.pref}`,
+ false,
+ ]),
+ });
+
+ let rebuildPromise = BrowserTestUtils.waitForEvent(
+ oneOffSearchButtons,
+ "rebuild"
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ await rebuildPromise;
+
+ Assert.equal(
+ UrlbarTestUtils.getOneOffSearchButtonsVisible(window),
+ true,
+ "One-offs are visible"
+ );
+
+ Assert.equal(
+ oneOffSearchButtons.localButtons.length,
+ 0,
+ "All local shortcuts should be hidden"
+ );
+
+ Assert.greater(
+ oneOffSearchButtons.getSelectableButtons(false).filter(b => b.engine)
+ .length,
+ 0,
+ "Engine one-offs should not be hidden"
+ );
+
+ await hidePopup();
+ await SpecialPowers.popPrefEnv();
+});
+
+// Hides all the engines but none of the local shortcuts.
+add_task(async function localShortcutsShownWhenEnginesHidden() {
+ let engines = await Services.search.getVisibleEngines();
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.hiddenOneOffs", engines.map(e => e.name).join(",")]],
+ });
+
+ let rebuildPromise = BrowserTestUtils.waitForEvent(
+ oneOffSearchButtons,
+ "rebuild"
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ await rebuildPromise;
+
+ Assert.equal(
+ UrlbarTestUtils.getOneOffSearchButtonsVisible(window),
+ true,
+ "One-offs are visible"
+ );
+
+ Assert.equal(
+ oneOffSearchButtons.localButtons.length,
+ UrlbarUtils.LOCAL_SEARCH_MODES.length,
+ "All local shortcuts are visible"
+ );
+
+ Assert.equal(
+ oneOffSearchButtons.getSelectableButtons(false).filter(b => b.engine)
+ .length,
+ 0,
+ "All engine one-offs are hidden"
+ );
+
+ await hidePopup();
+ await SpecialPowers.popPrefEnv();
+});
+
+/**
+ * Checks that the local shortcuts are shown correctly.
+ */
+async function doLocalShortcutsShownTest() {
+ let rebuildPromise = BrowserTestUtils.waitForEvent(
+ oneOffSearchButtons,
+ "rebuild"
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "doLocalShortcutsShownTest",
+ });
+ await rebuildPromise;
+
+ let buttons = oneOffSearchButtons.localButtons;
+ Assert.equal(buttons.length, 4, "Expected number of local shortcuts");
+
+ let expectedSource;
+ let seenIDs = new Set();
+ for (let button of buttons) {
+ Assert.ok(
+ !seenIDs.has(button.id),
+ "Should not have already seen button.id"
+ );
+ seenIDs.add(button.id);
+ switch (button.id) {
+ case "urlbar-engine-one-off-item-bookmarks":
+ expectedSource = UrlbarUtils.RESULT_SOURCE.BOOKMARKS;
+ break;
+ case "urlbar-engine-one-off-item-tabs":
+ expectedSource = UrlbarUtils.RESULT_SOURCE.TABS;
+ break;
+ case "urlbar-engine-one-off-item-history":
+ expectedSource = UrlbarUtils.RESULT_SOURCE.HISTORY;
+ break;
+ case "urlbar-engine-one-off-item-actions":
+ expectedSource = UrlbarUtils.RESULT_SOURCE.ACTIONS;
+ break;
+ default:
+ Assert.ok(false, `Unexpected local shortcut ID: ${button.id}`);
+ break;
+ }
+ Assert.equal(button.source, expectedSource, "Expected button.source");
+ }
+
+ await hidePopup();
+}
+
+function assertState(result, oneOff, textValue = undefined) {
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ result,
+ "Expected result should be selected"
+ );
+ Assert.equal(
+ oneOffSearchButtons.selectedButtonIndex,
+ oneOff,
+ "Expected one-off should be selected"
+ );
+ if (textValue !== undefined) {
+ Assert.equal(gURLBar.value, textValue, "Expected value");
+ }
+}
+
+function hidePopup() {
+ return UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeKey("KEY_Escape");
+ });
+}
diff --git a/browser/components/urlbar/tests/browser/browser_oneOffs_contextMenu.js b/browser/components/urlbar/tests/browser/browser_oneOffs_contextMenu.js
new file mode 100644
index 0000000000..60d46608cd
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_oneOffs_contextMenu.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that the right-click menu works correctly for the one-off buttons.
+ */
+
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+
+let gMaxResults;
+
+XPCOMUtils.defineLazyGetter(this, "oneOffSearchButtons", () => {
+ return UrlbarTestUtils.getOneOffSearchButtons(window);
+});
+
+let originalEngine;
+let newEngine;
+
+// The one-off context menu should not be shown.
+add_task(async function contextMenu_not_shown() {
+ // Add a popupshown listener on the context menu that sets this
+ // popupshownFired boolean.
+ let popupshownFired = false;
+ let onPopupshown = () => {
+ popupshownFired = true;
+ };
+ let contextMenu = oneOffSearchButtons.querySelector(
+ ".search-one-offs-context-menu"
+ );
+ contextMenu.addEventListener("popupshown", onPopupshown);
+
+ // Do a search to open the view.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ });
+
+ // First, try to open the context menu on a remote engine.
+ let allOneOffs = oneOffSearchButtons.getSelectableButtons(true);
+ Assert.greater(allOneOffs.length, 0, "There should be at least one one-off");
+ Assert.ok(
+ allOneOffs[0].engine,
+ "The first one-off should be a remote one-off"
+ );
+ EventUtils.synthesizeMouseAtCenter(allOneOffs[0], {
+ type: "contextmenu",
+ button: 2,
+ });
+ let timeout = 500;
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, timeout));
+ Assert.ok(
+ !popupshownFired,
+ "popupshown should not be fired on a remote one-off"
+ );
+
+ // Now try to open the context menu on a local one-off.
+ let localOneOffs = oneOffSearchButtons.localButtons;
+ Assert.greater(
+ localOneOffs.length,
+ 0,
+ "There should be at least one local one-off"
+ );
+ EventUtils.synthesizeMouseAtCenter(localOneOffs[0], {
+ type: "contextmenu",
+ button: 2,
+ });
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, timeout));
+ Assert.ok(
+ !popupshownFired,
+ "popupshown should not be fired on a local one-off"
+ );
+
+ contextMenu.removeEventListener("popupshown", onPopupshown);
+ await UrlbarTestUtils.promisePopupClose(window);
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_oneOffs_heuristicRestyle.js b/browser/components/urlbar/tests/browser/browser_oneOffs_heuristicRestyle.js
new file mode 100644
index 0000000000..3513fd2dac
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_oneOffs_heuristicRestyle.js
@@ -0,0 +1,516 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that heuristic results are updated/restyled to search results when a
+ * one-off is selected.
+ */
+
+"use strict";
+
+XPCOMUtils.defineLazyGetter(this, "oneOffSearchButtons", () => {
+ return UrlbarTestUtils.getOneOffSearchButtons(window);
+});
+
+const TEST_DEFAULT_ENGINE_NAME = "Test";
+
+const HISTORY_URL = "https://mozilla.org/";
+
+const KEYWORD = "kw";
+const KEYWORD_URL = "https://mozilla.org/search?q=%s";
+
+// Expected result data for our test results.
+const RESULT_DATA_BY_TYPE = {
+ [UrlbarUtils.RESULT_TYPE.URL]: {
+ icon: `page-icon:${HISTORY_URL}`,
+ actionL10n: {
+ id: "urlbar-result-action-visit",
+ },
+ },
+ [UrlbarUtils.RESULT_TYPE.SEARCH]: {
+ icon: "chrome://global/skin/icons/search-glass.svg",
+ actionL10n: {
+ id: "urlbar-result-action-search-w-engine",
+ args: { engine: TEST_DEFAULT_ENGINE_NAME },
+ },
+ },
+ [UrlbarUtils.RESULT_TYPE.KEYWORD]: {
+ icon: `page-icon:${KEYWORD_URL}`,
+ },
+};
+
+function getSourceIcon(source) {
+ switch (source) {
+ case UrlbarUtils.RESULT_SOURCE.BOOKMARKS:
+ return "chrome://browser/skin/bookmark.svg";
+ case UrlbarUtils.RESULT_SOURCE.HISTORY:
+ return "chrome://browser/skin/history.svg";
+ case UrlbarUtils.RESULT_SOURCE.TABS:
+ return "chrome://browser/skin/tab.svg";
+ default:
+ return null;
+ }
+}
+
+/**
+ * Asserts that the heuristic result is *not* restyled to look like a search
+ * result.
+ *
+ * @param {UrlbarUtils.RESULT_TYPE} expectedType
+ * The expected type of the heuristic.
+ * @param {object} resultDetails
+ * The return value of UrlbarTestUtils.getDetailsOfResultAt(window, 0).
+ */
+async function heuristicIsNotRestyled(expectedType, resultDetails) {
+ Assert.equal(
+ resultDetails.type,
+ expectedType,
+ "The restyled result is the expected type."
+ );
+
+ Assert.equal(
+ resultDetails.displayed.title,
+ resultDetails.title,
+ "The displayed title is equal to the payload title."
+ );
+
+ let data = RESULT_DATA_BY_TYPE[expectedType];
+ Assert.ok(data, "Sanity check: Expected type is recognized");
+
+ let [actionText] = data.actionL10n
+ ? await document.l10n.formatValues([data.actionL10n])
+ : [""];
+
+ if (
+ expectedType === UrlbarUtils.RESULT_TYPE.URL &&
+ resultDetails.result.heuristic &&
+ resultDetails.result.payload.title
+ ) {
+ Assert.equal(
+ resultDetails.displayed.url,
+ resultDetails.result.payload.displayUrl
+ );
+ } else {
+ Assert.equal(
+ resultDetails.displayed.action,
+ actionText,
+ "The result has the expected non-styled action text."
+ );
+ }
+
+ Assert.equal(
+ BrowserTestUtils.is_visible(resultDetails.element.separator),
+ !!actionText,
+ "The title separator is " + (actionText ? "visible" : "hidden")
+ );
+ Assert.equal(
+ BrowserTestUtils.is_visible(resultDetails.element.action),
+ !!actionText,
+ "The action text is " + (actionText ? "visible" : "hidden")
+ );
+
+ Assert.equal(
+ resultDetails.image,
+ data.icon,
+ "The result has the expected non-styled icon."
+ );
+}
+
+/**
+ * Asserts that the heuristic result is restyled to look like a search result.
+ *
+ * @param {UrlbarUtils.RESULT_TYPE} expectedType
+ * The expected type of the heuristic.
+ * @param {object} resultDetails
+ * The return value of UrlbarTestUtils.getDetailsOfResultAt(window, 0).
+ * @param {string} searchString
+ * The current search string. The restyled heuristic result's title is
+ * expected to be this string.
+ * @param {element} selectedOneOff
+ * The selected one-off button.
+ */
+async function heuristicIsRestyled(
+ expectedType,
+ resultDetails,
+ searchString,
+ selectedOneOff
+) {
+ let engine = selectedOneOff.engine;
+ let source = selectedOneOff.source;
+ if (!engine && !source) {
+ Assert.ok(false, "An invalid one-off was passed to urlbarResultIsRestyled");
+ return;
+ }
+ Assert.equal(
+ resultDetails.type,
+ expectedType,
+ "The restyled result is still the expected type."
+ );
+
+ let actionText;
+ if (engine) {
+ [actionText] = await document.l10n.formatValues([
+ {
+ id: "urlbar-result-action-search-w-engine",
+ args: { engine: engine.name },
+ },
+ ]);
+ } else if (source) {
+ [actionText] = await document.l10n.formatValues([
+ {
+ id: `urlbar-result-action-search-${UrlbarUtils.getResultSourceName(
+ source
+ )}`,
+ },
+ ]);
+ }
+ Assert.equal(
+ resultDetails.displayed.action,
+ actionText,
+ "Restyled result's action text should be updated"
+ );
+
+ Assert.equal(
+ resultDetails.displayed.title,
+ searchString,
+ "The restyled result's title should be equal to the search string."
+ );
+
+ Assert.ok(
+ BrowserTestUtils.is_visible(resultDetails.element.separator),
+ "The restyled result's title separator should be visible"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_visible(resultDetails.element.action),
+ "The restyled result's action text should be visible"
+ );
+
+ if (engine) {
+ Assert.equal(
+ resultDetails.image,
+ engine.iconURI?.spec || UrlbarUtils.ICON.SEARCH_GLASS,
+ "The restyled result's icon should be the engine's icon."
+ );
+ } else if (source) {
+ Assert.equal(
+ resultDetails.image,
+ getSourceIcon(source),
+ "The restyled result's icon should be the local one-off's icon."
+ );
+ }
+}
+
+/**
+ * Asserts that the specified one-off (if any) is selected and that the
+ * heuristic result is either restyled or not restyled as appropriate. If
+ * there's a selected one-off, then the heuristic is expected to be restyled; if
+ * there's no selected one-off, then it's expected not to be restyled.
+ *
+ * @param {string} searchString
+ * The current search string. If a one-off is selected, then the restyled
+ * heuristic result's title is expected to be this string.
+ * @param {UrlbarUtils.RESULT_TYPE} expectedHeuristicType
+ * The expected type of the heuristic.
+ * @param {number} expectedSelectedOneOffIndex
+ * The index of the expected selected one-off button. If no one-off is
+ * expected to be selected, then pass -1.
+ */
+async function assertState(
+ searchString,
+ expectedHeuristicType,
+ expectedSelectedOneOffIndex
+) {
+ Assert.equal(
+ oneOffSearchButtons.selectedButtonIndex,
+ expectedSelectedOneOffIndex,
+ "Expected one-off should be selected"
+ );
+
+ let resultDetails = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ if (expectedSelectedOneOffIndex >= 0) {
+ await heuristicIsRestyled(
+ expectedHeuristicType,
+ resultDetails,
+ searchString,
+ oneOffSearchButtons.selectedButton
+ );
+ } else {
+ await heuristicIsNotRestyled(expectedHeuristicType, resultDetails);
+ }
+}
+
+add_setup(async function () {
+ await SearchTestUtils.installSearchExtension(
+ {
+ name: TEST_DEFAULT_ENGINE_NAME,
+ keyword: "@test",
+ },
+ { setAsDefault: true }
+ );
+ let engine = Services.search.getEngineByName(TEST_DEFAULT_ENGINE_NAME);
+ await Services.search.moveEngine(engine, 0);
+
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits(HISTORY_URL);
+ }
+
+ await PlacesUtils.keywords.insert({
+ keyword: KEYWORD,
+ url: KEYWORD_URL,
+ });
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.history.clear();
+ await PlacesUtils.keywords.remove(KEYWORD);
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.searches", false]],
+ });
+
+ // Move the mouse away from the view so that a result or one-off isn't
+ // inadvertently highlighted. See bug 1659011.
+ EventUtils.synthesizeMouse(
+ gURLBar.inputField,
+ 0,
+ 0,
+ { type: "mousemove" },
+ window
+ );
+});
+
+add_task(async function arrow_engine_url() {
+ await doArrowTest("mozilla.or", UrlbarUtils.RESULT_TYPE.URL, false);
+});
+
+add_task(async function arrow_engine_search() {
+ await doArrowTest("test", UrlbarUtils.RESULT_TYPE.SEARCH, false);
+});
+
+add_task(async function arrow_engine_keyword() {
+ await doArrowTest(`${KEYWORD} test`, UrlbarUtils.RESULT_TYPE.KEYWORD, false);
+});
+
+add_task(async function arrow_local_url() {
+ await doArrowTest("mozilla.or", UrlbarUtils.RESULT_TYPE.URL, true);
+});
+
+add_task(async function arrow_local_search() {
+ await doArrowTest("test", UrlbarUtils.RESULT_TYPE.SEARCH, true);
+});
+
+add_task(async function arrow_local_keyword() {
+ await doArrowTest(`${KEYWORD} test`, UrlbarUtils.RESULT_TYPE.KEYWORD, true);
+});
+
+/**
+ * Arrows down to the one-offs, checks the heuristic, and clicks it.
+ *
+ * @param {string} searchString
+ * The search string to use.
+ * @param {UrlbarUtils.RESULT_TYPE} expectedHeuristicType
+ * The type of heuristic result that the search string is expected to trigger.
+ * @param {boolean} useLocal
+ * Whether to test a local one-off or an engine one-off. If true, test a
+ * local one-off. If false, test an engine one-off.
+ */
+async function doArrowTest(searchString, expectedHeuristicType, useLocal) {
+ await doTest(searchString, expectedHeuristicType, useLocal, async () => {
+ info(
+ "Arrow down to the one-offs, observe heuristic is restyled as a search result."
+ );
+ let resultCount = UrlbarTestUtils.getResultCount(window);
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: resultCount });
+ await searchPromise;
+ await assertState(searchString, expectedHeuristicType, 0);
+
+ let depth = 1;
+ if (useLocal) {
+ for (; !oneOffSearchButtons.selectedButton.source; depth++) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+ Assert.ok(
+ oneOffSearchButtons.selectedButton.source,
+ "Selected one-off is local"
+ );
+ await assertState(searchString, expectedHeuristicType, depth - 1);
+ }
+
+ info(
+ "Arrow up out of the one-offs, observe heuristic styling is restored."
+ );
+ EventUtils.synthesizeKey("KEY_ArrowUp", { repeat: depth });
+ await assertState(searchString, expectedHeuristicType, -1);
+
+ info(
+ "Arrow back down into the one-offs, observe heuristic is restyled as a search result."
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: depth });
+ await assertState(searchString, expectedHeuristicType, depth - 1);
+ });
+}
+
+add_task(async function altArrow_engine_url() {
+ await doAltArrowTest("mozilla.or", UrlbarUtils.RESULT_TYPE.URL, false);
+});
+
+add_task(async function altArrow_engine_search() {
+ await doAltArrowTest("test", UrlbarUtils.RESULT_TYPE.SEARCH, false);
+});
+
+add_task(async function altArrow_engine_keyword() {
+ await doAltArrowTest(
+ `${KEYWORD} test`,
+ UrlbarUtils.RESULT_TYPE.KEYWORD,
+ false
+ );
+});
+
+add_task(async function altArrow_local_url() {
+ await doAltArrowTest("mozilla.or", UrlbarUtils.RESULT_TYPE.URL, true);
+});
+
+add_task(async function altArrow_local_search() {
+ await doAltArrowTest("test", UrlbarUtils.RESULT_TYPE.SEARCH, true);
+});
+
+add_task(async function altArrow_local_keyword() {
+ await doAltArrowTest(
+ `${KEYWORD} test`,
+ UrlbarUtils.RESULT_TYPE.KEYWORD,
+ true
+ );
+});
+
+/**
+ * Alt-arrows down to the one-offs so that the heuristic remains selected,
+ * checks the heuristic, and clicks it.
+ *
+ * @param {string} searchString
+ * The search string to use.
+ * @param {UrlbarUtils.RESULT_TYPE} expectedHeuristicType
+ * The type of heuristic result that the search string is expected to trigger.
+ * @param {boolean} useLocal
+ * Whether to test a local one-off or an engine one-off. If true, test a
+ * local one-off. If false, test an engine one-off.
+ */
+async function doAltArrowTest(searchString, expectedHeuristicType, useLocal) {
+ await doTest(searchString, expectedHeuristicType, useLocal, async () => {
+ info(
+ "Alt+down into the one-offs, observe heuristic is restyled as a search result."
+ );
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ await searchPromise;
+ await assertState(searchString, expectedHeuristicType, 0);
+
+ let depth = 1;
+ if (useLocal) {
+ for (; !oneOffSearchButtons.selectedButton.source; depth++) {
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ }
+ Assert.ok(
+ oneOffSearchButtons.selectedButton.source,
+ "Selected one-off is local"
+ );
+ await assertState(searchString, expectedHeuristicType, depth - 1);
+ }
+
+ info(
+ "Arrow down and then up to re-select the heuristic, observe its styling is restored."
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ await assertState(searchString, expectedHeuristicType, -1);
+
+ info(
+ "Alt+down into the one-offs, observe the heuristic is restyled as a search result."
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true, repeat: depth });
+ await assertState(searchString, expectedHeuristicType, depth - 1);
+
+ info("Alt+up out of the one-offs, observe the heuristic is restored.");
+ EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true, repeat: depth });
+ await assertState(searchString, expectedHeuristicType, -1);
+
+ info(
+ "Alt+down into the one-offs, observe the heuristic is restyled as a search result."
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true, repeat: depth });
+ await assertState(searchString, expectedHeuristicType, depth - 1);
+ });
+}
+
+/**
+ * The main test function. Starts a search, asserts that the heuristic has the
+ * expected type, calls a callback to run more checks, and then finally clicks
+ * the restyled heuristic to make sure search mode is confirmed.
+ *
+ * @param {string} searchString
+ * The search string to use.
+ * @param {UrlbarUtils.RESULT_TYPE} expectedHeuristicType
+ * The type of heuristic result that the search string is expected to trigger.
+ * @param {boolean} useLocal
+ * Whether to test a local one-off or an engine one-off. If true, test a
+ * local one-off. If false, test an engine one-off.
+ * @param {Function} callback
+ * This is called after the search completes. It should perform whatever
+ * checks are necessary for the test task. Important: When it returns, it
+ * should make sure that the first one-off is selected.
+ */
+async function doTest(searchString, expectedHeuristicType, useLocal, callback) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: searchString,
+ fireInputEvent: true,
+ });
+ await TestUtils.waitForCondition(
+ () => !oneOffSearchButtons._rebuilding,
+ "Waiting for one-offs to finish rebuilding"
+ );
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(result.heuristic, "First result is heuristic");
+ Assert.equal(
+ result.type,
+ expectedHeuristicType,
+ "Heuristic is expected type"
+ );
+ await assertState(searchString, expectedHeuristicType, -1);
+
+ await callback();
+
+ Assert.ok(
+ oneOffSearchButtons.selectedButton,
+ "The callback should leave a one-off selected so that the heuristic remains re-styled"
+ );
+
+ info("Click the heuristic result and observe it confirms search mode.");
+ let selectedButton = oneOffSearchButtons.selectedButton;
+ let expectedSearchMode = {
+ entry: "oneoff",
+ isPreview: true,
+ };
+ if (useLocal) {
+ expectedSearchMode.source = selectedButton.source;
+ } else {
+ expectedSearchMode.engineName = selectedButton.engine.name;
+ }
+
+ await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode);
+
+ let heuristicRow = await UrlbarTestUtils.waitForAutocompleteResultAt(
+ window,
+ 0
+ );
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeMouseAtCenter(heuristicRow, {});
+ await searchPromise;
+
+ expectedSearchMode.isPreview = false;
+ await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode);
+
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+}
diff --git a/browser/components/urlbar/tests/browser/browser_oneOffs_keyModifiers.js b/browser/components/urlbar/tests/browser/browser_oneOffs_keyModifiers.js
new file mode 100644
index 0000000000..30b241b3b3
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_oneOffs_keyModifiers.js
@@ -0,0 +1,392 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that one-offs behave differently with key modifiers.
+ */
+
+"use strict";
+
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+const SEARCH_STRING = "foo.bar";
+
+XPCOMUtils.defineLazyGetter(this, "oneOffSearchButtons", () => {
+ return UrlbarTestUtils.getOneOffSearchButtons(window);
+});
+
+let engine;
+
+async function searchAndOpenPopup(value) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value,
+ fireInputEvent: true,
+ });
+ await TestUtils.waitForCondition(
+ () => !oneOffSearchButtons._rebuilding,
+ "Waiting for one-offs to finish rebuilding"
+ );
+}
+
+add_setup(async function () {
+ // Add a search suggestion engine and move it to the front so that it appears
+ // as the first one-off.
+ engine = await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME,
+ });
+ await Services.search.moveEngine(engine, 0);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.search.separatePrivateDefault.ui.enabled", false],
+ ["browser.urlbar.suggest.quickactions", false],
+ ],
+ });
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.formHistory.clear();
+ });
+
+ // Initialize history with enough visits to fill up the view.
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.formHistory.clear();
+ let maxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults");
+ for (let i = 0; i < maxResults; i++) {
+ await PlacesTestUtils.addVisits(
+ "http://mochi.test:8888/browser_urlbarOneOffs.js/?" + i
+ );
+ }
+
+ // Add some more visits to the last URL added above so that the top-sites view
+ // will be non-empty.
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits(
+ "http://mochi.test:8888/browser_urlbarOneOffs.js/?" + (maxResults - 1)
+ );
+ }
+ await updateTopSites(sites => {
+ return (
+ sites && sites[0] && sites[0].url.startsWith("http://mochi.test:8888/")
+ );
+ });
+
+ // Move the mouse away from the view so that a result or one-off isn't
+ // inadvertently highlighted. See bug 1659011.
+ EventUtils.synthesizeMouse(
+ gURLBar.inputField,
+ 0,
+ 0,
+ { type: "mousemove" },
+ window
+ );
+});
+
+// Shift clicking with no search string should open search mode, like an
+// unmodified click.
+add_task(async function shift_click_empty() {
+ await searchAndOpenPopup("");
+ let oneOffs = oneOffSearchButtons.getSelectableButtons(true);
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeMouseAtCenter(oneOffs[0], { shiftKey: true });
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: oneOffs[0].engine.name,
+ entry: "oneoff",
+ });
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+// Shift clicking with a search string should perform a search in the current
+// tab.
+add_task(async function shift_click_search() {
+ await searchAndOpenPopup(SEARCH_STRING);
+ let resultsPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ "http://mochi.test:8888/?terms=foo.bar"
+ );
+ let oneOffs = oneOffSearchButtons.getSelectableButtons(true);
+ EventUtils.synthesizeMouseAtCenter(oneOffs[0], { shiftKey: true });
+ await resultsPromise;
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+// Pressing Shift+Enter on a one-off with no search string should open search
+// mode, like an unmodified click.
+add_task(async function shift_enter_empty() {
+ await searchAndOpenPopup("");
+ // Alt+Down to select the first one-off.
+ let oneOffs = oneOffSearchButtons.getSelectableButtons(true);
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_Enter", { shiftKey: true });
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: oneOffs[0].engine.name,
+ entry: "oneoff",
+ });
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+// Pressing Shift+Enter on a one-off with a search string should perform a
+// search in the current tab.
+add_task(async function shift_enter_search() {
+ await searchAndOpenPopup(SEARCH_STRING);
+ let resultsPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ "http://mochi.test:8888/?terms=foo.bar"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ EventUtils.synthesizeKey("KEY_Enter", { shiftKey: true });
+ await resultsPromise;
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+// Pressing Alt+Enter on a one-off on an "empty" page (e.g. new tab) should open
+// search mode in the current tab.
+add_task(async function alt_enter_emptypage() {
+ await BrowserTestUtils.withNewTab("about:home", async function (browser) {
+ await searchAndOpenPopup(SEARCH_STRING);
+ let oneOffs = oneOffSearchButtons.getSelectableButtons(true);
+ // Alt+Down to select the first one-off.
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_Enter", { altKey: true });
+ await searchPromise;
+ Assert.equal(
+ browser,
+ gBrowser.selectedBrowser,
+ "The foreground tab hasn't changed."
+ );
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: oneOffs[0].engine.name,
+ entry: "oneoff",
+ });
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+ });
+});
+
+// Pressing Alt+Enter on a one-off with no search string and on a "non-empty"
+// page should open search mode in a new foreground tab.
+add_task(async function alt_enter_empty() {
+ await BrowserTestUtils.withNewTab("about:robots", async function (browser) {
+ await searchAndOpenPopup("");
+ let oneOffs = oneOffSearchButtons.getSelectableButtons(true);
+ // Alt+Down to select the first one-off.
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ let tabOpenPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabOpen"
+ );
+ EventUtils.synthesizeKey("KEY_Enter", { altKey: true });
+ await tabOpenPromise;
+ Assert.notEqual(
+ browser,
+ gBrowser.selectedBrowser,
+ "The current foreground tab is new."
+ );
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: oneOffs[0].engine.name,
+ entry: "oneoff",
+ });
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ Assert.equal(
+ browser,
+ gBrowser.selectedBrowser,
+ "We're back in the original tab."
+ );
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ await UrlbarTestUtils.promisePopupClose(window);
+ });
+});
+
+// Pressing Alt+Enter on a remote one-off with a search string and on a
+// "non-empty" page should perform a search in a new foreground tab.
+add_task(async function alt_enter_search_remote() {
+ await BrowserTestUtils.withNewTab("about:robots", async function (browser) {
+ await searchAndOpenPopup(SEARCH_STRING);
+ // Alt+Down to select the first one-off.
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ let tabOpenPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "http://mochi.test:8888/?terms=foo.bar",
+ true
+ );
+ EventUtils.synthesizeKey("KEY_Enter", { altKey: true });
+ // This implictly checks the correct page is loaded.
+ let newTab = await tabOpenPromise;
+ Assert.equal(
+ newTab,
+ gBrowser.selectedTab,
+ "The current foreground tab is new."
+ );
+ // Check search mode is not activated in the new tab.
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ BrowserTestUtils.removeTab(newTab);
+ Assert.equal(
+ browser,
+ gBrowser.selectedBrowser,
+ "We're back in the original tab."
+ );
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ await UrlbarTestUtils.promisePopupClose(window);
+ });
+});
+
+// Pressing Alt+Enter on a local one-off with a search string and on a
+// "non-empty" page should open search mode in a new foreground tab with the
+// search string already populated.
+add_task(async function alt_enter_search_local() {
+ await BrowserTestUtils.withNewTab("about:robots", async function (browser) {
+ await searchAndOpenPopup(SEARCH_STRING);
+ // Alt+Down to select the first local one-off.
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ while (
+ oneOffSearchButtons.selectedButton.id !=
+ "urlbar-engine-one-off-item-bookmarks"
+ ) {
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ }
+ let tabOpenPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabOpen"
+ );
+ EventUtils.synthesizeKey("KEY_Enter", { altKey: true });
+ await tabOpenPromise;
+ Assert.notEqual(
+ browser,
+ gBrowser.selectedBrowser,
+ "The current foreground tab is new."
+ );
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ entry: "oneoff",
+ });
+ Assert.equal(
+ gURLBar.value,
+ SEARCH_STRING,
+ "The search term was duplicated to the new tab."
+ );
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ Assert.equal(
+ browser,
+ gBrowser.selectedBrowser,
+ "We're back in the original tab."
+ );
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ await UrlbarTestUtils.promisePopupClose(window);
+ });
+});
+
+// Accel+Clicking a one-off with an empty search string should open search mode
+// in a new background tab.
+add_task(async function accel_click_empty() {
+ await searchAndOpenPopup("");
+ let oneOffs = oneOffSearchButtons.getSelectableButtons(true);
+
+ // We have to listen for the new tab using this brute force method.
+ // about:newtab is preloaded in the background. When about:newtab is opened,
+ // the cached version is shown. Since the page is already loaded,
+ // waitForNewTab does not detect it. It also doesn't fire the TabOpen event.
+ let tabCount = gBrowser.tabs.length;
+ let tabOpenPromise = TestUtils.waitForCondition(
+ () =>
+ gBrowser.tabs.length == tabCount + 1
+ ? gBrowser.tabs[gBrowser.tabs.length - 1]
+ : false,
+ "Waiting for background about:newtab to open."
+ );
+ EventUtils.synthesizeMouseAtCenter(oneOffs[0], { accelKey: true });
+ let newTab = await tabOpenPromise;
+ Assert.notEqual(
+ newTab.linkedBrowser,
+ gBrowser.selectedBrowser,
+ "The foreground tab hasn't changed."
+ );
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ BrowserTestUtils.switchTab(gBrowser, newTab);
+ // Check the new background tab is already in search mode.
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: oneOffs[0].engine.name,
+ entry: "oneoff",
+ });
+ BrowserTestUtils.removeTab(newTab);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+// Accel+Clicking a remote one-off with a search string should execute a search
+// in a new background tab.
+add_task(async function accel_click_search_remote() {
+ await searchAndOpenPopup(SEARCH_STRING);
+ let oneOffs = oneOffSearchButtons.getSelectableButtons(true);
+ let tabOpenPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "http://mochi.test:8888/?terms=foo.bar",
+ true
+ );
+ EventUtils.synthesizeMouseAtCenter(oneOffs[0], { accelKey: true });
+ // This implictly checks the correct page is loaded.
+ let newTab = await tabOpenPromise;
+ Assert.notEqual(
+ gBrowser.selectedTab,
+ newTab,
+ "The foreground tab hasn't changed."
+ );
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ // Switch to the background tab, which is the last tab in gBrowser.tabs.
+ BrowserTestUtils.switchTab(gBrowser, newTab);
+ // Check the new background tab is not search mode.
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ BrowserTestUtils.removeTab(newTab);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+// Accel+Clicking a local one-off with a search string should open search mode
+// in a new background tab with the search string already populated.
+add_task(async function accel_click_search_local() {
+ await searchAndOpenPopup(SEARCH_STRING);
+ let oneOffs = oneOffSearchButtons.getSelectableButtons(true);
+ let oneOff;
+ for (oneOff of oneOffs) {
+ if (oneOff.id == "urlbar-engine-one-off-item-bookmarks") {
+ break;
+ }
+ }
+ let tabCount = gBrowser.tabs.length;
+ let tabOpenPromise = TestUtils.waitForCondition(
+ () =>
+ gBrowser.tabs.length == tabCount + 1
+ ? gBrowser.tabs[gBrowser.tabs.length - 1]
+ : false,
+ "Waiting for background about:newtab to open."
+ );
+ EventUtils.synthesizeMouseAtCenter(oneOff, { accelKey: true });
+ let newTab = await tabOpenPromise;
+ Assert.notEqual(
+ newTab.linkedBrowser,
+ gBrowser.selectedBrowser,
+ "The foreground tab hasn't changed."
+ );
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ BrowserTestUtils.switchTab(gBrowser, newTab);
+ // Check the new background tab is already in search mode.
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ entry: "oneoff",
+ });
+ // Check the search string is already populated.
+ Assert.equal(
+ gURLBar.value,
+ SEARCH_STRING,
+ "The search term was duplicated to the new tab."
+ );
+ BrowserTestUtils.removeTab(newTab);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_oneOffs_searchSuggestions.js b/browser/components/urlbar/tests/browser/browser_oneOffs_searchSuggestions.js
new file mode 100644
index 0000000000..ef324b08cd
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_oneOffs_searchSuggestions.js
@@ -0,0 +1,358 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests various actions relating to search suggestions and the one-off buttons.
+ */
+
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+const TEST_ENGINE2_BASENAME = "searchSuggestionEngine2.xml";
+
+const serverInfo = {
+ scheme: "http",
+ host: "localhost",
+ port: 20709, // Must be identical to what is in searchSuggestionEngine2.xml
+};
+
+var gEngine;
+var gEngine2;
+
+add_setup(async function () {
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.formHistory.clear();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.searches", true],
+ ["browser.urlbar.maxHistoricalSearchSuggestions", 2],
+ ],
+ });
+ gEngine = await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME,
+ });
+ gEngine2 = await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + TEST_ENGINE2_BASENAME,
+ });
+ let oldDefaultEngine = await Services.search.getDefault();
+ await Services.search.moveEngine(gEngine2, 0);
+ await Services.search.moveEngine(gEngine, 0);
+ await Services.search.setDefault(
+ gEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ registerCleanupFunction(async function () {
+ await Services.search.setDefault(
+ oldDefaultEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.formHistory.clear();
+ });
+});
+
+async function withSuggestions(testFn) {
+ // First run with remote suggestions, and then run with form history.
+ await withSuggestionOnce(false, testFn);
+ await withSuggestionOnce(true, testFn);
+}
+
+async function withSuggestionOnce(useFormHistory, testFn) {
+ if (useFormHistory) {
+ // Add foofoo twice so it's more frecent so it appears first so that the
+ // order of form history results matches the order of remote suggestion
+ // results.
+ await UrlbarTestUtils.formHistory.add(["foofoo", "foofoo", "foobar"]);
+ }
+ await BrowserTestUtils.withNewTab(gBrowser, async () => {
+ let value = "foo";
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value,
+ fireInputEvent: true,
+ });
+ let index = await UrlbarTestUtils.promiseSuggestionsPresent(window);
+ await assertState({
+ inputValue: value,
+ resultIndex: 0,
+ });
+ await withHttpServer(serverInfo, () => {
+ return testFn(index, useFormHistory);
+ });
+ });
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.formHistory.clear();
+}
+
+async function selectSecondSuggestion(index, isFormHistory) {
+ // Down to select the first search suggestion.
+ for (let i = index; i > 0; --i) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+ await assertState({
+ inputValue: "foofoo",
+ resultIndex: index,
+ suggestion: {
+ isFormHistory,
+ },
+ });
+
+ // Down to select the next search suggestion.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await assertState({
+ inputValue: "foobar",
+ resultIndex: index + 1,
+ suggestion: {
+ isFormHistory,
+ },
+ });
+}
+
+// Presses the Return key when a one-off is selected after selecting a search
+// suggestion.
+add_task(async function test_returnAfterSuggestion() {
+ await withSuggestions(async (index, usingFormHistory) => {
+ await selectSecondSuggestion(index, usingFormHistory);
+
+ // Alt+Down to select the first one-off.
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ await assertState({
+ inputValue: "foobar",
+ resultIndex: index + 1,
+ oneOffIndex: 0,
+ suggestion: {
+ isFormHistory: usingFormHistory,
+ },
+ });
+
+ let heuristicResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(
+ !BrowserTestUtils.is_visible(heuristicResult.element.action),
+ "The heuristic action should not be visible"
+ );
+
+ let resultsPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await resultsPromise;
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: gEngine.name,
+ entry: "oneoff",
+ });
+ await UrlbarTestUtils.exitSearchMode(window, { backspace: true });
+ });
+});
+
+// Presses the Return key when a non-default one-off is selected after selecting
+// a search suggestion.
+add_task(async function test_returnAfterSuggestion_nonDefault() {
+ await withSuggestions(async (index, usingFormHistory) => {
+ await selectSecondSuggestion(index, usingFormHistory);
+
+ // Alt+Down twice to select the second one-off.
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ await assertState({
+ inputValue: "foobar",
+ resultIndex: index + 1,
+ oneOffIndex: 1,
+ suggestion: {
+ isFormHistory: usingFormHistory,
+ },
+ });
+
+ let resultsPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await resultsPromise;
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: gEngine2.name,
+ entry: "oneoff",
+ });
+ await UrlbarTestUtils.exitSearchMode(window, { backspace: true });
+ });
+});
+
+// Clicks a one-off engine after selecting a search suggestion.
+add_task(async function test_clickAfterSuggestion() {
+ await withSuggestions(async (index, usingFormHistory) => {
+ await selectSecondSuggestion(index, usingFormHistory);
+
+ let oneOffs =
+ UrlbarTestUtils.getOneOffSearchButtons(window).getSelectableButtons(true);
+ let resultsPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeMouseAtCenter(oneOffs[1], {});
+ await resultsPromise;
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: gEngine2.name,
+ entry: "oneoff",
+ });
+ await UrlbarTestUtils.exitSearchMode(window, { backspace: true });
+ });
+});
+
+// Clicks a non-default one-off engine after selecting a search suggestion.
+add_task(async function test_clickAfterSuggestion_nonDefault() {
+ await withSuggestions(async (index, usingFormHistory) => {
+ await selectSecondSuggestion(index, usingFormHistory);
+
+ let oneOffs =
+ UrlbarTestUtils.getOneOffSearchButtons(window).getSelectableButtons(true);
+ let resultsPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeMouseAtCenter(oneOffs[1], {});
+ await resultsPromise;
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: gEngine2.name,
+ entry: "oneoff",
+ });
+ await UrlbarTestUtils.exitSearchMode(window, { backspace: true });
+ });
+});
+
+// Selects a non-default one-off engine and then clicks a search suggestion.
+add_task(async function test_selectOneOffThenSuggestion() {
+ await withSuggestions(async (index, usingFormHistory) => {
+ // Select a non-default one-off engine.
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ await assertState({
+ inputValue: "foo",
+ resultIndex: 0,
+ oneOffIndex: 1,
+ });
+
+ let heuristicResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(
+ BrowserTestUtils.is_visible(heuristicResult.element.action),
+ "The heuristic action should be visible because the result is selected"
+ );
+
+ // Now click the second suggestion.
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index + 1);
+ // Note search history results don't change their engine when the selected
+ // one-off button changes!
+ let resultsPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ usingFormHistory
+ ? `http://mochi.test:8888/?terms=foobar`
+ : `http://localhost:20709/?terms=foobar`
+ );
+ EventUtils.synthesizeMouseAtCenter(result.element.row, {});
+ await resultsPromise;
+ });
+});
+
+add_task(async function overridden_engine_not_reused() {
+ info(
+ "An overridden search suggestion item should not be reused by a search with another engine"
+ );
+ await BrowserTestUtils.withNewTab(gBrowser, async () => {
+ let typedValue = "foo";
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: typedValue,
+ fireInputEvent: true,
+ });
+ let index = await UrlbarTestUtils.promiseSuggestionsPresent(window);
+ // Down to select the first search suggestion.
+ for (let i = index; i > 0; --i) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+ await assertState({
+ inputValue: "foofoo",
+ resultIndex: index,
+ suggestion: {
+ isFormHistory: false,
+ },
+ });
+
+ // ALT+Down to select the second search engine.
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ await assertState({
+ inputValue: "foofoo",
+ resultIndex: index,
+ oneOffIndex: 1,
+ suggestion: {
+ isFormHistory: false,
+ },
+ });
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ let label = result.displayed.action;
+ // Run again the query, check the label has been replaced.
+ await UrlbarTestUtils.promisePopupClose(window);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: typedValue,
+ fireInputEvent: true,
+ });
+ index = await UrlbarTestUtils.promiseSuggestionsPresent(window);
+ await assertState({
+ inputValue: "foo",
+ resultIndex: 0,
+ });
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, index);
+ Assert.notEqual(
+ result.displayed.action,
+ label,
+ "The label should have been updated"
+ );
+ });
+});
+
+async function assertState({
+ resultIndex,
+ inputValue,
+ oneOffIndex = -1,
+ suggestion = null,
+}) {
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ resultIndex,
+ "Expected result should be selected"
+ );
+ Assert.equal(
+ UrlbarTestUtils.getOneOffSearchButtons(window).selectedButtonIndex,
+ oneOffIndex,
+ "Expected one-off should be selected"
+ );
+ if (inputValue !== undefined) {
+ Assert.equal(gURLBar.value, inputValue, "Expected input value");
+ }
+
+ if (suggestion) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ resultIndex
+ );
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "Result type should be SEARCH"
+ );
+ if (suggestion.isFormHistory) {
+ Assert.equal(
+ result.source,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ "Result source should be HISTORY"
+ );
+ } else {
+ Assert.equal(
+ result.source,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ "Result source should be SEARCH"
+ );
+ }
+ Assert.equal(
+ typeof result.searchParams.suggestion,
+ "string",
+ "Result should have a suggestion"
+ );
+ Assert.equal(
+ result.searchParams.suggestion,
+ suggestion.value || inputValue,
+ "Result should have the expected suggestion"
+ );
+ }
+}
diff --git a/browser/components/urlbar/tests/browser/browser_oneOffs_settings.js b/browser/components/urlbar/tests/browser/browser_oneOffs_settings.js
new file mode 100644
index 0000000000..b4b1e7006e
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_oneOffs_settings.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This tests that the settings button in the one-off buttons display correctly
+ * loads the search preferences.
+ */
+
+let gMaxResults;
+
+add_setup(async function () {
+ gMaxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults");
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.formHistory.clear();
+ });
+
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.formHistory.clear();
+
+ let visits = [];
+ for (let i = 0; i < gMaxResults; i++) {
+ visits.push({
+ uri: makeURI("http://example.com/browser_urlbarOneOffs.js/?" + i),
+ // TYPED so that the visit shows up when the urlbar's drop-down arrow is
+ // pressed.
+ transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ });
+ }
+ await PlacesTestUtils.addVisits(visits);
+});
+
+async function selectSettings(win, activateFn) {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser: win.gBrowser, url: "about:blank" },
+ async browser => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "example.com",
+ });
+ await UrlbarTestUtils.waitForAutocompleteResultAt(win, gMaxResults - 1);
+
+ await UrlbarTestUtils.promisePopupClose(win, async () => {
+ let prefPaneLoaded = TestUtils.topicObserved(
+ "sync-pane-loaded",
+ () => true
+ );
+
+ activateFn();
+
+ await prefPaneLoaded;
+ });
+
+ Assert.equal(
+ win.gBrowser.contentWindow.history.state,
+ "paneSearch",
+ "Should have opened the search preferences pane"
+ );
+ }
+ );
+}
+
+add_task(async function test_open_settings_with_enter() {
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+ await selectSettings(win, () => {
+ EventUtils.synthesizeKey("KEY_ArrowUp", {}, win);
+
+ Assert.ok(
+ UrlbarTestUtils.getOneOffSearchButtons(
+ win
+ ).selectedButton.classList.contains("search-setting-button"),
+ "Should have selected the settings button"
+ );
+
+ EventUtils.synthesizeKey("KEY_Enter", {}, win);
+ });
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_open_settings_with_click() {
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+ await selectSettings(win, () => {
+ UrlbarTestUtils.getOneOffSearchButtons(win).settingsButton.click();
+ });
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_pasteAndGo.js b/browser/components/urlbar/tests/browser/browser_pasteAndGo.js
new file mode 100644
index 0000000000..8d2a27afc3
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_pasteAndGo.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests for the paste and go functionality of the urlbar.
+ */
+
+add_task(async function () {
+ const kURLs = [
+ "http://example.com/1",
+ "http://example.org/2\n",
+ "http://\nexample.com/3\n",
+ ];
+ for (let url of kURLs) {
+ await BrowserTestUtils.withNewTab("about:blank", async function (browser) {
+ gURLBar.focus();
+
+ await SimpleTest.promiseClipboardChange(url, () => {
+ clipboardHelper.copyString(url);
+ });
+ let menuitem = await promiseContextualMenuitem("paste-and-go");
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ url.replace(/\n/g, "")
+ );
+ menuitem.closest("menupopup").activateItem(menuitem);
+ // Using toSource in order to get the newlines escaped:
+ info("Paste and go, loading " + url.toSource());
+ await browserLoadedPromise;
+ ok(true, "Successfully loaded " + url);
+ });
+ }
+});
+
+add_task(async function test_invisible_char() {
+ const url = "http://example.com/4\u2028";
+ await BrowserTestUtils.withNewTab("about:blank", async function (browser) {
+ gURLBar.focus();
+ await SimpleTest.promiseClipboardChange(url, () => {
+ clipboardHelper.copyString(url);
+ });
+ let menuitem = await promiseContextualMenuitem("paste-and-go");
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ url.replace(/\u2028/g, "")
+ );
+ menuitem.closest("menupopup").activateItem(menuitem);
+ // Using toSource in order to get the newlines escaped:
+ info("Paste and go, loading " + url.toSource());
+ await browserLoadedPromise;
+ ok(true, "Successfully loaded " + url);
+ });
+});
+
+add_task(async function test_with_input_and_results() {
+ // Test paste and go When there's some input and the results pane is open.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ });
+ const url = "http://example.com/";
+ await SimpleTest.promiseClipboardChange(url, () => {
+ clipboardHelper.copyString(url);
+ });
+ let menuitem = await promiseContextualMenuitem("paste-and-go");
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ url
+ );
+ menuitem.closest("menupopup").activateItem(menuitem);
+ // Using toSource in order to get the newlines escaped:
+ info("Paste and go, loading " + url.toSource());
+ await browserLoadedPromise;
+ ok(true, "Successfully loaded " + url);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_paste_multi_lines.js b/browser/components/urlbar/tests/browser/browser_paste_multi_lines.js
new file mode 100644
index 0000000000..8c4be18a7b
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_paste_multi_lines.js
@@ -0,0 +1,239 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test handling whitespace chars such as "\n”.
+
+const TEST_DATA = [
+ {
+ input: "this is a\ntest",
+ expected: {
+ urlbar: "this is a test",
+ autocomplete: "this is a test",
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ },
+ },
+ {
+ input: "this is a\n\ttest",
+ expected: {
+ urlbar: "this is a test",
+ autocomplete: "this is a test",
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ },
+ },
+ {
+ input: "http:\n//\nexample.\ncom",
+ expected: {
+ urlbar: "http://example.com",
+ autocomplete: "http://example.com/",
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ },
+ },
+ {
+ input: "htp:example.\ncom",
+ expected: {
+ urlbar: "htp:example.com",
+ autocomplete: "http://example.com/",
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ },
+ },
+ {
+ input: "example.\ncom",
+ expected: {
+ urlbar: "example.com",
+ autocomplete: "http://example.com/",
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ },
+ },
+ {
+ input: "http://example.com/foo bar/",
+ expected: {
+ urlbar: "http://example.com/foo bar/",
+ autocomplete: "http://example.com/foo bar/",
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ },
+ },
+ {
+ input: "http://exam\nple.com/foo bar/",
+ expected: {
+ urlbar: "http://example.com/foo bar/",
+ autocomplete: "http://example.com/foo bar/",
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ },
+ },
+ {
+ input: "javasc\nript:\nalert(1)",
+ expected: {
+ urlbar: "alert(1)",
+ autocomplete: "alert(1)",
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ },
+ },
+ {
+ input: "a\nb\nc",
+ expected: {
+ urlbar: "a b c",
+ autocomplete: "a b c",
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ },
+ },
+ {
+ input: "lo\ncal\nhost",
+ expected: {
+ urlbar: "localhost",
+ autocomplete: "http://localhost/",
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ },
+ },
+ {
+ input: "data:text/html,<iframe\n src='example\n.com'>\n</iframe>",
+ expected: {
+ urlbar: "data:text/html,<iframe src='example .com'> </iframe>",
+ autocomplete: "data:text/html,<iframe src='example .com'> </iframe>",
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ },
+ },
+ {
+ input: "data:,123\n4 5\n6",
+ expected: {
+ urlbar: "data:,123 4 5 6",
+ autocomplete: "data:,123 4 5 6",
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ },
+ },
+ {
+ input: "data:text/html;base64,123\n4 5\n6",
+ expected: {
+ urlbar: "data:text/html;base64,1234 56",
+ autocomplete: "data:text/html;base64,123456",
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ },
+ },
+ {
+ input: "http://example.com\n",
+ expected: {
+ urlbar: "http://example.com",
+ autocomplete: "http://example.com/",
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ },
+ },
+ {
+ input: "http://example.com\r",
+ expected: {
+ urlbar: "http://example.com",
+ autocomplete: "http://example.com/",
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ },
+ },
+ {
+ input: "http://ex\ra\nmp\r\nle.com\r\n",
+ expected: {
+ urlbar: "http://example.com",
+ autocomplete: "http://example.com/",
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ },
+ },
+ {
+ input: "http://example.com/titled",
+ expected: {
+ urlbar: "http://example.com/titled",
+ autocomplete: "example title",
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ },
+ },
+ {
+ input: "127.0.0.1\r",
+ expected: {
+ urlbar: "127.0.0.1",
+ autocomplete: "http://127.0.0.1/",
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ },
+ },
+ {
+ input: "\r\n\r\n\r\n\r\n\r\n",
+ expected: {
+ urlbar: "",
+ autocomplete: "",
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ },
+ },
+];
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // There are cases that URLBar loses focus before assertion of this test.
+ // In that case, this test will be failed since the result is closed
+ // before it. We use this pref so that keep the result even if lose focus.
+ ["ui.popup.disable_autohide", true],
+ ],
+ });
+
+ await PlacesUtils.history.clear();
+ await PlacesTestUtils.addVisits({
+ uri: "http://example.com/titled",
+ title: "example title",
+ });
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.history.clear();
+ SpecialPowers.clipboardCopyString("");
+ });
+});
+
+add_task(async function test_paste_onto_urlbar() {
+ for (const { input, expected } of TEST_DATA) {
+ gURLBar.value = "";
+ gURLBar.focus();
+
+ await paste(input);
+ await assertResult(expected);
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ }
+});
+
+add_task(async function test_paste_after_opening_autocomplete_panel() {
+ for (const { input, expected } of TEST_DATA) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+
+ await paste(input);
+ await assertResult(expected);
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ }
+});
+
+async function assertResult(expected) {
+ Assert.equal(gURLBar.value, expected.urlbar, "Pasted value is correct");
+
+ const result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ result.title,
+ expected.autocomplete,
+ "Title of autocomplete is correct"
+ );
+ Assert.equal(result.type, expected.type, "Type of autocomplete is correct");
+
+ if (gURLBar.value) {
+ Assert.equal(gURLBar.getAttribute("usertyping"), "true");
+ Assert.ok(BrowserTestUtils.is_visible(gURLBar.goButton));
+ } else {
+ Assert.ok(!gURLBar.hasAttribute("usertyping"));
+ Assert.ok(BrowserTestUtils.is_hidden(gURLBar.goButton));
+ }
+}
+
+async function paste(input) {
+ await SimpleTest.promiseClipboardChange(input.replace(/\r\n?/g, "\n"), () => {
+ clipboardHelper.copyString(input);
+ });
+
+ document.commandDispatcher
+ .getControllerForCommand("cmd_paste")
+ .doCommand("cmd_paste");
+}
diff --git a/browser/components/urlbar/tests/browser/browser_paste_then_focus.js b/browser/components/urlbar/tests/browser/browser_paste_then_focus.js
new file mode 100644
index 0000000000..23d603fd80
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_paste_then_focus.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the urlbar value when focusing after pasting value.
+
+const TEST_DATA = [
+ {
+ input: "this is a\ntest",
+ expected: "this is a test",
+ },
+ {
+ input: "http:\n//\nexample.\ncom",
+ expected: "http://example.com",
+ },
+ {
+ input: "javasc\nript:\nalert(1)",
+ expected: "alert(1)",
+ },
+ {
+ input: "javascript:alert(1)",
+ expected: "alert(1)",
+ },
+ {
+ input: "test",
+ expected: "test",
+ },
+];
+
+add_task(async function test_paste_then_focus() {
+ for (const testData of TEST_DATA) {
+ gURLBar.value = "";
+ gURLBar.focus();
+
+ EventUtils.synthesizeKey("x");
+ gURLBar.select();
+
+ await paste(testData.input);
+
+ gURLBar.blur();
+ gURLBar.focus();
+
+ Assert.equal(
+ gURLBar.value,
+ testData.expected,
+ "Value on urlbar is correct"
+ );
+ }
+});
+
+async function paste(input) {
+ await SimpleTest.promiseClipboardChange(input, () => {
+ clipboardHelper.copyString(input);
+ });
+
+ document.commandDispatcher
+ .getControllerForCommand("cmd_paste")
+ .doCommand("cmd_paste");
+}
diff --git a/browser/components/urlbar/tests/browser/browser_paste_then_switch_tab.js b/browser/components/urlbar/tests/browser/browser_paste_then_switch_tab.js
new file mode 100644
index 0000000000..883b128c60
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_paste_then_switch_tab.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the urlbar value when switching tab after pasting value.
+
+const TEST_DATA = [
+ {
+ input: "this is a\ntest",
+ expected: "this is a test",
+ },
+ {
+ input: "https:\n//\nexample.\ncom",
+ expected: "https://example.com",
+ },
+ {
+ input: "http:\n//\nexample.\ncom",
+ expected: "example.com",
+ },
+ {
+ input: "javasc\nript:\nalert(1)",
+ expected: "alert(1)",
+ },
+ {
+ input: "javascript:alert(1)",
+ expected: "alert(1)",
+ },
+ {
+ // Has U+3000 IDEOGRAPHIC SPACE.
+ input: "Mozilla Firefox",
+ expected: "Mozilla Firefox",
+ },
+ {
+ input: "test",
+ expected: "test",
+ },
+];
+
+add_task(async function test_paste_then_switch_tab() {
+ for (const testData of TEST_DATA) {
+ gURLBar.focus();
+ gURLBar.select();
+
+ await paste(testData.input);
+
+ // Switch to a new tab.
+ const originalTab = gBrowser.selectedTab;
+ const newTab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ await BrowserTestUtils.waitForCondition(() => !gURLBar.value);
+
+ // Switch back to original tab.
+ gBrowser.selectedTab = originalTab;
+
+ Assert.equal(
+ gURLBar.value,
+ testData.expected,
+ "Value on urlbar is correct"
+ );
+
+ BrowserTestUtils.removeTab(newTab);
+ }
+});
+
+async function paste(input) {
+ await SimpleTest.promiseClipboardChange(input, () => {
+ clipboardHelper.copyString(input);
+ });
+
+ document.commandDispatcher
+ .getControllerForCommand("cmd_paste")
+ .doCommand("cmd_paste");
+}
diff --git a/browser/components/urlbar/tests/browser/browser_percent_encoded.js b/browser/components/urlbar/tests/browser/browser_percent_encoded.js
new file mode 100644
index 0000000000..c334c03a09
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_percent_encoded.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that searching history works for both encoded or decoded strings.
+
+add_task(async function test() {
+ const decoded = "日本";
+ const TEST_URL = TEST_BASE_URL + "?" + decoded;
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ });
+
+ // Visit url in a new tab, going through normal urlbar workflow.
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+ let promise = PlacesTestUtils.waitForNotification("page-visited", visits => {
+ Assert.equal(
+ visits.length,
+ 1,
+ "Was notified for the right number of visits."
+ );
+ let { url, transitionType } = visits[0];
+ return (
+ url == encodeURI(TEST_URL) &&
+ transitionType == PlacesUtils.history.TRANSITIONS.TYPED
+ );
+ });
+ gURLBar.focus();
+ gURLBar.value = TEST_URL;
+ info("Visiting url");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await promise;
+ gBrowser.removeCurrentTab({ skipPermitUnload: true });
+
+ info("Search for the decoded string.");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: decoded,
+ });
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 2,
+ "Check number of results"
+ );
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.url, encodeURI(TEST_URL), "Check result url");
+
+ info("Search for the encoded string.");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: encodeURIComponent(decoded),
+ });
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 2,
+ "Check number of results"
+ );
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.url, encodeURI(TEST_URL), "Check result url");
+});
diff --git a/browser/components/urlbar/tests/browser/browser_placeholder.js b/browser/components/urlbar/tests/browser/browser_placeholder.js
new file mode 100644
index 0000000000..fbf0a5007c
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_placeholder.js
@@ -0,0 +1,412 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This test ensures the placeholder is set correctly for different search
+ * engines.
+ */
+
+"use strict";
+
+var originalEngine, extraEngine, extraPrivateEngine, expectedString;
+var tabs = [];
+
+var noEngineString;
+
+add_setup(async function () {
+ originalEngine = await Services.search.getDefault();
+ [noEngineString, expectedString] = (
+ await document.l10n.formatMessages([
+ { id: "urlbar-placeholder" },
+ {
+ id: "urlbar-placeholder-with-name",
+ args: { name: originalEngine.name },
+ },
+ ])
+ ).map(msg => msg.attributes[0].value);
+
+ let rootUrl = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://mochi.test:8888/"
+ );
+ await SearchTestUtils.installSearchExtension({
+ name: "extraEngine",
+ search_url: "https://mochi.test:8888/",
+ suggest_url: `${rootUrl}/searchSuggestionEngine.sjs`,
+ });
+ extraEngine = Services.search.getEngineByName("extraEngine");
+ await SearchTestUtils.installSearchExtension({
+ name: "extraPrivateEngine",
+ search_url: "https://mochi.test:8888/",
+ suggest_url: `${rootUrl}/searchSuggestionEngine.sjs`,
+ });
+ extraPrivateEngine = Services.search.getEngineByName("extraPrivateEngine");
+
+ // Force display of a tab with a URL bar, to clear out any possible placeholder
+ // initialization listeners that happen on startup.
+ let urlTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:mozilla"
+ );
+ BrowserTestUtils.removeTab(urlTab);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.search.separatePrivateDefault.ui.enabled", true],
+ ["browser.search.separatePrivateDefault", false],
+ ["browser.urlbar.suggest.quickactions", false],
+ ],
+ });
+
+ registerCleanupFunction(async () => {
+ await Services.search.setDefault(
+ originalEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ for (let tab of tabs) {
+ BrowserTestUtils.removeTab(tab);
+ }
+ });
+});
+
+add_task(async function test_change_default_engine_updates_placeholder() {
+ tabs.push(await BrowserTestUtils.openNewForegroundTab(gBrowser));
+
+ await Services.search.setDefault(
+ extraEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+
+ await TestUtils.waitForCondition(
+ () => gURLBar.placeholder == noEngineString,
+ "The placeholder should match the default placeholder for non-built-in engines."
+ );
+ Assert.equal(gURLBar.placeholder, noEngineString);
+
+ await Services.search.setDefault(
+ originalEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+
+ await TestUtils.waitForCondition(
+ () => gURLBar.placeholder == expectedString,
+ "The placeholder should include the engine name for built-in engines."
+ );
+ Assert.equal(gURLBar.placeholder, expectedString);
+});
+
+add_task(async function test_delayed_update_placeholder() {
+ // We remove the change of engine listener here as that is set so that
+ // if the engine is changed by the user then the placeholder is always updated
+ // straight away. As we want to test the delay update here, we remove the
+ // listener and call the placeholder update manually with the delay flag.
+ Services.obs.removeObserver(BrowserSearch, "browser-search-engine-modified");
+
+ // Since we can't easily test for startup changes, we'll at least test the delay
+ // of update for the placeholder works.
+ let urlTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:mozilla"
+ );
+ tabs.push(urlTab);
+
+ // Open a tab with a blank URL bar.
+ let blankTab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ tabs.push(blankTab);
+
+ await Services.search.setDefault(
+ extraEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ // Pretend we've "initialized".
+ BrowserSearch._updateURLBarPlaceholder(extraEngine.name, false, true);
+
+ Assert.equal(
+ gURLBar.placeholder,
+ expectedString,
+ "Placeholder should be unchanged."
+ );
+
+ // Now switch to a tab with something in the URL Bar.
+ await BrowserTestUtils.switchTab(gBrowser, urlTab);
+
+ await TestUtils.waitForCondition(
+ () => gURLBar.placeholder == noEngineString,
+ "The placeholder should have updated in the background."
+ );
+
+ // Do it the other way to check both named engine and fallback code paths.
+ await BrowserTestUtils.switchTab(gBrowser, blankTab);
+
+ await Services.search.setDefault(
+ originalEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ BrowserSearch._updateURLBarPlaceholder(originalEngine.name, false, true);
+
+ Assert.equal(
+ gURLBar.placeholder,
+ noEngineString,
+ "Placeholder should be unchanged."
+ );
+ Assert.deepEqual(
+ document.l10n.getAttributes(gURLBar.inputField),
+ { id: "urlbar-placeholder", args: null },
+ "Placeholder data should be unchanged."
+ );
+
+ await BrowserTestUtils.switchTab(gBrowser, urlTab);
+
+ await TestUtils.waitForCondition(
+ () => gURLBar.placeholder == expectedString,
+ "The placeholder should include the engine name for built-in engines."
+ );
+
+ // Now check when we have a URL displayed, the placeholder is updated straight away.
+ BrowserSearch._updateURLBarPlaceholder(extraEngine.name, false);
+
+ await TestUtils.waitForCondition(
+ () => gURLBar.placeholder == noEngineString,
+ "The placeholder should go back to the default"
+ );
+ Assert.equal(
+ gURLBar.placeholder,
+ noEngineString,
+ "Placeholder should be the default."
+ );
+
+ Services.obs.addObserver(BrowserSearch, "browser-search-engine-modified");
+});
+
+add_task(async function test_private_window_no_separate_engine() {
+ const win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+
+ await Services.search.setDefault(
+ extraEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+
+ await TestUtils.waitForCondition(
+ () => win.gURLBar.placeholder == noEngineString,
+ "The placeholder should match the default placeholder for non-built-in engines."
+ );
+ Assert.equal(win.gURLBar.placeholder, noEngineString);
+
+ await Services.search.setDefault(
+ originalEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+
+ await TestUtils.waitForCondition(
+ () => win.gURLBar.placeholder == expectedString,
+ "The placeholder should include the engine name for built-in engines."
+ );
+ Assert.equal(win.gURLBar.placeholder, expectedString);
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_private_window_separate_engine() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.separatePrivateDefault", true]],
+ });
+ const originalPrivateEngine = await Services.search.getDefaultPrivate();
+ registerCleanupFunction(async () => {
+ await Services.search.setDefaultPrivate(
+ originalPrivateEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ });
+
+ const win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+
+ // Keep the normal default as a different string to the private, so that we
+ // can be sure we're testing the right thing.
+ await Services.search.setDefault(
+ originalEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ await Services.search.setDefaultPrivate(
+ extraPrivateEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+
+ await TestUtils.waitForCondition(
+ () => win.gURLBar.placeholder == noEngineString,
+ "The placeholder should match the default placeholder for non-built-in engines."
+ );
+ Assert.equal(win.gURLBar.placeholder, noEngineString);
+
+ await Services.search.setDefault(
+ extraEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ await Services.search.setDefaultPrivate(
+ originalEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+
+ await TestUtils.waitForCondition(
+ () => win.gURLBar.placeholder == expectedString,
+ "The placeholder should include the engine name for built-in engines."
+ );
+ Assert.equal(win.gURLBar.placeholder, expectedString);
+
+ await BrowserTestUtils.closeWindow(win);
+
+ // Verify that the placeholder for private windows is updated even when no
+ // private window is visible (https://bugzilla.mozilla.org/1792816).
+ await Services.search.setDefault(
+ originalEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ await Services.search.setDefaultPrivate(
+ extraPrivateEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ const win2 = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+ Assert.equal(win2.gURLBar.placeholder, noEngineString);
+ await BrowserTestUtils.closeWindow(win2);
+
+ // And ensure this doesn't affect the placeholder for non private windows.
+ tabs.push(await BrowserTestUtils.openNewForegroundTab(gBrowser));
+ Assert.equal(win.gURLBar.placeholder, expectedString);
+});
+
+add_task(async function test_search_mode_engine_web() {
+ // Add our test engine to WEB_ENGINE_NAMES so that it's recognized as a web
+ // engine.
+ SearchUtils.GENERAL_SEARCH_ENGINE_IDS.add(
+ extraEngine.wrappedJSObject._extensionID
+ );
+
+ await doSearchModeTest(
+ {
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ engineName: extraEngine.name,
+ },
+ {
+ id: "urlbar-placeholder-search-mode-web-2",
+ args: { name: extraEngine.name },
+ }
+ );
+
+ SearchUtils.GENERAL_SEARCH_ENGINE_IDS.delete(
+ extraEngine.wrappedJSObject._extensionID
+ );
+});
+
+add_task(async function test_search_mode_engine_other() {
+ await doSearchModeTest(
+ { engineName: extraEngine.name },
+ {
+ id: "urlbar-placeholder-search-mode-other-engine",
+ args: { name: extraEngine.name },
+ }
+ );
+});
+
+add_task(async function test_search_mode_bookmarks() {
+ await doSearchModeTest(
+ { source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS },
+ { id: "urlbar-placeholder-search-mode-other-bookmarks", args: null }
+ );
+});
+
+add_task(async function test_search_mode_tabs() {
+ await doSearchModeTest(
+ { source: UrlbarUtils.RESULT_SOURCE.TABS },
+ { id: "urlbar-placeholder-search-mode-other-tabs", args: null }
+ );
+});
+
+add_task(async function test_search_mode_history() {
+ await doSearchModeTest(
+ { source: UrlbarUtils.RESULT_SOURCE.HISTORY },
+ { id: "urlbar-placeholder-search-mode-other-history", args: null }
+ );
+});
+
+add_task(async function test_change_default_engine_updates_placeholder() {
+ tabs.push(await BrowserTestUtils.openNewForegroundTab(gBrowser));
+
+ info(`Set engine to ${extraEngine.name}`);
+ await Services.search.setDefault(
+ extraEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ await TestUtils.waitForCondition(
+ () => gURLBar.placeholder == noEngineString,
+ "The placeholder should match the default placeholder for non-built-in engines."
+ );
+ Assert.equal(gURLBar.placeholder, noEngineString);
+
+ info(`Set engine to ${originalEngine.name}`);
+ await Services.search.setDefault(
+ originalEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ await TestUtils.waitForCondition(
+ () => gURLBar.placeholder == expectedString,
+ "The placeholder should include the engine name for built-in engines."
+ );
+
+ // Simulate the placeholder not having changed due to the delayed update
+ // on startup.
+ BrowserSearch._setURLBarPlaceholder("");
+ await TestUtils.waitForCondition(
+ () => gURLBar.placeholder == noEngineString,
+ "The placeholder should have been reset."
+ );
+
+ info("Show search engine removal info bar");
+ BrowserSearch.removalOfSearchEngineNotificationBox(
+ extraEngine.name,
+ originalEngine.name
+ );
+ const notificationBox = gNotificationBox.getNotificationWithValue(
+ "search-engine-removal"
+ );
+ Assert.ok(notificationBox, "Search engine removal should be shown.");
+
+ await TestUtils.waitForCondition(
+ () => gURLBar.placeholder == expectedString,
+ "The placeholder should include the engine name for built-in engines."
+ );
+
+ Assert.equal(gURLBar.placeholder, expectedString);
+
+ notificationBox.close();
+});
+
+/**
+ * Opens the view, clicks a one-off button to enter search mode, and asserts
+ * that the placeholder is corrrect.
+ *
+ * @param {object} expectedSearchMode
+ * The expected search mode object for the one-off.
+ * @param {object} expectedPlaceholderL10n
+ * The expected l10n object for the one-off.
+ */
+async function doSearchModeTest(expectedSearchMode, expectedPlaceholderL10n) {
+ // Click the urlbar to open the top-sites view.
+ if (gURLBar.getAttribute("pageproxystate") == "invalid") {
+ gURLBar.handleRevert();
+ }
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+
+ // Enter search mode.
+ await UrlbarTestUtils.enterSearchMode(window, expectedSearchMode);
+
+ // Check the placeholder.
+ Assert.deepEqual(
+ document.l10n.getAttributes(gURLBar.inputField),
+ expectedPlaceholderL10n,
+ "Placeholder has expected l10n"
+ );
+
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+ await UrlbarTestUtils.promisePopupClose(window);
+}
diff --git a/browser/components/urlbar/tests/browser/browser_populateAfterPushState.js b/browser/components/urlbar/tests/browser/browser_populateAfterPushState.js
new file mode 100644
index 0000000000..8d383092fe
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_populateAfterPushState.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* When a user clears the URL bar, and then the page pushes state, we should
+ * re-fill the URL bar so it doesn't remain empty indefinitely. See bug 1441039.
+ * For normal loads, this happens automatically because a non-same-document state
+ * change takes place.
+ */
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ TEST_BASE_URL + "dummy_page.html",
+ async function (browser) {
+ gURLBar.value = "";
+
+ let locationChangePromise = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ TEST_BASE_URL + "dummy_page2.html"
+ );
+ await SpecialPowers.spawn(browser, [], function () {
+ content.history.pushState({}, "Page 2", "dummy_page2.html");
+ });
+ await locationChangePromise;
+ is(
+ gURLBar.value,
+ TEST_BASE_URL + "dummy_page2.html",
+ "Should have updated the URL bar."
+ );
+ }
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_primary_selection_safe_on_new_tab.js b/browser/components/urlbar/tests/browser/browser_primary_selection_safe_on_new_tab.js
new file mode 100644
index 0000000000..941c44441d
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_primary_selection_safe_on_new_tab.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Verify that the primary selection is unaffected by opening a new tab.
+ *
+ * The steps here follow STR for regression
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=1457355.
+ */
+
+"use strict";
+
+let tabs = [];
+let supportsPrimary = Services.clipboard.isClipboardTypeSupported(
+ Services.clipboard.kSelectionClipboard
+);
+const NON_EMPTY_URL = "data:text/html,Hello";
+const TEXT_FOR_PRIMARY = "Text for PRIMARY selection";
+
+add_task(async function () {
+ tabs.push(
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, NON_EMPTY_URL)
+ );
+
+ // Bug 1457355 reproduced only when the url had a non-empty selection.
+ gURLBar.select();
+ Assert.equal(gURLBar.inputField.selectionStart, 0);
+ Assert.equal(
+ gURLBar.inputField.selectionEnd,
+ gURLBar.inputField.value.length
+ );
+
+ if (supportsPrimary) {
+ clipboardHelper.copyStringToClipboard(
+ TEXT_FOR_PRIMARY,
+ Services.clipboard.kSelectionClipboard
+ );
+ }
+
+ tabs.push(
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: () => {
+ // Simulate tab open from user input such as keyboard shortcut or new
+ // tab button.
+ let userInput = window.windowUtils.setHandlingUserInput(true);
+ try {
+ BrowserOpenTab();
+ } finally {
+ userInput.destruct();
+ }
+ },
+ waitForLoad: false,
+ })
+ );
+
+ if (!supportsPrimary) {
+ info("Primary selection not supported. Skipping assertion.");
+ return;
+ }
+
+ let primaryAsText = SpecialPowers.getClipboardData(
+ "text/plain",
+ SpecialPowers.Ci.nsIClipboard.kSelectionClipboard
+ );
+ Assert.equal(primaryAsText, TEXT_FOR_PRIMARY);
+});
+
+registerCleanupFunction(() => {
+ for (let tab of tabs) {
+ BrowserTestUtils.removeTab(tab);
+ }
+});
diff --git a/browser/components/urlbar/tests/browser/browser_privateBrowsingWindowChange.js b/browser/components/urlbar/tests/browser/browser_privateBrowsingWindowChange.js
new file mode 100644
index 0000000000..eeeda93687
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_privateBrowsingWindowChange.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test that when opening a private browsing window and typing in it before
+ * about:privatebrowsing loads, we don't clear the URL bar.
+ */
+add_task(async function () {
+ let urlbarTestValue = "Mary had a little lamb";
+ let win = OpenBrowserWindow({ private: true });
+ registerCleanupFunction(() => BrowserTestUtils.closeWindow(win));
+ await BrowserTestUtils.waitForEvent(win, "load");
+ let promise = new Promise(resolve => {
+ let wpl = {
+ onLocationChange(aWebProgress, aRequest, aLocation) {
+ if (aLocation && aLocation.spec == "about:privatebrowsing") {
+ win.gBrowser.removeProgressListener(wpl);
+ resolve();
+ }
+ },
+ };
+ win.gBrowser.addProgressListener(wpl);
+ });
+ Assert.notEqual(
+ win.gBrowser.selectedBrowser.currentURI.spec,
+ "about:privatebrowsing",
+ "Check privatebrowsing page has not been loaded yet"
+ );
+ info("Search in urlbar");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: urlbarTestValue,
+ fireInputEvent: true,
+ });
+ info("waiting for about:privatebrowsing load");
+ await promise;
+
+ let urlbar = win.gURLBar;
+ is(
+ urlbar.value,
+ urlbarTestValue,
+ "URL bar value should be the same once about:privatebrowsing has loaded"
+ );
+ is(
+ win.gBrowser.selectedBrowser.userTypedValue,
+ urlbarTestValue,
+ "User typed value should be the same once about:privatebrowsing has loaded"
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_queryContextCache.js b/browser/components/urlbar/tests/browser/browser_queryContextCache.js
new file mode 100644
index 0000000000..758043233d
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_queryContextCache.js
@@ -0,0 +1,482 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests the view's QueryContextCache. When the view opens and a context is
+// cached for the search, the view should *synchronously* open and update.
+
+"use strict";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ UrlbarProviderTopSites: "resource:///modules/UrlbarProviderTopSites.sys.mjs",
+});
+
+const TEST_URLS = [];
+const TEST_URLS_COUNT = 5;
+const TOP_SITES_VISIT_COUNT = 5;
+const SEARCH_STRING = "example";
+
+// Allow more time for Mac machines so they don't time out in verify mode.
+if (AppConstants.platform == "macosx") {
+ requestLongerTimeout(3);
+}
+
+add_setup(async function () {
+ // Clear history and bookmarks to make sure the URLs we add below are truly
+ // the top sites. If any existing history or bookmarks were the top sites,
+ // which is likely but not guaranteed, one or more "newtab-top-sites-changed"
+ // notifications will be sent, potentially interfering with the rest of the
+ // test. Waiting for Places updates to finish and then an extra tick should be
+ // enough to make sure no more notfications occur.
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesTestUtils.promiseAsyncUpdates();
+ await TestUtils.waitForTick();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.quickactions", false]],
+ });
+
+ // Add some URLs to populate both history and top sites. Each URL needs to
+ // match `SEARCH_STRING`.
+ for (let i = 0; i < TEST_URLS_COUNT; i++) {
+ let url = `https://${i}.example.com/${SEARCH_STRING}`;
+ TEST_URLS.unshift(url);
+ // Each URL needs to be added several times to boost its frecency enough to
+ // qualify as a top site.
+ for (let j = 0; j < TOP_SITES_VISIT_COUNT; j++) {
+ await PlacesTestUtils.addVisits(url);
+ }
+ }
+ await updateTopSitesAndAwaitChanged(TEST_URLS_COUNT);
+
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ });
+});
+
+add_task(async function search() {
+ await withNewBrowserWindow(async win => {
+ // Do a search and then close the view.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: SEARCH_STRING,
+ });
+ await UrlbarTestUtils.promisePopupClose(win);
+
+ // Open the view. It should open synchronously and the cached search context
+ // should be used.
+ await openViewAndAssertCached({
+ win,
+ searchString: SEARCH_STRING,
+ cached: true,
+ });
+ });
+});
+
+add_task(async function topSites_simple() {
+ await withNewBrowserWindow(async win => {
+ // Open the view to show top sites and then close it.
+ await openViewAndAssertCached({ win, cached: false });
+
+ // Open the view again. It should open synchronously and the cached
+ // top-sites context should be used.
+ await openViewAndAssertCached({ win, cached: true });
+ });
+});
+
+add_task(async function topSites_nonEmptySearch() {
+ await withNewBrowserWindow(async win => {
+ // Open the view to show top sites and then close it.
+ await openViewAndAssertCached({ win, cached: false });
+
+ // Do a search, close the view, and revert the input.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "test",
+ });
+ await UrlbarTestUtils.promisePopupClose(win);
+ win.gURLBar.handleRevert();
+
+ // Open the view. It should open synchronously and the cached top-sites
+ // context should be used.
+ await openViewAndAssertCached({ win, cached: true });
+ });
+});
+
+add_task(async function topSites_otherEmptySearch() {
+ await withNewBrowserWindow(async win => {
+ // Open the view to show top sites and then close it.
+ await openViewAndAssertCached({ win, cached: false });
+
+ // Enter search mode with an empty search string (by pressing accel+K),
+ // starting a new search. The view should *not* open synchronously and the
+ // cached top-sites context should not be used.
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(win);
+ EventUtils.synthesizeKey("k", { accelKey: true }, win);
+ Assert.ok(!win.gURLBar.view.isOpen, "View is not open");
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(win, {
+ engineName: Services.search.defaultEngine.name,
+ isGeneralPurposeEngine: true,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ isPreview: false,
+ entry: "shortcut",
+ });
+
+ // Close the view and revert the input.
+ await UrlbarTestUtils.promisePopupClose(win);
+ win.gURLBar.handleRevert();
+ await UrlbarTestUtils.assertSearchMode(win, null);
+
+ // Open the view. It should open synchronously and the cached top-sites
+ // context should be used.
+ await openViewAndAssertCached({ win, cached: true });
+ });
+});
+
+add_task(async function topSites_changed() {
+ await withNewBrowserWindow(async win => {
+ // Open the view to show top sites and then close it.
+ await openViewAndAssertCached({ win, cached: false });
+
+ // Change the top sites by adding visits to a new URL.
+ let newURL = "https://changed.example.com/";
+ for (let j = 0; j < TOP_SITES_VISIT_COUNT; j++) {
+ await PlacesTestUtils.addVisits(newURL);
+ }
+ await updateTopSitesAndAwaitChanged(TEST_URLS_COUNT + 1);
+
+ // Open the view. It should *not* open synchronously and the cached
+ // top-sites context should not be used.
+ await openViewAndAssertCached({ win, cached: false });
+
+ // Open the view again. It should open synchronously and the new cached
+ // top-sites context with the new URL should be used.
+ await openViewAndAssertCached({
+ win,
+ cached: true,
+ urls: [newURL, ...TEST_URLS],
+ // The new URL is sometimes at the end of the list of top sites instead of
+ // the start, so ignore the order of the results.
+ ignoreOrder: true,
+ });
+
+ // Remove the new URL. The top sites will update themselves automatically,
+ // so we only need to wait for newtab-top-sites-changed.
+ info("Removing new URL and awaiting newtab-top-sites-changed");
+ let changedPromise = TestUtils.topicObserved("newtab-top-sites-changed");
+ await PlacesUtils.history.remove([newURL]);
+ await changedPromise;
+
+ // Open the view. It should *not* open synchronously and the cached
+ // top-sites context should not be used.
+ await openViewAndAssertCached({ win, cached: false });
+
+ // Open the view again. It should open synchronously and the new cached
+ // top-sites context with the new URL should be used.
+ await openViewAndAssertCached({ win, cached: true });
+ });
+});
+
+add_task(async function topSites_nonTopSitesResults() {
+ await withNewBrowserWindow(async win => {
+ // Open the view to show top sites and then close it.
+ await openViewAndAssertCached({ win, cached: false });
+
+ // Add a provider that returns a result with a suggested index of zero so
+ // that the first result in the view is not from the top-sites provider.
+ let suggestedIndexURL = "https://example.com/suggested-index-0";
+ let provider = new UrlbarTestUtils.TestProvider({
+ priority: lazy.UrlbarProviderTopSites.PRIORITY,
+ results: [
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ url: suggestedIndexURL,
+ }
+ ),
+ { suggestedIndex: 0 }
+ ),
+ ],
+ });
+ UrlbarProvidersManager.registerProvider(provider);
+
+ // Open the view. It should open synchronously and the cached top-sites
+ // context should be used. The suggested-index result should not be
+ // immediately present in the view since it's not in the cached context.
+ await openViewAndAssertCached({ win, cached: true, keepOpen: true });
+
+ // After the search has finished, the suggested-index result should be in
+ // the first row. The search's context should become the newly cached
+ // top-sites context and it should include the suggested-index result.
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(win),
+ TEST_URLS.length + 1,
+ "Should be one more result after search finishes"
+ );
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(win, 0);
+ Assert.equal(
+ details.url,
+ suggestedIndexURL,
+ "First result after search finishes should be the suggested index result"
+ );
+
+ // At this point, the search's context should have become the newly cached
+ // top-sites context and it should include the suggested-index result.
+
+ await UrlbarTestUtils.promisePopupClose(win);
+
+ // Open the view again. It should open synchronously and the new cached
+ // top-sites context with the suggested-index URL should be used.
+ await openViewAndAssertCached({
+ win,
+ cached: true,
+ urls: [suggestedIndexURL, ...TEST_URLS],
+ });
+
+ UrlbarProvidersManager.unregisterProvider(provider);
+ });
+});
+
+add_task(async function topSites_disabled_1() {
+ await withNewBrowserWindow(async win => {
+ // Open the view to show top sites and then close it.
+ await openViewAndAssertCached({ win, cached: false });
+
+ // Disable `browser.urlbar.suggest.topsites`.
+ UrlbarPrefs.set("suggest.topsites", false);
+
+ // Open the view. It should *not* open synchronously and the cached
+ // top-sites context should not be used.
+ await openViewAndAssertCached({
+ win,
+ cached: false,
+ cachedAfterOpen: false,
+ });
+
+ // Clear the pref, open the view to show top sites, and close it.
+ UrlbarPrefs.clear("suggest.topsites");
+ await openViewAndAssertCached({ win, cached: false });
+
+ // Open the view. It should open synchronously and the cached top-sites
+ // context should be used.
+ await openViewAndAssertCached({ win, cached: true });
+ });
+});
+
+add_task(async function topSites_disabled_2() {
+ await withNewBrowserWindow(async win => {
+ // Open the view to show top sites and then close it.
+ await openViewAndAssertCached({ win, cached: false });
+
+ // Disable `browser.newtabpage.activity-stream.feeds.system.topsites`.
+ Services.prefs.setBoolPref(
+ "browser.newtabpage.activity-stream.feeds.system.topsites",
+ false
+ );
+
+ // Open the view. It should *not* open synchronously and the cached
+ // top-sites context should not be used.
+ await openViewAndAssertCached({
+ win,
+ cached: false,
+ cachedAfterOpen: false,
+ });
+
+ // Clear the pref, open the view to show top sites, and close it.
+ Services.prefs.clearUserPref(
+ "browser.newtabpage.activity-stream.feeds.system.topsites"
+ );
+ await openViewAndAssertCached({ win, cached: false });
+
+ // Open the view. It should open synchronously and the cached top-sites
+ // context should be used.
+ await openViewAndAssertCached({ win, cached: true });
+ });
+});
+
+add_task(async function evict() {
+ await withNewBrowserWindow(async win => {
+ let cache = win.gURLBar.view.queryContextCache;
+ Assert.equal(
+ typeof cache.size,
+ "number",
+ "Sanity check: queryContextCache.size is a number"
+ );
+
+ // Open the view to show top sites and then close it.
+ await openViewAndAssertCached({ win, cached: false });
+
+ // Do `cache.size` + 1 searches.
+ for (let i = 0; i < cache.size + 1; i++) {
+ let searchString = "test" + i;
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: searchString,
+ });
+ await UrlbarTestUtils.promisePopupClose(win);
+ Assert.ok(
+ cache.get(searchString),
+ "Cache includes search string: " + searchString
+ );
+ }
+
+ // The first search string should have been evicted from the cache, but the
+ // one after that should still be cached.
+ Assert.ok(!cache.get("test0"), "test0 has been evicted from the cache");
+ Assert.ok(cache.get("test1"), "Cache includes test1");
+
+ // Revert the input and open the view to show the top sites. It should open
+ // synchronously and the cached top-sites context should be used.
+ win.gURLBar.handleRevert();
+ Assert.equal(win.gURLBar.value, "", "Input is empty after reverting");
+ await openViewAndAssertCached({ win, cached: true });
+ });
+});
+
+/**
+ * Opens the view and checks that it is or is not synchronously opened and
+ * populated as specified.
+ *
+ * @param {object} options
+ * Options object.
+ * @param {window} options.win
+ * The window to open the view in.
+ * @param {boolean} options.cached
+ * Whether a query context is expected to already be cached for the search
+ * that's performed when the view opens. If true, then the view should
+ * synchronously open and populate using the cached context. If false, then
+ * the view should asynchronously open once the first results are fetched.
+ * @param {boolean} [options.cachedAfterOpen]
+ * Whether the context is expected to be cached after the view opens and the
+ * query finishes.
+ * @param {string} [options.searchString]
+ * The search string for which the context should or should not be cached. If
+ * falsey, then the relevant context is assumed to be the top-sites context.
+ * @param {Array} [options.urls]
+ * Array of URLs that are expected to be shown in the view.
+ * @param {boolean} [options.ignoreOrder]
+ * Whether to treat `urls` as an unordered set instead of an array. When true,
+ * the order of results is ignored.
+ * @param {boolean} [options.keepOpen]
+ * Whether to keep the view open when the function returns.
+ */
+async function openViewAndAssertCached({
+ win,
+ cached,
+ cachedAfterOpen = true,
+ searchString = "",
+ urls = TEST_URLS,
+ ignoreOrder = false,
+ keepOpen = false,
+}) {
+ let cache = win.gURLBar.view.queryContextCache;
+ let getContext = () =>
+ searchString ? cache.get(searchString) : cache.topSitesContext;
+
+ Assert.equal(
+ !!getContext(),
+ cached,
+ "Context is present or not in cache as expected for search string: " +
+ JSON.stringify(searchString)
+ );
+
+ // Open the view by performing the accel+L command.
+ await SimpleTest.promiseFocus(win);
+ win.document.getElementById("Browser:OpenLocation").doCommand();
+
+ Assert.equal(
+ win.gURLBar.view.isOpen,
+ cached,
+ "View is open or not as expected"
+ );
+
+ if (!cached && cachedAfterOpen) {
+ // Wait for the search to finish and the context to be cached since callers
+ // generally expect it.
+ await TestUtils.waitForCondition(
+ getContext,
+ "Waiting for context to be cached for search string: " +
+ JSON.stringify(searchString)
+ );
+ } else if (cached) {
+ // The view is expected to open synchronously. Check the results. We don't
+ // do this in the `!cached` case, when the view is expected to open
+ // asynchronously, because there are plenty of other tests for that. Here we
+ // want to make sure results are correct before the new search finishes in
+ // order to avoid any flicker.
+ let startIndex = 0;
+ let resultCount = urls.length;
+ if (searchString) {
+ // Plus heuristic
+ startIndex++;
+ resultCount++;
+ }
+
+ // In all the checks below, check the rows container directly instead of
+ // relying on `UrlbarTestUtils` functions that wait for the search to
+ // finish. Here we're specifically checking cached results that should be
+ // used before the search finishes.
+ let rows = UrlbarTestUtils.getResultsContainer(win).children;
+ Assert.equal(rows.length, resultCount, "View has expected row count");
+
+ // Check the search heuristic row.
+ if (searchString) {
+ let result = rows[0].result;
+ Assert.ok(result.heuristic, "First row should be a heuristic");
+ Assert.equal(
+ result.payload.query,
+ searchString,
+ "First row's query should be the search string"
+ );
+ }
+
+ // Check the URL rows.
+ let actualURLs = [];
+ let urlRows = Array.from(rows).slice(startIndex);
+ for (let row of urlRows) {
+ actualURLs.push(row.result.payload.url);
+ }
+ if (ignoreOrder) {
+ urls.sort();
+ actualURLs.sort();
+ }
+ Assert.deepEqual(actualURLs, urls, "View should contain the expected URLs");
+ }
+
+ // Now wait for the search to finish before returning. We await
+ // `lastQueryContextPromise` instead of the promise returned from
+ // `UrlbarTestUtils.promiseSearchComplete()` because the latter assumes the
+ // view will open, which isn't the case for every task here.
+ await win.gURLBar.lastQueryContextPromise;
+ if (!keepOpen) {
+ await UrlbarTestUtils.promisePopupClose(win);
+ }
+}
+
+/**
+ * Updates the top sites and waits for the "newtab-top-sites-changed"
+ * notification. Note that this notification is not sent if the sites don't
+ * actually change. In that case, use only `updateTopSites()` instead.
+ *
+ * @param {number} expectedCount
+ * The new expected number of top sites.
+ */
+async function updateTopSitesAndAwaitChanged(expectedCount) {
+ info("Updating top sites and awaiting newtab-top-sites-changed");
+ let changedPromise = TestUtils.topicObserved("newtab-top-sites-changed").then(
+ () => info("Observed newtab-top-sites-changed")
+ );
+ await updateTopSites(sites => sites?.length == expectedCount);
+ await changedPromise;
+}
+
+async function withNewBrowserWindow(callback) {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await callback(win);
+ await BrowserTestUtils.closeWindow(win);
+}
diff --git a/browser/components/urlbar/tests/browser/browser_quickactions.js b/browser/components/urlbar/tests/browser/browser_quickactions.js
new file mode 100644
index 0000000000..5945910067
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_quickactions.js
@@ -0,0 +1,783 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests QuickActions.
+ */
+
+"use strict";
+
+requestLongerTimeout(3);
+
+ChromeUtils.defineESModuleGetters(this, {
+ AppConstants: "resource://gre/modules/AppConstants.sys.mjs",
+ UpdateService: "resource://gre/modules/UpdateService.sys.mjs",
+
+ UrlbarProviderQuickActions:
+ "resource:///modules/UrlbarProviderQuickActions.sys.mjs",
+});
+XPCOMUtils.defineLazyModuleGetters(this, {
+ BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
+});
+
+const DUMMY_PAGE =
+ "http://example.com/browser/browser/base/content/test/general/dummy_page.html";
+
+let testActionCalled = 0;
+
+add_setup(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.quickactions.enabled", true],
+ ["browser.urlbar.suggest.quickactions", true],
+ ["browser.urlbar.shortcuts.quickactions", true],
+ ],
+ });
+
+ UrlbarProviderQuickActions.addAction("testaction", {
+ commands: ["testaction"],
+ label: "quickactions-downloads2",
+ onPick: () => testActionCalled++,
+ });
+
+ registerCleanupFunction(() => {
+ UrlbarProviderQuickActions.removeAction("testaction");
+ });
+});
+
+add_task(async function basic() {
+ info("The action isnt shown when not matched");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "nomatch",
+ });
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 1,
+ "We did no match anything"
+ );
+
+ info("A prefix of the command matches");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "testact",
+ });
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 2,
+ "We matched the action"
+ );
+
+ info("The callback of the action is fired when selected");
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
+ EventUtils.synthesizeKey("KEY_Enter", {}, window);
+ Assert.equal(testActionCalled, 1, "Test actionwas called");
+});
+
+add_task(async function test_label_command() {
+ info("A prefix of the label matches");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "View Dow",
+ });
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 2,
+ "We matched the action"
+ );
+
+ let { result } = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.DYNAMIC);
+ Assert.equal(result.providerName, "quickactions");
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeKey("KEY_Escape");
+ });
+});
+
+add_task(async function enter_search_mode_button() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+ let oneOffButton = await TestUtils.waitForCondition(() =>
+ window.document.getElementById("urlbar-engine-one-off-item-actions")
+ );
+ Assert.ok(oneOffButton, "One off button is available when preffed on");
+
+ EventUtils.synthesizeMouseAtCenter(oneOffButton, {}, window);
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.ACTIONS,
+ entry: "oneoff",
+ });
+
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
+ Assert.ok(true, "Actions are shown when we enter actions search mode.");
+
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+ EventUtils.synthesizeKey("KEY_Escape");
+});
+
+add_task(async function enter_search_mode_oneoff_by_key() {
+ // Select actions oneoff button by keyboard.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ const oneOffButtons = UrlbarTestUtils.getOneOffSearchButtons(window);
+ for (;;) {
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ if (
+ oneOffButtons.selectedButton.source === UrlbarUtils.RESULT_SOURCE.ACTIONS
+ ) {
+ break;
+ }
+ }
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: " ",
+ });
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.ACTIONS,
+ entry: "oneoff",
+ });
+
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+ EventUtils.synthesizeKey("KEY_Escape");
+});
+
+add_task(async function enter_search_mode_key() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "> ",
+ });
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.ACTIONS,
+ entry: "typed",
+ });
+ Assert.equal(
+ await hasQuickActions(window),
+ true,
+ "Actions are shown in search mode"
+ );
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+ EventUtils.synthesizeKey("KEY_Escape");
+});
+
+add_task(async function test_disabled() {
+ UrlbarProviderQuickActions.addAction("disabledaction", {
+ commands: ["disabledaction"],
+ isActive: () => false,
+ label: "quickactions-restart",
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "disabled",
+ });
+
+ Assert.equal(
+ await hasQuickActions(window),
+ false,
+ "Result for quick actions is hidden"
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ UrlbarProviderQuickActions.removeAction("disabledaction");
+});
+
+/**
+ * The first part of this test confirms that when the screenshots component is enabled
+ * the screenshot quick action button will be enabled on about: pages.
+ * The second part confirms that when the screenshots extension is enabled the
+ * screenshot quick action button will be disbaled on about: pages.
+ */
+add_task(async function test_screenshot_enabled_or_disabled() {
+ let onLoaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ "about:blank"
+ );
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, "about:blank");
+ await onLoaded;
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "screenshot",
+ });
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 2,
+ "The action is displayed"
+ );
+ let screenshotButton = window.document.querySelector(
+ ".urlbarView-row[dynamicType=quickactions] .urlbarView-quickaction-row"
+ );
+ Assert.ok(
+ !screenshotButton.hasAttribute("disabled"),
+ "Screenshot button is enabled on about pages"
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ EventUtils.synthesizeKey("KEY_Escape");
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["screenshots.browser.component.enabled", false]],
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "screenshot",
+ });
+ Assert.equal(
+ await hasQuickActions(window),
+ false,
+ "Result for quick actions is hidden"
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+add_task(async function match_in_phrase() {
+ UrlbarProviderQuickActions.addAction("newtestaction", {
+ commands: ["matchingstring"],
+ label: "quickactions-downloads2",
+ });
+
+ info("The action is matched when at end of input");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "Test we match at end of matchingstring",
+ });
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 2,
+ "We matched the action"
+ );
+ await UrlbarTestUtils.promisePopupClose(window);
+ EventUtils.synthesizeKey("KEY_Escape");
+ UrlbarProviderQuickActions.removeAction("newtestaction");
+});
+
+async function isScreenshotInitialized() {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ let screenshotsChild = content.windowGlobalChild.getActor(
+ "ScreenshotsComponent"
+ );
+ return screenshotsChild?._overlay?._initialized;
+ });
+}
+
+add_task(async function test_screenshot() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["screenshots.browser.component.enabled", true]],
+ });
+
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, DUMMY_PAGE);
+ await BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ DUMMY_PAGE
+ );
+
+ info("The action is matched when at end of input");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "screenshot",
+ });
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 2,
+ "We matched the action"
+ );
+ let { result } = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.DYNAMIC);
+ Assert.equal(result.providerName, "quickactions");
+
+ info("Trigger the screenshot mode");
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
+ EventUtils.synthesizeKey("KEY_Enter", {}, window);
+ await TestUtils.waitForCondition(
+ isScreenshotInitialized,
+ "Screenshot component is active",
+ 200,
+ 100
+ );
+
+ info("Press Escape to exit screenshot mode");
+ EventUtils.synthesizeKey("KEY_Escape", {}, window);
+ await TestUtils.waitForCondition(
+ async () => !(await isScreenshotInitialized()),
+ "Screenshot component has been dismissed"
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeKey("KEY_Escape");
+ });
+});
+
+add_task(async function test_other_search_mode() {
+ let defaultEngine = await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + "searchSuggestionEngine.xml",
+ });
+ defaultEngine.alias = "testalias";
+ let oldDefaultEngine = await Services.search.getDefault();
+ Services.search.setDefault(
+ defaultEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: defaultEngine.alias + " ",
+ });
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 0,
+ "The results should be empty as no actions are displayed in other search modes"
+ );
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: defaultEngine.name,
+ entry: "typed",
+ });
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeKey("KEY_Escape");
+ });
+ Services.search.setDefault(
+ oldDefaultEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+});
+
+add_task(async function test_no_quickactions_suggestions() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.quickactions", false]],
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "screenshot",
+ });
+ Assert.ok(
+ !window.document.querySelector(
+ ".urlbarView-row[dynamicType=quickactions] .urlbarView-quickaction-row"
+ ),
+ "Screenshot button is not suggested"
+ );
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "> screenshot",
+ });
+ Assert.ok(
+ window.document.querySelector(
+ ".urlbarView-row[dynamicType=quickactions] .urlbarView-quickaction-row"
+ ),
+ "Screenshot button is suggested"
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ EventUtils.synthesizeKey("KEY_Escape");
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_quickactions_disabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.quickactions.enabled", false],
+ ["browser.urlbar.suggest.quickactions", true],
+ ],
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "screenshot",
+ });
+
+ Assert.ok(
+ !window.document.querySelector(
+ ".urlbarView-row[dynamicType=quickactions] .urlbarView-quickaction-row"
+ ),
+ "Screenshot button is not suggested"
+ );
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "> screenshot",
+ });
+ Assert.ok(
+ !window.document.querySelector(
+ ".urlbarView-row[dynamicType=quickactions] .urlbarView-quickaction-row"
+ ),
+ "Screenshot button is not suggested"
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ EventUtils.synthesizeKey("KEY_Escape");
+
+ await SpecialPowers.popPrefEnv();
+});
+
+let COMMANDS_TESTS = [
+ {
+ cmd: "add-ons",
+ uri: "about:addons",
+ testFun: async () => isSelected("button[name=discover]"),
+ },
+ {
+ cmd: "plugins",
+ uri: "about:addons",
+ testFun: async () => isSelected("button[name=plugin]"),
+ },
+ {
+ cmd: "extensions",
+ uri: "about:addons",
+ testFun: async () => isSelected("button[name=extension]"),
+ },
+ {
+ cmd: "themes",
+ uri: "about:addons",
+ testFun: async () => isSelected("button[name=theme]"),
+ },
+ {
+ cmd: "add-ons",
+ setup: async () => {
+ const onLoad = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ "http://example.com/"
+ );
+ BrowserTestUtils.loadURIString(
+ gBrowser.selectedBrowser,
+ "http://example.com/"
+ );
+ await onLoad;
+ },
+ uri: "about:addons",
+ isNewTab: true,
+ testFun: async () => isSelected("button[name=discover]"),
+ },
+ {
+ cmd: "plugins",
+ setup: async () => {
+ const onLoad = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ "http://example.com/"
+ );
+ BrowserTestUtils.loadURIString(
+ gBrowser.selectedBrowser,
+ "http://example.com/"
+ );
+ await onLoad;
+ },
+ uri: "about:addons",
+ isNewTab: true,
+ testFun: async () => isSelected("button[name=plugin]"),
+ },
+ {
+ cmd: "extensions",
+ setup: async () => {
+ const onLoad = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ "http://example.com/"
+ );
+ BrowserTestUtils.loadURIString(
+ gBrowser.selectedBrowser,
+ "http://example.com/"
+ );
+ await onLoad;
+ },
+ uri: "about:addons",
+ isNewTab: true,
+ testFun: async () => isSelected("button[name=extension]"),
+ },
+ {
+ cmd: "themes",
+ setup: async () => {
+ const onLoad = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ "http://example.com/"
+ );
+ BrowserTestUtils.loadURIString(
+ gBrowser.selectedBrowser,
+ "http://example.com/"
+ );
+ await onLoad;
+ },
+ uri: "about:addons",
+ isNewTab: true,
+ testFun: async () => isSelected("button[name=theme]"),
+ },
+];
+
+let isSelected = async selector =>
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [selector], arg => {
+ return ContentTaskUtils.waitForCondition(() =>
+ content.document.querySelector(arg)?.hasAttribute("selected")
+ );
+ });
+
+add_task(async function test_pages() {
+ for (const { cmd, uri, setup, isNewTab, testFun } of COMMANDS_TESTS) {
+ info(`Testing ${cmd} command is triggered`);
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ if (setup) {
+ info("Setup");
+ await setup();
+ }
+
+ let onLoad = isNewTab
+ ? BrowserTestUtils.waitForNewTab(gBrowser, uri, true)
+ : BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false, uri);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: cmd,
+ });
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
+ EventUtils.synthesizeKey("KEY_Enter", {}, window);
+
+ const newTab = await onLoad;
+
+ Assert.ok(
+ await testFun(),
+ `The command "${cmd}" passed completed its test`
+ );
+
+ if (isNewTab) {
+ await BrowserTestUtils.removeTab(newTab);
+ }
+ await BrowserTestUtils.removeTab(tab);
+ }
+});
+
+const assertActionButtonStatus = async (name, expectedEnabled, description) => {
+ await BrowserTestUtils.waitForCondition(() =>
+ window.document.querySelector(`[data-key=${name}]`)
+ );
+ const target = window.document.querySelector(`[data-key=${name}]`);
+ Assert.equal(!target.hasAttribute("disabled"), expectedEnabled, description);
+};
+
+add_task(async function test_viewsource() {
+ info("Check the button status of when the page is not web content");
+ const tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "about:home",
+ waitForLoad: true,
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "viewsource",
+ });
+ await assertActionButtonStatus(
+ "viewsource",
+ true,
+ "Should be enabled even if the page is not web content"
+ );
+
+ info("Check the button status of when the page is web content");
+ BrowserTestUtils.loadURIString(
+ gBrowser.selectedBrowser,
+ "http://example.com"
+ );
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "viewsource",
+ });
+ await assertActionButtonStatus(
+ "viewsource",
+ true,
+ "Should be enabled on web content as well"
+ );
+
+ info("Do view source action");
+ const onLoad = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "view-source:http://example.com/"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
+ EventUtils.synthesizeKey("KEY_Enter", {}, window);
+ const viewSourceTab = await onLoad;
+
+ info("Do view source action on the view-source page");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "viewsource",
+ });
+
+ Assert.equal(
+ await hasQuickActions(window),
+ false,
+ "Result for quick actions is hidden"
+ );
+
+ // Clean up.
+ BrowserTestUtils.removeTab(viewSourceTab);
+ BrowserTestUtils.removeTab(tab);
+});
+
+async function doAlertDialogTest({ input, dialogContentURI }) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: input,
+ });
+
+ const onDialog = BrowserTestUtils.promiseAlertDialog(null, null, {
+ isSubDialog: true,
+ callback: win => {
+ Assert.equal(win.location.href, dialogContentURI, "The dialog is opened");
+ EventUtils.synthesizeKey("KEY_Escape", {}, win);
+ },
+ });
+
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
+ EventUtils.synthesizeKey("KEY_Enter", {}, window);
+
+ await onDialog;
+}
+
+add_task(async function test_refresh() {
+ await doAlertDialogTest({
+ input: "refresh",
+ dialogContentURI: "chrome://global/content/resetProfile.xhtml",
+ });
+});
+
+add_task(async function test_clear() {
+ await doAlertDialogTest({
+ input: "clear",
+ dialogContentURI: "chrome://browser/content/sanitize.xhtml",
+ });
+});
+
+async function doUpdateActionTest(isActiveExpected, description) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "update",
+ });
+
+ if (isActiveExpected) {
+ await assertActionButtonStatus("update", isActiveExpected, description);
+ } else {
+ Assert.equal(await hasQuickActions(window), false, description);
+ }
+}
+
+add_task(async function test_update() {
+ if (!AppConstants.MOZ_UPDATER) {
+ await doUpdateActionTest(
+ false,
+ "Should be disabled since not AppConstants.MOZ_UPDATER"
+ );
+ return;
+ }
+
+ const sandbox = sinon.createSandbox();
+ try {
+ sandbox
+ .stub(UpdateService.prototype, "currentState")
+ .get(() => Ci.nsIApplicationUpdateService.STATE_IDLE);
+ await doUpdateActionTest(
+ false,
+ "Should be disabled since current update state is not pending"
+ );
+ sandbox
+ .stub(UpdateService.prototype, "currentState")
+ .get(() => Ci.nsIApplicationUpdateService.STATE_PENDING);
+ await doUpdateActionTest(
+ true,
+ "Should be enabled since current update state is pending"
+ );
+ } finally {
+ sandbox.restore();
+ }
+});
+
+async function hasQuickActions(win) {
+ for (let i = 0, count = UrlbarTestUtils.getResultCount(win); i < count; i++) {
+ const { result } = await UrlbarTestUtils.getDetailsOfResultAt(win, i);
+ if (result.providerName === "quickactions") {
+ return true;
+ }
+ }
+ return false;
+}
+
+add_task(async function test_show_in_zero_prefix() {
+ for (const minimumSearchString of [0, 3]) {
+ info(
+ `Test when quickactions.minimumSearchString pref is ${minimumSearchString}`
+ );
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "browser.urlbar.quickactions.minimumSearchString",
+ minimumSearchString,
+ ],
+ ],
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+
+ Assert.equal(
+ await hasQuickActions(window),
+ !minimumSearchString,
+ "Result for quick actions is as expected"
+ );
+ await SpecialPowers.popPrefEnv();
+ }
+});
+
+add_task(async function test_whitespace() {
+ info("Test with quickactions.showInZeroPrefix pref is false");
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.quickactions.showInZeroPrefix", false]],
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: " ",
+ });
+ Assert.equal(
+ await hasQuickActions(window),
+ false,
+ "Result for quick actions is not shown"
+ );
+ await SpecialPowers.popPrefEnv();
+
+ info("Test with quickactions.showInZeroPrefix pref is true");
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.quickactions.showInZeroPrefix", true]],
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ const countForEmpty = window.document.querySelectorAll(
+ ".urlbarView-quickaction-row"
+ ).length;
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: " ",
+ });
+ const countForWhitespace = window.document.querySelectorAll(
+ ".urlbarView-quickaction-row"
+ ).length;
+ Assert.equal(
+ countForEmpty,
+ countForWhitespace,
+ "Count of quick actions of empty and whitespace are same"
+ );
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_quickactions_devtools.js b/browser/components/urlbar/tests/browser/browser_quickactions_devtools.js
new file mode 100644
index 0000000000..d113a4c3a8
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_quickactions_devtools.js
@@ -0,0 +1,176 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests QuickActions related to DevTools.
+ */
+
+"use strict";
+
+requestLongerTimeout(2);
+
+ChromeUtils.defineESModuleGetters(this, {
+ DevToolsShim: "chrome://devtools-startup/content/DevToolsShim.sys.mjs",
+});
+
+add_setup(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.quickactions.enabled", true],
+ ["browser.urlbar.suggest.quickactions", true],
+ ["browser.urlbar.shortcuts.quickactions", true],
+ ],
+ });
+});
+
+const assertActionButtonStatus = async (name, expectedEnabled, description) => {
+ await BrowserTestUtils.waitForCondition(() =>
+ window.document.querySelector(`[data-key=${name}]`)
+ );
+ const target = window.document.querySelector(`[data-key=${name}]`);
+ Assert.equal(!target.hasAttribute("disabled"), expectedEnabled, description);
+};
+
+async function hasQuickActions(win) {
+ for (let i = 0, count = UrlbarTestUtils.getResultCount(win); i < count; i++) {
+ const { result } = await UrlbarTestUtils.getDetailsOfResultAt(win, i);
+ if (result.providerName === "quickactions") {
+ return true;
+ }
+ }
+ return false;
+}
+
+add_task(async function test_inspector() {
+ const testData = [
+ {
+ description: "Test for 'about:' page",
+ page: "about:home",
+ isDevToolsUser: true,
+ actionVisible: true,
+ actionEnabled: true,
+ },
+ {
+ description: "Test for another 'about:' page",
+ page: "about:about",
+ isDevToolsUser: true,
+ actionVisible: true,
+ actionEnabled: true,
+ },
+ {
+ description: "Test for another devtools-toolbox page",
+ page: "about:devtools-toolbox",
+ isDevToolsUser: true,
+ actionVisible: true,
+ actionEnabled: false,
+ },
+ {
+ description: "Test for web content",
+ page: "https://example.com",
+ isDevToolsUser: true,
+ actionVisible: true,
+ actionEnabled: true,
+ },
+ {
+ description: "Test for disabled DevTools",
+ page: "https://example.com",
+ prefs: [["devtools.policy.disabled", true]],
+ isDevToolsUser: true,
+ actionVisible: true,
+ actionEnabled: false,
+ },
+ {
+ description: "Test for not DevTools user",
+ page: "https://example.com",
+ isDevToolsUser: false,
+ actionVisible: true,
+ actionEnabled: false,
+ },
+ {
+ description: "Test for fully disabled",
+ page: "https://example.com",
+ prefs: [["devtools.policy.disabled", true]],
+ isDevToolsUser: false,
+ actionVisible: false,
+ },
+ ];
+
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ for (const {
+ description,
+ page,
+ prefs = [],
+ isDevToolsUser,
+ actionEnabled,
+ actionVisible,
+ } of testData) {
+ info(description);
+
+ info("Set preferences");
+ await SpecialPowers.pushPrefEnv({
+ set: [...prefs, ["devtools.selfxss.count", isDevToolsUser ? 5 : 0]],
+ });
+
+ info("Check the button status");
+ const onLoad = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, page);
+ await onLoad;
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "inspector",
+ });
+
+ if (actionVisible && actionEnabled) {
+ await assertActionButtonStatus(
+ "inspect",
+ true,
+ "The status of action button is correct"
+ );
+ } else {
+ Assert.equal(
+ await hasQuickActions(window),
+ false,
+ "Result for quick actions is not shown since the inspector tool is disabled"
+ );
+ }
+
+ await SpecialPowers.popPrefEnv();
+
+ if (!actionVisible || !actionEnabled) {
+ continue;
+ }
+
+ info("Do inspect action");
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
+ EventUtils.synthesizeKey("KEY_Enter", {}, window);
+ await BrowserTestUtils.waitForCondition(
+ () => DevToolsShim.hasToolboxForTab(gBrowser.selectedTab),
+ "Wait for opening inspector for current selected tab"
+ );
+ const toolbox = await DevToolsShim.getToolboxForTab(gBrowser.selectedTab);
+ await BrowserTestUtils.waitForCondition(
+ () => toolbox.getPanel("inspector"),
+ "Wait until the inspector is ready"
+ );
+
+ info("Do inspect action again in the same page during opening inspector");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "inspector",
+ });
+ Assert.equal(
+ await hasQuickActions(window),
+ false,
+ "Result for quick actions is not shown since the inspector is already opening"
+ );
+
+ info(
+ "Select another tool to check whether the inspector will be selected in next test even if the previous tool is not inspector"
+ );
+ await toolbox.selectTool("options");
+ await toolbox.destroy();
+ }
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_quickactions_tab_refocus.js b/browser/components/urlbar/tests/browser/browser_quickactions_tab_refocus.js
new file mode 100644
index 0000000000..f969528806
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_quickactions_tab_refocus.js
@@ -0,0 +1,194 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests for QuickActions that re-focus tab..
+ */
+
+"use strict";
+
+requestLongerTimeout(3);
+
+add_setup(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.quickactions.enabled", true],
+ ["browser.urlbar.suggest.quickactions", true],
+ ["browser.urlbar.shortcuts.quickactions", true],
+ ],
+ });
+});
+
+let isSelected = async selector =>
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [selector], arg => {
+ return ContentTaskUtils.waitForCondition(() =>
+ content.document.querySelector(arg)?.hasAttribute("selected")
+ );
+ });
+
+add_task(async function test_about_pages() {
+ const testData = [
+ {
+ firstInput: "downloads",
+ uri: "about:downloads",
+ },
+ {
+ firstInput: "logins",
+ uri: "about:logins",
+ },
+ {
+ firstInput: "settings",
+ uri: "about:preferences",
+ },
+ {
+ firstInput: "add-ons",
+ uri: "about:addons",
+ component: "button[name=discover]",
+ },
+ {
+ firstInput: "extensions",
+ uri: "about:addons",
+ component: "button[name=extension]",
+ },
+ {
+ firstInput: "plugins",
+ uri: "about:addons",
+ component: "button[name=plugin]",
+ },
+ {
+ firstInput: "themes",
+ uri: "about:addons",
+ component: "button[name=theme]",
+ },
+ {
+ firstLoad: "about:preferences#home",
+ secondInput: "settings",
+ uri: "about:preferences#home",
+ },
+ ];
+
+ for (const {
+ firstInput,
+ firstLoad,
+ secondInput,
+ uri,
+ component,
+ } of testData) {
+ info("Setup initial state");
+ let firstTab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ let onLoad = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ uri
+ );
+ if (firstLoad) {
+ info("Load initial URI");
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, uri);
+ } else {
+ info("Open about page by quick action");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: firstInput,
+ });
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
+ EventUtils.synthesizeKey("KEY_Enter", {}, window);
+ }
+ await onLoad;
+
+ if (component) {
+ info("Check whether the component is in the page");
+ Assert.ok(await isSelected(component), "There is expected component");
+ }
+
+ info("Do the second quick action in second tab");
+ let secondTab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: secondInput || firstInput,
+ });
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
+ EventUtils.synthesizeKey("KEY_Enter", {}, window);
+ Assert.equal(
+ gBrowser.selectedTab,
+ firstTab,
+ "Switched to the tab that is opening the about page"
+ );
+ Assert.equal(
+ gBrowser.selectedBrowser.currentURI.spec,
+ uri,
+ "URI is not changed"
+ );
+ Assert.equal(gBrowser.tabs.length, 3, "Not opened a new tab");
+
+ if (component) {
+ info("Check whether the component is still in the page");
+ Assert.ok(await isSelected(component), "There is expected component");
+ }
+
+ BrowserTestUtils.removeTab(secondTab);
+ BrowserTestUtils.removeTab(firstTab);
+ }
+});
+
+add_task(async function test_about_addons_pages() {
+ let testData = [
+ {
+ cmd: "add-ons",
+ testFun: async () => isSelected("button[name=discover]"),
+ },
+ {
+ cmd: "plugins",
+ testFun: async () => isSelected("button[name=plugin]"),
+ },
+ {
+ cmd: "extensions",
+ testFun: async () => isSelected("button[name=extension]"),
+ },
+ {
+ cmd: "themes",
+ testFun: async () => isSelected("button[name=theme]"),
+ },
+ ];
+
+ info("Pick all actions related about:addons");
+ let originalTab = gBrowser.selectedTab;
+ for (const { cmd, testFun } of testData) {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: cmd,
+ });
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
+ EventUtils.synthesizeKey("KEY_Enter", {}, window);
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ Assert.ok(await testFun(), "The page content is correct");
+ }
+ Assert.equal(
+ gBrowser.tabs.length,
+ testData.length + 1,
+ "Tab length is correct"
+ );
+
+ info("Pick all again");
+ for (const { cmd, testFun } of testData) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: cmd,
+ });
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
+ EventUtils.synthesizeKey("KEY_Enter", {}, window);
+ await BrowserTestUtils.waitForCondition(() => testFun());
+ Assert.ok(true, "The tab correspondent action is selected");
+ }
+ Assert.equal(
+ gBrowser.tabs.length,
+ testData.length + 1,
+ "Tab length is not changed"
+ );
+
+ for (const tab of gBrowser.tabs) {
+ if (tab !== originalTab) {
+ BrowserTestUtils.removeTab(tab);
+ }
+ }
+});
diff --git a/browser/components/urlbar/tests/browser/browser_raceWithTabs.js b/browser/components/urlbar/tests/browser/browser_raceWithTabs.js
new file mode 100644
index 0000000000..17560ea101
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_raceWithTabs.js
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URL = `${TEST_BASE_URL}dummy_page.html`;
+
+async function addBookmark(bookmark) {
+ info("Creating bookmark and keyword");
+ let bm = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: bookmark.url,
+ title: bookmark.title,
+ });
+ if (bookmark.keyword) {
+ await PlacesUtils.keywords.insert({
+ keyword: bookmark.keyword,
+ url: bookmark.url,
+ });
+ }
+
+ registerCleanupFunction(async function () {
+ if (bookmark.keyword) {
+ await PlacesUtils.keywords.remove(bookmark.keyword);
+ }
+ await PlacesUtils.bookmarks.remove(bm);
+ });
+}
+
+/**
+ * Check that if the user hits enter and ctrl-t at the same time, we open the
+ * URL in the right tab.
+ */
+add_task(async function hitEnterLoadInRightTab() {
+ await addBookmark({
+ title: "Test for keyword bookmark and URL",
+ url: TEST_URL,
+ keyword: "urlbarkeyword",
+ });
+
+ info("Opening a tab");
+ let oldTabOpenPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabOpen"
+ );
+ BrowserOpenTab();
+ let oldTab = (await oldTabOpenPromise).target;
+ let oldTabLoadedPromise = BrowserTestUtils.browserLoaded(
+ oldTab.linkedBrowser,
+ false,
+ TEST_URL
+ ).then(() => info("Old tab loaded"));
+
+ info("Filling URL bar, sending <return> and opening a tab");
+ let tabOpenPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabOpen"
+ );
+ gURLBar.value = "urlbarkeyword";
+ gURLBar.focus();
+ gURLBar.select();
+ EventUtils.sendKey("return");
+
+ info("Immediately open a second tab");
+ BrowserOpenTab();
+ let newTab = (await tabOpenPromise).target;
+
+ info("Created new tab; waiting for tabs to load");
+ let newTabLoadedPromise = BrowserTestUtils.browserLoaded(
+ newTab.linkedBrowser,
+ false,
+ "about:newtab"
+ ).then(() => info("New tab loaded"));
+ // If one of the tabs loads the wrong page, this will timeout, and that
+ // indicates we regressed this bug fix.
+ await Promise.all([newTabLoadedPromise, oldTabLoadedPromise]);
+ // These are not particularly useful, but the test must contain some checks.
+ is(
+ newTab.linkedBrowser.currentURI.spec,
+ "about:newtab",
+ "New tab loaded about:newtab"
+ );
+ is(oldTab.linkedBrowser.currentURI.spec, TEST_URL, "Old tab loaded URL");
+
+ info("Closing tabs");
+ BrowserTestUtils.removeTab(newTab);
+ BrowserTestUtils.removeTab(oldTab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_redirect_error.js b/browser/components/urlbar/tests/browser/browser_redirect_error.js
new file mode 100644
index 0000000000..2fc6155cd5
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_redirect_error.js
@@ -0,0 +1,137 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const REDIRECT_FROM = `${TEST_BASE_URL}redirect_error.sjs`;
+
+const REDIRECT_TO = "https://www.bank1.com/"; // Bad-cert host.
+
+function isRedirectedURISpec(aURISpec) {
+ return isRedirectedURI(Services.io.newURI(aURISpec));
+}
+
+function isRedirectedURI(aURI) {
+ // Compare only their before-hash portion.
+ return Services.io.newURI(REDIRECT_TO).equalsExceptRef(aURI);
+}
+
+/*
+ Test.
+
+1. Load redirect_bug623155.sjs#BG in a background tab.
+
+2. The redirected URI is <https://www.bank1.com/#BG>, which displayes a cert
+ error page.
+
+3. Switch the tab to foreground.
+
+4. Check the URLbar's value, expecting <https://www.bank1.com/#BG>
+
+5. Load redirect_bug623155.sjs#FG in the foreground tab.
+
+6. The redirected URI is <https://www.bank1.com/#FG>. And this is also
+ a cert-error page.
+
+7. Check the URLbar's value, expecting <https://www.bank1.com/#FG>
+
+8. End.
+
+ */
+
+var gNewTab;
+
+function test() {
+ waitForExplicitFinish();
+
+ // Load a URI in the background.
+ gNewTab = BrowserTestUtils.addTab(gBrowser, REDIRECT_FROM + "#BG");
+ gBrowser
+ .getBrowserForTab(gNewTab)
+ .webProgress.addProgressListener(
+ gWebProgressListener,
+ Ci.nsIWebProgress.NOTIFY_LOCATION
+ );
+}
+
+var gWebProgressListener = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+
+ // ---------------------------------------------------------------------------
+ // NOTIFY_LOCATION mode should work fine without these methods.
+ //
+ // onStateChange: function() {},
+ // onStatusChange: function() {},
+ // onProgressChange: function() {},
+ // onSecurityChange: function() {},
+ // ----------------------------------------------------------------------------
+
+ onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
+ if (!aRequest) {
+ // This is bug 673752, or maybe initial "about:blank".
+ return;
+ }
+
+ ok(gNewTab, "There is a new tab.");
+ ok(
+ isRedirectedURI(aLocation),
+ "onLocationChange catches only redirected URI."
+ );
+
+ if (aLocation.ref == "BG") {
+ // This is background tab's request.
+ isnot(gNewTab, gBrowser.selectedTab, "This is a background tab.");
+ } else if (aLocation.ref == "FG") {
+ // This is foreground tab's request.
+ is(gNewTab, gBrowser.selectedTab, "This is a foreground tab.");
+ } else {
+ // We shonuld not reach here.
+ ok(false, "This URI hash is not expected:" + aLocation.ref);
+ }
+
+ let isSelectedTab = gNewTab.selected;
+ setTimeout(delayed, 0, isSelectedTab);
+ },
+};
+
+function delayed(aIsSelectedTab) {
+ // Switch tab and confirm URL bar.
+ if (!aIsSelectedTab) {
+ gBrowser.selectedTab = gNewTab;
+ }
+
+ let currentURI = gBrowser.selectedBrowser.currentURI.spec;
+ ok(
+ isRedirectedURISpec(currentURI),
+ "The content area is redirected. aIsSelectedTab:" + aIsSelectedTab
+ );
+ is(
+ gURLBar.value,
+ currentURI,
+ "The URL bar shows the content URI. aIsSelectedTab:" + aIsSelectedTab
+ );
+
+ if (!aIsSelectedTab) {
+ // If this was a background request, go on a foreground request.
+ BrowserTestUtils.loadURIString(
+ gBrowser.selectedBrowser,
+ REDIRECT_FROM + "#FG"
+ );
+ } else {
+ // Othrewise, nothing to do remains.
+ finish();
+ }
+}
+
+/* Cleanup */
+registerCleanupFunction(function () {
+ if (gNewTab) {
+ gBrowser
+ .getBrowserForTab(gNewTab)
+ .webProgress.removeProgressListener(gWebProgressListener);
+
+ gBrowser.removeTab(gNewTab);
+ }
+ gNewTab = null;
+});
diff --git a/browser/components/urlbar/tests/browser/browser_remoteness_switch.js b/browser/components/urlbar/tests/browser/browser_remoteness_switch.js
new file mode 100644
index 0000000000..364eeff1b2
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_remoteness_switch.js
@@ -0,0 +1,56 @@
+"use strict";
+
+/**
+ * Verify that when loading and going back/forward through history between URLs
+ * loaded in the content process, and URLs loaded in the parent process, we
+ * don't set the URL for the tab to about:blank inbetween the loads.
+ */
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.navigation.requireUserInteraction", false]],
+ });
+ let url = "http://www.example.com/foo.html";
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url },
+ async function (browser) {
+ let wpl = {
+ onLocationChange(unused, unused2, location) {
+ if (location.schemeIs("about")) {
+ is(
+ location.spec,
+ "about:config",
+ "Only about: location change should be for about:preferences"
+ );
+ } else {
+ is(
+ location.spec,
+ url,
+ "Only non-about: location change should be for the http URL we're dealing with."
+ );
+ }
+ },
+ };
+ gBrowser.addProgressListener(wpl);
+
+ let didLoad = BrowserTestUtils.browserLoaded(
+ browser,
+ null,
+ function (loadedURL) {
+ return loadedURL == "about:config";
+ }
+ );
+ BrowserTestUtils.loadURIString(browser, "about:config");
+ await didLoad;
+
+ gBrowser.goBack();
+ await BrowserTestUtils.browserLoaded(browser, null, function (loadedURL) {
+ return url == loadedURL;
+ });
+ gBrowser.goForward();
+ await BrowserTestUtils.browserLoaded(browser, null, function (loadedURL) {
+ return loadedURL == "about:config";
+ });
+ gBrowser.removeProgressListener(wpl);
+ }
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_remotetab.js b/browser/components/urlbar/tests/browser/browser_remotetab.js
new file mode 100644
index 0000000000..1fde855dbd
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_remotetab.js
@@ -0,0 +1,111 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This tests checks that the remote tab result is displayed and can be
+ * selected.
+ */
+
+"use strict";
+
+const { SyncedTabs } = ChromeUtils.importESModule(
+ "resource://services-sync/SyncedTabs.sys.mjs"
+);
+
+const TEST_URL = `${TEST_BASE_URL}dummy_page.html`;
+
+const REMOTE_TAB = {
+ id: "7cqCr77ptzX3",
+ type: "client",
+ lastModified: 1492201200,
+ name: "zcarter's Nightly on MacBook-Pro-25",
+ clientType: "desktop",
+ tabs: [
+ {
+ type: "tab",
+ title: "Test Remote",
+ url: TEST_URL,
+ icon: UrlbarUtils.ICON.DEFAULT,
+ client: "7cqCr77ptzX3",
+ lastUsed: Math.floor(Date.now() / 1000),
+ },
+ ],
+};
+
+add_setup(async function () {
+ sandbox = sinon.createSandbox();
+
+ let originalSyncedTabsInternal = SyncedTabs._internal;
+ SyncedTabs._internal = {
+ isConfiguredToSyncTabs: true,
+ hasSyncedThisSession: true,
+ getTabClients() {
+ return Promise.resolve([]);
+ },
+ syncTabs() {
+ return Promise.resolve();
+ },
+ };
+
+ // Tell the Sync XPCOM service it is initialized.
+ let weaveXPCService = Cc["@mozilla.org/weave/service;1"].getService(
+ Ci.nsISupports
+ ).wrappedJSObject;
+ let oldWeaveServiceReady = weaveXPCService.ready;
+ weaveXPCService.ready = true;
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.autoFill", false],
+ ["services.sync.username", "fake"],
+ ["services.sync.syncedTabs.showRemoteTabs", true],
+ ],
+ });
+
+ sandbox
+ .stub(SyncedTabs._internal, "getTabClients")
+ .callsFake(() => Promise.resolve(Cu.cloneInto([REMOTE_TAB], {})));
+
+ registerCleanupFunction(async () => {
+ sandbox.restore();
+ weaveXPCService.ready = oldWeaveServiceReady;
+ SyncedTabs._internal = originalSyncedTabsInternal;
+ });
+});
+
+add_task(async function test_remotetab_opens() {
+ await BrowserTestUtils.withNewTab(
+ { url: "about:robots", gBrowser },
+ async function () {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "Test Remote",
+ });
+
+ // There should be two items in the pop-up, the first is the default search
+ // suggestion, the second is the remote tab.
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.REMOTE_TAB,
+ "Should be the remote tab entry"
+ );
+
+ // The URL is going to open in the current tab as it is currently about:blank
+ let promiseTabLoaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await promiseTabLoaded;
+
+ Assert.equal(
+ gBrowser.selectedTab.linkedBrowser.currentURI.spec,
+ TEST_URL,
+ "correct URL loaded"
+ );
+ }
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_removeUnsafeProtocolsFromURLBarPaste.js b/browser/components/urlbar/tests/browser/browser_removeUnsafeProtocolsFromURLBarPaste.js
new file mode 100644
index 0000000000..4dfbc5c01b
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_removeUnsafeProtocolsFromURLBarPaste.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Ensures that pasting unsafe protocols in the urlbar have the protocol
+ * correctly stripped.
+ */
+
+var pairs = [
+ ["javascript:", ""],
+ ["javascript:1+1", "1+1"],
+ ["javascript:document.domain", "document.domain"],
+ [
+ " \u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\u0009javascript:document.domain",
+ "document.domain",
+ ],
+ ["java\nscript:foo", "foo"],
+ ["java\tscript:foo", "foo"],
+ ["http://\nexample.com", "http://example.com"],
+ ["http://\nexample.com\n", "http://example.com"],
+ ["data:text/html,<body>hi</body>", "data:text/html,<body>hi</body>"],
+ ["javaScript:foopy", "foopy"],
+ ["javaScript:javaScript:alert('hi')", "alert('hi')"],
+ // Nested things get confusing because some things don't parse as URIs:
+ ["javascript:javascript:alert('hi!')", "alert('hi!')"],
+ [
+ "data:data:text/html,<body>hi</body>",
+ "data:data:text/html,<body>hi</body>",
+ ],
+ ["javascript:data:javascript:alert('hi!')", "data:javascript:alert('hi!')"],
+ [
+ "javascript:data:text/html,javascript:alert('hi!')",
+ "data:text/html,javascript:alert('hi!')",
+ ],
+ [
+ "data:data:text/html,javascript:alert('hi!')",
+ "data:data:text/html,javascript:alert('hi!')",
+ ],
+];
+
+let supportsNullBytes = AppConstants.platform == "macosx";
+// Note that \u000d (\r) is missing here; we test it separately because it
+// makes the test sad on Windows.
+let nonsense =
+ "\u000a\u000b\u000c\u000e\u000f\u0010\u0011\u0012\u0013\u0014javascript:foo";
+if (supportsNullBytes) {
+ nonsense = "\u0000" + nonsense;
+}
+pairs.push([nonsense, "foo"]);
+
+let supportsReturnWithoutNewline =
+ AppConstants.platform != "win" && AppConstants.platform != "linux";
+if (supportsReturnWithoutNewline) {
+ pairs.push(["java\rscript:foo", "foo"]);
+}
+
+async function paste(input) {
+ try {
+ await SimpleTest.promiseClipboardChange(
+ aData => {
+ // This test checks how "\r" is treated. Therefore, we cannot specify
+ // string here and instead, we need to compare strictly with this
+ // function.
+ return aData === input;
+ },
+ () => {
+ clipboardHelper.copyString(input);
+ }
+ );
+ } catch (ex) {
+ Assert.ok(false, "Failed to copy string '" + input + "' to clipboard");
+ }
+
+ document.commandDispatcher
+ .getControllerForCommand("cmd_paste")
+ .doCommand("cmd_paste");
+}
+
+add_task(async function test_stripUnsafeProtocolPaste() {
+ for (let [inputValue, expectedURL] of pairs) {
+ gURLBar.value = "";
+ gURLBar.focus();
+ await paste(inputValue);
+
+ Assert.equal(
+ gURLBar.value,
+ expectedURL,
+ `entering ${inputValue} strips relevant bits.`
+ );
+
+ await new Promise(resolve => setTimeout(resolve, 0));
+ }
+});
diff --git a/browser/components/urlbar/tests/browser/browser_remove_match.js b/browser/components/urlbar/tests/browser/browser_remove_match.js
new file mode 100644
index 0000000000..94a1c874bf
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_remove_match.js
@@ -0,0 +1,297 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+ChromeUtils.defineESModuleGetters(this, {
+ FormHistory: "resource://gre/modules/FormHistory.sys.mjs",
+});
+
+add_setup(async function () {
+ await SearchTestUtils.installSearchExtension({}, { setAsDefault: true });
+
+ let engine = Services.search.getEngineByName("Example");
+ await Services.search.moveEngine(engine, 0);
+});
+
+add_task(async function test_remove_history() {
+ const TEST_URL = "http://remove.me/from_urlbar/";
+ await PlacesTestUtils.addVisits(TEST_URL);
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.history.clear();
+ });
+
+ let promiseVisitRemoved = PlacesTestUtils.waitForNotification(
+ "page-removed",
+ events => events[0].url === TEST_URL
+ );
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "from_urlbar",
+ });
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.url, TEST_URL, "Found the expected result");
+
+ let expectedResultCount = UrlbarTestUtils.getResultCount(window) - 1;
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), 1);
+ EventUtils.synthesizeKey("KEY_Delete", { shiftKey: true });
+
+ const removeEvents = await promiseVisitRemoved;
+ Assert.ok(
+ removeEvents[0].isRemovedFromStore,
+ "isRemovedFromStore should be true"
+ );
+
+ await TestUtils.waitForCondition(
+ () => UrlbarTestUtils.getResultCount(window) == expectedResultCount,
+ "Waiting for the result to disappear"
+ );
+
+ for (let i = 0; i < expectedResultCount; i++) {
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ Assert.notEqual(
+ details.url,
+ TEST_URL,
+ "Should not find the test URL in the remaining results"
+ );
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+add_task(async function test_remove_form_history() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.searches", true],
+ ["browser.urlbar.maxHistoricalSearchSuggestions", 1],
+ ],
+ });
+
+ let formHistoryValue = "foobar";
+ await UrlbarTestUtils.formHistory.add([formHistoryValue]);
+
+ let formHistory = (
+ await UrlbarTestUtils.formHistory.search({
+ value: formHistoryValue,
+ })
+ ).map(entry => entry.value);
+ Assert.deepEqual(
+ formHistory,
+ [formHistoryValue],
+ "Should find form history after adding it"
+ );
+
+ let promiseRemoved = UrlbarTestUtils.formHistory.promiseChanged("remove");
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ });
+
+ let index = 1;
+ let count = UrlbarTestUtils.getResultCount(window);
+ for (; index < count; index++) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index);
+ if (
+ result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
+ result.source == UrlbarUtils.RESULT_SOURCE.HISTORY
+ ) {
+ break;
+ }
+ }
+ Assert.ok(index < count, "Result found");
+
+ EventUtils.synthesizeKey("KEY_Tab", { repeat: index });
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), index);
+ EventUtils.synthesizeKey("KEY_Delete", { shiftKey: true });
+ await promiseRemoved;
+
+ await TestUtils.waitForCondition(
+ () => UrlbarTestUtils.getResultCount(window) == count - 1,
+ "Waiting for the result to disappear"
+ );
+
+ for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ Assert.ok(
+ result.type != UrlbarUtils.RESULT_TYPE.SEARCH ||
+ result.source != UrlbarUtils.RESULT_SOURCE.HISTORY,
+ "Should not find the form history result in the remaining results"
+ );
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ formHistory = (
+ await UrlbarTestUtils.formHistory.search({
+ value: formHistoryValue,
+ })
+ ).map(entry => entry.value);
+ Assert.deepEqual(
+ formHistory,
+ [],
+ "Should not find form history after removing it"
+ );
+
+ await SpecialPowers.popPrefEnv();
+});
+
+// We shouldn't be able to remove a bookmark item.
+add_task(async function test_remove_bookmark_doesnt() {
+ const TEST_URL = "http://dont.remove.me/from_urlbar/";
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "test",
+ url: TEST_URL,
+ });
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "from_urlbar",
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.url, TEST_URL, "Found the expected result");
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), 1);
+ EventUtils.synthesizeKey("KEY_Delete", { shiftKey: true });
+
+ // We don't have an easy way of determining if the event was process or not,
+ // so let any event queues clear before testing.
+ await new Promise(resolve => setTimeout(resolve, 0));
+ await PlacesTestUtils.promiseAsyncUpdates();
+
+ Assert.ok(
+ await PlacesUtils.bookmarks.fetch({ url: TEST_URL }),
+ "Should still have the URL bookmarked."
+ );
+});
+
+add_task(async function test_searchMode_removeRestyledHistory() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.searches", true],
+ ["browser.urlbar.maxHistoricalSearchSuggestions", 1],
+ ],
+ });
+
+ let query = "ciao";
+ let url = `https://example.com/?q=${query}bar`;
+ await PlacesTestUtils.addVisits(url);
+
+ await BrowserTestUtils.withNewTab("about:robots", async function (browser) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: query,
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.SEARCH);
+ Assert.equal(result.source, UrlbarUtils.RESULT_SOURCE.HISTORY);
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), 1);
+ EventUtils.synthesizeKey("KEY_Delete", { shiftKey: true });
+ await TestUtils.waitForCondition(
+ async () => !(await PlacesTestUtils.isPageInDB(url)),
+ "Wait for url to be removed from history"
+ );
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 1,
+ "Urlbar result should be removed"
+ );
+
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+ await UrlbarTestUtils.promisePopupClose(window);
+ });
+ await PlacesUtils.history.clear();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function blockButton() {
+ if (UrlbarPrefs.get("resultMenu")) {
+ // This case is covered by browser_result_menu.js.
+ return;
+ }
+
+ let url = "https://example.com/has-block-button";
+ let provider = new UrlbarTestUtils.TestProvider({
+ priority: Infinity,
+ results: [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ url,
+ isBlockable: true,
+ blockL10n: { id: "firefox-suggest-urlbar-block" },
+ }
+ ),
+ ],
+ });
+
+ // Implement the provider's `onEngagement()` so it removes the result.
+ let onEngagementCallCount = 0;
+ provider.onEngagement = (isPrivate, state, queryContext, details) => {
+ onEngagementCallCount++;
+ queryContext.view.controller.removeResult(details.result);
+ };
+
+ UrlbarProvidersManager.registerProvider(provider);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 1,
+ "There should be one result"
+ );
+
+ let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
+ Assert.equal(
+ row.result.payload.url,
+ url,
+ "The result should be in the first row"
+ );
+
+ let button = row.querySelector(".urlbarView-button-block");
+ Assert.ok(button, "The row should have a block button");
+
+ info("Tabbing down to block button");
+ EventUtils.synthesizeKey("KEY_Tab", { repeat: 2 });
+
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElement(window),
+ button,
+ "The block button should be selected after tabbing down"
+ );
+
+ info("Pressing Enter on block button");
+ EventUtils.synthesizeKey("KEY_Enter");
+
+ Assert.equal(
+ onEngagementCallCount,
+ 1,
+ "onEngagement() should have been called once"
+ );
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 0,
+ "There should be no results after blocking"
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ UrlbarProvidersManager.unregisterProvider(provider);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_restoreEmptyInput.js b/browser/components/urlbar/tests/browser/browser_restoreEmptyInput.js
new file mode 100644
index 0000000000..096d8e2134
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_restoreEmptyInput.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// When the input is empty and the view is opened, keying down through the
+// results and then out of the results should restore the empty input.
+
+"use strict";
+
+add_task(async function test() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.quickactions", false]],
+ });
+
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits("http://example.com/");
+ }
+ // Update Top Sites to make sure the last Top Site is a URL. Otherwise, it
+ // would be a search shortcut and thus would not fill the Urlbar when
+ // selected.
+ await updateTopSites(sites => {
+ return (
+ sites &&
+ sites[sites.length - 1] &&
+ sites[sites.length - 1].url == "http://example.com/"
+ );
+ });
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.history.clear();
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ fireInputEvent: true,
+ });
+
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ -1,
+ "Nothing selected"
+ );
+
+ let resultCount = UrlbarTestUtils.getResultCount(window);
+ Assert.greater(resultCount, 0, "At least one result");
+
+ for (let i = 0; i < resultCount; i++) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ resultCount - 1,
+ "Last result selected"
+ );
+ Assert.notEqual(gURLBar.value, "", "Input should not be empty");
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ -1,
+ "Nothing selected"
+ );
+ Assert.equal(gURLBar.value, "", "Input should be empty");
+});
diff --git a/browser/components/urlbar/tests/browser/browser_resultSpan.js b/browser/components/urlbar/tests/browser/browser_resultSpan.js
new file mode 100644
index 0000000000..9b17fb71f5
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_resultSpan.js
@@ -0,0 +1,254 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that displaying results with resultSpan > 1 limits other results in
+// the view.
+
+const TEST_RESULTS = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/1" }
+ ),
+ makeTipResult(),
+];
+
+const MAX_RESULTS = UrlbarPrefs.get("maxRichResults");
+const TIP_SPAN = UrlbarUtils.getSpanForResult({
+ type: UrlbarUtils.RESULT_TYPE.TIP,
+});
+
+add_setup(async function () {
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.quickactions", false]],
+ });
+});
+
+// A restricting provider with one tip result and many history results.
+add_task(async function oneTip() {
+ let results = Array.from(TEST_RESULTS);
+ for (let i = TEST_RESULTS.length; i < MAX_RESULTS; i++) {
+ results.push(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: `http://mozilla.org/${i}` }
+ )
+ );
+ }
+
+ let expectedResults = Array.from(results).slice(
+ 0,
+ MAX_RESULTS - TIP_SPAN + 1
+ );
+
+ let provider = new UrlbarTestUtils.TestProvider({ results, priority: 1 });
+ UrlbarProvidersManager.registerProvider(provider);
+
+ let context = await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ value: "test",
+ window,
+ });
+
+ checkResults(context.results, expectedResults);
+
+ UrlbarProvidersManager.unregisterProvider(provider);
+ gURLBar.view.close();
+});
+
+// A restricting provider with three tip results and many history results.
+add_task(async function threeTips() {
+ let results = Array.from(TEST_RESULTS);
+ for (let i = 1; i < 3; i++) {
+ results.push(makeTipResult());
+ }
+ for (let i = 2; i < 15; i++) {
+ results.push(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: `http://mozilla.org/${i}` }
+ )
+ );
+ }
+
+ let expectedResults = Array.from(results).slice(
+ 0,
+ MAX_RESULTS - 3 * (TIP_SPAN - 1)
+ );
+
+ let provider = new UrlbarTestUtils.TestProvider({ results, priority: 1 });
+ UrlbarProvidersManager.registerProvider(provider);
+
+ let context = await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ value: "test",
+ window,
+ });
+
+ checkResults(context.results, expectedResults);
+
+ UrlbarProvidersManager.unregisterProvider(provider);
+ gURLBar.view.close();
+});
+
+// A non-restricting provider with one tip result and many history results.
+add_task(async function oneTip_nonRestricting() {
+ let results = Array.from(TEST_RESULTS);
+ for (let i = 2; i < 15; i++) {
+ results.push(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: `http://mozilla.org/${i}` }
+ )
+ );
+ }
+
+ let expectedResults = Array.from(results);
+
+ // UrlbarProviderHeuristicFallback's heuristic search result
+ expectedResults.unshift({
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ payload: {
+ engine: Services.search.defaultEngine.name,
+ query: "test",
+ },
+ });
+
+ expectedResults = expectedResults.slice(0, MAX_RESULTS - TIP_SPAN + 1);
+
+ let provider = new UrlbarTestUtils.TestProvider({ results });
+ UrlbarProvidersManager.registerProvider(provider);
+
+ let context = await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ value: "test",
+ window,
+ });
+
+ checkResults(context.results, expectedResults);
+
+ UrlbarProvidersManager.unregisterProvider(provider);
+ gURLBar.view.close();
+});
+
+// A non-restricting provider with three tip results and many history results.
+add_task(async function threeTips_nonRestricting() {
+ let results = Array.from(TEST_RESULTS);
+ for (let i = 1; i < 3; i++) {
+ results.push(makeTipResult());
+ }
+ for (let i = 2; i < 15; i++) {
+ results.push(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: `http://mozilla.org/${i}` }
+ )
+ );
+ }
+
+ let expectedResults = Array.from(results);
+
+ // UrlbarProviderHeuristicFallback's heuristic search result
+ expectedResults.unshift({
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ payload: {
+ engine: Services.search.defaultEngine.name,
+ query: "test",
+ },
+ });
+
+ expectedResults = expectedResults.slice(0, MAX_RESULTS - 3 * (TIP_SPAN - 1));
+
+ let provider = new UrlbarTestUtils.TestProvider({ results });
+ UrlbarProvidersManager.registerProvider(provider);
+
+ let context = await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ value: "test",
+ window,
+ });
+
+ checkResults(context.results, expectedResults);
+
+ UrlbarProvidersManager.unregisterProvider(provider);
+ gURLBar.view.close();
+});
+
+add_task(async function customValue() {
+ let results = [];
+ for (let i = 0; i < 15; i++) {
+ results.push(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: `http://mozilla.org/${i}` }
+ )
+ );
+ }
+
+ results[1].resultSpan = 5;
+
+ let expectedResults = Array.from(results);
+ expectedResults = expectedResults.slice(0, 6);
+
+ let provider = new UrlbarTestUtils.TestProvider({ results });
+ UrlbarProvidersManager.registerProvider(provider);
+
+ let context = await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ value: "test",
+ window,
+ });
+
+ checkResults(context.results, expectedResults);
+
+ UrlbarProvidersManager.unregisterProvider(provider);
+ gURLBar.view.close();
+});
+
+function checkResults(actual, expected) {
+ Assert.equal(actual.length, expected.length, "Number of results");
+ for (let i = 0; i < expected.length; i++) {
+ info(`Checking results at index ${i}`);
+ let actualResult = collectExpectedProperties(actual[i], expected[i]);
+ Assert.deepEqual(actualResult, expected[i], "Actual vs. expected result");
+ }
+}
+
+function collectExpectedProperties(actualObj, expectedObj) {
+ let newActualObj = {};
+ for (let name in expectedObj) {
+ if (typeof expectedObj[name] == "object") {
+ newActualObj[name] = collectExpectedProperties(
+ actualObj[name],
+ expectedObj[name]
+ );
+ } else {
+ newActualObj[name] = expectedObj[name];
+ }
+ }
+ return newActualObj;
+}
+
+function makeTipResult() {
+ return new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TIP,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ helpUrl: "http://example.com/",
+ type: "test",
+ titleL10n: { id: "urlbar-search-tips-confirm" },
+ buttons: [
+ {
+ url: "http://example.com/",
+ l10n: { id: "urlbar-search-tips-confirm" },
+ },
+ ],
+ }
+ );
+}
diff --git a/browser/components/urlbar/tests/browser/browser_result_menu.js b/browser/components/urlbar/tests/browser/browser_result_menu.js
new file mode 100644
index 0000000000..b5df97f863
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_result_menu.js
@@ -0,0 +1,266 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.resultMenu", true]],
+ });
+});
+
+add_task(async function test_history() {
+ const TEST_URL = "https://remove.me/from_urlbar/";
+ await PlacesTestUtils.addVisits(TEST_URL);
+ registerCleanupFunction(async function () {
+ await PlacesUtils.history.clear();
+ });
+
+ const resultIndex = 1;
+ let result;
+ let startQuery = async () => {
+ await UrlbarTestUtils.promisePopupClose(window);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "from_urlbar",
+ });
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex);
+ Assert.equal(result.url, TEST_URL, "Found the expected result");
+ gURLBar.view.selectedRowIndex = resultIndex;
+ };
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.resultMenu.keyboardAccessible", false]],
+ });
+ await startQuery();
+ EventUtils.synthesizeKey("KEY_Tab");
+ isnot(
+ UrlbarTestUtils.getSelectedElement(window),
+ UrlbarTestUtils.getButtonForResultIndex(window, "menu", resultIndex),
+ "Tab key skips over menu button with resultMenu.keyboardAccessible pref set to false"
+ );
+ info(
+ "Checking that the mouse can still activate the menu button with resultMenu.keyboardAccessible = false"
+ );
+ await UrlbarTestUtils.openResultMenu(window, {
+ byMouse: true,
+ resultIndex,
+ });
+ gURLBar.view.resultMenu.hidePopup();
+ await SpecialPowers.popPrefEnv();
+ await startQuery();
+ EventUtils.synthesizeKey("KEY_Tab");
+ is(
+ UrlbarTestUtils.getSelectedElement(window),
+ UrlbarTestUtils.getButtonForResultIndex(window, "menu", resultIndex),
+ "Tab key doesn't skip over menu button with resultMenu.keyboardAccessible pref reset to true"
+ );
+
+ info("Checking that Space activates the menu button");
+ await startQuery();
+ await UrlbarTestUtils.openResultMenu(window, {
+ activationKey: " ",
+ });
+ gURLBar.view.resultMenu.hidePopup();
+
+ info("Selecting Learn more item from the result menu");
+ let tabOpenPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ Services.urlFormatter.formatURLPref("app.support.baseURL") +
+ "awesome-bar-result-menu"
+ );
+ await UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "L");
+ info("Waiting for Learn more link to open in a new tab");
+ await tabOpenPromise;
+ gBrowser.removeCurrentTab();
+
+ info("Restarting query in order to remove history entry via the menu");
+ await startQuery();
+ let promiseVisitRemoved = PlacesTestUtils.waitForNotification(
+ "page-removed",
+ events => events[0].url === TEST_URL
+ );
+ let expectedResultCount = UrlbarTestUtils.getResultCount(window) - 1;
+
+ await UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "R");
+ const removeEvents = await promiseVisitRemoved;
+ Assert.ok(
+ removeEvents[0].isRemovedFromStore,
+ "isRemovedFromStore should be true"
+ );
+ await TestUtils.waitForCondition(
+ () => UrlbarTestUtils.getResultCount(window) == expectedResultCount,
+ "Waiting for the result to disappear"
+ );
+ for (let i = 0; i < expectedResultCount; i++) {
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ Assert.notEqual(
+ details.url,
+ TEST_URL,
+ "Should not find the test URL in the remaining results"
+ );
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+add_task(async function test_remove_search_history() {
+ await SearchTestUtils.installSearchExtension({}, { setAsDefault: true });
+ let engine = Services.search.getEngineByName("Example");
+ await Services.search.moveEngine(engine, 0);
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.searches", true],
+ ["browser.urlbar.maxHistoricalSearchSuggestions", 1],
+ ],
+ });
+
+ let formHistoryValue = "foobar";
+ await UrlbarTestUtils.formHistory.add([formHistoryValue]);
+
+ let formHistory = (
+ await UrlbarTestUtils.formHistory.search({
+ value: formHistoryValue,
+ })
+ ).map(entry => entry.value);
+ Assert.deepEqual(
+ formHistory,
+ [formHistoryValue],
+ "Should find form history after adding it"
+ );
+
+ let promiseRemoved = UrlbarTestUtils.formHistory.promiseChanged("remove");
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ });
+
+ let resultIndex = 1;
+ let count = UrlbarTestUtils.getResultCount(window);
+ for (; resultIndex < count; resultIndex++) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ resultIndex
+ );
+ if (
+ result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
+ result.source == UrlbarUtils.RESULT_SOURCE.HISTORY
+ ) {
+ break;
+ }
+ }
+ Assert.ok(resultIndex < count, "Result found");
+
+ await UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "R", {
+ resultIndex,
+ });
+ await promiseRemoved;
+
+ await TestUtils.waitForCondition(
+ () => UrlbarTestUtils.getResultCount(window) == count - 1,
+ "Waiting for the result to disappear"
+ );
+
+ for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ Assert.ok(
+ result.type != UrlbarUtils.RESULT_TYPE.SEARCH ||
+ result.source != UrlbarUtils.RESULT_SOURCE.HISTORY,
+ "Should not find the form history result in the remaining results"
+ );
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ formHistory = (
+ await UrlbarTestUtils.formHistory.search({
+ value: formHistoryValue,
+ })
+ ).map(entry => entry.value);
+ Assert.deepEqual(
+ formHistory,
+ [],
+ "Should not find form history after removing it"
+ );
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function firefoxSuggest() {
+ const url = "https://example.com/hey-there";
+ const helpUrl = "https://example.com/help";
+ let provider = new UrlbarTestUtils.TestProvider({
+ priority: Infinity,
+ results: [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ url,
+ isBlockable: true,
+ blockL10n: { id: "urlbar-result-menu-dismiss-firefox-suggest" },
+ helpUrl,
+ helpL10n: {
+ id: "urlbar-result-menu-learn-more-about-firefox-suggest",
+ },
+ }
+ ),
+ ],
+ });
+
+ // Implement the provider's `onEngagement()` so it removes the result.
+ let onEngagementCallCount = 0;
+ provider.onEngagement = (isPrivate, state, queryContext, details) => {
+ onEngagementCallCount++;
+ queryContext.view.controller.removeResult(details.result);
+ };
+
+ UrlbarProvidersManager.registerProvider(provider);
+
+ async function openResults() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 1,
+ "There should be one result"
+ );
+
+ let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
+ Assert.equal(
+ row.result.payload.url,
+ url,
+ "The result should be in the first row"
+ );
+ }
+
+ await openResults();
+ let tabOpenPromise = BrowserTestUtils.waitForNewTab(gBrowser, helpUrl);
+ await UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "L", {
+ resultIndex: 0,
+ });
+ info("Waiting for help URL to load in a new tab");
+ await tabOpenPromise;
+ gBrowser.removeCurrentTab();
+
+ await openResults();
+ await UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "D", {
+ resultIndex: 0,
+ });
+
+ Assert.greater(
+ onEngagementCallCount,
+ 0,
+ "onEngagement() should have been called"
+ );
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 0,
+ "There should be no results after blocking"
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ UrlbarProvidersManager.unregisterProvider(provider);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_result_onSelection.js b/browser/components/urlbar/tests/browser/browser_result_onSelection.js
new file mode 100644
index 0000000000..18c16a3072
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_result_onSelection.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test() {
+ let results = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/1" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/2" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TIP,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ helpUrl: "http://example.com/",
+ type: "test",
+ titleL10n: { id: "urlbar-search-tips-confirm" },
+ buttons: [
+ {
+ url: "http://example.com/",
+ l10n: { id: "urlbar-search-tips-confirm" },
+ },
+ ],
+ }
+ ),
+ ];
+
+ results[0].heuristic = true;
+
+ let selectionCount = 0;
+ let provider = new UrlbarTestUtils.TestProvider({
+ results,
+ priority: 1,
+ onSelection: (result, element) => {
+ selectionCount++;
+ },
+ });
+ UrlbarProvidersManager.registerProvider(provider);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+
+ EventUtils.synthesizeKey("KEY_Tab", {
+ repeat: UrlbarPrefs.get("resultMenu") ? 5 : 3,
+ });
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ ok(
+ UrlbarTestUtils.getOneOffSearchButtons(window).selectedButton,
+ "a one off button is selected"
+ );
+
+ Assert.equal(
+ selectionCount,
+ UrlbarPrefs.get("resultMenu") ? 6 : 4,
+ "Number of elements selected in the view."
+ );
+ UrlbarProvidersManager.unregisterProvider(provider);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_retainedResultsOnFocus.js b/browser/components/urlbar/tests/browser/browser_retainedResultsOnFocus.js
new file mode 100644
index 0000000000..22ec47403e
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_retainedResultsOnFocus.js
@@ -0,0 +1,435 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests retained results.
+// When there is a pending search (user typed a search string and blurred
+// without picking a result), on focus we should the search results again.
+
+async function checkPanelStatePersists(win, isOpen) {
+ // Check for popup events, we should not see any of them because the urlbar
+ // popup state should not change. This also ensures we don't cause flickering
+ // open/close actions.
+ function handler(event) {
+ Assert.ok(false, `Received unexpected event ${event.type}`);
+ }
+ win.gURLBar.addEventListener("popupshowing", handler);
+ win.gURLBar.addEventListener("popuphiding", handler);
+ // Because the panel opening may not be immediate, we must wait a bit.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 300));
+ win.gURLBar.removeEventListener("popupshowing", handler);
+ win.gURLBar.removeEventListener("popuphiding", handler);
+ Assert.equal(
+ isOpen,
+ win.gURLBar.view.isOpen,
+ `check urlbar remains ${isOpen ? "open" : "closed"}`
+ );
+}
+
+async function checkOpensOnFocus(win, state) {
+ Assert.ok(!win.gURLBar.view.isOpen, "check urlbar panel is not open");
+ win.gURLBar.blur();
+
+ info("Check the keyboard shortcut.");
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ win.document.getElementById("Browser:OpenLocation").doCommand();
+ });
+
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ Assert.equal(state.selectionStart, win.gURLBar.selectionStart);
+ Assert.equal(state.selectionEnd, win.gURLBar.selectionEnd);
+
+ await UrlbarTestUtils.promisePopupClose(win, () => {
+ win.gURLBar.blur();
+ });
+ info("Focus with the mouse.");
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ EventUtils.synthesizeMouseAtCenter(win.gURLBar.inputField, {}, win);
+ });
+
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ Assert.equal(state.selectionStart, win.gURLBar.selectionStart);
+ Assert.equal(state.selectionEnd, win.gURLBar.selectionEnd);
+}
+
+async function checkDoesNotOpenOnFocus(win) {
+ Assert.ok(!win.gURLBar.view.isOpen, "check urlbar panel is not open");
+ win.gURLBar.blur();
+
+ info("Check the keyboard shortcut.");
+ let promiseState = checkPanelStatePersists(win, false);
+ win.document.getElementById("Browser:OpenLocation").doCommand();
+ await promiseState;
+ win.gURLBar.blur();
+ info("Focus with the mouse.");
+ promiseState = checkPanelStatePersists(win, false);
+ EventUtils.synthesizeMouseAtCenter(win.gURLBar.inputField, {}, win);
+ await promiseState;
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.autoFill", true]],
+ });
+ // Add some history for the empty panel and autofill.
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "https://example.com/",
+ transition: PlacesUtils.history.TRANSITIONS.TYPED,
+ },
+ {
+ uri: "https://example.com/foo/",
+ transition: PlacesUtils.history.TRANSITIONS.TYPED,
+ },
+ ]);
+ registerCleanupFunction(async function () {
+ await PlacesUtils.history.clear();
+ });
+});
+
+async function test_window(win) {
+ for (let url of ["about:newtab", "about:home", "https://example.com/"]) {
+ // withNewTab may hang on preloaded pages, thus instead of waiting for load
+ // we just wait for the expected currentURI value.
+ await BrowserTestUtils.withNewTab(
+ { gBrowser: win.gBrowser, url, waitForLoad: false },
+ async browser => {
+ await TestUtils.waitForCondition(
+ () => win.gBrowser.currentURI.spec == url,
+ "Ensure we're on the expected page"
+ );
+
+ // In one case use a value that triggers autofill.
+ let autofill = url == "https://example.com/";
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: autofill ? "ex" : "foo",
+ fireInputEvent: true,
+ });
+ let { value, selectionStart, selectionEnd } = win.gURLBar;
+ if (!autofill) {
+ selectionStart = 0;
+ }
+ info("expected " + value + " " + selectionStart + " " + selectionEnd);
+ await UrlbarTestUtils.promisePopupClose(win, () => {
+ win.gURLBar.blur();
+ });
+
+ info("The panel should open when there's a search string");
+ await checkOpensOnFocus(win, { value, selectionStart, selectionEnd });
+ await UrlbarTestUtils.promisePopupClose(win, () => {
+ win.gURLBar.blur();
+ });
+ }
+ );
+ }
+}
+
+add_task(async function test_normalWindow() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await test_window(win);
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_privateWindow() {
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ await test_window(privateWin);
+ await BrowserTestUtils.closeWindow(privateWin);
+});
+
+add_task(async function test_tabSwitch() {
+ info("Check that switching tabs reopens the view.");
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "ex",
+ fireInputEvent: true,
+ });
+ let { value, selectionStart, selectionEnd } = win.gURLBar;
+ Assert.equal(value, "example.com/", "Check autofill value");
+ Assert.ok(
+ selectionStart > 0 && selectionEnd > selectionStart,
+ "Check autofill selection"
+ );
+
+ Assert.ok(win.gURLBar.focused, "The urlbar should be focused");
+ let tab1 = win.gBrowser.selectedTab;
+
+ async function check_autofill() {
+ // The urlbar code waits for both TabSelect and the focus change, thus
+ // we can't just wait for search completion here, we have to poll for a
+ // value.
+ await TestUtils.waitForCondition(
+ () => win.gURLBar.value == "example.com/",
+ "wait for autofill value"
+ );
+ // Ensure stable results.
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ Assert.equal(selectionStart, win.gURLBar.selectionStart);
+ Assert.equal(selectionEnd, win.gURLBar.selectionEnd);
+ }
+
+ info("Open a new tab with the same search");
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(win.gBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "ex",
+ fireInputEvent: true,
+ });
+
+ info("Switch across tabs");
+ for (let tab of win.gBrowser.tabs) {
+ await UrlbarTestUtils.promisePopupOpen(win, async () => {
+ await BrowserTestUtils.switchTab(win.gBrowser, tab);
+ });
+ await check_autofill();
+ }
+
+ info("Close tab and check the view is open.");
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ BrowserTestUtils.removeTab(tab2);
+ });
+ await check_autofill();
+
+ info("Open a new tab with a different search");
+ tab2 = await BrowserTestUtils.openNewForegroundTab(win.gBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "xam",
+ fireInputEvent: true,
+ });
+
+ info("Switch to the first tab and check the panel remains open");
+ let promiseState = checkPanelStatePersists(win, true);
+ await BrowserTestUtils.switchTab(win.gBrowser, tab1);
+ await promiseState;
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ await check_autofill();
+
+ info("Switch to the second tab and check the panel remains open");
+ promiseState = checkPanelStatePersists(win, true);
+ await BrowserTestUtils.switchTab(win.gBrowser, tab2);
+ await promiseState;
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ Assert.equal(win.gURLBar.value, "xam", "check value");
+ Assert.equal(win.gURLBar.selectionStart, 3);
+ Assert.equal(win.gURLBar.selectionEnd, 3);
+
+ info("autofill in tab2, switch to tab1, then back to tab2 with the mouse");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "e",
+ fireInputEvent: true,
+ });
+ // Adjust selection start, we are using a different search string.
+ await BrowserTestUtils.switchTab(win.gBrowser, tab1);
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ await check_autofill();
+ tab2.click();
+ selectionStart = 1;
+ await check_autofill();
+
+ info("Check we don't rerun a search if the shortcut is used on an open view");
+ EventUtils.synthesizeKey("KEY_Backspace", {}, win);
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ Assert.ok(win.gURLBar.view.isOpen, "The view should be open");
+ Assert.equal(win.gURLBar.value, "e", "The value should be the typed one");
+ win.document.getElementById("Browser:OpenLocation").doCommand();
+ // A search should not run here, so there's nothing to wait for.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 300));
+ Assert.ok(win.gURLBar.view.isOpen, "The view should be open");
+ Assert.equal(win.gURLBar.value, "e", "The value should not change");
+
+ info(
+ "Tab switch from an empty search tab with unfocused urlbar to a tab with a search string and a focused urlbar"
+ );
+ win.gURLBar.value = "";
+ win.gURLBar.blur();
+ await UrlbarTestUtils.promisePopupOpen(win, async () => {
+ await BrowserTestUtils.switchTab(win.gBrowser, tab1);
+ });
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_tabSwitch_pageproxystate() {
+ info("Switching tabs on valid pageproxystate doesn't reopen.");
+
+ info("Adding some visits for the empty panel");
+ await PlacesTestUtils.addVisits([
+ "https://example.com/",
+ "https://example.org/",
+ ]);
+ registerCleanupFunction(PlacesUtils.history.clear);
+
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ BrowserTestUtils.loadURIString(win.gBrowser.selectedBrowser, "about:robots");
+ let tab1 = win.gBrowser.selectedTab;
+
+ info("Open a new tab and the empty search");
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(win.gBrowser);
+ await UrlbarTestUtils.promisePopupOpen(win, async () => {
+ win.gURLBar.focus();
+ // On Linux and Mac down moves caret to the end of the text unless it's
+ // there already.
+ win.gURLBar.selectionStart = win.gURLBar.selectionEnd =
+ win.gURLBar.value.length;
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ });
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(win, 0);
+ Assert.notEqual(result.url, "about:robots");
+
+ info("Switch to the first tab and start searching with DOWN");
+ await UrlbarTestUtils.promisePopupClose(win, async () => {
+ await BrowserTestUtils.switchTab(win.gBrowser, tab1);
+ });
+ await checkPanelStatePersists(win, false);
+ await UrlbarTestUtils.promisePopupOpen(win, async () => {
+ win.gURLBar.focus();
+ // On Linux and Mac down moves caret to the end of the text unless it's
+ // there already.
+ win.gURLBar.selectionStart = win.gURLBar.selectionEnd =
+ win.gURLBar.value.length;
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ });
+ await UrlbarTestUtils.promiseSearchComplete(win);
+
+ info("Switcihng to the second tab should not reopen the search");
+ await UrlbarTestUtils.promisePopupClose(win, async () => {
+ await BrowserTestUtils.switchTab(win.gBrowser, tab2);
+ });
+ await checkPanelStatePersists(win, false);
+
+ info("Switching to the first tab should not reopen the search");
+ let promiseState = await checkPanelStatePersists(win, false);
+ await BrowserTestUtils.switchTab(win.gBrowser, tab1);
+ await promiseState;
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_tabSwitch_emptySearch() {
+ info("Switching between empty-search tabs should not reopen the view.");
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+
+ info("Open the empty search");
+ let tab1 = win.gBrowser.selectedTab;
+ await UrlbarTestUtils.promisePopupOpen(win, async () => {
+ win.gURLBar.focus();
+ // On Linux and Mac down moves caret to the end of the text unless it's
+ // there already.
+ win.gURLBar.selectionStart = win.gURLBar.selectionEnd =
+ win.gURLBar.value.length;
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ });
+ await UrlbarTestUtils.promiseSearchComplete(win);
+
+ info("Open a new tab and the empty search");
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(win.gBrowser);
+ await UrlbarTestUtils.promisePopupOpen(win, async () => {
+ win.gURLBar.focus();
+ // On Linux and Mac down moves caret to the end of the text unless it's
+ // there already.
+ win.gURLBar.selectionStart = win.gURLBar.selectionEnd =
+ win.gURLBar.value.length;
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ });
+ await UrlbarTestUtils.promiseSearchComplete(win);
+
+ info("Switching to the first tab should not reopen the view");
+ await UrlbarTestUtils.promisePopupClose(win, async () => {
+ await BrowserTestUtils.switchTab(win.gBrowser, tab1);
+ });
+ await checkPanelStatePersists(win, false);
+
+ info("Switching to the second tab should not reopen the view");
+ let promiseState = await checkPanelStatePersists(win, false);
+ await BrowserTestUtils.switchTab(win.gBrowser, tab2);
+ await promiseState;
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_pageproxystate_valid() {
+ info("Focusing on valid pageproxystate should not reopen the view.");
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+
+ info("Search for a full url and confirm it with Enter");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "about:robots",
+ fireInputEvent: true,
+ });
+ let loadedPromise = BrowserTestUtils.browserLoaded(
+ win.gBrowser.selectedBrowser
+ );
+ EventUtils.synthesizeKey("KEY_Enter", {}, win);
+ await loadedPromise;
+
+ Assert.ok(!win.gURLBar.focused, "The urlbar should not be focused");
+ info("Focus the urlbar");
+ win.document.getElementById("Browser:OpenLocation").doCommand();
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_allowAutofill() {
+ info("Check we respect allowAutofill from the last search");
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+
+ await UrlbarTestUtils.promisePopupOpen(win, async () => {
+ await selectAndPaste("e", win);
+ });
+ Assert.equal(win.gURLBar.value, "e", "Should not autofill");
+ let context = await win.gURLBar.lastQueryContextPromise;
+ Assert.equal(context.allowAutofill, false, "Check initial allowAutofill");
+ await UrlbarTestUtils.promisePopupClose(win);
+
+ await UrlbarTestUtils.promisePopupOpen(win, async () => {
+ win.document.getElementById("Browser:OpenLocation").doCommand();
+ });
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ Assert.equal(win.gURLBar.value, "e", "Should not autofill");
+ context = await win.gURLBar.lastQueryContextPromise;
+ Assert.equal(context.allowAutofill, false, "Check reopened allowAutofill");
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_clicks_after_autofill() {
+ info(
+ "Check that clickin on an autofilled input field doesn't requery, causing loss of the caret position"
+ );
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ info("autofill in tab2, switch to tab1, then back to tab2 with the mouse");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "e",
+ fireInputEvent: true,
+ });
+ Assert.equal(win.gURLBar.value, "example.com/", "Should have autofilled");
+
+ // Check single click.
+ let input = win.gURLBar.inputField;
+ EventUtils.synthesizeMouse(input, 30, 10, {}, win);
+ // Wait a bit, in case of a mistake this would run a query, otherwise not.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 300));
+ Assert.ok(win.gURLBar.selectionStart < win.gURLBar.value.length);
+ Assert.equal(win.gURLBar.selectionStart, win.gURLBar.selectionEnd);
+
+ // Check double click.
+ EventUtils.synthesizeMouse(input, 30, 10, { clickCount: 2 }, win);
+ // Wait a bit, in case of a mistake this would run a query, otherwise not.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 300));
+ Assert.ok(win.gURLBar.selectionStart < win.gURLBar.value.length);
+ Assert.ok(win.gURLBar.selectionEnd > win.gURLBar.selectionStart);
+
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_revert.js b/browser/components/urlbar/tests/browser/browser_revert.js
new file mode 100644
index 0000000000..b68ad0ff91
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_revert.js
@@ -0,0 +1,33 @@
+// Test reverting the urlbar value with ESC after a tab switch.
+
+add_task(async function () {
+ registerCleanupFunction(PlacesUtils.history.clear);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "http://example.com",
+ },
+ async function (browser) {
+ let originalValue = gURLBar.value;
+ let tab = gBrowser.selectedTab;
+ info("Put a typed value.");
+ gBrowser.userTypedValue = "foobar";
+ info("Switch tabs.");
+ gBrowser.selectedTab = gBrowser.tabs[0];
+ gBrowser.selectedTab = tab;
+ Assert.equal(
+ gURLBar.value,
+ "foobar",
+ "location bar displays typed value"
+ );
+
+ gURLBar.focus();
+ EventUtils.synthesizeKey("KEY_Escape");
+ Assert.equal(
+ gURLBar.value,
+ originalValue,
+ "ESC reverted the location bar value"
+ );
+ }
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_searchFunction.js b/browser/components/urlbar/tests/browser/browser_searchFunction.js
new file mode 100644
index 0000000000..0a272f9f01
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchFunction.js
@@ -0,0 +1,278 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test checks the urlbar.search() function.
+
+"use strict";
+
+const ALIAS = "@enginealias";
+let aliasEngine;
+
+add_setup(async function () {
+ // Run this in a new tab, to ensure all the locationchange notifications have
+ // fired.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ await SearchTestUtils.installSearchExtension({
+ keyword: ALIAS,
+ });
+ aliasEngine = Services.search.getEngineByName("Example");
+
+ registerCleanupFunction(async function () {
+ BrowserTestUtils.removeTab(tab);
+ gURLBar.handleRevert();
+ });
+});
+
+// Calls search() with a normal, non-"@engine" search-string argument.
+add_task(async function basic() {
+ gURLBar.blur();
+ gURLBar.search("basic");
+ ok(gURLBar.hasAttribute("focused"), "url bar is focused");
+ await assertUrlbarValue("basic");
+
+ assertOneOffButtonsVisible(true);
+
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+});
+
+// Calls search() with an invalid "@engine" search engine alias so that the
+// one-off search buttons are disabled.
+add_task(async function searchEngineAlias() {
+ gURLBar.blur();
+ await UrlbarTestUtils.promisePopupOpen(window, () =>
+ gURLBar.search("@example")
+ );
+ ok(gURLBar.hasAttribute("focused"), "url bar is focused");
+ UrlbarTestUtils.assertSearchMode(window, null);
+ await assertUrlbarValue("@example");
+
+ assertOneOffButtonsVisible(false);
+
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+
+ // Open the popup again (by doing another search) to make sure the one-off
+ // buttons are shown -- i.e., that we didn't accidentally break them.
+ await UrlbarTestUtils.promisePopupOpen(window, () =>
+ gURLBar.search("not an engine alias")
+ );
+ await assertUrlbarValue("not an engine alias");
+ assertOneOffButtonsVisible(true);
+
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+});
+
+add_task(async function searchRestriction() {
+ gURLBar.blur();
+ await UrlbarTestUtils.promisePopupOpen(window, () =>
+ gURLBar.search(UrlbarTokenizer.RESTRICT.SEARCH)
+ );
+ ok(gURLBar.hasAttribute("focused"), "url bar is focused");
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: UrlbarSearchUtils.getDefaultEngine().name,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ // Entry is "other" because we didn't pass searchModeEntry to search().
+ entry: "other",
+ });
+ assertOneOffButtonsVisible(true);
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+add_task(async function historyRestriction() {
+ gURLBar.blur();
+ await UrlbarTestUtils.promisePopupOpen(window, () =>
+ gURLBar.search(UrlbarTokenizer.RESTRICT.HISTORY)
+ );
+ ok(gURLBar.hasAttribute("focused"), "url bar is focused");
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ entry: "other",
+ });
+ assertOneOffButtonsVisible(true);
+ Assert.ok(!gURLBar.value, "The Urlbar has no value.");
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+add_task(async function historyRestrictionWithString() {
+ gURLBar.blur();
+ // The leading and trailing spaces are intentional to verify that search()
+ // preserves them.
+ let searchString = " foo bar ";
+ await UrlbarTestUtils.promisePopupOpen(window, () =>
+ gURLBar.search(`${UrlbarTokenizer.RESTRICT.HISTORY} ${searchString}`)
+ );
+ ok(gURLBar.hasAttribute("focused"), "url bar is focused");
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ entry: "other",
+ });
+ // We don't use assertUrlbarValue here since we expect to open a local search
+ // mode. In those modes, we don't show a heuristic search result, which
+ // assertUrlbarValue checks for.
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ Assert.equal(
+ gURLBar.value,
+ searchString,
+ "The Urlbar value should be the search string."
+ );
+ assertOneOffButtonsVisible(true);
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+add_task(async function tagRestriction() {
+ gURLBar.blur();
+ await UrlbarTestUtils.promisePopupOpen(window, () =>
+ gURLBar.search(UrlbarTokenizer.RESTRICT.TAG)
+ );
+ ok(gURLBar.hasAttribute("focused"), "url bar is focused");
+ // Since tags are not a supported search mode, we should just insert the tag
+ // restriction token and not enter search mode.
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ await assertUrlbarValue(`${UrlbarTokenizer.RESTRICT.TAG} `);
+ assertOneOffButtonsVisible(true);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+// Calls search() twice with the same value. The popup should reopen.
+add_task(async function searchTwice() {
+ gURLBar.blur();
+ await UrlbarTestUtils.promisePopupOpen(window, () => gURLBar.search("test"));
+ ok(gURLBar.hasAttribute("focused"), "url bar is focused");
+ await assertUrlbarValue("test");
+ assertOneOffButtonsVisible(true);
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ await UrlbarTestUtils.promisePopupOpen(window, () => gURLBar.search("test"));
+ ok(gURLBar.hasAttribute("focused"), "url bar is focused");
+ await assertUrlbarValue("test");
+ assertOneOffButtonsVisible(true);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+// Calls search() during an IME composition.
+add_task(async function searchIME() {
+ // First run a search.
+ gURLBar.blur();
+ await UrlbarTestUtils.promisePopupOpen(window, () => gURLBar.search("test"));
+ ok(gURLBar.hasAttribute("focused"), "url bar is focused");
+ await assertUrlbarValue("test");
+ // Start composition.
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeComposition({ type: "compositionstart" })
+ );
+
+ gURLBar.search("test");
+ // Unfortunately there's no other way to check we don't open the view than to
+ // wait for it.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ ok(!UrlbarTestUtils.isPopupOpen(window), "The panel should still be closed");
+
+ await UrlbarTestUtils.promisePopupOpen(window, () =>
+ EventUtils.synthesizeComposition({ type: "compositioncommitasis" })
+ );
+
+ assertOneOffButtonsVisible(true);
+
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+// Calls search() with an engine alias.
+add_task(async function searchWithAlias() {
+ await UrlbarTestUtils.promisePopupOpen(window, async () =>
+ gURLBar.search(`${ALIAS} test`, {
+ searchEngine: aliasEngine,
+ searchModeEntry: "topsites_urlbar",
+ })
+ );
+ Assert.ok(gURLBar.hasAttribute("focused"), "Urlbar is focused");
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: aliasEngine.name,
+ entry: "topsites_urlbar",
+ });
+ await assertUrlbarValue("test");
+ assertOneOffButtonsVisible(true);
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+// Calls search() and passes in a search engine without including a restriction
+// token or engine alias in the search string. Simulates pasting into the newtab
+// handoff field with search suggestions disabled.
+add_task(async function searchEngineWithNoToken() {
+ await UrlbarTestUtils.promisePopupOpen(window, async () =>
+ gURLBar.search("no-alias", {
+ searchEngine: aliasEngine,
+ searchModeEntry: "handoff",
+ })
+ );
+
+ Assert.ok(gURLBar.hasAttribute("focused"), "Urlbar is focused");
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: aliasEngine.name,
+ entry: "handoff",
+ });
+ await assertUrlbarValue("no-alias");
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+/**
+ * Asserts that the one-off search buttons are or aren't visible.
+ *
+ * @param {boolean} visible
+ * True if they should be visible, false if not.
+ */
+function assertOneOffButtonsVisible(visible) {
+ Assert.equal(
+ UrlbarTestUtils.getOneOffSearchButtonsVisible(window),
+ visible,
+ "Should show or not the one-off search buttons"
+ );
+}
+
+/**
+ * Asserts that the urlbar's input value is the given value. Also asserts that
+ * the first (heuristic) result in the popup is a search suggestion whose search
+ * query is the given value.
+ *
+ * @param {string} value
+ * The urlbar's expected value.
+ */
+async function assertUrlbarValue(value) {
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
+
+ Assert.equal(gURLBar.value, value);
+ Assert.greater(
+ UrlbarTestUtils.getResultCount(window),
+ 0,
+ "Should have at least one result"
+ );
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "Should have type search for the first result"
+ );
+ // Strip search restriction token from value.
+ if (value[0] == UrlbarTokenizer.RESTRICT.SEARCH) {
+ value = value.substring(1).trim();
+ }
+ Assert.equal(
+ result.searchParams.query,
+ value,
+ "Should have the correct query for the first result"
+ );
+}
diff --git a/browser/components/urlbar/tests/browser/browser_searchHistoryLimit.js b/browser/components/urlbar/tests/browser/browser_searchHistoryLimit.js
new file mode 100644
index 0000000000..6fcde0882b
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchHistoryLimit.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This test checks that search values longer than
+ * SearchSuggestionController.SEARCH_HISTORY_MAX_VALUE_LENGTH are not added to
+ * search history.
+ */
+
+"use strict";
+
+const { SearchSuggestionController } = ChromeUtils.importESModule(
+ "resource://gre/modules/SearchSuggestionController.sys.mjs"
+);
+
+let gEngine;
+
+add_setup(async function () {
+ await SearchTestUtils.installSearchExtension({}, { setAsDefault: true });
+ gEngine = Services.search.getEngineByName("Example");
+ await UrlbarTestUtils.formHistory.clear();
+
+ registerCleanupFunction(async function () {
+ await UrlbarTestUtils.formHistory.clear();
+ });
+});
+
+add_task(async function sanityCheckShortString() {
+ const shortString = new Array(
+ SearchSuggestionController.SEARCH_HISTORY_MAX_VALUE_LENGTH
+ )
+ .fill("a")
+ .join("");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: shortString,
+ });
+ let url = gEngine.getSubmission(shortString).uri.spec;
+ let loadPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ url
+ );
+ let addPromise = UrlbarTestUtils.formHistory.promiseChanged("add");
+ EventUtils.synthesizeKey("VK_RETURN");
+ await Promise.all([loadPromise, addPromise]);
+
+ let formHistory = (
+ await UrlbarTestUtils.formHistory.search({ source: gEngine.name })
+ ).map(entry => entry.value);
+ Assert.deepEqual(
+ formHistory,
+ [shortString],
+ "Should find form history after adding it"
+ );
+
+ await UrlbarTestUtils.formHistory.clear();
+});
+
+add_task(async function urlbar_checkLongString() {
+ const longString = new Array(
+ SearchSuggestionController.SEARCH_HISTORY_MAX_VALUE_LENGTH + 1
+ )
+ .fill("a")
+ .join("");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: longString,
+ });
+ let url = gEngine.getSubmission(longString).uri.spec;
+ let loadPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ url
+ );
+ EventUtils.synthesizeKey("VK_RETURN");
+ await loadPromise;
+ // There's nothing we can wait for, since addition should not be happening.
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ await new Promise(resolve => setTimeout(resolve, 500));
+ let formHistory = (
+ await UrlbarTestUtils.formHistory.search({ source: gEngine.name })
+ ).map(entry => entry.value);
+ Assert.deepEqual(formHistory, [], "Should not find form history");
+
+ await UrlbarTestUtils.formHistory.clear();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_alias_replacement.js b/browser/components/urlbar/tests/browser/browser_searchMode_alias_replacement.js
new file mode 100644
index 0000000000..9f4558e6c9
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchMode_alias_replacement.js
@@ -0,0 +1,274 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that user-defined aliases are replaced by the search mode indicator.
+ */
+
+const ALIAS = "testalias";
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+
+// We make sure that aliases and search terms are correctly recognized when they
+// are separated by each of these different types of spaces and combinations of
+// spaces. U+3000 is the ideographic space in CJK and is commonly used by CJK
+// speakers.
+const TEST_SPACES = [" ", "\u3000", " \u3000", "\u3000 "];
+
+let defaultEngine, aliasEngine;
+
+add_setup(async function () {
+ defaultEngine = await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME,
+ setAsDefault: true,
+ });
+ defaultEngine.alias = "@default";
+ await SearchTestUtils.installSearchExtension({
+ keyword: ALIAS,
+ });
+ aliasEngine = Services.search.getEngineByName("Example");
+});
+
+// An incomplete alias should not be replaced.
+add_task(async function incompleteAlias() {
+ // Check that a non-fully typed alias is not replaced.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: ALIAS.slice(0, -1),
+ });
+ await UrlbarTestUtils.assertSearchMode(window, null);
+
+ // Type a space just to make sure it's not replaced.
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey(" ");
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ Assert.equal(
+ gURLBar.value,
+ ALIAS.slice(0, -1) + " ",
+ "The typed value should be unchanged except for the space."
+ );
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+});
+
+// A complete alias without a trailing space should not be replaced.
+add_task(async function noTrailingSpace() {
+ let value = ALIAS;
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value,
+ });
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+});
+
+// A complete typed alias without a trailing space should not be replaced.
+add_task(async function noTrailingSpace_typed() {
+ // Start by searching for the alias minus its last char.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: ALIAS.slice(0, -1),
+ });
+ await UrlbarTestUtils.assertSearchMode(window, null);
+
+ // Now type the last char.
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey(ALIAS.slice(-1));
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ Assert.equal(
+ gURLBar.value,
+ ALIAS,
+ "The typed value should be the full alias."
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+});
+
+// A complete alias with a trailing space should be replaced.
+add_task(async function trailingSpace() {
+ for (let spaces of TEST_SPACES) {
+ info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) }));
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: ALIAS + spaces,
+ });
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: aliasEngine.name,
+ entry: "typed",
+ });
+ Assert.ok(!gURLBar.value, "The urlbar value should be cleared.");
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+ }
+});
+
+// A complete alias should be replaced after typing a space.
+add_task(async function trailingSpace_typed() {
+ for (let spaces of TEST_SPACES) {
+ if (spaces.length != 1) {
+ continue;
+ }
+ info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) }));
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: ALIAS,
+ });
+ await UrlbarTestUtils.assertSearchMode(window, null);
+
+ // We need to wait for two searches: The first enters search mode, the second
+ // does the search in search mode.
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey(spaces);
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: aliasEngine.name,
+ entry: "typed",
+ });
+ Assert.ok(!gURLBar.value, "The urlbar value should be cleared.");
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+ }
+});
+
+// A complete alias with a trailing space should be replaced, and the query
+// after the trailing space should be the new value of the input.
+add_task(async function trailingSpace_query() {
+ for (let spaces of TEST_SPACES) {
+ info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) }));
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: ALIAS + spaces + "query",
+ });
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: aliasEngine.name,
+ entry: "typed",
+ });
+ Assert.equal(
+ gURLBar.value,
+ "query",
+ "The urlbar value should be the query."
+ );
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+ }
+});
+
+add_task(async function () {
+ info("Test search mode when typing an alias after selecting one-off button");
+
+ info("Open the result popup");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+
+ info("Select one of one-off button");
+ const oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window);
+ await TestUtils.waitForCondition(
+ () => !oneOffs._rebuilding,
+ "Waiting for one-offs to finish rebuilding"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ ok(oneOffs.selectedButton, "There is a selected one-off button");
+ const selectedEngine = oneOffs.selectedButton.engine;
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: selectedEngine.name,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ entry: "oneoff",
+ isPreview: true,
+ });
+
+ info("Type a search engine alias and query");
+ const inputString = "@default query";
+ inputString.split("").forEach(c => EventUtils.synthesizeKey(c));
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ Assert.equal(
+ gURLBar.value,
+ inputString,
+ "Alias and query is inputed correctly to the urlbar"
+ );
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: selectedEngine.name,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ entry: "oneoff",
+ });
+
+ // When starting typing, as the search mode is confirmed, the one-off
+ // selection is removed.
+ ok(!oneOffs.selectedButton, "There is no any selected one-off button");
+
+ // Clean up
+ gURLBar.value = "";
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+add_task(async function () {
+ info(
+ "Test search mode after removing current search mode when multiple aliases are written"
+ );
+
+ info("Open the result popup with multiple aliases");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@default testalias @default",
+ });
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: defaultEngine.name,
+ entry: "typed",
+ });
+ Assert.equal(
+ gURLBar.value,
+ "testalias @default",
+ "The value on the urlbar is correct"
+ );
+
+ info("Exit search mode by clicking");
+ const indicator = gURLBar.querySelector("#urlbar-search-mode-indicator");
+ EventUtils.synthesizeMouseAtCenter(indicator, { type: "mouseover" }, window);
+ const closeButton = gURLBar.querySelector(
+ "#urlbar-search-mode-indicator-close"
+ );
+ const searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeMouseAtCenter(closeButton, {}, window);
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: aliasEngine.name,
+ entry: "typed",
+ });
+ Assert.equal(gURLBar.value, "@default", "The value on the urlbar is correct");
+
+ // Clean up
+ gURLBar.value = "";
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+/**
+ * Returns an array of code points in the given string. Each code point is
+ * returned as a hexidecimal string.
+ *
+ * @param {string} str
+ * The code points of this string will be returned.
+ * @returns {Array}
+ * Array of code points in the string, where each is a hexidecimal string.
+ */
+function codePoints(str) {
+ return str.split("").map(s => s.charCodeAt(0).toString(16));
+}
diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_autofill.js b/browser/components/urlbar/tests/browser/browser_searchMode_autofill.js
new file mode 100644
index 0000000000..3186d96b92
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchMode_autofill.js
@@ -0,0 +1,133 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that autofill is cleared if a remote search mode is entered but still
+ * works for local search modes.
+ */
+
+"use strict";
+
+add_setup(async function () {
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits([{ uri: "http://example.com/" }]);
+ }
+
+ await SearchTestUtils.installSearchExtension({}, { setAsDefault: true });
+ let defaultEngine = Services.search.getEngineByName("Example");
+ await Services.search.moveEngine(defaultEngine, 0);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.quickactions", false]],
+ });
+
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ });
+});
+
+// Tests that autofill is cleared when entering a remote search mode and that
+// autofill doesn't happen when in that mode.
+add_task(async function remote() {
+ info("Sanity check: we autofill in a normal search.");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "ex",
+ fireInputEvent: true,
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill, "We're autofilling.");
+ Assert.equal(
+ gURLBar.value,
+ "example.com/",
+ "Urlbar contains the autofilled URL."
+ );
+ info("Enter remote search mode and check autofill is cleared.");
+ await UrlbarTestUtils.enterSearchMode(window);
+ Assert.equal(gURLBar.value, "ex", "Urlbar contains the typed string.");
+
+ info("Continue typing and check that we're not autofilling.");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exa",
+ fireInputEvent: true,
+ });
+
+ details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(!details.autofill, "We're not autofilling.");
+ Assert.equal(gURLBar.value, "exa", "Urlbar contains the typed string.");
+
+ info("Exit remote search mode and check that we now autofill.");
+ await UrlbarTestUtils.exitSearchMode(window, { backspace: true });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ fireInputEvent: true,
+ });
+ details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill, "We're autofilling.");
+ Assert.equal(
+ gURLBar.value,
+ "example.com/",
+ "Urlbar contains the typed string."
+ );
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+});
+
+// Tests that autofill works as normal when entering and when in a local search
+// mode.
+add_task(async function local() {
+ info("Sanity check: we autofill in a normal search.");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "ex",
+ fireInputEvent: true,
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill, "We're autofilling.");
+ Assert.equal(
+ gURLBar.value,
+ "example.com/",
+ "Urlbar contains the autofilled URL."
+ );
+ info("Enter local search mode and check autofill is preserved.");
+ await UrlbarTestUtils.enterSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ });
+ Assert.equal(
+ gURLBar.value,
+ "example.com/",
+ "Urlbar contains the autofilled URL."
+ );
+
+ info("Continue typing and check that we're autofilling.");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exa",
+ fireInputEvent: true,
+ });
+
+ details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill, "We're autofilling.");
+ Assert.equal(
+ gURLBar.value,
+ "example.com/",
+ "Urlbar contains the autofilled URL."
+ );
+
+ info("Exit local search mode and check that nothing has changed.");
+ await UrlbarTestUtils.exitSearchMode(window, { backspace: true });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ fireInputEvent: true,
+ });
+ details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill, "We're autofilling.");
+ Assert.equal(
+ gURLBar.value,
+ "example.com/",
+ "Urlbar contains the typed string."
+ );
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+});
diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_clickLink.js b/browser/components/urlbar/tests/browser/browser_searchMode_clickLink.js
new file mode 100644
index 0000000000..d037c77bbb
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchMode_clickLink.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that search mode is exited after clicking a link and loading a page in
+ * the current tab.
+ */
+
+"use strict";
+
+const LINK_PAGE_URL =
+ "http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/dummy_page.html";
+
+// Opens a new tab containing a link, enters search mode, and clicks the link.
+// Uses a variety of search strings and link hrefs in order to hit different
+// branches in setURI. Search mode should be exited in all cases, and the href
+// in the link should be opened.
+add_task(async function clickLink() {
+ for (let test of [
+ // searchString, href to use in the link
+ [LINK_PAGE_URL, LINK_PAGE_URL],
+ [LINK_PAGE_URL, "http://www.example.com/"],
+ ["test", LINK_PAGE_URL],
+ ["test", "http://www.example.com/"],
+ [null, LINK_PAGE_URL],
+ [null, "http://www.example.com/"],
+ ]) {
+ await doClickLinkTest(...test);
+ }
+});
+
+async function doClickLinkTest(searchString, href) {
+ info(
+ "doClickLinkTest with args: " +
+ JSON.stringify({
+ searchString,
+ href,
+ })
+ );
+
+ await BrowserTestUtils.withNewTab(LINK_PAGE_URL, async () => {
+ if (searchString) {
+ // Do a search with the search string.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: searchString,
+ fireInputEvent: true,
+ });
+ Assert.ok(
+ gBrowser.selectedBrowser.userTypedValue,
+ "userTypedValue should be defined"
+ );
+ } else {
+ // Open top sites.
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ document.getElementById("Browser:OpenLocation").doCommand();
+ });
+ Assert.strictEqual(
+ gBrowser.selectedBrowser.userTypedValue,
+ null,
+ "userTypedValue should be null"
+ );
+ }
+
+ // Enter search mode and then close the popup so we can click the link in
+ // the page.
+ await UrlbarTestUtils.enterSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ entry: "oneoff",
+ });
+
+ // Add a link to the page and click it.
+ let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await ContentTask.spawn(gBrowser.selectedBrowser, href, async cHref => {
+ let link = this.content.document.createElement("a");
+ link.textContent = "Click me";
+ link.href = cHref;
+ this.content.document.body.append(link);
+ link.click();
+ });
+ await loadPromise;
+ Assert.equal(
+ gBrowser.currentURI.spec,
+ href,
+ "Should have loaded the href URL"
+ );
+
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ });
+}
diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_engineRemoval.js b/browser/components/urlbar/tests/browser/browser_searchMode_engineRemoval.js
new file mode 100644
index 0000000000..f5eab77789
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchMode_engineRemoval.js
@@ -0,0 +1,109 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that we exit search mode when the search mode engine is removed.
+ */
+
+"use strict";
+
+// Tests that we exit search mode in the active tab when the search mode engine
+// is removed.
+add_task(async function activeTab() {
+ let extension = await SearchTestUtils.installSearchExtension(
+ {},
+ { skipUnload: true }
+ );
+ let engine = Services.search.getEngineByName("Example");
+ await Services.search.moveEngine(engine, 0);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "ex",
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ // Sanity check: we are in the correct search mode.
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: engine.name,
+ entry: "oneoff",
+ });
+ await extension.unload();
+ // Check that we are no longer in search mode.
+ await UrlbarTestUtils.assertSearchMode(window, null);
+});
+
+// Tests that we exit search mode in a background tab when the search mode
+// engine is removed.
+add_task(async function backgroundTab() {
+ let extension = await SearchTestUtils.installSearchExtension(
+ {},
+ { skipUnload: true }
+ );
+ let engine = Services.search.getEngineByName("Example");
+ await Services.search.moveEngine(engine, 0);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "ex",
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ let tab1 = gBrowser.selectedTab;
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ // Sanity check: tab1 is still in search mode.
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: engine.name,
+ entry: "oneoff",
+ });
+
+ // Switch back to tab2 so tab1 is in the background when the engine is
+ // removed.
+ await BrowserTestUtils.switchTab(gBrowser, tab2);
+ // tab2 shouldn't be in search mode.
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ await extension.unload();
+
+ // tab1 should have exited search mode.
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ BrowserTestUtils.removeTab(tab2);
+});
+
+// Tests that we exit search mode in a background window when the search mode
+// engine is removed.
+add_task(async function backgroundWindow() {
+ let extension = await SearchTestUtils.installSearchExtension(
+ {},
+ { skipUnload: true }
+ );
+ let engine = Services.search.getEngineByName("Example");
+ await Services.search.moveEngine(engine, 0);
+
+ let win1 = window;
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win1,
+ value: "ex",
+ });
+ await UrlbarTestUtils.enterSearchMode(win1);
+ let win2 = await BrowserTestUtils.openNewBrowserWindow();
+
+ // Sanity check: win1 is still in search mode.
+ win1.focus();
+ await UrlbarTestUtils.assertSearchMode(win1, {
+ engineName: engine.name,
+ entry: "oneoff",
+ });
+
+ // Switch back to win2 so win1 is in the background when the engine is
+ // removed.
+ win2.focus();
+ // win2 shouldn't be in search mode.
+ await UrlbarTestUtils.assertSearchMode(win2, null);
+ await extension.unload();
+
+ // win1 should not be in search mode.
+ win1.focus();
+ await UrlbarTestUtils.assertSearchMode(win1, null);
+ await BrowserTestUtils.closeWindow(win2);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_excludeResults.js b/browser/components/urlbar/tests/browser/browser_searchMode_excludeResults.js
new file mode 100644
index 0000000000..0e9471280e
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchMode_excludeResults.js
@@ -0,0 +1,217 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that results with hostnames other than the search mode engine are not
+ * shown.
+ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ SyncedTabs: "resource://services-sync/SyncedTabs.sys.mjs",
+});
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.searches", false],
+ ["browser.urlbar.autoFill", false],
+ // Special prefs for remote tabs.
+ ["services.sync.username", "fake"],
+ ["services.sync.syncedTabs.showRemoteTabs", true],
+ ],
+ });
+
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ // Note that the result domain is subdomain.example.ca. We still expect to
+ // match with example.com results because we ignore subdomains and the public
+ // suffix in this check.
+ await SearchTestUtils.installSearchExtension(
+ {
+ search_url: "https://subdomain.example.ca/",
+ },
+ { setAsDefault: true }
+ );
+ let engine = Services.search.getEngineByName("Example");
+ await Services.search.moveEngine(engine, 0);
+
+ const REMOTE_TAB = {
+ id: "7cqCr77ptzX3",
+ type: "client",
+ lastModified: 1492201200,
+ name: "Nightly on MacBook-Pro",
+ clientType: "desktop",
+ tabs: [
+ {
+ type: "tab",
+ title: "Test Remote",
+ url: "https://example.com",
+ icon: UrlbarUtils.ICON.DEFAULT,
+ client: "7cqCr77ptzX3",
+ lastUsed: Math.floor(Date.now() / 1000),
+ },
+ {
+ type: "tab",
+ title: "Test Remote 2",
+ url: "https://example-2.com",
+ icon: UrlbarUtils.ICON.DEFAULT,
+ client: "7cqCr77ptzX3",
+ lastUsed: Math.floor(Date.now() / 1000),
+ },
+ ],
+ };
+
+ const sandbox = sinon.createSandbox();
+
+ let originalSyncedTabsInternal = SyncedTabs._internal;
+ SyncedTabs._internal = {
+ isConfiguredToSyncTabs: true,
+ hasSyncedThisSession: true,
+ getTabClients() {
+ return Promise.resolve([]);
+ },
+ syncTabs() {
+ return Promise.resolve();
+ },
+ };
+
+ // Tell the Sync XPCOM service it is initialized.
+ let weaveXPCService = Cc["@mozilla.org/weave/service;1"].getService(
+ Ci.nsISupports
+ ).wrappedJSObject;
+ let oldWeaveServiceReady = weaveXPCService.ready;
+ weaveXPCService.ready = true;
+
+ sandbox
+ .stub(SyncedTabs._internal, "getTabClients")
+ .callsFake(() => Promise.resolve(Cu.cloneInto([REMOTE_TAB], {})));
+
+ // Reset internal cache in UrlbarProviderRemoteTabs.
+ Services.obs.notifyObservers(null, "weave:engine:sync:finish", "tabs");
+
+ registerCleanupFunction(async function () {
+ sandbox.restore();
+ weaveXPCService.ready = oldWeaveServiceReady;
+ SyncedTabs._internal = originalSyncedTabsInternal;
+ await PlacesUtils.history.clear();
+ });
+});
+
+add_task(async function basic() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "example",
+ });
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 3,
+ "We have three results"
+ );
+ let firstResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ firstResult.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "The first result is the heuristic search result."
+ );
+ let secondResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(
+ secondResult.type,
+ UrlbarUtils.RESULT_TYPE.REMOTE_TAB,
+ "The second result is a remote tab."
+ );
+ let thirdResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(
+ thirdResult.type,
+ UrlbarUtils.RESULT_TYPE.REMOTE_TAB,
+ "The third result is a remote tab."
+ );
+
+ await UrlbarTestUtils.enterSearchMode(window);
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 2,
+ "We have two results. The second remote tab result is excluded despite matching the search string."
+ );
+ firstResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ firstResult.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "The first result is the heuristic search result."
+ );
+ secondResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(
+ secondResult.type,
+ UrlbarUtils.RESULT_TYPE.REMOTE_TAB,
+ "The second result is a remote tab."
+ );
+
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+// For engines with an invalid TLD, we filter on the entire domain.
+add_task(async function malformedEngine() {
+ await SearchTestUtils.installSearchExtension({
+ name: "TestMalformed",
+ search_url: "https://example.foobar/",
+ });
+ let badEngine = Services.search.getEngineByName("TestMalformed");
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "example",
+ });
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 4,
+ "We have four results"
+ );
+ let firstResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ firstResult.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "The first result is the heuristic search result."
+ );
+ let secondResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(
+ secondResult.type,
+ UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ "The second result is the tab-to-search onboarding result for our malformed engine."
+ );
+ let thirdResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 2);
+ Assert.equal(
+ thirdResult.type,
+ UrlbarUtils.RESULT_TYPE.REMOTE_TAB,
+ "The third result is a remote tab."
+ );
+ let fourthResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 3);
+ Assert.equal(
+ fourthResult.type,
+ UrlbarUtils.RESULT_TYPE.REMOTE_TAB,
+ "The fourth result is a remote tab."
+ );
+
+ await UrlbarTestUtils.enterSearchMode(window, {
+ engineName: badEngine.name,
+ });
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 1,
+ "We only have one result."
+ );
+ firstResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(firstResult.heuristic, "The first result is heuristic.");
+ Assert.equal(
+ firstResult.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "The first result is the heuristic search result."
+ );
+
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_heuristic.js b/browser/components/urlbar/tests/browser/browser_searchMode_heuristic.js
new file mode 100644
index 0000000000..bd8f00a512
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchMode_heuristic.js
@@ -0,0 +1,221 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests heuristic results in search mode.
+ */
+
+"use strict";
+
+add_setup(async function () {
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ // Add a new mock default engine so we don't hit the network.
+ await SearchTestUtils.installSearchExtension(
+ { name: "Test" },
+ { setAsDefault: true }
+ );
+
+ // Add one bookmark we'll use below.
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.com/bookmark",
+ });
+ registerCleanupFunction(async () => {
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+});
+
+// Enters search mode with no results.
+add_task(async function noResults() {
+ // Do a search that doesn't match our bookmark and enter bookmark search mode.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "doesn't match anything",
+ });
+ await UrlbarTestUtils.enterSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ });
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 0,
+ "Zero results since no bookmark matches"
+ );
+
+ // Press enter. Nothing should happen.
+ let loadPromise = waitForLoadOrTimeout();
+ EventUtils.synthesizeKey("KEY_Enter");
+ let loadEvent = await loadPromise;
+ Assert.ok(!loadEvent, "Nothing should have loaded");
+
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+// Enters a local search mode (bookmarks) with a matching result. No heuristic
+// should be present.
+add_task(async function localNoHeuristic() {
+ // Do a search that matches our bookmark and enter bookmarks search mode.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "bookmark",
+ });
+ await UrlbarTestUtils.enterSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ });
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 1,
+ "There should be one result"
+ );
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ result.source,
+ UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ "Result source should be BOOKMARKS"
+ );
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.URL,
+ "Result type should be URL"
+ );
+ Assert.equal(
+ result.url,
+ "http://example.com/bookmark",
+ "Result URL is our bookmark URL"
+ );
+ Assert.ok(!result.heuristic, "Result should not be heuristic");
+
+ // Press enter. Nothing should happen.
+ let loadPromise = waitForLoadOrTimeout();
+ EventUtils.synthesizeKey("KEY_Enter");
+ let loadEvent = await loadPromise;
+ Assert.ok(!loadEvent, "Nothing should have loaded");
+
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+// Enters a local search mode (bookmarks) with a matching autofill result. The
+// result should be the heuristic.
+add_task(async function localAutofill() {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ // Do a search that autofills our bookmark's origin and enter bookmarks
+ // search mode.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "example",
+ });
+ await UrlbarTestUtils.enterSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ });
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 2,
+ "There should be two results"
+ );
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ result.source,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ "Result source should be HISTORY"
+ );
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.URL,
+ "Result type should be URL"
+ );
+ Assert.equal(
+ result.url,
+ "http://example.com/",
+ "Result URL is our bookmark's origin"
+ );
+ Assert.ok(result.heuristic, "Result should be heuristic");
+ Assert.ok(result.autofill, "Result should be autofill");
+
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(
+ result.source,
+ UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ "Result source should be BOOKMARKS"
+ );
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.URL,
+ "Result type should be URL"
+ );
+ Assert.equal(
+ result.url,
+ "http://example.com/bookmark",
+ "Result URL is our bookmark URL"
+ );
+
+ // Press enter. Our bookmark's origin should be loaded.
+ let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+ Assert.equal(
+ gBrowser.currentURI.spec,
+ "http://example.com/",
+ "Bookmark's origin should have loaded"
+ );
+ });
+});
+
+// Enters a remote engine search mode. There should be a heuristic.
+add_task(async function remote() {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ // Do a search and enter search mode with our test engine.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "remote",
+ });
+ await UrlbarTestUtils.enterSearchMode(window, {
+ engineName: "Test",
+ });
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 1,
+ "There should be one result"
+ );
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ result.source,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ "Result source should be SEARCH"
+ );
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "Result type should be SEARCH"
+ );
+ Assert.ok(result.searchParams, "searchParams should be present");
+ Assert.equal(
+ result.searchParams.engine,
+ "Test",
+ "searchParams.engine should be our test engine"
+ );
+ Assert.equal(
+ result.searchParams.query,
+ "remote",
+ "searchParams.query should be our query"
+ );
+ Assert.ok(result.heuristic, "Result should be heuristic");
+
+ // Press enter. The engine's SERP should load.
+ let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+ Assert.equal(
+ gBrowser.currentURI.spec,
+ "https://example.com/?q=remote",
+ "Engine's SERP should have loaded"
+ );
+ });
+});
diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_indicator.js b/browser/components/urlbar/tests/browser/browser_searchMode_indicator.js
new file mode 100644
index 0000000000..357a5d17f9
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchMode_indicator.js
@@ -0,0 +1,377 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests interactions with the search mode indicator. See browser_oneOffs.js for
+ * more coverage.
+ */
+
+const TEST_QUERY = "test string";
+const SUGGESTIONS_ENGINE_NAME = "searchSuggestionEngine.xml";
+
+// These need to have different domains because otherwise new tab and/or
+// activity stream collapses them.
+const TOP_SITES_URLS = [
+ "http://top-site-0.com/",
+ "http://top-site-1.com/",
+ "http://top-site-2.com/",
+];
+
+let suggestionsEngine;
+let defaultEngine;
+
+add_setup(async function () {
+ suggestionsEngine = await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + SUGGESTIONS_ENGINE_NAME,
+ });
+
+ await SearchTestUtils.installSearchExtension({}, { setAsDefault: true });
+ defaultEngine = Services.search.getEngineByName("Example");
+ await Services.search.moveEngine(suggestionsEngine, 0);
+
+ // Set our top sites.
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "browser.newtabpage.activity-stream.default.sites",
+ TOP_SITES_URLS.join(","),
+ ],
+ ],
+ });
+ await updateTopSites(sites =>
+ ObjectUtils.deepEqual(
+ sites.map(s => s.url),
+ TOP_SITES_URLS
+ )
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.search.separatePrivateDefault.ui.enabled", false],
+ ["browser.urlbar.suggest.quickactions", false],
+ ],
+ });
+});
+
+async function verifySearchModeResultsAdded(window) {
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 3,
+ "There should be three results."
+ );
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ result.searchParams.engine,
+ suggestionsEngine.name,
+ "The first result should be a search result for our suggestion engine."
+ );
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(
+ result.searchParams.suggestion,
+ `${TEST_QUERY}foo`,
+ "The second result should be a suggestion result."
+ );
+ Assert.equal(
+ result.searchParams.engine,
+ suggestionsEngine.name,
+ "The second result should be a search result for our suggestion engine."
+ );
+}
+
+async function verifySearchModeResultsRemoved(window) {
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 1,
+ "There should only be one result."
+ );
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ result.searchParams.engine,
+ defaultEngine.name,
+ "The first result should be a search result for our default engine."
+ );
+}
+
+async function verifyTopSitesResultsAdded(window) {
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ TOP_SITES_URLS.length,
+ "Expected number of top sites results"
+ );
+ for (let i = 0; i < TOP_SITES_URLS; i++) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ Assert.equal(
+ result.url,
+ TOP_SITES_URLS[i],
+ `Expected top sites result URL at index ${i}`
+ );
+ }
+}
+
+// Tests that the indicator is removed when backspacing at the beginning of
+// the search string.
+add_task(async function backspace() {
+ // View open, with string.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_QUERY,
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ await verifySearchModeResultsAdded(window);
+ await UrlbarTestUtils.exitSearchMode(window, { backspace: true });
+ await verifySearchModeResultsRemoved(window);
+ Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Urlbar view is open.");
+
+ // View open, no string (i.e., top sites).
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ if (gURLBar.getAttribute("pageproxystate") == "invalid") {
+ gURLBar.handleRevert();
+ }
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ await UrlbarTestUtils.exitSearchMode(window, { backspace: true });
+ Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Urlbar view is open.");
+ await verifyTopSitesResultsAdded(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ // View closed, with string.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_QUERY,
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ await verifySearchModeResultsAdded(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+ await UrlbarTestUtils.exitSearchMode(window, { backspace: true });
+ await verifySearchModeResultsRemoved(window);
+ Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Urlbar view is now open.");
+
+ // View closed, no string (i.e., top sites).
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ if (gURLBar.getAttribute("pageproxystate") == "invalid") {
+ gURLBar.handleRevert();
+ }
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+ await UrlbarTestUtils.exitSearchMode(window, { backspace: true });
+ Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Urlbar view is open.");
+ await verifyTopSitesResultsAdded(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+add_task(async function escapeOnInitialPage() {
+ info("Tests the indicator's interaction with the ESC key");
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_QUERY,
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ await verifySearchModeResultsAdded(window);
+
+ EventUtils.synthesizeKey("KEY_Escape");
+ Assert.ok(!UrlbarTestUtils.isPopupOpen(window, "UrlbarView is closed."));
+ Assert.equal(gURLBar.value, TEST_QUERY, "Urlbar value hasn't changed.");
+
+ let oneOffs =
+ UrlbarTestUtils.getOneOffSearchButtons(window).getSelectableButtons(true);
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: oneOffs[0].engine.name,
+ entry: "oneoff",
+ });
+
+ EventUtils.synthesizeKey("KEY_Escape");
+ Assert.ok(!UrlbarTestUtils.isPopupOpen(window, "UrlbarView is closed."));
+ Assert.ok(!gURLBar.value, "Urlbar value is empty.");
+ await UrlbarTestUtils.assertSearchMode(window, null);
+});
+
+add_task(async function escapeOnBrowsingPage() {
+ info("Tests the indicator's interaction with the ESC key on browsing page");
+
+ await BrowserTestUtils.withNewTab("http://example.com", async browser => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_QUERY,
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ await verifySearchModeResultsAdded(window);
+
+ EventUtils.synthesizeKey("KEY_Escape");
+ Assert.ok(!UrlbarTestUtils.isPopupOpen(window, "UrlbarView is closed."));
+ Assert.equal(gURLBar.value, TEST_QUERY, "Urlbar value hasn't changed.");
+
+ const oneOffs =
+ UrlbarTestUtils.getOneOffSearchButtons(window).getSelectableButtons(true);
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: oneOffs[0].engine.name,
+ entry: "oneoff",
+ });
+
+ EventUtils.synthesizeKey("KEY_Escape");
+ Assert.ok(!UrlbarTestUtils.isPopupOpen(window, "UrlbarView is closed."));
+ Assert.equal(
+ gURLBar.value,
+ "example.com",
+ "Urlbar value indicates the browsing page."
+ );
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ });
+});
+
+// Tests that the indicator is removed when its close button is clicked.
+add_task(async function click_close() {
+ // Clicking close with the view open, with string.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_QUERY,
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ await verifySearchModeResultsAdded(window);
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+ await verifySearchModeResultsRemoved(window);
+ Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Urlbar view is open.");
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ // Clicking close with the view open, no string (i.e., top sites).
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ if (gURLBar.getAttribute("pageproxystate") == "invalid") {
+ gURLBar.handleRevert();
+ }
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+ Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Urlbar view is open.");
+ await verifyTopSitesResultsAdded(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ // Clicking close with the view closed, with string.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_QUERY,
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ await verifySearchModeResultsAdded(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+ await UrlbarTestUtils.exitSearchMode(window, {
+ clickClose: true,
+ waitForSearch: false,
+ });
+ Assert.ok(!UrlbarTestUtils.isPopupOpen(window), "Urlbar view is closed.");
+
+ // Clicking close with the view closed, no string (i.e., top sites).
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ if (gURLBar.getAttribute("pageproxystate") == "invalid") {
+ gURLBar.handleRevert();
+ }
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+ await UrlbarTestUtils.exitSearchMode(window, {
+ clickClose: true,
+ waitForSearch: false,
+ });
+ Assert.ok(!UrlbarTestUtils.isPopupOpen(window), "Urlbar view is closed.");
+});
+
+// Tests that Accel+K enters search mode with the default engine. Also tests
+// that Accel+K highlights the typed search string.
+add_task(async function keyboard_shortcut() {
+ const query = "test query";
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: query,
+ });
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ Assert.equal(
+ gURLBar.selectionStart,
+ gURLBar.selectionEnd,
+ "The search string is not highlighted."
+ );
+ EventUtils.synthesizeKey("k", { accelKey: true });
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ engineName: defaultEngine.name,
+ entry: "shortcut",
+ });
+ Assert.equal(gURLBar.value, query, "The search string was not cleared.");
+ Assert.equal(gURLBar.selectionStart, 0);
+ Assert.equal(
+ gURLBar.selectionEnd,
+ query.length,
+ "The search string is highlighted."
+ );
+ await UrlbarTestUtils.exitSearchMode(window, {
+ clickClose: true,
+ waitForSearch: false,
+ });
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+});
+
+// Tests that the Tools:Search menu item enters search mode with the default
+// engine. Also tests that Tools:Search highlights the typed search string.
+add_task(async function menubar_item() {
+ const query = "test query 2";
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: query,
+ });
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ Assert.equal(
+ gURLBar.selectionStart,
+ gURLBar.selectionEnd,
+ "The search string is not highlighted."
+ );
+ let command = window.document.getElementById("Tools:Search");
+ command.doCommand();
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ engineName: defaultEngine.name,
+ entry: "shortcut",
+ });
+ Assert.equal(gURLBar.value, query, "The search string was not cleared.");
+ Assert.equal(gURLBar.selectionStart, 0);
+ Assert.equal(
+ gURLBar.selectionEnd,
+ query.length,
+ "The search string is highlighted."
+ );
+ await UrlbarTestUtils.exitSearchMode(window, {
+ clickClose: true,
+ waitForSearch: false,
+ });
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+});
+
+// Tests that entering search mode invalidates pageproxystate and that
+// pageproxystate remains invalid after exiting search mode.
+add_task(async function invalidate_pageproxystate() {
+ await BrowserTestUtils.withNewTab("about:robots", async function (browser) {
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ Assert.equal(gURLBar.getAttribute("pageproxystate"), "valid");
+ await UrlbarTestUtils.enterSearchMode(window);
+ Assert.equal(
+ gURLBar.getAttribute("pageproxystate"),
+ "invalid",
+ "Entering search mode should clear pageproxystate."
+ );
+ Assert.equal(gURLBar.value, "", "Urlbar value should be cleared.");
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+ Assert.equal(
+ gURLBar.getAttribute("pageproxystate"),
+ "invalid",
+ "Pageproxystate should still be invalid after exiting search mode."
+ );
+ });
+});
diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_indicator_clickthrough.js b/browser/components/urlbar/tests/browser/browser_searchMode_indicator_clickthrough.js
new file mode 100644
index 0000000000..acfb60922d
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchMode_indicator_clickthrough.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check clicking on the search mode indicator when the urlbar is not focused puts
+ * focus in the urlbar.
+ */
+
+add_task(async function test() {
+ // Avoid remote connections.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.suggest.enabled", false]],
+ });
+
+ await BrowserTestUtils.withNewTab("about:robots", async browser => {
+ // View open, with string.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+ const indicator = document.getElementById("urlbar-search-mode-indicator");
+ Assert.ok(!BrowserTestUtils.is_visible(indicator));
+ const indicatorCloseButton = document.getElementById(
+ "urlbar-search-mode-indicator-close"
+ );
+ Assert.ok(!BrowserTestUtils.is_visible(indicatorCloseButton));
+ const labelBox = document.getElementById("urlbar-label-box");
+ Assert.ok(!BrowserTestUtils.is_visible(labelBox));
+
+ await UrlbarTestUtils.enterSearchMode(window);
+ Assert.ok(BrowserTestUtils.is_visible(indicator));
+ Assert.ok(BrowserTestUtils.is_visible(indicatorCloseButton));
+ Assert.ok(!BrowserTestUtils.is_visible(labelBox));
+
+ info("Blur the urlbar");
+ gURLBar.blur();
+ Assert.ok(BrowserTestUtils.is_visible(indicator));
+ Assert.ok(BrowserTestUtils.is_visible(indicatorCloseButton));
+ Assert.ok(!BrowserTestUtils.is_visible(labelBox));
+ Assert.notEqual(
+ document.activeElement,
+ gURLBar.inputField,
+ "URL Bar should not be focused"
+ );
+
+ info("Focus the urlbar clicking on the indicator");
+ EventUtils.synthesizeMouseAtCenter(indicator, {});
+ Assert.ok(BrowserTestUtils.is_visible(indicator));
+ Assert.ok(BrowserTestUtils.is_visible(indicatorCloseButton));
+ Assert.ok(!BrowserTestUtils.is_visible(labelBox));
+ Assert.equal(
+ document.activeElement,
+ gURLBar.inputField,
+ "URL Bar should be focused"
+ );
+
+ info("Leave search mode clicking on the close button");
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+ Assert.ok(!BrowserTestUtils.is_visible(indicator));
+ Assert.ok(!BrowserTestUtils.is_visible(indicatorCloseButton));
+ Assert.ok(!BrowserTestUtils.is_visible(labelBox));
+ });
+
+ await BrowserTestUtils.withNewTab("about:robots", async browser => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+ const indicator = document.getElementById("urlbar-search-mode-indicator");
+ Assert.ok(!BrowserTestUtils.is_visible(indicator));
+ const indicatorCloseButton = document.getElementById(
+ "urlbar-search-mode-indicator-close"
+ );
+ Assert.ok(!BrowserTestUtils.is_visible(indicatorCloseButton));
+
+ await UrlbarTestUtils.enterSearchMode(window);
+ Assert.ok(BrowserTestUtils.is_visible(indicator));
+ Assert.ok(BrowserTestUtils.is_visible(indicatorCloseButton));
+
+ info("Blur the urlbar");
+ gURLBar.blur();
+ Assert.ok(BrowserTestUtils.is_visible(indicator));
+ Assert.ok(BrowserTestUtils.is_visible(indicatorCloseButton));
+ Assert.notEqual(
+ document.activeElement,
+ gURLBar.inputField,
+ "URL Bar should not be focused"
+ );
+
+ info("Leave search mode clicking on the close button while unfocussing");
+ await UrlbarTestUtils.exitSearchMode(window, {
+ clickClose: true,
+ waitForSearch: false,
+ });
+ Assert.ok(!BrowserTestUtils.is_visible(indicator));
+ Assert.ok(!BrowserTestUtils.is_visible(indicatorCloseButton));
+ });
+});
diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_localOneOffs_actionText.js b/browser/components/urlbar/tests/browser/browser_searchMode_localOneOffs_actionText.js
new file mode 100644
index 0000000000..74a2a3caba
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchMode_localOneOffs_actionText.js
@@ -0,0 +1,459 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests action text shown on heuristic and search suggestions when keyboard
+ * navigating local one-off buttons.
+ */
+
+"use strict";
+
+const DEFAULT_ENGINE_NAME = "Test";
+const SUGGESTIONS_ENGINE_NAME = "searchSuggestionEngine.xml";
+
+let engine;
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.searches", true],
+ ["browser.urlbar.suggest.quickactions", false],
+ ["browser.urlbar.shortcuts.quickactions", false],
+ ],
+ });
+ engine = await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + SUGGESTIONS_ENGINE_NAME,
+ setAsDefault: true,
+ });
+ await Services.search.moveEngine(engine, 0);
+
+ await PlacesUtils.history.clear();
+
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ });
+});
+
+add_task(async function localOneOff() {
+ info("Type some text, select a local one-off, check heuristic action");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "query",
+ });
+ Assert.ok(UrlbarTestUtils.getResultCount(window) > 1, "Sanity check results");
+
+ info("Alt UP to select the last local one-off.");
+ EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true });
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 0,
+ "the heuristic result should be selected"
+ );
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ let oneOffButtons = UrlbarTestUtils.getOneOffSearchButtons(window);
+ Assert.equal(
+ oneOffButtons.selectedButton.source,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ "A local one-off button should be selected"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_visible(result.element.action),
+ "The heuristic action should be visible"
+ );
+ let [actionHistory, actionBookmarks] = await document.l10n.formatValues([
+ { id: "urlbar-result-action-search-history" },
+ { id: "urlbar-result-action-search-bookmarks" },
+ ]);
+ Assert.equal(
+ result.displayed.action,
+ actionHistory,
+ "Check the heuristic action"
+ );
+ Assert.equal(
+ result.image,
+ "chrome://browser/skin/history.svg",
+ "Check the heuristic icon"
+ );
+
+ info("Move to an engine one-off and check heuristic action");
+ EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true });
+ EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true });
+ EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true });
+ Assert.ok(
+ oneOffButtons.selectedButton.engine,
+ "A one-off search button should be selected"
+ );
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(
+ BrowserTestUtils.is_visible(result.element.action),
+ "The heuristic action should be visible"
+ );
+ Assert.ok(
+ result.displayed.action.includes(oneOffButtons.selectedButton.engine.name),
+ "Check the heuristic action"
+ );
+ Assert.equal(
+ result.image,
+ oneOffButtons.selectedButton.engine.iconURI.spec,
+ "Check the heuristic icon"
+ );
+
+ info("Move again to a local one-off, deselect and reselect the heuristic");
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ oneOffButtons.selectedButton.source,
+ UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ "A local one-off button should be selected"
+ );
+ Assert.equal(
+ result.displayed.action,
+ actionBookmarks,
+ "Check the heuristic action"
+ );
+ Assert.equal(
+ result.image,
+ "chrome://browser/skin/bookmark.svg",
+ "Check the heuristic icon"
+ );
+
+ info(
+ "Select the next result, then reselect the heuristic, check it's a search with the default engine"
+ );
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 0,
+ "the heuristic result should be selected"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown", {});
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 1,
+ "the heuristic result should not be selected"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowUp", {});
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 0,
+ "the heuristic result should be selected"
+ );
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.SEARCH);
+ Assert.equal(result.searchParams.engine, engine.name);
+ Assert.ok(
+ result.displayed.action.includes(engine.name),
+ "Check the heuristic action"
+ );
+ Assert.equal(
+ result.image,
+ "chrome://global/skin/icons/search-glass.svg",
+ "Check the heuristic icon"
+ );
+});
+
+add_task(async function localOneOff_withVisit() {
+ info("Type a url, select a local one-off, check heuristic action");
+ await PlacesUtils.history.clear();
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits("https://mozilla.org/");
+ await PlacesTestUtils.addVisits("https://other.mozilla.org/");
+ }
+ const searchString = "mozilla.org";
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: searchString,
+ });
+ Assert.ok(UrlbarTestUtils.getResultCount(window) > 1, "Sanity check results");
+ let oneOffButtons = UrlbarTestUtils.getOneOffSearchButtons(window);
+
+ let [actionHistory, actionTabs, actionBookmarks] =
+ await document.l10n.formatValues([
+ { id: "urlbar-result-action-search-history" },
+ { id: "urlbar-result-action-search-tabs" },
+ { id: "urlbar-result-action-search-bookmarks" },
+ ]);
+
+ info("Alt UP to select the history one-off.");
+ EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true });
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 0,
+ "the heuristic result should be selected"
+ );
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ oneOffButtons.selectedButton.source,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ "The history one-off button should be selected"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_visible(result.element.action),
+ "The heuristic action should be visible"
+ );
+ Assert.equal(
+ result.displayed.action,
+ actionHistory,
+ "Check the heuristic action"
+ );
+ Assert.equal(
+ result.image,
+ "chrome://browser/skin/history.svg",
+ "Check the heuristic icon"
+ );
+ let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
+ Assert.equal(
+ row.querySelector(".urlbarView-title").textContent,
+ searchString,
+ "Check that the result title has been replaced with the search string."
+ );
+
+ info("Alt UP to select the tabs one-off.");
+ EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true });
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ oneOffButtons.selectedButton.source,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ "The tabs one-off button should be selected"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_visible(result.element.action),
+ "The heuristic action should be visible"
+ );
+ Assert.equal(
+ result.displayed.action,
+ actionTabs,
+ "Check the heuristic action"
+ );
+ Assert.equal(
+ result.image,
+ "chrome://browser/skin/tab.svg",
+ "Check the heuristic icon"
+ );
+
+ info("Alt UP to select the bookmarks one-off.");
+ EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true });
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ oneOffButtons.selectedButton.source,
+ UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ "The bookmarks one-off button should be selected"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_visible(result.element.action),
+ "The heuristic action should be visible"
+ );
+ Assert.equal(
+ result.displayed.action,
+ actionBookmarks,
+ "Check the heuristic action"
+ );
+ Assert.equal(
+ result.image,
+ "chrome://browser/skin/bookmark.svg",
+ "Check the heuristic icon"
+ );
+
+ info(
+ "Select the next result, then reselect the heuristic, check it's a visit"
+ );
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 0,
+ "the heuristic result should be selected"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown", {});
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 1,
+ "the heuristic result should not be selected"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowUp", {});
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 0,
+ "the heuristic result should be selected"
+ );
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ oneOffButtons.selectedButton,
+ null,
+ "No one-off button should be selected"
+ );
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.URL);
+ Assert.equal(
+ result.displayed.url,
+ result.result.payload.displayUrl,
+ "Check the heuristic action"
+ );
+ Assert.notEqual(
+ result.image,
+ "chrome://browser/skin/history.svg",
+ "Check the heuristic icon"
+ );
+ row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
+ Assert.equal(
+ row.querySelector(".urlbarView-title").textContent,
+ result.result.payload.title || `https://${searchString}`,
+ "Check that the result title has been restored to the fixed-up URI."
+ );
+
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function localOneOff_suggestion() {
+ info("Type some text, select the first suggestion, then a local one-off");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "query",
+ });
+ let count = UrlbarTestUtils.getResultCount(window);
+ Assert.ok(count > 1, "Sanity check results");
+ let result = null;
+ let suggestionIndex = -1;
+ for (let i = 1; i < count; ++i) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ let index = await UrlbarTestUtils.getSelectedRowIndex(window);
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, index);
+ if (
+ result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
+ result.searchParams.suggestion
+ ) {
+ suggestionIndex = i;
+ break;
+ }
+ }
+ Assert.ok(
+ result.searchParams.suggestion,
+ "Should have selected a search suggestion"
+ );
+ Assert.ok(
+ result.displayed.action.includes(result.searchParams.engine),
+ "Check the search suggestion action"
+ );
+
+ info("Alt UP to select the last local one-off.");
+ EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true });
+ Assert.equal(
+ await UrlbarTestUtils.getSelectedRowIndex(window),
+ suggestionIndex,
+ "the suggestion should still be selected"
+ );
+
+ let [actionHistory] = await document.l10n.formatValues([
+ { id: "urlbar-result-action-search-history" },
+ ]);
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, suggestionIndex);
+ Assert.equal(
+ result.displayed.action,
+ actionHistory,
+ "Check the search suggestion action changed to local one-off"
+ );
+ // Like in the normal engine one-offs case, we don't replace the favicon.
+ Assert.equal(
+ result.image,
+ "chrome://global/skin/icons/search-glass.svg",
+ "Check the suggestion icon"
+ );
+
+ info("DOWN to select the next suggestion");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ result = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ suggestionIndex + 1
+ );
+ Assert.ok(
+ result.searchParams.suggestion,
+ "Should have selected a search suggestion"
+ );
+ Assert.ok(
+ result.displayed.action.includes(result.searchParams.engine),
+ "Check the search suggestion action"
+ );
+ Assert.equal(
+ result.image,
+ "chrome://global/skin/icons/search-glass.svg",
+ "Check the suggestion icon"
+ );
+
+ info("UP back to the previous suggestion");
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, suggestionIndex);
+ Assert.ok(
+ result.displayed.action.includes(result.searchParams.engine),
+ "Check the search suggestion action"
+ );
+ Assert.equal(
+ result.image,
+ "chrome://global/skin/icons/search-glass.svg",
+ "Check the suggestion icon"
+ );
+});
+
+add_task(async function localOneOff_shortcut() {
+ info("Select a search shortcut, then a local one-off");
+
+ await PlacesUtils.history.clear();
+ // Enough vists to get this site into Top Sites.
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits("http://example.com/");
+ }
+
+ await updateTopSites(
+ sites => sites && sites[0] && sites[0].searchTopSite,
+ true
+ );
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ let count = UrlbarTestUtils.getResultCount(window);
+ Assert.ok(count > 1, "Sanity check results");
+ let result = null;
+ let shortcutIndex = -1;
+ for (let i = 0; i < count; ++i) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ let index = await UrlbarTestUtils.getSelectedRowIndex(window);
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, index);
+ if (
+ result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
+ result.searchParams.keyword
+ ) {
+ shortcutIndex = i;
+ break;
+ }
+ }
+ Assert.ok(result.searchParams.keyword, "Should have selected a shortcut");
+ let shortcutEngine = result.searchParams.engine;
+
+ info("Alt UP to select the last local one-off.");
+ EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true });
+ Assert.equal(
+ await UrlbarTestUtils.getSelectedRowIndex(window),
+ shortcutIndex,
+ "the shortcut should still be selected"
+ );
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, shortcutIndex);
+ Assert.equal(
+ result.displayed.action,
+ "",
+ "Check the shortcut action is empty"
+ );
+ Assert.equal(
+ result.searchParams.engine,
+ shortcutEngine,
+ "Check the shortcut engine"
+ );
+ Assert.ok(
+ result.displayed.title.includes(shortcutEngine),
+ "Check the shortcut title"
+ );
+ Assert.notEqual(
+ result.image,
+ "chrome://global/skin/icons/search-glass.svg",
+ "Check the icon was not replaced"
+ );
+
+ await UrlbarTestUtils.exitSearchMode(window);
+ await PlacesUtils.history.clear();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_newWindow.js b/browser/components/urlbar/tests/browser/browser_searchMode_newWindow.js
new file mode 100644
index 0000000000..e5a3eb848a
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchMode_newWindow.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests immediately entering search mode in a new window and then exiting it.
+// No errors should be thrown and search mode should be exited successfully.
+
+"use strict";
+
+add_task(async function escape() {
+ await doTest(win =>
+ EventUtils.synthesizeKey("KEY_Escape", { repeat: 2 }, win)
+ );
+});
+
+add_task(async function backspace() {
+ await doTest(win => EventUtils.synthesizeKey("KEY_Backspace", {}, win));
+});
+
+async function doTest(exitSearchMode) {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+
+ // Press accel+K to enter search mode.
+ await UrlbarTestUtils.promisePopupOpen(win, () =>
+ EventUtils.synthesizeKey("k", { accelKey: true }, win)
+ );
+ await UrlbarTestUtils.assertSearchMode(win, {
+ engineName: Services.search.defaultEngine.name,
+ isGeneralPurposeEngine: true,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ isPreview: false,
+ entry: "shortcut",
+ });
+
+ // Exit search mode.
+ await exitSearchMode(win);
+ await UrlbarTestUtils.assertSearchMode(win, null);
+
+ await UrlbarTestUtils.promisePopupClose(win);
+ await BrowserTestUtils.closeWindow(win);
+}
diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_no_results.js b/browser/components/urlbar/tests/browser/browser_searchMode_no_results.js
new file mode 100644
index 0000000000..9ecc5573fc
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchMode_no_results.js
@@ -0,0 +1,290 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests entering search mode and there are no results in the view.
+ */
+
+"use strict";
+
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+
+add_setup(async function () {
+ // In order to open the view without any results, we need to be in search mode
+ // with an empty search string so that no heuristic result is shown, and the
+ // empty search must yield zero additional results. We'll enter search mode
+ // using the bookmarks one-off, and first we'll delete all bookmarks so that
+ // there are no results.
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ // Also clear history so that using the alias of our test engine doesn't
+ // inadvertently return any history results due to bug 1658646.
+ await PlacesUtils.history.clear();
+
+ // Add a top site so we're guaranteed the view has at least one result to
+ // show initially with an empty search. Otherwise the view won't even open.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "browser.newtabpage.activity-stream.default.sites",
+ "http://example.com/",
+ ],
+ ],
+ });
+ await updateTopSites(sites => sites.length);
+});
+
+// Basic test for entering search mode with no results.
+add_task(async function basic() {
+ await withNewWindow(async win => {
+ // Do an empty search.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "",
+ });
+
+ // Initially there should be at least the top site we added above.
+ Assert.greater(
+ UrlbarTestUtils.getResultCount(win),
+ 0,
+ "Top sites should be present initially"
+ );
+ Assert.ok(
+ !win.gURLBar.panel.hasAttribute("noresults"),
+ "Panel has results, therefore should not have noresults attribute"
+ );
+
+ // Enter search mode by clicking the bookmarks one-off.
+ await UrlbarTestUtils.enterSearchMode(win, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ });
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(win),
+ 0,
+ "Zero results since no bookmarks exist"
+ );
+ Assert.equal(
+ win.gURLBar.panel.getAttribute("noresults"),
+ "true",
+ "Panel has no results, therefore should have noresults attribute"
+ );
+
+ // Exit search mode by backspacing. The top sites should be shown again.
+ await UrlbarTestUtils.exitSearchMode(win, { backspace: true });
+ Assert.greater(
+ UrlbarTestUtils.getResultCount(win),
+ 0,
+ "Top sites should be present again"
+ );
+ Assert.ok(
+ !win.gURLBar.panel.hasAttribute("noresults"),
+ "noresults attribute should be absent again"
+ );
+
+ await UrlbarTestUtils.promisePopupClose(win);
+ });
+});
+
+// When the urlbar is in search mode, has no results, and is not focused,
+// focusing it should auto-open the view.
+add_task(async function autoOpen() {
+ await withNewWindow(async win => {
+ // Do an empty search.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "",
+ });
+
+ // Initially there should be at least the top site we added above.
+ Assert.greater(
+ UrlbarTestUtils.getResultCount(win),
+ 0,
+ "Top sites should be present initially"
+ );
+ Assert.ok(
+ !win.gURLBar.panel.hasAttribute("noresults"),
+ "Panel has results, therefore should not have noresults attribute"
+ );
+
+ // Enter search mode by clicking the bookmarks one-off.
+ await UrlbarTestUtils.enterSearchMode(win, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ });
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(win),
+ 0,
+ "Zero results since no bookmarks exist"
+ );
+ Assert.equal(
+ win.gURLBar.panel.getAttribute("noresults"),
+ "true",
+ "Panel has no results, therefore should have noresults attribute"
+ );
+
+ // Blur the urlbar.
+ win.gURLBar.blur();
+ await UrlbarTestUtils.assertSearchMode(win, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ entry: "oneoff",
+ });
+
+ // Click the urlbar.
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ EventUtils.synthesizeMouseAtCenter(win.gURLBar.inputField, {}, win);
+ });
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(win),
+ 0,
+ "Still zero results since no bookmarks exist"
+ );
+ Assert.equal(
+ win.gURLBar.panel.getAttribute("noresults"),
+ "true",
+ "Panel still has no results, therefore should have noresults attribute"
+ );
+
+ // Exit search mode by backspacing. The top sites should be shown again.
+ await UrlbarTestUtils.exitSearchMode(win, { backspace: true });
+ Assert.greater(
+ UrlbarTestUtils.getResultCount(win),
+ 0,
+ "Top sites should be present again"
+ );
+ Assert.ok(
+ !win.gURLBar.panel.hasAttribute("noresults"),
+ "noresults attribute should be absent again"
+ );
+
+ await UrlbarTestUtils.promisePopupClose(win);
+ });
+});
+
+// When the urlbar is in search mode, the user backspaces over the final char
+// (but remains in search mode), and there are no results, the view should
+// remain open.
+add_task(async function backspaceRemainOpen() {
+ await withNewWindow(async win => {
+ // Do a one-char search.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "x",
+ });
+ Assert.greater(
+ UrlbarTestUtils.getResultCount(win),
+ 0,
+ "At least the heuristic result should be shown"
+ );
+ Assert.ok(
+ !win.gURLBar.panel.hasAttribute("noresults"),
+ "Panel has results, therefore should not have noresults attribute"
+ );
+
+ // Enter search mode by clicking the bookmarks one-off.
+ await UrlbarTestUtils.enterSearchMode(win, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ });
+
+ // The heursitic should not be shown since we don't show it in local search
+ // modes.
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(win),
+ 0,
+ "No results should be present"
+ );
+ Assert.ok(
+ win.gURLBar.panel.hasAttribute("noresults"),
+ "Panel has no results, therefore should have noresults attribute"
+ );
+
+ // Backspace. The search string will now be empty.
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(win);
+ EventUtils.synthesizeKey("KEY_Backspace", {}, win);
+ await searchPromise;
+ Assert.ok(UrlbarTestUtils.isPopupOpen(win), "View remains open");
+ await UrlbarTestUtils.assertSearchMode(win, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ entry: "oneoff",
+ });
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(win),
+ 0,
+ "Zero results since no bookmarks exist"
+ );
+ Assert.equal(
+ win.gURLBar.panel.getAttribute("noresults"),
+ "true",
+ "Panel has no results, therefore should have noresults attribute"
+ );
+
+ // Exit search mode by backspacing. The top sites should be shown.
+ await UrlbarTestUtils.exitSearchMode(win, { backspace: true });
+ Assert.greater(
+ UrlbarTestUtils.getResultCount(win),
+ 0,
+ "Top sites should be present again"
+ );
+ Assert.ok(
+ !win.gURLBar.panel.hasAttribute("noresults"),
+ "noresults attribute should be absent again"
+ );
+
+ await UrlbarTestUtils.promisePopupClose(win);
+ });
+});
+
+// Types a search alias and then a space to enter search mode, with no results.
+// The one-offs should be shown.
+add_task(async function spaceToEnterSearchMode() {
+ let engine = await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME,
+ });
+ engine.alias = "@test";
+
+ await withNewWindow(async win => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: engine.alias,
+ });
+
+ // We need to wait for two searches: The first enters search mode, the
+ // second does the search in search mode.
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(win);
+ EventUtils.synthesizeKey(" ", {}, win);
+ await searchPromise;
+
+ Assert.equal(UrlbarTestUtils.getResultCount(win), 0, "Zero results");
+ Assert.equal(
+ win.gURLBar.panel.getAttribute("noresults"),
+ "true",
+ "Panel has no results, therefore should have noresults attribute"
+ );
+ await UrlbarTestUtils.assertSearchMode(win, {
+ engineName: engine.name,
+ entry: "typed",
+ });
+ this.Assert.equal(
+ UrlbarTestUtils.getOneOffSearchButtonsVisible(window),
+ true,
+ "One-offs are visible"
+ );
+
+ await UrlbarTestUtils.exitSearchMode(win, { backspace: true });
+ await UrlbarTestUtils.promisePopupClose(win);
+ });
+});
+
+/**
+ * Opens a new window, waits for it to load, calls a callback, and closes the
+ * window. We use a new window in each task so that the view starts with a
+ * blank slate each time.
+ *
+ * @param {Function} callback
+ * Will be called as: callback(newWindow)
+ */
+async function withNewWindow(callback) {
+ // Start in a new window so we have a blank slate.
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await callback(win);
+ await BrowserTestUtils.closeWindow(win);
+}
diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_oneOffButton.js b/browser/components/urlbar/tests/browser/browser_searchMode_oneOffButton.js
new file mode 100644
index 0000000000..1ba0d3283b
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchMode_oneOffButton.js
@@ -0,0 +1,108 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests one-off search button behavior with search mode.
+ */
+
+const TEST_ENGINE_NAME = "test engine";
+
+add_setup(async function () {
+ await SearchTestUtils.installSearchExtension({
+ name: TEST_ENGINE_NAME,
+ keyword: "@test",
+ });
+});
+
+add_task(async function test() {
+ info("Test no one-off buttons are selected when entering search mode");
+
+ info("Open the result popup");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+
+ info("Select one of one-off button");
+ const oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window);
+ await TestUtils.waitForCondition(
+ () => !oneOffs._rebuilding,
+ "Waiting for one-offs to finish rebuilding"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ ok(oneOffs.selectedButton, "There is a selected one-off button");
+
+ info("Enter search mode");
+ await UrlbarTestUtils.enterSearchMode(window, {
+ engineName: TEST_ENGINE_NAME,
+ });
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ENGINE_NAME,
+ entry: "oneoff",
+ });
+ ok(!oneOffs.selectedButton, "There is no selected one-off button");
+});
+
+add_task(async function () {
+ info(
+ "Test the status of the selected one-off button when exiting search mode with backspace"
+ );
+
+ info("Open the result popup");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+
+ info("Select one of one-off button");
+ const oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window);
+ await TestUtils.waitForCondition(
+ () => !oneOffs._rebuilding,
+ "Waiting for one-offs to finish rebuilding"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ ok(oneOffs.selectedButton, "There is a selected one-off button");
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: oneOffs.selectedButton.engine.name,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ entry: "oneoff",
+ isPreview: true,
+ });
+
+ info("Exit from search mode");
+ await UrlbarTestUtils.exitSearchMode(window);
+ ok(!oneOffs.selectedButton, "There is no any selected one-off button");
+});
+
+add_task(async function () {
+ info(
+ "Test the status of the selected one-off button when exiting search mode with clicking close button"
+ );
+
+ info("Open the result popup");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+
+ info("Select one of one-off button");
+ const oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window);
+ await TestUtils.waitForCondition(
+ () => !oneOffs._rebuilding,
+ "Waiting for one-offs to finish rebuilding"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ ok(oneOffs.selectedButton, "There is a selected one-off button");
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: oneOffs.selectedButton.engine.name,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ entry: "oneoff",
+ isPreview: true,
+ });
+
+ info("Exit from search mode");
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+ ok(!oneOffs.selectedButton, "There is no any selected one-off button");
+});
diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_pickResult.js b/browser/components/urlbar/tests/browser/browser_searchMode_pickResult.js
new file mode 100644
index 0000000000..ac45b3e5c7
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchMode_pickResult.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that search mode is exited after picking a result.
+ */
+
+"use strict";
+
+const BOOKMARK_URL = "http://www.example.com/browser_searchMode_pickResult.js";
+
+add_setup(async function () {
+ // Add a bookmark so we can enter bookmarks search mode and pick it.
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: BOOKMARK_URL,
+ });
+ registerCleanupFunction(async () => {
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+});
+
+// Opens a new tab, enters search mode, does a search for our test bookmark, and
+// picks it. Uses a variety of initial URLs and search strings in order to hit
+// different branches in setURI. Search mode should be exited in all cases.
+add_task(async function pickResult() {
+ for (let test of [
+ // initialURL, searchString
+ ["about:blank", BOOKMARK_URL],
+ ["about:blank", new URL(BOOKMARK_URL).origin],
+ ["about:blank", new URL(BOOKMARK_URL).pathname],
+ [BOOKMARK_URL, BOOKMARK_URL],
+ [BOOKMARK_URL, new URL(BOOKMARK_URL).origin],
+ [BOOKMARK_URL, new URL(BOOKMARK_URL).pathname],
+ ]) {
+ await doPickResultTest(...test);
+ }
+});
+
+async function doPickResultTest(initialURL, searchString) {
+ info(
+ "doPickResultTest with args: " +
+ JSON.stringify({
+ initialURL,
+ searchString,
+ })
+ );
+
+ await BrowserTestUtils.withNewTab(initialURL, async () => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: searchString,
+ fireInputEvent: true,
+ });
+
+ await UrlbarTestUtils.enterSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ });
+
+ // Arrow down to the bookmark result.
+ let firstResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ if (!firstResult.heuristic) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+ let foundResult = false;
+ for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ if (result.url == BOOKMARK_URL) {
+ foundResult = true;
+ break;
+ }
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+ Assert.ok(foundResult, "The bookmark result should have been found");
+
+ // Press enter.
+ let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+ Assert.equal(
+ gBrowser.currentURI.spec,
+ BOOKMARK_URL,
+ "Should have loaded the bookmarked URL"
+ );
+
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ });
+}
diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_preview.js b/browser/components/urlbar/tests/browser/browser_searchMode_preview.js
new file mode 100644
index 0000000000..19df744663
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchMode_preview.js
@@ -0,0 +1,489 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests search mode preview.
+ */
+
+"use strict";
+
+const TEST_ENGINE_NAME = "Test";
+
+add_setup(async function () {
+ await SearchTestUtils.installSearchExtension({
+ name: TEST_ENGINE_NAME,
+ keyword: "@test",
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.quickactions", false]],
+ });
+
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ });
+});
+
+/**
+ * @param {Node} button
+ * A one-off button.
+ * @param {boolean} [isPreview]
+ * Whether the expected search mode should be a preview. Defaults to true.
+ * @returns {object}
+ * The search mode object expected when that one-off is selected.
+ */
+function getExpectedSearchMode(button, isPreview = true) {
+ let expectedSearchMode = {
+ entry: "oneoff",
+ isPreview,
+ };
+ if (button.engine) {
+ expectedSearchMode.engineName = button.engine.name;
+ let engine = Services.search.getEngineByName(button.engine.name);
+ if (engine.isGeneralPurposeEngine) {
+ expectedSearchMode.source = UrlbarUtils.RESULT_SOURCE.SEARCH;
+ }
+ } else {
+ expectedSearchMode.source = button.source;
+ }
+
+ return expectedSearchMode;
+}
+
+// Tests that cycling through token alias engines enters search mode preview.
+add_task(async function tokenAlias() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@",
+ });
+
+ let result;
+ while (gURLBar.searchMode?.engineName != TEST_ENGINE_NAME) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ let index = UrlbarTestUtils.getSelectedRowIndex(window);
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, index);
+ let expectedSearchMode = {
+ engineName: result.searchParams.engine,
+ isPreview: true,
+ entry: "keywordoffer",
+ };
+ let engine = Services.search.getEngineByName(result.searchParams.engine);
+ if (engine.isGeneralPurposeEngine) {
+ expectedSearchMode.source = UrlbarUtils.RESULT_SOURCE.SEARCH;
+ }
+ await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode);
+ }
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await searchPromise;
+ // Test that we are in confirmed search mode.
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: result.searchParams.engine,
+ entry: "keywordoffer",
+ });
+ await UrlbarTestUtils.exitSearchMode(window);
+});
+
+// Tests that starting to type a query exits search mode preview in favour of
+// full search mode.
+add_task(async function startTyping() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@",
+ });
+ while (gURLBar.searchMode?.engineName != TEST_ENGINE_NAME) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ENGINE_NAME,
+ isPreview: true,
+ entry: "keywordoffer",
+ });
+
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("M");
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ENGINE_NAME,
+ entry: "keywordoffer",
+ });
+ await UrlbarTestUtils.exitSearchMode(window);
+});
+
+// Tests that highlighting a search shortcut Top Site enters search mode
+// preview.
+add_task(async function topSites() {
+ // Enable search shortcut Top Sites.
+ await PlacesUtils.history.clear();
+ await updateTopSites(
+ sites => sites && sites[0] && sites[0].searchTopSite,
+ true
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ fireInputEvent: true,
+ });
+
+ // We previously verified that the first Top Site is a search shortcut.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ let searchTopSite = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: searchTopSite.searchParams.engine,
+ isPreview: true,
+ entry: "topsites_urlbar",
+ });
+ await UrlbarTestUtils.exitSearchMode(window);
+});
+
+// Tests that search mode preview is exited when the view is closed.
+add_task(async function closeView() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@",
+ });
+
+ while (gURLBar.searchMode?.engineName != TEST_ENGINE_NAME) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ENGINE_NAME,
+ isPreview: true,
+ entry: "keywordoffer",
+ });
+
+ // We should close search mode when closing the view.
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+ await UrlbarTestUtils.assertSearchMode(window, null);
+
+ // Check search mode isn't re-entered when re-opening the view.
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ if (gURLBar.getAttribute("pageproxystate") == "invalid") {
+ gURLBar.handleRevert();
+ }
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+// Tests that search more preview is exited when the user switches tabs.
+add_task(async function tabSwitch() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@",
+ });
+
+ while (gURLBar.searchMode?.engineName != TEST_ENGINE_NAME) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ENGINE_NAME,
+ isPreview: true,
+ entry: "keywordoffer",
+ });
+
+ // Open a new tab then switch back to the original tab.
+ let tab1 = gBrowser.selectedTab;
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ BrowserTestUtils.removeTab(tab2);
+});
+
+// Tests that search mode is previewed when the user down arrows through the
+// one-offs.
+add_task(async function oneOff_downArrow() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ let oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window);
+ await TestUtils.waitForCondition(
+ () => !oneOffs._rebuilding,
+ "Waiting for one-offs to finish rebuilding"
+ );
+ let resultCount = UrlbarTestUtils.getResultCount(window);
+
+ // Key down through all results.
+ for (let i = 0; i < resultCount; i++) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+
+ // Key down again. The first one-off should be selected.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+
+ // Check for the one-off's search mode previews.
+ while (oneOffs.selectedButton != oneOffs.settingsButton) {
+ await UrlbarTestUtils.assertSearchMode(
+ window,
+ getExpectedSearchMode(oneOffs.selectedButton)
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+
+ // Check that selecting the search settings button leaves search mode preview.
+ Assert.equal(
+ oneOffs.selectedButton,
+ oneOffs.settingsButton,
+ "The settings button is selected."
+ );
+ await UrlbarTestUtils.assertSearchMode(window, null);
+
+ // Closing the view should also exit search mode preview.
+ await UrlbarTestUtils.promisePopupClose(window);
+ await UrlbarTestUtils.assertSearchMode(window, null);
+});
+
+// Tests that search mode is previewed when the user Alt+down arrows through the
+// one-offs. This subtest also highlights a keywordoffer result (the first Top
+// Site) before Alt+Arrowing to the one-offs. This checks that the search mode
+// previews from keywordoffer results are overwritten by selected one-offs.
+add_task(async function oneOff_alt_downArrow() {
+ // Add some visits to a URL so we have multiple Top Sites.
+ await PlacesUtils.history.clear();
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits("https://example.com/");
+ }
+ await updateTopSites(
+ sites =>
+ sites &&
+ sites[0]?.searchTopSite &&
+ sites[1]?.url == "https://example.com/",
+ true
+ );
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ let oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window);
+ await TestUtils.waitForCondition(
+ () => !oneOffs._rebuilding,
+ "Waiting for one-offs to finish rebuilding"
+ );
+
+ // Key down to the first result and check that it enters search mode preview.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ let searchTopSite = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: searchTopSite.searchParams.engine,
+ isPreview: true,
+ entry: "topsites_urlbar",
+ });
+
+ // Alt+down. The first one-off should be selected.
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ // Check for the one-offs' search mode previews.
+ while (oneOffs.selectedButton) {
+ await UrlbarTestUtils.assertSearchMode(
+ window,
+ getExpectedSearchMode(oneOffs.selectedButton)
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ }
+
+ // Now key down without a modifier. We should move to the second result and
+ // have no search mode preview.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 1,
+ "The second result is selected."
+ );
+ await UrlbarTestUtils.assertSearchMode(window, null);
+
+ // Arrow back up to the keywordoffer result and check for search mode preview.
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: searchTopSite.searchParams.engine,
+ isPreview: true,
+ entry: "topsites_urlbar",
+ });
+
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.promisePopupClose(window);
+ await UrlbarTestUtils.assertSearchMode(window, null);
+});
+
+// Tests that search mode is previewed when the user is in full search mode
+// and down arrows through the one-offs.
+add_task(async function fullSearchMode_oneOff_downArrow() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ let oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window);
+ await TestUtils.waitForCondition(
+ () => !oneOffs._rebuilding,
+ "Waiting for one-offs to finish rebuilding"
+ );
+ let oneOffButtons = oneOffs.getSelectableButtons(true);
+
+ await UrlbarTestUtils.enterSearchMode(window);
+ let expectedSearchMode = getExpectedSearchMode(oneOffButtons[0], false);
+ // Sanity check: we are in the correct search mode.
+ await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode);
+
+ // Key down through all results.
+ let resultCount = UrlbarTestUtils.getResultCount(window);
+ for (let i = 0; i < resultCount; i++) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ // If the result is a shortcut, it will enter preview mode.
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ await UrlbarTestUtils.assertSearchMode(
+ window,
+ Object.assign(expectedSearchMode, {
+ isPreview: !!result.searchParams.keyword,
+ })
+ );
+ }
+
+ // Key down again. The first one-off should be selected.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ // Check that we show the correct preview as we cycle through the one-offs.
+ while (oneOffs.selectedButton != oneOffs.settingsButton) {
+ await UrlbarTestUtils.assertSearchMode(
+ window,
+ getExpectedSearchMode(oneOffs.selectedButton, true)
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+
+ // We should still be in the same search mode after cycling through all the
+ // one-offs.
+ await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode);
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+// Tests that search mode is previewed when the user is in full search mode
+// and alt+down arrows through the one-offs. This subtest also checks that
+// exiting full search mode while alt+arrowing through the one-offs enters
+// search mode preview for subsequent one-offs.
+add_task(async function fullSearchMode_oneOff_alt_downArrow() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ let oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window);
+ let oneOffButtons = oneOffs.getSelectableButtons(true);
+ await TestUtils.waitForCondition(
+ () => !oneOffs._rebuilding,
+ "Waiting for one-offs to finish rebuilding"
+ );
+
+ await UrlbarTestUtils.enterSearchMode(window);
+ let expectedSearchMode = getExpectedSearchMode(oneOffButtons[0], false);
+ await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode);
+
+ // Key down to the first result.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+
+ // Alt+down. The first one-off should be selected.
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ // Cycle through the first half of the one-offs and verify that search mode
+ // preview is entered.
+ Assert.greater(
+ oneOffButtons.length,
+ 1,
+ "Sanity check: We should have at least two one-offs."
+ );
+ for (let i = 1; i < oneOffButtons.length / 2; i++) {
+ await UrlbarTestUtils.assertSearchMode(
+ window,
+ getExpectedSearchMode(oneOffs.selectedButton, true)
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ }
+ // Now click out of search mode.
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+ // Now check for the remaining one-offs' search mode previews.
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ while (oneOffs.selectedButton) {
+ await UrlbarTestUtils.assertSearchMode(
+ window,
+ getExpectedSearchMode(oneOffs.selectedButton, true)
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ await UrlbarTestUtils.assertSearchMode(window, null);
+});
+
+// Tests that the original search mode is preserved when going through some
+// one-off buttons and then back down in the results list.
+add_task(async function fullSearchMode_oneOff_restore_on_down() {
+ info("Add a few visits to top sites");
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits([
+ "https://1.example.com/",
+ "https://2.example.com/",
+ "https://3.example.com/",
+ ]);
+ }
+ await updateTopSites(sites => sites?.length > 2, false);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ let oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window);
+ let oneOffButtons = oneOffs.getSelectableButtons(true);
+ await TestUtils.waitForCondition(
+ () => !oneOffs._rebuilding,
+ "Waiting for one-offs to finish rebuilding"
+ );
+
+ await UrlbarTestUtils.enterSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ });
+ let expectedSearchMode = getExpectedSearchMode(
+ oneOffButtons.find(b => b.source == UrlbarUtils.RESULT_SOURCE.HISTORY),
+ false
+ );
+ await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode);
+ info("Down to the first result");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode);
+ info("Alt+down to the first one-off.");
+ Assert.greater(
+ oneOffButtons.length,
+ 1,
+ "Sanity check: We should have at least two one-offs."
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ await UrlbarTestUtils.assertSearchMode(
+ window,
+ getExpectedSearchMode(oneOffs.selectedButton, true)
+ );
+ info("Go again down through the list of results");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode);
+
+ // Now do a similar test without initial search mode.
+ info("Exit search mode.");
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ info("Down to the first result");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ info("select a one-off to start preview");
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ await UrlbarTestUtils.assertSearchMode(
+ window,
+ getExpectedSearchMode(oneOffs.selectedButton, true)
+ );
+ info("Go again through the list of results");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await UrlbarTestUtils.assertSearchMode(window, null);
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ await PlacesUtils.history.clear();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_sessionStore.js b/browser/components/urlbar/tests/browser/browser_searchMode_sessionStore.js
new file mode 100644
index 0000000000..ef3fabe636
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchMode_sessionStore.js
@@ -0,0 +1,332 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests search mode and session store. Also tests that search mode is
+ * duplicated when duplicating tabs, since tab duplication is handled by session
+ * store.
+ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
+ TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.sys.mjs",
+});
+
+// This test takes a long time on the OS X 10.14 machines, so request a longer
+// timeout. See bug 1671045. This may also fix a different failure on Linux in
+// bug 1671087, but it's not clear. Regardless, a longer timeout won't hurt.
+requestLongerTimeout(5);
+
+const SEARCH_STRING = "test browser_sessionStore.js";
+const URL = "http://example.com/";
+
+// A URL in gInitialPages. We test this separately since SessionStore sometimes
+// takes different paths for these URLs.
+const INITIAL_URL = "about:newtab";
+
+// The following tasks make sure non-null search mode is restored.
+
+add_task(async function initialPageOnRestore() {
+ await doTest({
+ urls: [INITIAL_URL],
+ searchModeTabIndex: 0,
+ exitSearchMode: false,
+ switchTabsAfterEnteringSearchMode: false,
+ });
+});
+
+add_task(async function switchToInitialPage() {
+ await doTest({
+ urls: [URL, INITIAL_URL],
+ searchModeTabIndex: 1,
+ exitSearchMode: false,
+ switchTabsAfterEnteringSearchMode: true,
+ });
+});
+
+add_task(async function nonInitialPageOnRestore() {
+ await doTest({
+ urls: [URL],
+ searchModeTabIndex: 0,
+ exitSearchMode: false,
+ switchTabsAfterEnteringSearchMode: false,
+ });
+});
+
+add_task(async function switchToNonInitialPage() {
+ await doTest({
+ urls: [INITIAL_URL, URL],
+ searchModeTabIndex: 1,
+ exitSearchMode: false,
+ switchTabsAfterEnteringSearchMode: true,
+ });
+});
+
+// The following tasks enter and then exit search mode to make sure that no
+// search mode is restored.
+
+add_task(async function initialPageOnRestore_exit() {
+ await doTest({
+ urls: [INITIAL_URL],
+ searchModeTabIndex: 0,
+ exitSearchMode: true,
+ switchTabsAfterEnteringSearchMode: false,
+ });
+});
+
+add_task(async function switchToInitialPage_exit() {
+ await doTest({
+ urls: [URL, INITIAL_URL],
+ searchModeTabIndex: 1,
+ exitSearchMode: true,
+ switchTabsAfterEnteringSearchMode: true,
+ });
+});
+
+add_task(async function nonInitialPageOnRestore_exit() {
+ await doTest({
+ urls: [URL],
+ searchModeTabIndex: 0,
+ exitSearchMode: true,
+ switchTabsAfterEnteringSearchMode: false,
+ });
+});
+
+add_task(async function switchToNonInitialPage_exit() {
+ await doTest({
+ urls: [INITIAL_URL, URL],
+ searchModeTabIndex: 1,
+ exitSearchMode: true,
+ switchTabsAfterEnteringSearchMode: true,
+ });
+});
+
+/**
+ * The main test function. Opens some URLs in a new window, enters search mode
+ * in one of the tabs, closes the window, restores it, and makes sure that
+ * search mode is restored properly.
+ *
+ * @param {object} options
+ * Options object
+ * @param {Array} options.urls
+ * Array of string URLs to open.
+ * @param {number} options.searchModeTabIndex
+ * The index of the tab in which to enter search mode.
+ * @param {boolean} options.exitSearchMode
+ * If true, search mode will be immediately exited after entering it. Use
+ * this to make sure search mode is *not* restored after it's exited.
+ * @param {boolean} options.switchTabsAfterEnteringSearchMode
+ * If true, we'll switch to a tab other than the one that search mode was
+ * entered in before closing the window. `urls` should contain more than one
+ * URL in this case.
+ */
+async function doTest({
+ urls,
+ searchModeTabIndex,
+ exitSearchMode,
+ switchTabsAfterEnteringSearchMode,
+}) {
+ let searchModeURL = urls[searchModeTabIndex];
+ let otherTabIndex = (searchModeTabIndex + 1) % urls.length;
+ let otherURL = urls[otherTabIndex];
+
+ await withNewWindow(urls, async win => {
+ if (win.gBrowser.selectedTab != win.gBrowser.tabs[searchModeTabIndex]) {
+ await BrowserTestUtils.switchTab(
+ win.gBrowser,
+ win.gBrowser.tabs[searchModeTabIndex]
+ );
+ }
+
+ Assert.equal(
+ win.gBrowser.currentURI.spec,
+ searchModeURL,
+ `Sanity check: Tab at index ${searchModeTabIndex} is correct`
+ );
+ Assert.equal(
+ searchModeURL == INITIAL_URL,
+ win.gInitialPages.includes(win.gBrowser.currentURI.spec),
+ `Sanity check: ${searchModeURL} is or is not in gInitialPages as expected`
+ );
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: SEARCH_STRING,
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.enterSearchMode(win, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ });
+
+ if (exitSearchMode) {
+ await UrlbarTestUtils.exitSearchMode(win);
+ }
+
+ // Make sure session store is updated.
+ await TabStateFlusher.flush(win.gBrowser.selectedBrowser);
+
+ if (switchTabsAfterEnteringSearchMode) {
+ await BrowserTestUtils.switchTab(
+ win.gBrowser,
+ win.gBrowser.tabs[otherTabIndex]
+ );
+ }
+ });
+
+ let restoredURL = switchTabsAfterEnteringSearchMode
+ ? otherURL
+ : searchModeURL;
+
+ let win = await restoreWindow(restoredURL);
+
+ Assert.equal(
+ win.gBrowser.currentURI.spec,
+ restoredURL,
+ "Sanity check: Initially selected tab in restored window is correct"
+ );
+
+ if (switchTabsAfterEnteringSearchMode) {
+ // Switch back to the tab with search mode.
+ await BrowserTestUtils.switchTab(
+ win.gBrowser,
+ win.gBrowser.tabs[searchModeTabIndex]
+ );
+ }
+
+ if (exitSearchMode) {
+ // If we exited search mode, it should be null.
+ await new Promise(r => win.setTimeout(r, 500));
+ await UrlbarTestUtils.assertSearchMode(win, null);
+ } else {
+ // If we didn't exit search mode, it should be restored.
+ await TestUtils.waitForCondition(
+ () => win.gURLBar.searchMode,
+ "Waiting for search mode to be restored"
+ );
+ await UrlbarTestUtils.assertSearchMode(win, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ entry: "oneoff",
+ });
+ Assert.equal(
+ win.gURLBar.value,
+ SEARCH_STRING,
+ "Search string should be restored"
+ );
+ }
+
+ await BrowserTestUtils.closeWindow(win);
+}
+
+async function openTabMenuFor(tab) {
+ let tabMenu = tab.ownerDocument.getElementById("tabContextMenu");
+
+ let tabMenuShown = BrowserTestUtils.waitForEvent(tabMenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(
+ tab,
+ { type: "contextmenu" },
+ tab.ownerGlobal
+ );
+ await tabMenuShown;
+
+ return tabMenu;
+}
+
+// Tests that search mode is duplicated when duplicating tabs. Note that tab
+// duplication is handled by session store.
+add_task(async function duplicateTabs() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.net/"
+ );
+ gBrowser.selectedTab = tab;
+ // Enter search mode with a search string in the current tab.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: SEARCH_STRING,
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.enterSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ });
+
+ // Now duplicate the current tab using the context menu item.
+ const menu = await openTabMenuFor(gBrowser.selectedTab);
+ let tabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ gBrowser.currentURI.spec
+ );
+ menu.activateItem(document.getElementById("context_duplicateTab"));
+ let newTab = await tabPromise;
+ Assert.equal(
+ gBrowser.selectedTab,
+ newTab,
+ "Sanity check: The duplicated tab is now the selected tab"
+ );
+
+ // Wait for search mode, then check it and the input value.
+ await TestUtils.waitForCondition(
+ () => gURLBar.searchMode,
+ "Waiting for search mode to be duplicated/restored"
+ );
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ entry: "oneoff",
+ });
+ Assert.equal(
+ gURLBar.value,
+ SEARCH_STRING,
+ "Search string should be duplicated/restored"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(newTab);
+ gURLBar.handleRevert();
+});
+
+/**
+ * Opens a new browser window with the given URLs, calls a callback, and then
+ * closes the window.
+ *
+ * @param {Array} urls
+ * Array of string URLs to open.
+ * @param {Function} callback
+ * The callback.
+ */
+async function withNewWindow(urls, callback) {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ for (let url of urls) {
+ await BrowserTestUtils.openNewForegroundTab({
+ url,
+ gBrowser: win.gBrowser,
+ waitForLoad: url != "about:newtab",
+ });
+ if (url == "about:newtab") {
+ await TestUtils.waitForCondition(
+ () => win.gBrowser.currentURI.spec == "about:newtab",
+ "Waiting for about:newtab"
+ );
+ }
+ }
+ BrowserTestUtils.removeTab(win.gBrowser.tabs[0]);
+ await callback(win);
+ await BrowserTestUtils.closeWindow(win);
+}
+
+/**
+ * Uses SessionStore to reopen the last closed window.
+ *
+ * @param {string} expectedRestoredURL
+ * The URL you expect will be restored in the selected browser.
+ */
+async function restoreWindow(expectedRestoredURL) {
+ let winPromise = BrowserTestUtils.waitForNewWindow();
+ let win = SessionStore.undoCloseWindow(0);
+ await winPromise;
+ await TestUtils.waitForCondition(
+ () => win.gBrowser.currentURI.spec == expectedRestoredURL,
+ "Waiting for restored selected browser to have expected URI"
+ );
+ return win;
+}
diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_setURI.js b/browser/components/urlbar/tests/browser/browser_searchMode_setURI.js
new file mode 100644
index 0000000000..46f0a84256
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchMode_setURI.js
@@ -0,0 +1,119 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that search mode remains active or is exited when setURI is called,
+ * depending on the situation.
+ */
+
+"use strict";
+
+// Opens a new tab, does a search, enters search mode, and then manually calls
+// setURI. Uses a variety of initial URLs, search strings, and setURI arguments
+// in order to hit different branches in setURI. Search mode should remain
+// active or be exited as appropriate.
+add_task(async function setURI() {
+ for (let test of [
+ // initialURL, searchString, url, expectSearchMode
+
+ ["about:blank", "", null, true],
+ ["about:blank", "", "about:blank", true],
+ ["about:blank", "", "http://www.example.com/", true],
+
+ ["about:blank", "about:blank", null, false],
+ ["about:blank", "about:blank", "about:blank", false],
+ ["about:blank", "about:blank", "http://www.example.com/", false],
+
+ ["about:blank", "http://www.example.com/", null, true],
+ ["about:blank", "http://www.example.com/", "about:blank", true],
+ ["about:blank", "http://www.example.com/", "http://www.example.com/", true],
+
+ ["about:blank", "not a URL", null, true],
+ ["about:blank", "not a URL", "about:blank", true],
+ ["about:blank", "not a URL", "http://www.example.com/", true],
+
+ ["http://www.example.com/", "", null, true],
+ ["http://www.example.com/", "", "about:blank", true],
+ ["http://www.example.com/", "", "http://www.example.com/", true],
+
+ ["http://www.example.com/", "about:blank", null, false],
+ ["http://www.example.com/", "about:blank", "about:blank", false],
+ [
+ "http://www.example.com/",
+ "about:blank",
+ "http://www.example.com/",
+ false,
+ ],
+
+ ["http://www.example.com/", "http://www.example.com/", null, true],
+ ["http://www.example.com/", "http://www.example.com/", "about:blank", true],
+ [
+ "http://www.example.com/",
+ "http://www.example.com/",
+ "http://www.example.com/",
+ true,
+ ],
+
+ ["http://www.example.com/", "not a URL", null, true],
+ ["http://www.example.com/", "not a URL", "about:blank", true],
+ ["http://www.example.com/", "not a URL", "http://www.example.com/", true],
+ ]) {
+ await doSetURITest(...test);
+ }
+});
+
+async function doSetURITest(initialURL, searchString, url, expectSearchMode) {
+ info(
+ "doSetURITest with args: " +
+ JSON.stringify({
+ initialURL,
+ searchString,
+ url,
+ expectSearchMode,
+ })
+ );
+
+ await BrowserTestUtils.withNewTab(initialURL, async () => {
+ if (searchString) {
+ // Do a search with the search string.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: searchString,
+ fireInputEvent: true,
+ });
+ } else {
+ // Open top sites.
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ document.getElementById("Browser:OpenLocation").doCommand();
+ });
+ }
+
+ // Enter search mode and close the view.
+ await UrlbarTestUtils.enterSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+ Assert.strictEqual(
+ gBrowser.selectedBrowser.userTypedValue,
+ searchString,
+ `userTypedValue should be ${searchString}`
+ );
+
+ // Call setURI.
+ let uri = url ? Services.io.newURI(url) : null;
+ gURLBar.setURI(uri);
+
+ await UrlbarTestUtils.assertSearchMode(
+ window,
+ !expectSearchMode
+ ? null
+ : {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ entry: "oneoff",
+ }
+ );
+
+ gURLBar.handleRevert();
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ });
+}
diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_suggestions.js b/browser/components/urlbar/tests/browser/browser_searchMode_suggestions.js
new file mode 100644
index 0000000000..5aa3412580
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchMode_suggestions.js
@@ -0,0 +1,579 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests search suggestions in search mode.
+ */
+
+const DEFAULT_ENGINE_NAME = "Test";
+const SUGGESTIONS_ENGINE_NAME = "searchSuggestionEngine.xml";
+const MANY_SUGGESTIONS_ENGINE_NAME = "searchSuggestionEngineMany.xml";
+const MAX_RESULT_COUNT = UrlbarPrefs.get("maxRichResults");
+
+let suggestionsEngine;
+let expectedFormHistoryResults = [];
+
+add_setup(async function () {
+ suggestionsEngine = await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + SUGGESTIONS_ENGINE_NAME,
+ });
+
+ await SearchTestUtils.installSearchExtension(
+ {
+ name: DEFAULT_ENGINE_NAME,
+ keyword: "@test",
+ },
+ { setAsDefault: true }
+ );
+ await Services.search.moveEngine(suggestionsEngine, 0);
+
+ async function cleanup() {
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ }
+ await cleanup();
+ registerCleanupFunction(cleanup);
+
+ // Add some form history for our test engine.
+ for (let i = 0; i < MAX_RESULT_COUNT; i++) {
+ let value = `hello formHistory ${i}`;
+ await UrlbarTestUtils.formHistory.add([
+ { value, source: suggestionsEngine.name },
+ ]);
+ expectedFormHistoryResults.push({
+ heuristic: false,
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ searchParams: {
+ suggestion: value,
+ engine: suggestionsEngine.name,
+ },
+ });
+ }
+
+ // Add other form history.
+ await UrlbarTestUtils.formHistory.add([
+ { value: "hello formHistory global" },
+ { value: "hello formHistory other", source: "other engine" },
+ ]);
+
+ registerCleanupFunction(async () => {
+ await UrlbarTestUtils.formHistory.clear();
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.search.separatePrivateDefault.ui.enabled", false],
+ ["browser.urlbar.suggest.quickactions", false],
+ ],
+ });
+});
+
+add_task(async function emptySearch() {
+ await BrowserTestUtils.withNewTab("about:robots", async function (browser) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.update2.emptySearchBehavior", 2]],
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ Assert.equal(gURLBar.value, "", "Urlbar value should be cleared.");
+ // For the empty search case, we expect to get the form history relative to
+ // the picked engine and no heuristic.
+ await checkResults(expectedFormHistoryResults);
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+ await UrlbarTestUtils.promisePopupClose(window);
+ await SpecialPowers.popPrefEnv();
+ });
+});
+
+add_task(async function emptySearch_withRestyledHistory() {
+ // URLs with the same host as the search engine.
+ await PlacesTestUtils.addVisits([
+ `http://mochi.test/`,
+ `http://mochi.test/redirect`,
+ // Should not be returned because it's a redirect target.
+ {
+ uri: `http://mochi.test/target`,
+ transition: PlacesUtils.history.TRANSITIONS.REDIRECT_TEMPORARY,
+ referrer: `http://mochi.test/redirect`,
+ },
+ // Can be restyled and dupes form history.
+ "http://mochi.test:8888/?terms=hello+formHistory+0",
+ // Can be restyled but does not dupe form history.
+ "http://mochi.test:8888/?terms=ciao",
+ ]);
+ await BrowserTestUtils.withNewTab("about:robots", async function (browser) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.update2.emptySearchBehavior", 2]],
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ Assert.equal(gURLBar.value, "", "Urlbar value should be cleared.");
+ // For the empty search case, we expect to get the form history relative to
+ // the picked engine, history without redirects, and no heuristic.
+ await checkResults([
+ {
+ heuristic: false,
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ searchParams: {
+ suggestion: "ciao",
+ engine: suggestionsEngine.name,
+ },
+ },
+ ...expectedFormHistoryResults.slice(0, MAX_RESULT_COUNT - 3),
+ {
+ heuristic: false,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ url: `http://mochi.test/redirect`,
+ },
+ {
+ heuristic: false,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ url: `http://mochi.test/`,
+ },
+ ]);
+
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+ await UrlbarTestUtils.promisePopupClose(window);
+ await SpecialPowers.popPrefEnv();
+ });
+
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function emptySearch_withRestyledHistory_noSearchHistory() {
+ // URLs with the same host as the search engine.
+ await PlacesTestUtils.addVisits([
+ `http://mochi.test/`,
+ `http://mochi.test/redirect`,
+ // Can be restyled but does not dupe form history.
+ "http://mochi.test:8888/?terms=ciao",
+ ]);
+ await BrowserTestUtils.withNewTab("about:robots", async function (browser) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.update2.emptySearchBehavior", 2],
+ ["browser.urlbar.maxHistoricalSearchSuggestions", 0],
+ ],
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ Assert.equal(gURLBar.value, "", "Urlbar value should be cleared.");
+ // maxHistoricalSearchSuggestions == 0, so form history should not be
+ // present.
+ await checkResults([
+ {
+ heuristic: false,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ url: `http://mochi.test/redirect`,
+ },
+ {
+ heuristic: false,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ url: `http://mochi.test/`,
+ },
+ ]);
+
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+ await UrlbarTestUtils.promisePopupClose(window);
+ await SpecialPowers.popPrefEnv();
+ });
+
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function emptySearch_behavior() {
+ // URLs with the same host as the search engine.
+ await PlacesTestUtils.addVisits([`http://mochi.test/`]);
+
+ await BrowserTestUtils.withNewTab("about:robots", async function (browser) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.update2.emptySearchBehavior", 0]],
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ Assert.equal(gURLBar.value, "", "Urlbar value should be cleared.");
+ // For the empty search case, we expect to get the form history relative to
+ // the picked engine, history without redirects, and no heuristic.
+ await checkResults([]);
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+
+ // We should still show history for empty searches when not in search mode.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: " ",
+ });
+ await checkResults([
+ {
+ heuristic: true,
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ searchParams: {
+ query: " ",
+ engine: DEFAULT_ENGINE_NAME,
+ },
+ },
+ {
+ heuristic: false,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ url: `http://mochi.test/`,
+ },
+ ]);
+ await UrlbarTestUtils.promisePopupClose(window);
+ await SpecialPowers.popPrefEnv();
+ });
+
+ await BrowserTestUtils.withNewTab("about:robots", async function (browser) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.update2.emptySearchBehavior", 1]],
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ Assert.equal(gURLBar.value, "", "Urlbar value should be cleared.");
+ // For the empty search case, we expect to get the form history relative to
+ // the picked engine, history without redirects, and no heuristic.
+ await checkResults([...expectedFormHistoryResults]);
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+ await UrlbarTestUtils.promisePopupClose(window);
+ await SpecialPowers.popPrefEnv();
+ });
+
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function emptySearch_local() {
+ await PlacesTestUtils.addVisits([`http://mochi.test/`]);
+
+ await BrowserTestUtils.withNewTab("about:robots", async function (browser) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.update2.emptySearchBehavior", 0]],
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ await UrlbarTestUtils.enterSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ });
+ Assert.equal(gURLBar.value, "", "Urlbar value should be cleared.");
+ // Even when emptySearchBehavior is 0, we still show the user's most frecent
+ // history for an empty search.
+ await checkResults([
+ {
+ heuristic: false,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ url: `http://mochi.test/`,
+ },
+ ]);
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+ await UrlbarTestUtils.promisePopupClose(window);
+ await SpecialPowers.popPrefEnv();
+ });
+
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function nonEmptySearch() {
+ await BrowserTestUtils.withNewTab("about:robots", async function (browser) {
+ let query = "hello";
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: query,
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ Assert.equal(gURLBar.value, query, "Urlbar value should be set.");
+ // We expect to get the heuristic and all the suggestions.
+ await checkResults([
+ {
+ heuristic: true,
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ searchParams: {
+ query,
+ engine: suggestionsEngine.name,
+ },
+ },
+ ...expectedFormHistoryResults.slice(0, MAX_RESULT_COUNT - 3),
+ {
+ heuristic: false,
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ searchParams: {
+ query,
+ suggestion: `${query}foo`,
+ engine: suggestionsEngine.name,
+ },
+ },
+ {
+ heuristic: false,
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ searchParams: {
+ query,
+ suggestion: `${query}bar`,
+ engine: suggestionsEngine.name,
+ },
+ },
+ ]);
+
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+ await UrlbarTestUtils.promisePopupClose(window);
+ });
+});
+
+add_task(async function nonEmptySearch_nonMatching() {
+ await BrowserTestUtils.withNewTab("about:robots", async function (browser) {
+ let query = "ciao";
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: query,
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+ Assert.equal(gURLBar.value, query, "Urlbar value should be set.");
+ // We expect to get the heuristic and the remote suggestions since the local
+ // ones don't match.
+ await checkResults([
+ {
+ heuristic: true,
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ searchParams: {
+ query,
+ engine: suggestionsEngine.name,
+ },
+ },
+ {
+ heuristic: false,
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ searchParams: {
+ query,
+ suggestion: `${query}foo`,
+ engine: suggestionsEngine.name,
+ },
+ },
+ {
+ heuristic: false,
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ searchParams: {
+ query,
+ suggestion: `${query}bar`,
+ engine: suggestionsEngine.name,
+ },
+ },
+ ]);
+
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+ await UrlbarTestUtils.promisePopupClose(window);
+ });
+});
+
+add_task(async function nonEmptySearch_withHistory() {
+ let manySuggestionsEngine = await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + MANY_SUGGESTIONS_ENGINE_NAME,
+ });
+ // URLs with the same host as the search engine.
+ let query = "ciao";
+ await PlacesTestUtils.addVisits([
+ `http://mochi.test/${query}`,
+ `http://mochi.test/${query}1`,
+ // Should not be returned because it has a different host, even if it
+ // matches the host in the path.
+ `http://example.com/mochi.test/${query}`,
+ ]);
+
+ function makeSuggestionResult(suffix) {
+ return {
+ heuristic: false,
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ searchParams: {
+ query,
+ suggestion: `${query}${suffix}`,
+ engine: manySuggestionsEngine.name,
+ },
+ };
+ }
+
+ await BrowserTestUtils.withNewTab("about:robots", async function (browser) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: query,
+ });
+ await UrlbarTestUtils.enterSearchMode(window, {
+ engineName: manySuggestionsEngine.name,
+ });
+ Assert.equal(gURLBar.value, query, "Urlbar value should be set.");
+
+ await checkResults([
+ {
+ heuristic: true,
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ searchParams: {
+ query,
+ engine: manySuggestionsEngine.name,
+ },
+ },
+ makeSuggestionResult("foo"),
+ makeSuggestionResult("bar"),
+ makeSuggestionResult("1"),
+ makeSuggestionResult("2"),
+ makeSuggestionResult("3"),
+ makeSuggestionResult("4"),
+ makeSuggestionResult("5"),
+ {
+ heuristic: false,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ url: `http://mochi.test/${query}1`,
+ },
+ {
+ heuristic: false,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ url: `http://mochi.test/${query}`,
+ },
+ ]);
+
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ info("Test again with history before suggestions");
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.showSearchSuggestionsFirst", false]],
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: query,
+ });
+ await UrlbarTestUtils.enterSearchMode(window, {
+ engineName: manySuggestionsEngine.name,
+ });
+ Assert.equal(gURLBar.value, query, "Urlbar value should be set.");
+
+ await checkResults([
+ {
+ heuristic: true,
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ searchParams: {
+ query,
+ engine: manySuggestionsEngine.name,
+ },
+ },
+ makeSuggestionResult("foo"),
+ makeSuggestionResult("bar"),
+ makeSuggestionResult("1"),
+ makeSuggestionResult("2"),
+ makeSuggestionResult("3"),
+ makeSuggestionResult("4"),
+ makeSuggestionResult("5"),
+ {
+ heuristic: false,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ url: `http://mochi.test/${query}1`,
+ },
+ {
+ heuristic: false,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ url: `http://mochi.test/${query}`,
+ },
+ ]);
+
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+ await UrlbarTestUtils.promisePopupClose(window);
+ await SpecialPowers.popPrefEnv();
+ });
+
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function nonEmptySearch_url() {
+ await BrowserTestUtils.withNewTab("about:robots", async function (browser) {
+ let query = "http://www.example.com/";
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: query,
+ });
+ await UrlbarTestUtils.enterSearchMode(window);
+
+ // The heuristic result for a search that's a valid URL should be a search
+ // result, not a URL result.
+ await checkResults([
+ {
+ heuristic: true,
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ searchParams: {
+ query,
+ engine: suggestionsEngine.name,
+ },
+ },
+ ]);
+
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+ await UrlbarTestUtils.promisePopupClose(window);
+ });
+});
+
+async function checkResults(expectedResults) {
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ expectedResults.length,
+ "Check results count."
+ );
+ for (let i = 0; i < expectedResults.length; ++i) {
+ info(`Checking result at index ${i}`);
+ let expected = expectedResults[i];
+ let actual = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+
+ // Check each property defined in the expected result against the property
+ // in the actual result.
+ for (let key of Object.keys(expected)) {
+ // For searchParams, remove undefined properties in the actual result so
+ // that the expected result doesn't need to include them.
+ if (key == "searchParams") {
+ let actualSearchParams = actual.searchParams;
+ for (let spKey of Object.keys(actualSearchParams)) {
+ if (actualSearchParams[spKey] === undefined) {
+ delete actualSearchParams[spKey];
+ }
+ }
+ }
+ Assert.deepEqual(
+ actual[key],
+ expected[key],
+ `${key} should match at result index ${i}.`
+ );
+ }
+ }
+}
diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_switchTabs.js b/browser/components/urlbar/tests/browser/browser_searchMode_switchTabs.js
new file mode 100644
index 0000000000..c4f541c9cd
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchMode_switchTabs.js
@@ -0,0 +1,317 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that search mode is stored per tab and restored when switching tabs.
+ */
+
+"use strict";
+
+// Enters search mode using the one-off buttons.
+add_task(async function switchTabs() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.quickactions", false]],
+ });
+
+ // Open three tabs. We'll enter search mode in tabs 0 and 2.
+ let tabs = [];
+ for (let i = 0; i < 3; i++) {
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "http://example.com/" + i,
+ });
+ tabs.push(tab);
+ }
+
+ // Switch to tab 0.
+ await BrowserTestUtils.switchTab(gBrowser, tabs[0]);
+
+ // Do a search and enter search mode. Pass fireInputEvent so that
+ // userTypedValue is set and restored when we switch back to this tab. This
+ // isn't really necessary but it simulates the user's typing, and it also
+ // means that we'll start a search when we switch back to this tab.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.enterSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ });
+
+ // Switch to tab 1. Search mode should be exited.
+ await BrowserTestUtils.switchTab(gBrowser, tabs[1]);
+ await UrlbarTestUtils.assertSearchMode(window, null);
+
+ // Switch back to tab 0. We should do a search (for "test") and re-enter
+ // search mode.
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ await BrowserTestUtils.switchTab(gBrowser, tabs[0]);
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ entry: "oneoff",
+ });
+ Assert.equal(
+ gURLBar.value,
+ "test",
+ "Value should remain the search string after switching back"
+ );
+
+ // Switch to tab 2. Search mode should be exited.
+ await BrowserTestUtils.switchTab(gBrowser, tabs[2]);
+ await UrlbarTestUtils.assertSearchMode(window, null);
+
+ // Do another search (in tab 2) and enter search mode. Use a different source
+ // from tab 0 just to use something different.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test tab 2",
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.enterSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.TABS,
+ });
+
+ // Switch back to tab 0. We should do a search and still be in search mode.
+ searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ await BrowserTestUtils.switchTab(gBrowser, tabs[0]);
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ entry: "oneoff",
+ });
+ Assert.equal(
+ gURLBar.value,
+ "test",
+ "Value should remain the search string after switching back"
+ );
+
+ // Switch to tab 1. Search mode should be exited.
+ await BrowserTestUtils.switchTab(gBrowser, tabs[1]);
+ await UrlbarTestUtils.assertSearchMode(window, null);
+
+ // Switch back to tab 2. We should do a search and re-enter search mode.
+ searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ await BrowserTestUtils.switchTab(gBrowser, tabs[2]);
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.TABS,
+ entry: "oneoff",
+ });
+ Assert.equal(
+ gURLBar.value,
+ "test tab 2",
+ "Value should remain the search string after switching back"
+ );
+
+ // Exit search mode.
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+
+ // Switch to tab 0. We should do a search and re-enter search mode.
+ searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ await BrowserTestUtils.switchTab(gBrowser, tabs[0]);
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ entry: "oneoff",
+ });
+ Assert.equal(
+ gURLBar.value,
+ "test",
+ "Value should remain the search string after switching back"
+ );
+
+ // Switch back to tab 2. We should do a search but search mode should be
+ // inactive.
+ searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ await BrowserTestUtils.switchTab(gBrowser, tabs[2]);
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ Assert.equal(
+ gURLBar.value,
+ "test tab 2",
+ "Value should remain the search string after switching back"
+ );
+
+ // Switch back to tab 0. We should do a search and re-enter search mode.
+ searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ await BrowserTestUtils.switchTab(gBrowser, tabs[0]);
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ entry: "oneoff",
+ });
+ Assert.equal(
+ gURLBar.value,
+ "test",
+ "Value should remain the search string after switching back"
+ );
+
+ // Exit search mode.
+ await UrlbarTestUtils.exitSearchMode(window, { clickClose: true });
+
+ // Switch back to tab 2. We should do a search but search mode should be
+ // inactive.
+ searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ await BrowserTestUtils.switchTab(gBrowser, tabs[2]);
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ Assert.equal(
+ gURLBar.value,
+ "test tab 2",
+ "Value should remain the search string after switching back"
+ );
+
+ // Switch back to tab 0. We should do a search but search mode should be
+ // inactive.
+ searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ await BrowserTestUtils.switchTab(gBrowser, tabs[0]);
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ Assert.equal(
+ gURLBar.value,
+ "test",
+ "Value should remain the search string after switching back"
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ for (let tab of tabs) {
+ BrowserTestUtils.removeTab(tab);
+ }
+});
+
+// Start loading a SERP from search mode then immediately switch to a new tab so
+// the SERP finishes loading in the background. Switch back to the SERP tab and
+// observe that we don't re-enter search mode despite having an entry for that
+// tab in UrlbarInput._searchModesByBrowser. See bug 1675926.
+//
+// This subtest intermittently does not test bug 1675926 (NB: this does not mean
+// it is an intermittent failure). The false-positive occurs if the SERP page
+// finishes loading before we switch tabs. We include this subtest so we have
+// one covering real-world behaviour. A subtest that is guaranteed to test this
+// behaviour that does not simulate real world behaviour is included below.
+add_task(async function slow_load() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.searches", false]],
+ });
+ const engineName = "Test";
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: engineName,
+ },
+ { skipUnload: true }
+ );
+
+ const originalTab = gBrowser.selectedTab;
+ const newTab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.enterSearchMode(window, { engineName });
+
+ const loadPromise = BrowserTestUtils.browserLoaded(newTab.linkedBrowser);
+ // Select the search mode heuristic to load the example.com SERP.
+ EventUtils.synthesizeKey("KEY_Enter");
+ // Switch away from the tab before we let it load.
+ await BrowserTestUtils.switchTab(gBrowser, originalTab);
+ await loadPromise;
+
+ // Switch back to the search mode tab and confirm we don't restore search
+ // mode.
+ await BrowserTestUtils.switchTab(gBrowser, newTab);
+ await UrlbarTestUtils.assertSearchMode(window, null);
+
+ BrowserTestUtils.removeTab(newTab);
+ await SpecialPowers.popPrefEnv();
+ await extension.unload();
+});
+
+// Tests the same behaviour as slow_load, but in a more reliable way using
+// non-real-world behaviour.
+add_task(async function slow_load_guaranteed() {
+ const engineName = "Test";
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: engineName,
+ },
+ { skipUnload: true }
+ );
+
+ const backgroundTab = BrowserTestUtils.addTab(gBrowser);
+
+ // Simulate a tab that was in search mode, loaded a SERP, then was switched
+ // away from before setURI was called.
+ backgroundTab.ownerGlobal.gURLBar.searchMode = { engineName };
+ let loadPromise = BrowserTestUtils.browserLoaded(backgroundTab.linkedBrowser);
+ BrowserTestUtils.loadURIString(
+ backgroundTab.linkedBrowser,
+ "http://example.com/?search=test"
+ );
+ await loadPromise;
+
+ // Switch to the background mode tab and confirm we don't restore search mode.
+ await BrowserTestUtils.switchTab(gBrowser, backgroundTab);
+ await UrlbarTestUtils.assertSearchMode(window, null);
+
+ BrowserTestUtils.removeTab(backgroundTab);
+ await extension.unload();
+});
+
+// Enters search mode by typing a restriction char with no search string.
+// Search mode and the search string should be restored after switching back to
+// the tab.
+add_task(async function userTypedValue_empty() {
+ await doUserTypedValueTest("");
+});
+
+// Enters search mode by typing a restriction char followed by a search string.
+// Search mode and the search string should be restored after switching back to
+// the tab.
+add_task(async function userTypedValue_nonEmpty() {
+ await doUserTypedValueTest("foo bar");
+});
+
+/**
+ * Enters search mode by typing a restriction char followed by a search string,
+ * opens a new tab and immediately closes it so we switch back to the search
+ * mode tab, and checks the search mode state and input value.
+ *
+ * @param {string} searchString
+ * The search string to enter search mode with.
+ */
+async function doUserTypedValueTest(searchString) {
+ let value = `${UrlbarTokenizer.RESTRICT.BOOKMARK} ${searchString}`;
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value,
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ entry: "typed",
+ });
+ Assert.equal(
+ gURLBar.value,
+ searchString,
+ "Sanity check: Value is the search string"
+ );
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({ gBrowser });
+ BrowserTestUtils.removeTab(tab);
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ entry: "typed",
+ });
+ Assert.equal(
+ gURLBar.value,
+ searchString,
+ "Value should remain the search string after switching back"
+ );
+
+ gURLBar.handleRevert();
+}
diff --git a/browser/components/urlbar/tests/browser/browser_searchSettings.js b/browser/components/urlbar/tests/browser/browser_searchSettings.js
new file mode 100644
index 0000000000..2cded38c99
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchSettings.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async function () {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "a",
+ });
+
+ // Since the current tab is blank the preferences pane will load there
+ let loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ let button = document.getElementById("urlbar-anon-search-settings");
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ });
+ await loaded;
+
+ is(
+ gBrowser.selectedBrowser.currentURI.spec,
+ "about:preferences#search",
+ "Should have loaded the right page"
+ );
+ }
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_searchSingleWordNotification.js b/browser/components/urlbar/tests/browser/browser_searchSingleWordNotification.js
new file mode 100644
index 0000000000..36a065d58e
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchSingleWordNotification.js
@@ -0,0 +1,372 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+"use strict";
+
+let gDNSResolved = false;
+add_setup(async function () {
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref("browser.fixup.domainwhitelist.localhost");
+ });
+});
+
+function promiseNotification(aBrowser, value, expected, input) {
+ return new Promise(resolve => {
+ let notificationBox = aBrowser.getNotificationBox(aBrowser.selectedBrowser);
+ if (expected) {
+ info("Waiting for " + value + " notification");
+ resolve(
+ BrowserTestUtils.waitForNotificationInNotificationBox(
+ notificationBox,
+ value
+ )
+ );
+ } else {
+ setTimeout(() => {
+ is(
+ notificationBox.getNotificationWithValue(value),
+ null,
+ `We are expecting to not get a notification for ${input}`
+ );
+ resolve();
+ }, 1000);
+ }
+ });
+}
+
+async function runURLBarSearchTest({
+ valueToOpen,
+ enterSearchMode,
+ expectSearch,
+ expectNotification,
+ expectDNSResolve,
+ aWindow = window,
+}) {
+ gDNSResolved = false;
+ // Test both directly setting a value and pressing enter, or setting the
+ // value through input events, like the user would do.
+ const setValueFns = [
+ value => {
+ aWindow.gURLBar.value = value;
+ if (enterSearchMode) {
+ // Ensure to open the panel.
+ UrlbarTestUtils.fireInputEvent(aWindow);
+ }
+ },
+ value => {
+ return UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: aWindow,
+ value,
+ });
+ },
+ ];
+
+ for (let i = 0; i < setValueFns.length; ++i) {
+ await setValueFns[i](valueToOpen);
+ let topic = "uri-fixup-check-dns";
+ let observer = (aSubject, aTopicInner, aData) => {
+ if (aTopicInner == topic) {
+ gDNSResolved = true;
+ }
+ };
+ Services.obs.addObserver(observer, topic);
+
+ if (enterSearchMode) {
+ if (!expectSearch) {
+ throw new Error("Must execute a search in search mode");
+ }
+ await UrlbarTestUtils.enterSearchMode(aWindow);
+ }
+
+ let expectedURI;
+ if (!expectSearch) {
+ expectedURI = "http://" + valueToOpen + "/";
+ } else {
+ expectedURI = (await Services.search.getDefault()).getSubmission(
+ valueToOpen,
+ null,
+ "keyword"
+ ).uri.spec;
+ }
+ aWindow.gURLBar.focus();
+ let docLoadPromise = BrowserTestUtils.waitForDocLoadAndStopIt(
+ expectedURI,
+ aWindow.gBrowser.selectedBrowser
+ );
+ EventUtils.synthesizeKey("VK_RETURN", {}, aWindow);
+
+ if (!enterSearchMode) {
+ await promiseNotification(
+ aWindow.gBrowser,
+ "keyword-uri-fixup",
+ expectNotification,
+ valueToOpen
+ );
+ }
+ await docLoadPromise;
+
+ if (expectNotification) {
+ let notificationBox = aWindow.gBrowser.getNotificationBox(
+ aWindow.gBrowser.selectedBrowser
+ );
+ let notification =
+ notificationBox.getNotificationWithValue("keyword-uri-fixup");
+ // Confirm the notification only on the last loop.
+ if (i == setValueFns.length - 1) {
+ docLoadPromise = BrowserTestUtils.waitForDocLoadAndStopIt(
+ "http://" + valueToOpen + "/",
+ aWindow.gBrowser.selectedBrowser
+ );
+ notification.buttonContainer.querySelector("button").click();
+ await docLoadPromise;
+ } else {
+ notificationBox.currentNotification.close();
+ }
+ }
+
+ Services.obs.removeObserver(observer, topic);
+ Assert.equal(
+ gDNSResolved,
+ expectDNSResolve,
+ `Should${expectDNSResolve ? "" : " not"} DNS resolve "${valueToOpen}"`
+ );
+ }
+}
+
+add_task(async function test_navigate_full_domain() {
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "about:blank"
+ ));
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await runURLBarSearchTest({
+ valueToOpen: "www.singlewordtest.org",
+ expectSearch: false,
+ expectNotification: false,
+ expectDNSResolve: false,
+ });
+ gBrowser.removeTab(tab);
+});
+
+add_task(async function test_navigate_decimal_ip() {
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "about:blank"
+ ));
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await runURLBarSearchTest({
+ valueToOpen: "1234",
+ expectSearch: true,
+ expectNotification: false,
+ expectDNSResolve: false, // Possible IP in numeric format.
+ });
+ gBrowser.removeTab(tab);
+});
+
+add_task(async function test_navigate_decimal_ip_with_path() {
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "about:blank"
+ ));
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await runURLBarSearchTest({
+ valueToOpen: "1234/12",
+ expectSearch: true,
+ expectNotification: false,
+ expectDNSResolve: false,
+ });
+ gBrowser.removeTab(tab);
+});
+
+add_task(async function test_navigate_large_number() {
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "about:blank"
+ ));
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await runURLBarSearchTest({
+ valueToOpen: "123456789012345",
+ expectSearch: true,
+ expectNotification: false,
+ expectDNSResolve: false, // Possible IP in numeric format.
+ });
+ gBrowser.removeTab(tab);
+});
+
+add_task(async function test_navigate_small_hex_number() {
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "about:blank"
+ ));
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await runURLBarSearchTest({
+ valueToOpen: "0x1f00ffff",
+ expectSearch: true,
+ expectNotification: false,
+ expectDNSResolve: false, // Possible IP in numeric format.
+ });
+ gBrowser.removeTab(tab);
+});
+
+add_task(async function test_navigate_large_hex_number() {
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "about:blank"
+ ));
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await runURLBarSearchTest({
+ valueToOpen: "0x7f0000017f000001",
+ expectSearch: true,
+ expectNotification: false,
+ expectDNSResolve: false, // Possible IP in numeric format.
+ });
+ gBrowser.removeTab(tab);
+});
+
+function get_test_function_for_localhost_with_hostname(
+ hostName,
+ isPrivate = false
+) {
+ return async function test_navigate_single_host() {
+ info(`Test ${hostName}${isPrivate ? " in Private Browsing mode" : ""}`);
+ const pref = "browser.fixup.domainwhitelist.localhost";
+ let win;
+ if (isPrivate) {
+ let promiseWin = BrowserTestUtils.waitForNewWindow();
+ win = OpenBrowserWindow({ private: true });
+ await promiseWin;
+ await SimpleTest.promiseFocus(win);
+ } else {
+ win = window;
+ }
+
+ // Remove the domain from the whitelist
+ Services.prefs.setBoolPref(pref, false);
+
+ // The notification should not appear because the default value of
+ // browser.urlbar.dnsResolveSingleWordsAfterSearch is 0
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser: win.gBrowser,
+ url: "about:blank",
+ },
+ browser =>
+ runURLBarSearchTest({
+ valueToOpen: hostName,
+ expectSearch: true,
+ expectNotification: false,
+ expectDNSResolve: false,
+ aWindow: win,
+ })
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.dnsResolveSingleWordsAfterSearch", 1]],
+ });
+
+ // The notification should appear, unless we are in private browsing mode.
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser: win.gBrowser,
+ url: "about:blank",
+ },
+ browser =>
+ runURLBarSearchTest({
+ valueToOpen: hostName,
+ expectSearch: true,
+ expectNotification: true,
+ expectDNSResolve: true,
+ aWindow: win,
+ })
+ );
+
+ // check pref value
+ let prefValue = Services.prefs.getBoolPref(pref);
+ is(prefValue, !isPrivate, "Pref should have the correct state.");
+
+ // Now try again with the pref set.
+ // In a private window, the notification should appear again.
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser: win.gBrowser,
+ url: "about:blank",
+ },
+ browser =>
+ runURLBarSearchTest({
+ valueToOpen: hostName,
+ expectSearch: isPrivate,
+ expectNotification: isPrivate,
+ expectDNSResolve: isPrivate,
+ aWindow: win,
+ })
+ );
+
+ if (isPrivate) {
+ info("Waiting for private window to close");
+ await BrowserTestUtils.closeWindow(win);
+ await SimpleTest.promiseFocus(window);
+ }
+
+ await SpecialPowers.popPrefEnv();
+ };
+}
+
+add_task(get_test_function_for_localhost_with_hostname("localhost"));
+add_task(get_test_function_for_localhost_with_hostname("localhost."));
+add_task(get_test_function_for_localhost_with_hostname("localhost", true));
+
+add_task(async function test_dnsResolveSingleWordsAfterSearch() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.dnsResolveSingleWordsAfterSearch", 0],
+ ["browser.fixup.domainwhitelist.localhost", false],
+ ],
+ });
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:blank",
+ },
+ browser =>
+ runURLBarSearchTest({
+ valueToOpen: "localhost",
+ expectSearch: true,
+ expectNotification: false,
+ expectDNSResolve: false,
+ })
+ );
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_navigate_invalid_url() {
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "about:blank"
+ ));
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await runURLBarSearchTest({
+ valueToOpen: "mozilla is awesome",
+ expectSearch: true,
+ expectNotification: false,
+ expectDNSResolve: false,
+ });
+ gBrowser.removeTab(tab);
+});
+
+add_task(async function test_search_mode() {
+ info("When in search mode we should never query the DNS");
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.suggest.enabled", false]],
+ });
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "about:blank"
+ ));
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await runURLBarSearchTest({
+ enterSearchMode: true,
+ valueToOpen: "mozilla",
+ expectSearch: true,
+ expectNotification: false,
+ expectDNSResolve: false,
+ });
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_searchSuggestions.js b/browser/components/urlbar/tests/browser/browser_searchSuggestions.js
new file mode 100644
index 0000000000..8a226a3c4c
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchSuggestions.js
@@ -0,0 +1,341 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This tests checks that search suggestions can be acted upon correctly
+ * e.g. selection with modifiers, copying text.
+ */
+
+"use strict";
+
+const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches";
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+
+const MAX_CHARS_PREF = "browser.urlbar.maxCharsForSearchSuggestions";
+
+// Must run first.
+add_task(async function prepare() {
+ let suggestionsEnabled = Services.prefs.getBoolPref(SUGGEST_URLBAR_PREF);
+ Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, true);
+ await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME,
+ setAsDefault: true,
+ });
+ await UrlbarTestUtils.formHistory.clear();
+ registerCleanupFunction(async function () {
+ Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, suggestionsEnabled);
+
+ // Clicking suggestions causes visits to search results pages, so clear that
+ // history now.
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.formHistory.clear();
+ });
+});
+
+add_task(async function clickSuggestion() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ gURLBar.focus();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ });
+ let [idx, suggestion, engineName] = await getFirstSuggestion();
+ Assert.equal(
+ engineName,
+ "browser_searchSuggestionEngine searchSuggestionEngine.xml",
+ "Expected suggestion engine"
+ );
+
+ let uri = (await Services.search.getDefault()).getSubmission(suggestion).uri;
+ let loadPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ uri.spec
+ );
+ let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, idx);
+ EventUtils.synthesizeMouseAtCenter(element, {}, window);
+ await loadPromise;
+
+ let formHistory = (
+ await UrlbarTestUtils.formHistory.search({ source: engineName })
+ ).map(entry => entry.value);
+ Assert.deepEqual(
+ formHistory,
+ ["foofoo"],
+ "Should find form history after adding it"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ await UrlbarTestUtils.formHistory.clear();
+});
+
+async function testPressEnterOnSuggestion(
+ expectedUrl = null,
+ keyModifiers = {}
+) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ gURLBar.focus();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ });
+ let [idx, suggestion, engineName] = await getFirstSuggestion();
+ Assert.equal(
+ engineName,
+ "browser_searchSuggestionEngine searchSuggestionEngine.xml",
+ "Expected suggestion engine"
+ );
+
+ let hasExpectedUrl = !!expectedUrl;
+ if (!expectedUrl) {
+ expectedUrl = (await Services.search.getDefault()).getSubmission(suggestion)
+ .uri.spec;
+ }
+
+ let promiseLoad = BrowserTestUtils.waitForDocLoadAndStopIt(
+ expectedUrl,
+ gBrowser.selectedBrowser
+ );
+
+ let promiseFormHistory;
+ if (!hasExpectedUrl) {
+ promiseFormHistory = UrlbarTestUtils.formHistory.promiseChanged("add");
+ }
+
+ for (let i = 0; i < idx; ++i) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+ EventUtils.synthesizeKey("KEY_Enter", keyModifiers);
+
+ await promiseLoad;
+
+ if (!hasExpectedUrl) {
+ await promiseFormHistory;
+ let formHistory = (
+ await UrlbarTestUtils.formHistory.search({ source: engineName })
+ ).map(entry => entry.value);
+ Assert.deepEqual(
+ formHistory,
+ ["foofoo"],
+ "Should find form history after adding it"
+ );
+ }
+
+ BrowserTestUtils.removeTab(tab);
+ await UrlbarTestUtils.formHistory.clear();
+}
+
+add_task(async function plainEnterOnSuggestion() {
+ await testPressEnterOnSuggestion();
+});
+
+add_task(async function ctrlEnterOnSuggestion() {
+ await testPressEnterOnSuggestion("https://www.foofoo.com/", {
+ ctrlKey: true,
+ });
+});
+
+add_task(async function copySuggestionText() {
+ gURLBar.focus();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ });
+ let [idx, suggestion] = await getFirstSuggestion();
+ for (let i = 0; i < idx; ++i) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+ gURLBar.select();
+ await SimpleTest.promiseClipboardChange(suggestion, () => {
+ goDoCommand("cmd_copy");
+ });
+});
+
+add_task(async function typeMaxChars() {
+ gURLBar.focus();
+
+ let maxChars = 10;
+ await SpecialPowers.pushPrefEnv({
+ set: [[MAX_CHARS_PREF, maxChars]],
+ });
+
+ // Make a string as long as maxChars and type it.
+ let value = "";
+ for (let i = 0; i < maxChars; i++) {
+ value += String.fromCharCode("a".charCodeAt(0) + i);
+ }
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value,
+ });
+
+ // Suggestions should be fetched since we allow them when typing, and the
+ // value so far isn't longer than maxChars anyway.
+ await assertSuggestions([value + "foo", value + "bar"]);
+
+ // Now type some additional chars. Suggestions should still be fetched since
+ // we allow them when typing.
+ for (let i = 0; i < 3; i++) {
+ let char = String.fromCharCode("z".charCodeAt(0) - i);
+ value += char;
+ EventUtils.synthesizeKey(char);
+ await assertSuggestions([value + "foo", value + "bar"]);
+ }
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function pasteMaxChars() {
+ gURLBar.focus();
+
+ let maxChars = 10;
+ await SpecialPowers.pushPrefEnv({
+ set: [[MAX_CHARS_PREF, maxChars]],
+ });
+
+ // Make a string as long as maxChars and paste it.
+ let value = "";
+ for (let i = 0; i < maxChars; i++) {
+ value += String.fromCharCode("a".charCodeAt(0) + i);
+ }
+ await selectAndPaste(value);
+
+ // Suggestions should be fetched since the pasted string is not longer than
+ // maxChars.
+ await assertSuggestions([value + "foo", value + "bar"]);
+
+ // Now type some additional chars. Suggestions should still be fetched since
+ // we allow them when typing.
+ for (let i = 0; i < 3; i++) {
+ let char = String.fromCharCode("z".charCodeAt(0) - i);
+ value += char;
+ EventUtils.synthesizeKey(char);
+ await assertSuggestions([value + "foo", value + "bar"]);
+ }
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function pasteMoreThanMaxChars() {
+ gURLBar.focus();
+
+ let maxChars = 10;
+ await SpecialPowers.pushPrefEnv({
+ set: [[MAX_CHARS_PREF, maxChars]],
+ });
+
+ // Make a string longer than maxChars and paste it.
+ let value = "";
+ for (let i = 0; i < 2 * maxChars; i++) {
+ value += String.fromCharCode("a".charCodeAt(0) + i);
+ }
+ await selectAndPaste(value);
+
+ // Suggestions should not be fetched since the value was pasted and it was
+ // longer than maxChars.
+ await assertSuggestions([]);
+
+ // Now type some additional chars. Suggestions should now be fetched since we
+ // allow them when typing.
+ for (let i = 0; i < 3; i++) {
+ let char = String.fromCharCode("z".charCodeAt(0) - i);
+ value += char;
+ EventUtils.synthesizeKey(char);
+ await assertSuggestions([value + "foo", value + "bar"]);
+ }
+
+ // Paste again. The string is longer than maxChars, so suggestions should not
+ // be fetched.
+ await selectAndPaste(value);
+ await assertSuggestions([]);
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function heuristicAddsFormHistory() {
+ await UrlbarTestUtils.formHistory.clear();
+ let formHistory = (await UrlbarTestUtils.formHistory.search()).map(
+ entry => entry.value
+ );
+ Assert.deepEqual(formHistory, [], "Form history should be empty initially");
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ gURLBar.focus();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ });
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(result.heuristic);
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.SEARCH);
+ Assert.equal(result.searchParams.query, "foo");
+
+ let uri = (await Services.search.getDefault()).getSubmission("foo").uri;
+ let loadPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ uri.spec
+ );
+ let formHistoryPromise = UrlbarTestUtils.formHistory.promiseChanged("add");
+ let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
+ EventUtils.synthesizeMouseAtCenter(element, {}, window);
+ await loadPromise;
+
+ await formHistoryPromise;
+ formHistory = (
+ await UrlbarTestUtils.formHistory.search({
+ source: result.searchParams.engine,
+ })
+ ).map(entry => entry.value);
+ Assert.deepEqual(
+ formHistory,
+ ["foo"],
+ "Should find form history after adding it"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ await UrlbarTestUtils.formHistory.clear();
+});
+
+async function getFirstSuggestion() {
+ let results = await getSuggestionResults();
+ if (!results.length) {
+ return [-1, null, null];
+ }
+ let result = results[0];
+ return [
+ result.index,
+ result.searchParams.suggestion,
+ result.searchParams.engine,
+ ];
+}
+
+async function getSuggestionResults() {
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ let results = [];
+ let matchCount = UrlbarTestUtils.getResultCount(window);
+ for (let i = 0; i < matchCount; i++) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ if (
+ result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
+ result.searchParams.suggestion
+ ) {
+ result.index = i;
+ results.push(result);
+ }
+ }
+ return results;
+}
+
+async function assertSuggestions(expectedSuggestions) {
+ let results = await getSuggestionResults();
+ let actualSuggestions = results.map(r => r.searchParams.suggestion);
+ Assert.deepEqual(
+ actualSuggestions,
+ expectedSuggestions,
+ "Expected suggestions"
+ );
+}
diff --git a/browser/components/urlbar/tests/browser/browser_searchTelemetry.js b/browser/components/urlbar/tests/browser/browser_searchTelemetry.js
new file mode 100644
index 0000000000..61ddff4c2d
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_searchTelemetry.js
@@ -0,0 +1,220 @@
+"use strict";
+
+const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches";
+const MAX_FORM_HISTORY_PREF = "browser.urlbar.maxHistoricalSearchSuggestions";
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+
+// Must run first.
+add_task(async function prepare() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [SUGGEST_URLBAR_PREF, true],
+ [MAX_FORM_HISTORY_PREF, 2],
+ ],
+ });
+
+ await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME,
+ setAsDefault: true,
+ });
+
+ registerCleanupFunction(async function () {
+ // Clicking urlbar results causes visits to their associated pages, so clear
+ // that history now.
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.formHistory.clear();
+ });
+
+ // Move the mouse away from the urlbar one-offs so that a one-off engine is
+ // not inadvertently selected.
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mousemove",
+ target: window.document.documentElement,
+ offsetX: 0,
+ offsetY: 0,
+ });
+});
+
+add_task(async function heuristicResultMouse() {
+ await compareCounts(async function () {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ gURLBar.focus();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "heuristicResult",
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "Should be of type search"
+ );
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
+ EventUtils.synthesizeMouseAtCenter(element, {});
+ await loadPromise;
+ BrowserTestUtils.removeTab(tab);
+ await UrlbarTestUtils.formHistory.clear();
+ });
+});
+
+add_task(async function heuristicResultKeyboard() {
+ await compareCounts(async function () {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ gURLBar.focus();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "heuristicResult",
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "Should be of type search"
+ );
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ EventUtils.sendKey("return");
+ await loadPromise;
+ BrowserTestUtils.removeTab(tab);
+ await UrlbarTestUtils.formHistory.clear();
+ });
+});
+
+add_task(async function searchSuggestionMouse() {
+ await compareCounts(async function () {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ gURLBar.focus();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "searchSuggestion",
+ });
+ let idx = await getFirstSuggestionIndex();
+ Assert.greaterOrEqual(idx, 0, "there should be a first suggestion");
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ let element = await UrlbarTestUtils.waitForAutocompleteResultAt(
+ window,
+ idx
+ );
+ EventUtils.synthesizeMouseAtCenter(element, {});
+ await loadPromise;
+ BrowserTestUtils.removeTab(tab);
+ await UrlbarTestUtils.formHistory.clear();
+ });
+});
+
+add_task(async function searchSuggestionKeyboard() {
+ await compareCounts(async function () {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ gURLBar.focus();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "searchSuggestion",
+ });
+ let idx = await getFirstSuggestionIndex();
+ Assert.greaterOrEqual(idx, 0, "there should be a first suggestion");
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ while (idx--) {
+ EventUtils.sendKey("down");
+ }
+ EventUtils.sendKey("return");
+ await loadPromise;
+ BrowserTestUtils.removeTab(tab);
+ await UrlbarTestUtils.formHistory.clear();
+ });
+});
+
+add_task(async function formHistoryMouse() {
+ await compareCounts(async function () {
+ await UrlbarTestUtils.formHistory.add(["foofoo", "foobar"]);
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ gURLBar.focus();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ });
+ let index = await getFirstSuggestionIndex();
+ Assert.greaterOrEqual(index, 0, "there should be a first suggestion");
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index);
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.SEARCH);
+ Assert.equal(result.source, UrlbarUtils.RESULT_SOURCE.HISTORY);
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ let element = await UrlbarTestUtils.waitForAutocompleteResultAt(
+ window,
+ index
+ );
+ EventUtils.synthesizeMouseAtCenter(element, {});
+ await loadPromise;
+ BrowserTestUtils.removeTab(tab);
+ await UrlbarTestUtils.formHistory.clear();
+ });
+});
+
+add_task(async function formHistoryKeyboard() {
+ await compareCounts(async function () {
+ await UrlbarTestUtils.formHistory.add(["foofoo", "foobar"]);
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ gURLBar.focus();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ });
+ let index = await getFirstSuggestionIndex();
+ Assert.greaterOrEqual(index, 0, "there should be a first suggestion");
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index);
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.SEARCH);
+ Assert.equal(result.source, UrlbarUtils.RESULT_SOURCE.HISTORY);
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ while (index--) {
+ EventUtils.sendKey("down");
+ }
+ EventUtils.sendKey("return");
+ await loadPromise;
+ BrowserTestUtils.removeTab(tab);
+ await UrlbarTestUtils.formHistory.clear();
+ });
+});
+
+/**
+ * This does three things: gets current telemetry/FHR counts, calls
+ * clickCallback, gets telemetry/FHR counts again to compare them to the old
+ * counts.
+ *
+ * @param {Function} clickCallback Use this to open the urlbar popup and choose
+ * and click a result.
+ */
+async function compareCounts(clickCallback) {
+ // Search events triggered by clicks (not the Return key in the urlbar) are
+ // recorded in three places:
+ // * Telemetry histogram named "SEARCH_COUNTS"
+ // * FHR
+
+ let engine = await Services.search.getDefault();
+
+ let histogramKey = `other-${engine.name}.urlbar`;
+ let histogram = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS");
+ histogram.clear();
+
+ gURLBar.focus();
+ await clickCallback();
+
+ TelemetryTestUtils.assertKeyedHistogramSum(histogram, histogramKey, 1);
+}
+
+/**
+ * Returns the index of the first search suggestion in the urlbar results.
+ *
+ * @returns {number} An index, or -1 if there are no search suggestions.
+ */
+async function getFirstSuggestionIndex() {
+ const matchCount = UrlbarTestUtils.getResultCount(window);
+ for (let i = 0; i < matchCount; i++) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ if (
+ result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
+ result.searchParams.suggestion
+ ) {
+ return i;
+ }
+ }
+ return -1;
+}
diff --git a/browser/components/urlbar/tests/browser/browser_search_bookmarks_from_bookmarks_menu.js b/browser/components/urlbar/tests/browser/browser_search_bookmarks_from_bookmarks_menu.js
new file mode 100644
index 0000000000..e42fcc9f7f
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_search_bookmarks_from_bookmarks_menu.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function searchBookmarksFromBooksmarksMenu() {
+ // Add Button to toolbar
+ CustomizableUI.addWidgetToArea(
+ "bookmarks-menu-button",
+ CustomizableUI.AREA_NAVBAR,
+ 0
+ );
+ let bookmarksMenuButton = document.getElementById("bookmarks-menu-button");
+ ok(bookmarksMenuButton, "Bookmarks Menu Button added");
+
+ // Open Bookmarks-Menu-Popup
+ let bookmarksMenuPopup = document.getElementById("BMB_bookmarksPopup");
+ let PopupShownPromise = BrowserTestUtils.waitForEvent(
+ bookmarksMenuPopup,
+ "popupshown"
+ );
+ EventUtils.synthesizeMouseAtCenter(bookmarksMenuButton, {
+ type: "mousedown",
+ });
+ await PopupShownPromise;
+ ok(true, "Bookmarks Menu Popup shown");
+
+ // Click on 'Search Bookmarks'
+ let searchBookmarksButton = document.getElementById("BMB_searchBookmarks");
+ ok(
+ BrowserTestUtils.is_visible(
+ searchBookmarksButton,
+ "'Search Bookmarks Button' is visible."
+ )
+ );
+ EventUtils.synthesizeMouseAtCenter(searchBookmarksButton, {});
+
+ await new Promise(resolve => {
+ window.gURLBar.controller.addQueryListener({
+ onViewOpen() {
+ window.gURLBar.controller.removeQueryListener(this);
+ resolve();
+ },
+ });
+ });
+
+ // Verify URLBar is in search mode with correct restriction
+ is(
+ gURLBar.searchMode?.source,
+ UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ "Addressbar in correct mode."
+ );
+
+ resetCUIAndReinitUrlbarInput();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_search_history_from_history_panel.js b/browser/components/urlbar/tests/browser/browser_search_history_from_history_panel.js
new file mode 100644
index 0000000000..b901a87736
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_search_history_from_history_panel.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { CustomizableUITestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CustomizableUITestUtils.sys.mjs"
+);
+let gCUITestUtils = new CustomizableUITestUtils(window);
+
+add_task(async function searchHistoryFromHistoryPanel() {
+ // Add Button to toolbar
+ CustomizableUI.addWidgetToArea(
+ "history-panelmenu",
+ CustomizableUI.AREA_NAVBAR,
+ 0
+ );
+ registerCleanupFunction(() => {
+ resetCUIAndReinitUrlbarInput();
+ });
+
+ let historyButton = document.getElementById("history-panelmenu");
+ ok(historyButton, "History button appears in Panel Menu");
+
+ historyButton.click();
+
+ let historyPanel = document.getElementById("PanelUI-history");
+ let promise = BrowserTestUtils.waitForEvent(historyPanel, "ViewShown");
+ await promise;
+ ok(historyPanel.getAttribute("visible"), "History Panel is in view");
+
+ // Click on 'Search Bookmarks'
+ let searchHistoryButton = document.getElementById("appMenuSearchHistory");
+ ok(
+ BrowserTestUtils.is_visible(
+ searchHistoryButton,
+ "'Search History Button' is visible."
+ )
+ );
+ EventUtils.synthesizeMouseAtCenter(searchHistoryButton, {});
+
+ await new Promise(resolve => {
+ window.gURLBar.controller.addQueryListener({
+ onViewOpen() {
+ window.gURLBar.controller.removeQueryListener(this);
+ resolve();
+ },
+ });
+ });
+
+ // Verify URLBar is in search mode with correct restriction
+ is(
+ gURLBar.searchMode?.source,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ "Addressbar in correct mode."
+ );
+ gURLBar.searchMode = null;
+ gURLBar.blur();
+});
+
+add_task(async function searchHistoryFromAppMenuHistoryButton() {
+ // Open main menu and click on 'History' button
+ await gCUITestUtils.openMainMenu();
+ let historyButton = document.getElementById("appMenu-history-button");
+ historyButton.click();
+
+ let historyPanel = document.getElementById("PanelUI-history");
+ let promise = BrowserTestUtils.waitForEvent(historyPanel, "ViewShown");
+ await promise;
+ ok(historyPanel.getAttribute("visible"), "History Panel is in view");
+
+ // Click on 'Search Bookmarks'
+ let searchHistoryButton = document.getElementById("appMenuSearchHistory");
+ ok(
+ BrowserTestUtils.is_visible(
+ searchHistoryButton,
+ "'Search History Button' is visible."
+ )
+ );
+ EventUtils.synthesizeMouseAtCenter(searchHistoryButton, {});
+
+ await new Promise(resolve => {
+ window.gURLBar.controller.addQueryListener({
+ onViewOpen() {
+ window.gURLBar.controller.removeQueryListener(this);
+ resolve();
+ },
+ });
+ });
+
+ // Verify URLBar is in search mode with correct restriction
+ is(
+ gURLBar.searchMode?.source,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ "Addressbar in correct mode."
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_selectStaleResults.js b/browser/components/urlbar/tests/browser/browser_selectStaleResults.js
new file mode 100644
index 0000000000..16366f5b33
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_selectStaleResults.js
@@ -0,0 +1,311 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This test makes sure that arrowing down and up through the view's results
+// works correctly with regard to stale results.
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarView: "resource:///modules/UrlbarView.sys.mjs",
+});
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.quickactions", false]],
+ });
+
+ // Increase the timeout of the remove-stale-rows timer so that it doesn't
+ // interfere with the tests.
+ let originalRemoveStaleRowsTimeout = UrlbarView.removeStaleRowsTimeout;
+ UrlbarView.removeStaleRowsTimeout = 1000;
+ registerCleanupFunction(() => {
+ UrlbarView.removeStaleRowsTimeout = originalRemoveStaleRowsTimeout;
+ });
+});
+
+// This tests the case where queryContext.results.length < the number of rows in
+// the view, i.e., the view contains stale rows.
+add_task(async function viewContainsStaleRows() {
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ let maxResults = UrlbarPrefs.get("maxRichResults");
+ let halfResults = Math.floor(maxResults / 2);
+
+ // Add enough visits to pages with "xx" in the title to fill up half the view.
+ for (let i = 0; i < halfResults; i++) {
+ await PlacesTestUtils.addVisits({
+ uri: "http://mochi.test:8888/" + i,
+ title: "xx" + i,
+ });
+ }
+
+ // Add enough visits to pages with "x" in the title to fill up the entire
+ // view.
+ for (let i = 0; i < maxResults; i++) {
+ await PlacesTestUtils.addVisits({
+ uri: "http://example.com/" + i,
+ title: "x" + i,
+ });
+ }
+
+ gURLBar.focus();
+
+ // Search for "x" and wait for the search to finish. All the "x" results
+ // added above should be in the view. (Actually one fewer will be in the
+ // view due to the heuristic result, but that's not important.)
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "x",
+ fireInputEvent: true,
+ });
+
+ // Below we'll do a search for "xx". Get the row that will show the last
+ // result in that search.
+ let row = UrlbarTestUtils.getRowAt(window, halfResults);
+
+ // Add a mutation listener on that row. Wait for its "stale" attribute to be
+ // removed.
+ let mutationPromise = new Promise(resolve => {
+ let observer = new MutationObserver(mutations => {
+ for (let mut of mutations) {
+ if (mut.attributeName == "stale" && !row.hasAttribute("stale")) {
+ observer.disconnect();
+ resolve();
+ break;
+ }
+ }
+ });
+ observer.observe(row, { attributes: true });
+ });
+
+ // Type another "x" so that we search for "xx", but don't wait for the search
+ // to finish. Instead, wait for the row's stale attribute to be removed.
+ EventUtils.synthesizeKey("x");
+ info("Waiting for 'stale' attribute to be removed... ");
+ await mutationPromise;
+
+ // Now arrow down. The search, which is still ongoing, will now stop and the
+ // view won't be updated anymore.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+
+ // Wait for the search to stop.
+ info("Waiting for the search to stop... ");
+ await gURLBar.lastQueryContextPromise;
+
+ // The query context for the last search ("xx") should contain only
+ // halfResults + 1 results (+ 1 for the heuristic).
+ Assert.ok(gURLBar.controller._lastQueryContextWrapper);
+ let { queryContext } = gURLBar.controller._lastQueryContextWrapper;
+ Assert.ok(queryContext);
+ Assert.equal(queryContext.results.length, halfResults + 1);
+
+ // But there should be maxResults visible rows in the view.
+ let items = Array.from(
+ UrlbarTestUtils.getResultsContainer(window).children
+ ).filter(r => BrowserTestUtils.is_visible(r));
+ Assert.equal(items.length, maxResults);
+
+ // Arrow down through all the results. After arrowing down from the last "xx"
+ // result, the stale "x" results should be selected. We should *not* enter
+ // the one-off search buttons at that point.
+ for (let i = 1; i < maxResults; i++) {
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), i);
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ Assert.equal(result.element.row.result.rowIndex, i);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+
+ // Now the first one-off should be selected.
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), -1);
+ Assert.equal(gURLBar.view.oneOffSearchButtons.selectedButtonIndex, 0);
+
+ // Arrow back up through all the results.
+ for (let i = maxResults - 1; i >= 0; i--) {
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), i);
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+});
+
+// This tests the case where, before the search finishes, stale results have
+// been removed and replaced with non-stale results.
+add_task(async function staleReplacedWithFresh() {
+ // For this test, we need one set of results that's added quickly and another
+ // set that's added after a delay. We do an initial search and wait for both
+ // sets to be added. Then we do another search, but this time only wait for
+ // the fast results to be added, and then we arrow down to stop the search
+ // before the delayed results are added. The order in which things should
+ // happen after the second search goes like this:
+ //
+ // (1) second search
+ // (2) fast results are added
+ // (3) remove-stale-rows timer fires and removes stale rows (the rows from
+ // the delayed set of results from the first search)
+ // (4) we arrow down to stop the search
+ //
+ // We use history for the fast results and a slow search engine for the
+ // delayed results.
+ //
+ // NB: If this test ends up failing, it may be because the remove-stale-rows
+ // timer fires before the history results are added. i.e., steps 2 and 3
+ // above happen out of order. If that happens, try increasing
+ // UrlbarView.removeStaleRowsTimeout above.
+
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ // Enable search suggestions, and add an engine that returns suggestions on a
+ // delay.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.searches", true]],
+ });
+ let engine = await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + "searchSuggestionEngineSlow.xml",
+ });
+ let oldDefaultEngine = await Services.search.getDefault();
+ await Services.search.moveEngine(engine, 0);
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+
+ let maxResults = UrlbarPrefs.get("maxRichResults");
+
+ // Add enough visits to pages with "test" in the title to fill up the entire
+ // view.
+ for (let i = 0; i < maxResults; i++) {
+ await PlacesTestUtils.addVisits({
+ uri: "http://example.com/" + i,
+ title: "test" + i,
+ });
+ }
+
+ gURLBar.focus();
+
+ // Search for "tes" and wait for the search to finish.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "tes",
+ fireInputEvent: true,
+ });
+
+ // Sanity check the results. They should be:
+ //
+ // tes -- Search with searchSuggestionEngineSlow [heuristic]
+ // tesfoo [search suggestion]
+ // tesbar [search suggestion]
+ // test9 [history]
+ // test8 [history]
+ // test7 [history]
+ // test6 [history]
+ // test5 [history]
+ // test4 [history]
+ // test3 [history]
+ let count = UrlbarTestUtils.getResultCount(window);
+ Assert.equal(count, maxResults);
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(result.heuristic);
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.ok(result.searchParams);
+ Assert.equal(result.searchParams.suggestion, "tesfoo");
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2);
+ Assert.ok(result.searchParams);
+ Assert.equal(result.searchParams.suggestion, "tesbar");
+ for (let i = 3; i < maxResults; i++) {
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.URL);
+ Assert.equal(result.title, "test" + (maxResults - i + 2));
+ }
+
+ // Below we'll do a search for "test" *but* not wait for the two search
+ // suggestion results to be added. We'll only wait for the history results to
+ // be added. To determine when the history results are added, use a mutation
+ // listener on the node containing the rows, and wait until the title of the
+ // next-to-last row is "test2". At that point, the results should be:
+ //
+ // test -- Search with searchSuggestionEngineSlow
+ // test9
+ // test8
+ // test7
+ // test6
+ // test5
+ // test4
+ // test3
+ // test2
+ // test1
+ let mutationPromise = new Promise(resolve => {
+ let observer = new MutationObserver(mutations => {
+ let row = UrlbarTestUtils.getRowAt(window, maxResults - 2);
+ if (row && row._elements.get("title").textContent == "test2") {
+ observer.disconnect();
+ resolve();
+ }
+ });
+ observer.observe(UrlbarTestUtils.getResultsContainer(window), {
+ subtree: true,
+ characterData: true,
+ childList: true,
+ attributes: true,
+ });
+ });
+
+ // Now type a "t" so that we search for "test", but only wait for history
+ // results to be added, as described above.
+ EventUtils.synthesizeKey("t");
+ info("Waiting for the 'test2' row... ");
+ await mutationPromise;
+
+ // Now arrow down. The search, which is still ongoing, will now stop and the
+ // view won't be updated anymore.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+
+ // Wait for the search to stop.
+ info("Waiting for the search to stop... ");
+ await gURLBar.lastQueryContextPromise;
+
+ // Sanity check the results. They should be as described above.
+ count = UrlbarTestUtils.getResultCount(window);
+ Assert.equal(count, maxResults);
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(result.heuristic);
+ Assert.equal(result.element.row.result.rowIndex, 0);
+ for (let i = 1; i < maxResults; i++) {
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.URL);
+ Assert.equal(result.title, "test" + (maxResults - i));
+ Assert.equal(result.element.row.result.rowIndex, i);
+ }
+
+ // Arrow down through all the results. After arrowing down from "test3", we
+ // should continue on to "test2". We should *not* enter the one-off search
+ // buttons at that point.
+ for (let i = 1; i < maxResults; i++) {
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), i);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+
+ // Now the first one-off should be selected.
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), -1);
+ Assert.equal(gURLBar.view.oneOffSearchButtons.selectedButtonIndex, 0);
+
+ // Arrow back up through all the results.
+ for (let i = maxResults - 1; i >= 0; i--) {
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), i);
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+ await SpecialPowers.popPrefEnv();
+ await Services.search.setDefault(
+ oldDefaultEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_selectionKeyNavigation.js b/browser/components/urlbar/tests/browser/browser_selectionKeyNavigation.js
new file mode 100644
index 0000000000..89ba179833
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_selectionKeyNavigation.js
@@ -0,0 +1,200 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test makes sure that the up/down and page-up/down properly adjust the
+// selection. See also browser_caret_navigation.js and
+// browser_urlbar_tabKeyBehavior.js.
+
+"use strict";
+
+const MAX_RESULTS = UrlbarPrefs.get("maxRichResults");
+
+add_setup(async function () {
+ for (let i = 0; i < MAX_RESULTS; i++) {
+ await PlacesTestUtils.addVisits("http://example.com/" + i);
+ }
+ registerCleanupFunction(async function () {
+ await PlacesUtils.history.clear();
+ });
+});
+
+add_task(async function downKey() {
+ for (const ctrlKey of [false, true]) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ fireInputEvent: true,
+ });
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 0,
+ "The heuristic autofill result should be selected initially"
+ );
+ for (let i = 1; i < MAX_RESULTS; i++) {
+ EventUtils.synthesizeKey("KEY_ArrowDown", { ctrlKey });
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), i);
+ }
+ EventUtils.synthesizeKey("KEY_ArrowDown", { ctrlKey });
+ let oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window);
+ Assert.ok(oneOffs.selectedButton, "A one-off should now be selected");
+ while (oneOffs.selectedButton) {
+ EventUtils.synthesizeKey("KEY_ArrowDown", { ctrlKey });
+ }
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 0,
+ "The heuristic autofill result should be selected again"
+ );
+ }
+});
+
+add_task(async function upKey() {
+ for (const ctrlKey of [false, true]) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ fireInputEvent: true,
+ });
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 0,
+ "The heuristic autofill result should be selected initially"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowUp", { ctrlKey });
+ let oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window);
+ Assert.ok(oneOffs.selectedButton, "A one-off should now be selected");
+ while (oneOffs.selectedButton) {
+ EventUtils.synthesizeKey("KEY_ArrowUp", { ctrlKey });
+ }
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ MAX_RESULTS - 1,
+ "The last result should be selected"
+ );
+ for (let i = 1; i < MAX_RESULTS; i++) {
+ EventUtils.synthesizeKey("KEY_ArrowUp", { ctrlKey });
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ MAX_RESULTS - i - 1
+ );
+ }
+ }
+});
+
+add_task(async function pageDownKey() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ fireInputEvent: true,
+ });
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 0,
+ "The heuristic autofill result should be selected initially"
+ );
+ let pageCount = Math.ceil((MAX_RESULTS - 1) / UrlbarUtils.PAGE_UP_DOWN_DELTA);
+ for (let i = 0; i < pageCount; i++) {
+ EventUtils.synthesizeKey("KEY_PageDown");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ Math.min((i + 1) * UrlbarUtils.PAGE_UP_DOWN_DELTA, MAX_RESULTS - 1)
+ );
+ }
+ EventUtils.synthesizeKey("KEY_PageDown");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 0,
+ "Page down at end should wrap around to first result"
+ );
+});
+
+add_task(async function pageUpKey() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ fireInputEvent: true,
+ });
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 0,
+ "The heuristic autofill result should be selected initially"
+ );
+ EventUtils.synthesizeKey("KEY_PageUp");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ MAX_RESULTS - 1,
+ "Page up at start should wrap around to last result"
+ );
+ let pageCount = Math.ceil((MAX_RESULTS - 1) / UrlbarUtils.PAGE_UP_DOWN_DELTA);
+ for (let i = 0; i < pageCount; i++) {
+ EventUtils.synthesizeKey("KEY_PageUp");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ Math.max(MAX_RESULTS - 1 - (i + 1) * UrlbarUtils.PAGE_UP_DOWN_DELTA, 0)
+ );
+ }
+});
+
+add_task(async function pageDownKeyShowsView() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+ EventUtils.synthesizeKey("KEY_PageDown");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ Assert.ok(UrlbarTestUtils.isPopupOpen(window));
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), 0);
+});
+
+add_task(async function pageUpKeyShowsView() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+ EventUtils.synthesizeKey("KEY_PageUp");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ Assert.ok(UrlbarTestUtils.isPopupOpen(window));
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), 0);
+});
+
+add_task(async function pageDownKeyWithCtrlKey() {
+ const previousTab = gBrowser.selectedTab;
+ const currentTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("KEY_PageDown", { ctrlKey: true });
+ await UrlbarTestUtils.promisePopupClose(window);
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ Assert.equal(gBrowser.selectedTab, previousTab);
+ BrowserTestUtils.removeTab(currentTab);
+});
+
+add_task(async function pageUpKeyWithCtrlKey() {
+ const previousTab = gBrowser.selectedTab;
+ const currentTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("KEY_PageUp", { ctrlKey: true });
+ await UrlbarTestUtils.promisePopupClose(window);
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ Assert.equal(gBrowser.selectedTab, previousTab);
+ BrowserTestUtils.removeTab(currentTab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_separatePrivateDefault.js b/browser/components/urlbar/tests/browser/browser_separatePrivateDefault.js
new file mode 100644
index 0000000000..8cdc0e746b
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_separatePrivateDefault.js
@@ -0,0 +1,223 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the 'Search in a Private Window' result of the address bar.
+// Tests here don't have a different private engine, for that see
+// browser_separatePrivateDefault_differentPrivateEngine.js
+
+const serverInfo = {
+ scheme: "http",
+ host: "localhost",
+ port: 20709, // Must be identical to what is in searchSuggestionEngine2.xml
+};
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.search.separatePrivateDefault.ui.enabled", true],
+ ["browser.search.separatePrivateDefault.urlbarResult.enabled", true],
+ ["browser.search.separatePrivateDefault", true],
+ ["browser.urlbar.suggest.searches", true],
+ ],
+ });
+
+ // Add some history for the empty panel.
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com/",
+ transition: PlacesUtils.history.TRANSITIONS.TYPED,
+ },
+ ]);
+
+ // Add a search suggestion engine and move it to the front so that it appears
+ // as the first one-off.
+ await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + "searchSuggestionEngine.xml",
+ setAsDefault: true,
+ setAsDefaultPrivate: true,
+ });
+
+ // Add another engine in the first one-off position.
+ let engine2 = await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + "POSTSearchEngine.xml",
+ });
+ await Services.search.moveEngine(engine2, 0);
+
+ // Add an engine with an alias.
+ await SearchTestUtils.installSearchExtension({
+ name: "MozSearch",
+ keyword: "alias",
+ });
+
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ });
+});
+
+async function AssertNoPrivateResult(win) {
+ let count = await UrlbarTestUtils.getResultCount(win);
+ Assert.ok(count > 0, "Sanity check result count");
+ for (let i = 0; i < count; ++i) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(win, i);
+ Assert.ok(
+ result.type != UrlbarUtils.RESULT_TYPE.SEARCH ||
+ !result.searchParams.inPrivateWindow,
+ "Check this result is not a 'Search in a Private Window' one"
+ );
+ }
+}
+
+async function AssertPrivateResult(win, engine, isPrivateEngine) {
+ let count = await UrlbarTestUtils.getResultCount(win);
+ Assert.ok(count > 1, "Sanity check result count");
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "Check result type"
+ );
+ Assert.ok(result.searchParams.inPrivateWindow, "Check inPrivateWindow");
+ Assert.equal(
+ result.searchParams.isPrivateEngine,
+ isPrivateEngine,
+ "Check isPrivateEngine"
+ );
+ Assert.equal(
+ result.searchParams.engine,
+ engine.name,
+ "Check the search engine"
+ );
+ return result;
+}
+
+add_task(async function test_nonsearch() {
+ info(
+ "Test that 'Search in a Private Window' does not appear with non-search results"
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exa",
+ });
+ await AssertNoPrivateResult(window);
+});
+
+add_task(async function test_search() {
+ info(
+ "Test that 'Search in a Private Window' appears with only search results"
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "unique198273982173",
+ });
+ await AssertPrivateResult(window, await Services.search.getDefault(), false);
+});
+
+add_task(async function test_search_urlbar_result_disabled() {
+ info("Test that 'Search in a Private Window' does not appear when disabled");
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.search.separatePrivateDefault.urlbarResult.enabled", false],
+ ],
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "unique198273982173",
+ });
+ await AssertNoPrivateResult(window);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_search_disabled_suggestions() {
+ info(
+ "Test that 'Search in a Private Window' appears if suggestions are disabled"
+ );
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.searches", false]],
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "unique198273982173",
+ });
+ await AssertPrivateResult(window, await Services.search.getDefault(), false);
+ await SpecialPowers.popPrefEnv();
+});
+
+// TODO: (Bug 1658620) Write a new subtest for this behaviour with the update2
+// pref on.
+// add_task(async function test_oneoff_selected_keyboard() {
+// info(
+// "Test that 'Search in a Private Window' with keyboard opens the selected one-off engine if there's no private engine"
+// );
+// await SpecialPowers.pushPrefEnv({
+// set: [
+// ["browser.urlbar.update2", false],
+// ["browser.urlbar.update2.oneOffsRefresh", false],
+// ],
+// });
+// await UrlbarTestUtils.promiseAutocompleteResultPopup({
+// window,
+// value: "unique198273982173",
+// });
+// await AssertPrivateResult(window, await Services.search.getDefault(), false);
+// // Select the 'Search in a Private Window' result, alt down to select the
+// // first one-off button, Enter. It should open a pb window, but using the
+// // selected one-off engine.
+// let promiseWindow = BrowserTestUtils.waitForNewWindow({
+// url:
+// "http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/print_postdata.sjs",
+// });
+// // Select the private result.
+// EventUtils.synthesizeKey("KEY_ArrowDown");
+// // Select the first one-off button.
+// EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+// EventUtils.synthesizeKey("VK_RETURN");
+// let win = await promiseWindow;
+// Assert.ok(
+// PrivateBrowsingUtils.isWindowPrivate(win),
+// "Should open a private window"
+// );
+// await BrowserTestUtils.closeWindow(win);
+// await SpecialPowers.popPrefEnv();
+// });
+
+// TODO: (Bug 1658620) Write a new subtest for this behaviour with the update2
+// pref on.
+// add_task(async function test_oneoff_selected_mouse() {
+// info(
+// "Test that 'Search in a Private Window' with mouse opens the selected one-off engine if there's no private engine"
+// );
+// await SpecialPowers.pushPrefEnv({
+// set: [
+// ["browser.urlbar.update2", false],
+// ["browser.urlbar.update2.oneOffsRefresh", false],
+// ],
+// });
+// await UrlbarTestUtils.promiseAutocompleteResultPopup({
+// window,
+// value: "unique198273982173",
+// });
+// await AssertPrivateResult(window, await Services.search.getDefault(), false);
+// // Select the 'Search in a Private Window' result, alt down to select the
+// // first one-off button, Enter. It should open a pb window, but using the
+// // selected one-off engine.
+// let promiseWindow = BrowserTestUtils.waitForNewWindow({
+// url:
+// "http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/print_postdata.sjs",
+// });
+// // Select the private result.
+// EventUtils.synthesizeKey("KEY_ArrowDown");
+// // Select the first one-off button.
+// EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+// // Click on the result.
+// let element = UrlbarTestUtils.getSelectedRow(window);
+// EventUtils.synthesizeMouseAtCenter(element, {});
+// let win = await promiseWindow;
+// Assert.ok(
+// PrivateBrowsingUtils.isWindowPrivate(win),
+// "Should open a private window"
+// );
+// await BrowserTestUtils.closeWindow(win);
+// await SpecialPowers.popPrefEnv();
+// });
diff --git a/browser/components/urlbar/tests/browser/browser_separatePrivateDefault_differentEngine.js b/browser/components/urlbar/tests/browser/browser_separatePrivateDefault_differentEngine.js
new file mode 100644
index 0000000000..58a60d68a9
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_separatePrivateDefault_differentEngine.js
@@ -0,0 +1,354 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the 'Search in a Private Window' result of the address bar.
+
+const serverInfo = {
+ scheme: "http",
+ host: "localhost",
+ port: 20709, // Must be identical to what is in searchSuggestionEngine2.xml
+};
+
+let gAliasEngine;
+let gPrivateEngine;
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.search.separatePrivateDefault.ui.enabled", true],
+ ["browser.search.separatePrivateDefault.urlbarResult.enabled", true],
+ ["browser.search.separatePrivateDefault", true],
+ ["browser.urlbar.suggest.searches", true],
+ ],
+ });
+
+ // Add some history for the empty panel.
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com/",
+ transition: PlacesUtils.history.TRANSITIONS.TYPED,
+ },
+ ]);
+
+ // Add a search suggestion engine and move it to the front so that it appears
+ // as the first one-off.
+ await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + "searchSuggestionEngine.xml",
+ setAsDefault: true,
+ });
+ gPrivateEngine = await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + "searchSuggestionEngine2.xml",
+ setAsDefaultPrivate: true,
+ });
+
+ // Add another engine in the first one-off position.
+ let engine2 = await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + "POSTSearchEngine.xml",
+ });
+ await Services.search.moveEngine(engine2, 0);
+
+ // Add an engine with an alias.
+ await SearchTestUtils.installSearchExtension({
+ name: "MozSearch",
+ keyword: "alias",
+ });
+ gAliasEngine = Services.search.getEngineByName("MozSearch");
+
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ });
+});
+
+async function AssertNoPrivateResult(win) {
+ let count = await UrlbarTestUtils.getResultCount(win);
+ for (let i = 0; i < count; ++i) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(win, i);
+ Assert.ok(
+ result.type != UrlbarUtils.RESULT_TYPE.SEARCH ||
+ !result.searchParams.inPrivateWindow,
+ "Check this result is not a 'Search in a Private Window' one"
+ );
+ }
+}
+
+async function AssertPrivateResult(win, engine, isPrivateEngine) {
+ let count = await UrlbarTestUtils.getResultCount(win);
+ Assert.ok(count > 1, "Sanity check result count");
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "Check result type"
+ );
+ Assert.ok(result.searchParams.inPrivateWindow, "Check inPrivateWindow");
+ Assert.equal(
+ result.searchParams.isPrivateEngine,
+ isPrivateEngine,
+ "Check isPrivateEngine"
+ );
+ Assert.equal(
+ result.searchParams.engine,
+ engine.name,
+ "Check the search engine"
+ );
+ return result;
+}
+
+// Tests from here on have a different default private engine.
+
+add_task(async function test_search_private_engine() {
+ info(
+ "Test that 'Search in a Private Window' reports a separate private engine"
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "unique198273982173",
+ });
+ await AssertPrivateResult(window, gPrivateEngine, true);
+});
+
+add_task(async function test_privateWindow() {
+ info(
+ "Test that 'Search in a Private Window' does not appear in a private window"
+ );
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: privateWin,
+ value: "unique198273982173",
+ });
+ await AssertNoPrivateResult(privateWin);
+ await BrowserTestUtils.closeWindow(privateWin);
+});
+
+add_task(async function test_permanentPB() {
+ info(
+ "Test that 'Search in a Private Window' does not appear in Permanent Private Browsing"
+ );
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.privatebrowsing.autostart", true]],
+ });
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "unique198273982173",
+ });
+ await AssertNoPrivateResult(win);
+ await BrowserTestUtils.closeWindow(win);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_openPBWindow() {
+ info(
+ "Test that 'Search in a Private Window' opens the search in a new Private Window"
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "unique198273982173",
+ });
+ await AssertPrivateResult(
+ window,
+ await Services.search.getDefaultPrivate(),
+ true
+ );
+
+ await withHttpServer(serverInfo, async () => {
+ let promiseWindow = BrowserTestUtils.waitForNewWindow({
+ url: "http://localhost:20709/?terms=unique198273982173",
+ maybeErrorPage: true,
+ });
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("VK_RETURN");
+ let win = await promiseWindow;
+ Assert.ok(
+ PrivateBrowsingUtils.isWindowPrivate(win),
+ "Should open a private window"
+ );
+ await BrowserTestUtils.closeWindow(win);
+ });
+});
+
+// TODO: (Bug 1658620) Write a new subtest for this behaviour with the update2
+// pref on.
+// add_task(async function test_oneoff_selected_with_private_engine_mouse() {
+// info(
+// "Test that 'Search in a Private Window' opens the private engine even if a one-off is selected"
+// );
+// await SpecialPowers.pushPrefEnv({
+// set: [
+// ["browser.urlbar.update2", false],
+// ["browser.urlbar.update2.oneOffsRefresh", false],
+// ],
+// });
+// await UrlbarTestUtils.promiseAutocompleteResultPopup({
+// window,
+// value: "unique198273982173",
+// });
+// await AssertPrivateResult(
+// window,
+// await Services.search.getDefaultPrivate(),
+// true
+// );
+
+// await withHttpServer(serverInfo, async () => {
+// // Select the 'Search in a Private Window' result, alt down to select the
+// // first one-off button, Click on the result. It should open a pb window using
+// // the private search engine, because it has been set.
+// let promiseWindow = BrowserTestUtils.waitForNewWindow({
+// url: "http://localhost:20709/?terms=unique198273982173",
+// maybeErrorPage: true,
+// });
+// // Select the private result.
+// EventUtils.synthesizeKey("KEY_ArrowDown");
+// // Select the first one-off button.
+// EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+// // Click on the result.
+// let element = UrlbarTestUtils.getSelectedRow(window);
+// EventUtils.synthesizeMouseAtCenter(element, {});
+// let win = await promiseWindow;
+// Assert.ok(
+// PrivateBrowsingUtils.isWindowPrivate(win),
+// "Should open a private window"
+// );
+// await BrowserTestUtils.closeWindow(win);
+// });
+// await SpecialPowers.popPrefEnv();
+// });
+
+// TODO: (Bug 1658620) Write a new subtest for this behaviour with the update2
+// pref on.
+// add_task(async function test_oneoff_selected_with_private_engine_keyboard() {
+// info(
+// "Test that 'Search in a Private Window' opens the private engine even if a one-off is selected"
+// );
+// await SpecialPowers.pushPrefEnv({
+// set: [
+// ["browser.urlbar.update2", false],
+// ["browser.urlbar.update2.oneOffsRefresh", false],
+// ],
+// });
+// await UrlbarTestUtils.promiseAutocompleteResultPopup({
+// window,
+// value: "unique198273982173",
+// });
+// await AssertPrivateResult(
+// window,
+// await Services.search.getDefaultPrivate(),
+// true
+// );
+
+// await withHttpServer(serverInfo, async () => {
+// // Select the 'Search in a Private Window' result, alt down to select the
+// // first one-off button, Enter. It should open a pb window, but using the
+// // selected one-off engine.
+// let promiseWindow = BrowserTestUtils.waitForNewWindow({
+// url: "http://localhost:20709/?terms=unique198273982173",
+// maybeErrorPage: true,
+// });
+// // Select the private result.
+// EventUtils.synthesizeKey("KEY_ArrowDown");
+// // Select the first one-off button.
+// EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+// EventUtils.synthesizeKey("VK_RETURN");
+// let win = await promiseWindow;
+// Assert.ok(
+// PrivateBrowsingUtils.isWindowPrivate(win),
+// "Should open a private window"
+// );
+// await BrowserTestUtils.closeWindow(win);
+// });
+// await SpecialPowers.popPrefEnv();
+// });
+
+add_task(async function test_alias_no_query() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.update2.emptySearchBehavior", 2]],
+ });
+ info(
+ "Test that 'Search in a Private Window' doesn't appear if an alias is typed with no query"
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "alias ",
+ });
+ // Wait for the second new search that starts when search mode is entered.
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: gAliasEngine.name,
+ entry: "typed",
+ });
+ await AssertNoPrivateResult(window);
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_alias_query() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.update2.emptySearchBehavior", 2]],
+ });
+ info(
+ "Test that 'Search in a Private Window' appears when an alias is typed with a query"
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "alias something",
+ });
+ // Wait for the second new search that starts when search mode is entered.
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: "MozSearch",
+ entry: "typed",
+ });
+ await AssertPrivateResult(window, gAliasEngine, true);
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_restrict() {
+ info(
+ "Test that 'Search in a Private Window' doesn's appear for just the restriction token"
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: UrlbarTokenizer.RESTRICT.SEARCH,
+ });
+ await AssertNoPrivateResult(window);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: UrlbarTokenizer.RESTRICT.SEARCH + " ",
+ });
+ await AssertNoPrivateResult(window);
+ await UrlbarTestUtils.exitSearchMode(window);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: " " + UrlbarTokenizer.RESTRICT.SEARCH,
+ });
+ await AssertNoPrivateResult(window);
+});
+
+add_task(async function test_restrict_search() {
+ info(
+ "Test that 'Search in a Private Window' has the right string with the restriction token"
+ );
+ let engine = await Services.search.getDefaultPrivate();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: UrlbarTokenizer.RESTRICT.SEARCH + "test",
+ });
+ let result = await AssertPrivateResult(window, engine, true);
+ Assert.equal(result.searchParams.query, "test");
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test" + UrlbarTokenizer.RESTRICT.SEARCH,
+ });
+ result = await AssertPrivateResult(window, engine, true);
+ Assert.equal(result.searchParams.query, "test");
+});
diff --git a/browser/components/urlbar/tests/browser/browser_shortcuts_add_search_engine.js b/browser/components/urlbar/tests/browser/browser_shortcuts_add_search_engine.js
new file mode 100644
index 0000000000..1fef68de30
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_shortcuts_add_search_engine.js
@@ -0,0 +1,243 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test adding engines through search shortcut buttons.
+// A more complete coverage of the detection of engines is available in
+// browser_add_search_engine.js
+
+const { PromptTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromptTestUtils.sys.mjs"
+);
+const BASE_URL =
+ "http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.searches", false]],
+ });
+ // Ensure initial state.
+ UrlbarTestUtils.getOneOffSearchButtons(window).invalidateCache();
+});
+
+add_task(async function shortcuts_none() {
+ info("Checks the shortcuts with a page that doesn't offer any engines.");
+ let url = "http://mochi.test:8888/";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ let shortcutButtons = UrlbarTestUtils.getOneOffSearchButtons(window);
+ let rebuildPromise = BrowserTestUtils.waitForEvent(
+ shortcutButtons,
+ "rebuild"
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+ await rebuildPromise;
+
+ Assert.ok(
+ !Array.from(shortcutButtons.buttons.children).some(b =>
+ b.classList.contains("searchbar-engine-one-off-add-engine")
+ ),
+ "Check there's no buttons to add engines"
+ );
+ });
+});
+
+add_task(async function test_shortcuts() {
+ await do_test_shortcuts(button => {
+ info("Click on button");
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ });
+ await do_test_shortcuts(button => {
+ info("Enter on button");
+ let shortcuts = UrlbarTestUtils.getOneOffSearchButtons(window);
+ while (shortcuts.selectedButton != button) {
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ }
+ EventUtils.synthesizeKey("KEY_Enter");
+ });
+});
+
+/**
+ * Test add engine shortcuts.
+ *
+ * @param {Function} activateTask a function receiveing the shortcut button to
+ * activate as argument. The scope of this function is to activate the
+ * shortcut button.
+ */
+async function do_test_shortcuts(activateTask) {
+ info("Checks the shortcuts with a page that offers two engines.");
+ let url = getRootDirectory(gTestPath) + "add_search_engine_two.html";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ let shortcutButtons = UrlbarTestUtils.getOneOffSearchButtons(window);
+ let rebuildPromise = BrowserTestUtils.waitForEvent(
+ shortcutButtons,
+ "rebuild"
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+ await rebuildPromise;
+
+ let addEngineButtons = Array.from(shortcutButtons.buttons.children).filter(
+ b => b.classList.contains("searchbar-engine-one-off-add-engine")
+ );
+ Assert.equal(
+ addEngineButtons.length,
+ 2,
+ "Check there's two buttons to add engines"
+ );
+
+ for (let button of addEngineButtons) {
+ Assert.ok(BrowserTestUtils.is_visible(button));
+ Assert.ok(button.hasAttribute("image"));
+ await document.l10n.translateElements([button]);
+ Assert.ok(
+ button.getAttribute("tooltiptext").includes("add_search_engine_")
+ );
+ Assert.ok(
+ button.getAttribute("engine-name").startsWith("add_search_engine_")
+ );
+ Assert.ok(
+ button.classList.contains("searchbar-engine-one-off-add-engine")
+ );
+ }
+
+ info("Activate the first button");
+ rebuildPromise = BrowserTestUtils.waitForEvent(shortcutButtons, "rebuild");
+ let enginePromise = promiseEngine("engine-added", "add_search_engine_0");
+ await activateTask(addEngineButtons[0]);
+ info("await engine install");
+ let engine = await enginePromise;
+ info("await rebuild");
+ await rebuildPromise;
+
+ Assert.ok(
+ UrlbarTestUtils.isPopupOpen(window),
+ "Urlbar view is still open."
+ );
+
+ addEngineButtons = Array.from(shortcutButtons.buttons.children).filter(b =>
+ b.classList.contains("searchbar-engine-one-off-add-engine")
+ );
+ Assert.equal(
+ addEngineButtons.length,
+ 1,
+ "Check there's one button to add engines"
+ );
+ Assert.equal(
+ addEngineButtons[0].getAttribute("engine-name"),
+ "add_search_engine_1"
+ );
+ let installedEngineButton = addEngineButtons[0].previousElementSibling;
+ Assert.equal(installedEngineButton.engine.name, "add_search_engine_0");
+
+ info("Remove the added engine");
+ rebuildPromise = BrowserTestUtils.waitForEvent(shortcutButtons, "rebuild");
+ await Services.search.removeEngine(engine);
+ await rebuildPromise;
+ Assert.equal(
+ Array.from(shortcutButtons.buttons.children).filter(b =>
+ b.classList.contains("searchbar-engine-one-off-add-engine")
+ ).length,
+ 2,
+ "Check there's two buttons to add engines"
+ );
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ info("Switch to a new tab and check the buttons are not persisted");
+ await BrowserTestUtils.withNewTab("about:robots", async () => {
+ rebuildPromise = BrowserTestUtils.waitForEvent(
+ shortcutButtons,
+ "rebuild"
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+ await rebuildPromise;
+ Assert.ok(
+ !Array.from(shortcutButtons.buttons.children).some(b =>
+ b.classList.contains("searchbar-engine-one-off-add-engine")
+ ),
+ "Check there's no option to add engines"
+ );
+ });
+ });
+}
+
+add_task(async function shortcuts_many() {
+ info("Checks the shortcuts with a page that offers many engines.");
+ let url = getRootDirectory(gTestPath) + "add_search_engine_many.html";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ let shortcutButtons = UrlbarTestUtils.getOneOffSearchButtons(window);
+ let rebuildPromise = BrowserTestUtils.waitForEvent(
+ shortcutButtons,
+ "rebuild"
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+ await rebuildPromise;
+
+ let addEngineButtons = Array.from(shortcutButtons.buttons.children).filter(
+ b => b.classList.contains("searchbar-engine-one-off-add-engine")
+ );
+ Assert.equal(
+ addEngineButtons.length,
+ gURLBar.addSearchEngineHelper.maxInlineEngines,
+ "Check there's a maximum of `maxInlineEngines` buttons to add engines"
+ );
+ });
+});
+
+function promiseEngine(expectedData, expectedEngineName) {
+ info(`Waiting for engine ${expectedData}`);
+ return TestUtils.topicObserved(
+ "browser-search-engine-modified",
+ (engine, data) => {
+ info(`Got engine ${engine.wrappedJSObject.name} ${data}`);
+ return (
+ expectedData == data &&
+ expectedEngineName == engine.wrappedJSObject.name
+ );
+ }
+ ).then(([engine, data]) => engine);
+}
+
+add_task(async function shortcuts_without_other_engines() {
+ info("Checks the shortcuts without other engines.");
+
+ info("Remove search engines except default");
+ const defaultEngine = Services.search.defaultEngine;
+ const engines = await Services.search.getVisibleEngines();
+ for (const engine of engines) {
+ if (defaultEngine.name !== engine.name) {
+ await Services.search.removeEngine(engine);
+ }
+ }
+
+ info("Remove local engines");
+ for (const { pref } of UrlbarUtils.LOCAL_SEARCH_MODES) {
+ await SpecialPowers.pushPrefEnv({
+ set: [[`browser.urlbar.${pref}`, false]],
+ });
+ }
+
+ const url = getRootDirectory(gTestPath) + "add_search_engine_many.html";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+
+ const shortcutButtons = UrlbarTestUtils.getOneOffSearchButtons(window);
+ Assert.ok(shortcutButtons.container.hidden, "It should be hidden");
+ });
+
+ Services.search.restoreDefaultEngines();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_speculative_connect.js b/browser/components/urlbar/tests/browser/browser_speculative_connect.js
new file mode 100644
index 0000000000..3b98169699
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_speculative_connect.js
@@ -0,0 +1,199 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// This test ensures that we setup a speculative network connection to
+// the site in various cases:
+// 1. search engine if it's the first result
+// 2. mousedown event before the http request happens(in mouseup).
+
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine2.xml";
+
+const serverInfo = {
+ scheme: "http",
+ host: "localhost",
+ port: 20709, // Must be identical to what is in searchSuggestionEngine2.xml
+};
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.autoFill", true],
+ ["browser.urlbar.suggest.searches", true],
+ ["browser.urlbar.speculativeConnect.enabled", true],
+ // In mochitest this number is 0 by default but we have to turn it on.
+ ["network.http.speculative-parallel-limit", 6],
+ // The http server is using IPv4, so it's better to disable IPv6 to avoid
+ // weird networking problem.
+ ["network.dns.disableIPv6", true],
+ ],
+ });
+
+ // Ensure we start from a clean situation.
+ await PlacesUtils.history.clear();
+
+ await PlacesTestUtils.addVisits([
+ {
+ uri: `${serverInfo.scheme}://${serverInfo.host}:${serverInfo.port}`,
+ title: "test visit for speculative connection",
+ transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ },
+ ]);
+
+ await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME,
+ setAsDefault: true,
+ });
+ registerCleanupFunction(async function () {
+ await PlacesUtils.history.clear();
+ });
+});
+
+add_task(async function search_test() {
+ // We speculative connect to the search engine only if suggestions are enabled.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.suggest.enabled", true]],
+ });
+ await withHttpServer(serverInfo, async server => {
+ let connectionNumber = server.connectionNumber;
+ info("Searching for 'foo'");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ fireInputEvent: true,
+ });
+ // Check if the first result is with type "searchengine"
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ details.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "The first result is a search"
+ );
+ await UrlbarTestUtils.promiseSpeculativeConnections(
+ server,
+ connectionNumber + 1
+ );
+ });
+});
+
+add_task(async function popup_mousedown_test() {
+ // Disable search suggestions and autofill, to avoid other speculative
+ // connections.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.search.suggest.enabled", false],
+ ["browser.urlbar.autoFill", false],
+ ],
+ });
+ await withHttpServer(serverInfo, async server => {
+ let connectionNumber = server.connectionNumber;
+ let searchString = "ocal";
+ let completeValue = `${serverInfo.scheme}://${serverInfo.host}:${serverInfo.port}/`;
+ info(`Searching for '${searchString}'`);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: searchString,
+ fireInputEvent: true,
+ });
+ let listitem = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1);
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(
+ details.url,
+ completeValue,
+ "The second item has the url we visited."
+ );
+
+ info("Clicking on the second result");
+ EventUtils.synthesizeMouseAtCenter(listitem, { type: "mousedown" }, window);
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRow(window),
+ listitem,
+ "The second item is selected"
+ );
+ await UrlbarTestUtils.promiseSpeculativeConnections(
+ server,
+ connectionNumber + 1
+ );
+ });
+});
+
+add_task(async function test_autofill() {
+ // Disable search suggestions but enable autofill.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.search.suggest.enabled", false],
+ ["browser.urlbar.autoFill", true],
+ ],
+ });
+ await withHttpServer(serverInfo, async server => {
+ let connectionNumber = server.connectionNumber;
+ let searchString = serverInfo.host;
+ info(`Searching for '${searchString}'`);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: searchString,
+ fireInputEvent: true,
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ let completeValue = `${serverInfo.scheme}://${serverInfo.host}:${serverInfo.port}/`;
+ Assert.equal(details.url, completeValue, `Autofilled value is as expected`);
+ await UrlbarTestUtils.promiseSpeculativeConnections(
+ server,
+ connectionNumber + 1
+ );
+ });
+});
+
+add_task(async function test_autofill_privateContext() {
+ info("Autofill in private context.");
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ registerCleanupFunction(async () => {
+ let promisePBExit = TestUtils.topicObserved("last-pb-context-exited");
+ await BrowserTestUtils.closeWindow(privateWin);
+ await promisePBExit;
+ });
+ await withHttpServer(serverInfo, async server => {
+ let connectionNumber = server.connectionNumber;
+ let searchString = serverInfo.host;
+ info(`Searching for '${searchString}'`);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: privateWin,
+ value: searchString,
+ fireInputEvent: true,
+ });
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(privateWin, 0);
+ let completeValue = `${serverInfo.scheme}://${serverInfo.host}:${serverInfo.port}/`;
+ Assert.equal(details.url, completeValue, `Autofilled value is as expected`);
+ await UrlbarTestUtils.promiseSpeculativeConnections(
+ server,
+ connectionNumber
+ );
+ });
+});
+
+add_task(async function test_no_heuristic_result() {
+ info("Don't speculative connect on results addition if there's no heuristic");
+ await withHttpServer(serverInfo, async server => {
+ let connectionNumber = server.connectionNumber;
+ info(`Searching for the empty string`);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ fireInputEvent: true,
+ });
+ ok(UrlbarTestUtils.getResultCount(window) > 0, "Has results");
+ let result = await UrlbarTestUtils.getSelectedRow(window);
+ Assert.strictEqual(result, null, `Should have no selection`);
+ await UrlbarTestUtils.promiseSpeculativeConnections(
+ server,
+ connectionNumber
+ );
+ });
+});
diff --git a/browser/components/urlbar/tests/browser/browser_speculative_connect_not_with_client_cert.js b/browser/components/urlbar/tests/browser/browser_speculative_connect_not_with_client_cert.js
new file mode 100644
index 0000000000..de352efb59
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_speculative_connect_not_with_client_cert.js
@@ -0,0 +1,236 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Tests that we don't speculatively connect when user certificates are installed
+
+const { MockRegistrar } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistrar.sys.mjs"
+);
+
+const certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+].getService(Ci.nsICertOverrideService);
+
+const host = "localhost";
+let uri;
+let handshakeDone = false;
+let expectingChooseCertificate = false;
+let chooseCertificateCalled = false;
+
+const clientAuthDialogs = {
+ chooseCertificate(
+ hostname,
+ port,
+ organization,
+ issuerOrg,
+ certList,
+ selectedIndex,
+ rememberClientAuthCertificate
+ ) {
+ ok(
+ expectingChooseCertificate,
+ `${
+ expectingChooseCertificate ? "" : "not "
+ }expecting chooseCertificate to be called`
+ );
+ is(certList.length, 1, "should have only one client certificate available");
+ selectedIndex.value = 0;
+ rememberClientAuthCertificate.value = false;
+ ok(
+ !chooseCertificateCalled,
+ "chooseCertificate should only be called once"
+ );
+ chooseCertificateCalled = true;
+ return true;
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIClientAuthDialogs"]),
+};
+
+/**
+ * A helper class to use with nsITLSServerConnectionInfo.setSecurityObserver.
+ * Implements nsITLSServerSecurityObserver and simulates an extremely
+ * rudimentary HTTP server that expects an HTTP/1.1 GET request and responds
+ * with a 200 OK.
+ */
+class SecurityObserver {
+ constructor(input, output) {
+ this.input = input;
+ this.output = output;
+ }
+
+ onHandshakeDone(socket, status) {
+ info("TLS handshake done");
+ handshakeDone = true;
+
+ let output = this.output;
+ this.input.asyncWait(
+ {
+ onInputStreamReady(readyInput) {
+ try {
+ let request = NetUtil.readInputStreamToString(
+ readyInput,
+ readyInput.available()
+ );
+ ok(
+ request.startsWith("GET /") && request.includes("HTTP/1.1"),
+ "expecting an HTTP/1.1 GET request"
+ );
+ let response =
+ "HTTP/1.1 200 OK\r\nContent-Type:text/plain\r\n" +
+ "Connection:Close\r\nContent-Length:2\r\n\r\nOK";
+ output.write(response, response.length);
+ } catch (e) {
+ console.log(e.message);
+ // This will fail when we close the speculative connection.
+ }
+ },
+ },
+ 0,
+ 0,
+ Services.tm.currentThread
+ );
+ }
+}
+
+function startServer(cert) {
+ let tlsServer = Cc["@mozilla.org/network/tls-server-socket;1"].createInstance(
+ Ci.nsITLSServerSocket
+ );
+ tlsServer.init(-1, true, -1);
+ tlsServer.serverCert = cert;
+
+ let securityObservers = [];
+
+ let listener = {
+ onSocketAccepted(socket, transport) {
+ info("Accepted TLS client connection");
+ let connectionInfo = transport.securityCallbacks.getInterface(
+ Ci.nsITLSServerConnectionInfo
+ );
+ let input = transport.openInputStream(0, 0, 0);
+ let output = transport.openOutputStream(0, 0, 0);
+ connectionInfo.setSecurityObserver(new SecurityObserver(input, output));
+ },
+
+ onStopListening() {
+ info("onStopListening");
+ for (let securityObserver of securityObservers) {
+ securityObserver.input.close();
+ securityObserver.output.close();
+ }
+ },
+ };
+
+ tlsServer.setSessionTickets(false);
+ tlsServer.setRequestClientCertificate(Ci.nsITLSServerSocket.REQUEST_ALWAYS);
+
+ tlsServer.asyncListen(listener);
+
+ return tlsServer;
+}
+
+let server;
+
+function getTestServerCertificate() {
+ const certDB = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ for (const cert of certDB.getCerts()) {
+ if (cert.commonName == "Mochitest client") {
+ return cert;
+ }
+ }
+ return null;
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.autoFill", true],
+ // Turn off search suggestion so we won't speculative connect to the search engine.
+ ["browser.search.suggest.enabled", false],
+ ["browser.urlbar.speculativeConnect.enabled", true],
+ // In mochitest this number is 0 by default but we have to turn it on.
+ ["network.http.speculative-parallel-limit", 6],
+ // The http server is using IPv4, so it's better to disable IPv6 to avoid weird
+ // networking problem.
+ ["network.dns.disableIPv6", true],
+ ["security.default_personal_cert", "Ask Every Time"],
+ ],
+ });
+
+ let clientAuthDialogsCID = MockRegistrar.register(
+ "@mozilla.org/nsClientAuthDialogs;1",
+ clientAuthDialogs
+ );
+
+ let cert = getTestServerCertificate();
+ server = startServer(cert);
+ uri = `https://${host}:${server.port}/`;
+ info(`running tls server at ${uri}`);
+ await PlacesTestUtils.addVisits([
+ {
+ uri,
+ title: "test visit for speculative connection",
+ transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ },
+ ]);
+
+ certOverrideService.rememberValidityOverride(
+ "localhost",
+ server.port,
+ {},
+ cert,
+ true
+ );
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.history.clear();
+ MockRegistrar.unregister(clientAuthDialogsCID);
+ certOverrideService.clearValidityOverride("localhost", server.port, {});
+ });
+});
+
+add_task(
+ async function popup_mousedown_no_client_cert_dialog_until_navigate_test() {
+ // To not trigger autofill, search keyword starts from the second character.
+ let searchString = host.substr(1, 4);
+ let completeValue = uri;
+ info(`Searching for '${searchString}'`);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: searchString,
+ fireInputEvent: true,
+ });
+ let listitem = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1);
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ info(`The url of the second item is ${details.url}`);
+ is(details.url, completeValue, "The second item has the url we visited.");
+
+ expectingChooseCertificate = false;
+ EventUtils.synthesizeMouseAtCenter(listitem, { type: "mousedown" }, window);
+ is(
+ UrlbarTestUtils.getSelectedRow(window),
+ listitem,
+ "The second item is selected"
+ );
+
+ // We shouldn't have triggered a speculative connection, because a client
+ // certificate is installed.
+ SimpleTest.requestFlakyTimeout("Wait for UI");
+ await new Promise(resolve => setTimeout(resolve, 200));
+
+ // Now mouseup, expect that we choose a client certificate, and expect that
+ // we successfully load a page.
+ expectingChooseCertificate = true;
+ EventUtils.synthesizeMouseAtCenter(listitem, { type: "mouseup" }, window);
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ ok(chooseCertificateCalled, "chooseCertificate must have been called");
+ server.close();
+ }
+);
diff --git a/browser/components/urlbar/tests/browser/browser_stop.js b/browser/components/urlbar/tests/browser/browser_stop.js
new file mode 100644
index 0000000000..285071a3ff
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_stop.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This tests ensures the urlbar reflects the correct value if a page load is
+ * stopped immediately after loading.
+ */
+
+"use strict";
+
+const goodURL = "http://mochi.test:8888/";
+const badURL = "http://mochi.test:8888/whatever.html";
+
+add_task(async function () {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, goodURL);
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ is(
+ gURLBar.value,
+ BrowserUIUtils.trimURL(goodURL),
+ "location bar reflects loaded page"
+ );
+
+ await typeAndSubmitAndStop(badURL);
+ is(
+ gURLBar.value,
+ BrowserUIUtils.trimURL(goodURL),
+ "location bar reflects loaded page after stop()"
+ );
+ gBrowser.removeCurrentTab();
+
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank");
+ is(gURLBar.value, "", "location bar is empty");
+
+ await typeAndSubmitAndStop(badURL);
+ is(
+ gURLBar.value,
+ BrowserUIUtils.trimURL(badURL),
+ "location bar reflects stopped page in an empty tab"
+ );
+ gBrowser.removeCurrentTab();
+});
+
+async function typeAndSubmitAndStop(url) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: url,
+ fireInputEvent: true,
+ });
+
+ let docLoadPromise = BrowserTestUtils.waitForDocLoadAndStopIt(
+ url,
+ gBrowser.selectedBrowser
+ );
+
+ // When the load is stopped, tabbrowser calls gURLBar.setURI and then calls
+ // onStateChange on its progress listeners. So to properly wait until the
+ // urlbar value has been updated, add our own progress listener here.
+ let progressPromise = new Promise(resolve => {
+ let listener = {
+ onStateChange(browser, webProgress, request, stateFlags, status) {
+ if (
+ webProgress.isTopLevel &&
+ stateFlags & Ci.nsIWebProgressListener.STATE_STOP
+ ) {
+ gBrowser.removeTabsProgressListener(listener);
+ resolve();
+ }
+ },
+ };
+ gBrowser.addTabsProgressListener(listener);
+ });
+
+ gURLBar.handleCommand();
+ await Promise.all([docLoadPromise, progressPromise]);
+}
diff --git a/browser/components/urlbar/tests/browser/browser_stopSearchOnSelection.js b/browser/components/urlbar/tests/browser/browser_stopSearchOnSelection.js
new file mode 100644
index 0000000000..0a1ef1b057
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_stopSearchOnSelection.js
@@ -0,0 +1,113 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This tests that when a search is stopped due to the user selecting a result,
+ * the view doesn't update after that.
+ */
+
+"use strict";
+
+const TEST_ENGINE_BASENAME = "searchSuggestionEngineSlow.xml";
+
+// This should match the `timeout` query param used in the suggestions URL in
+// the test engine.
+const TEST_ENGINE_SUGGESTIONS_TIMEOUT = 3000;
+
+// The number of suggestions returned by the test engine.
+const TEST_ENGINE_NUM_EXPECTED_RESULTS = 2;
+
+add_setup(async function () {
+ await PlacesUtils.history.clear();
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.searches", true]],
+ });
+ // Add a test search engine that returns suggestions on a delay.
+ let engine = await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME,
+ setAsDefault: true,
+ });
+ await Services.search.moveEngine(engine, 0);
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ });
+});
+
+add_task(async function mainTest() {
+ // Open a tab that will match the search string below so that we're guaranteed
+ // to have more than one result (the heuristic result) so that we can change
+ // the selected result. We open a tab instead of adding a page in history
+ // because open tabs are kept in a memory SQLite table, so open-tab results
+ // are more likely than history results to be fetched before our slow search
+ // suggestions. This is important when the test runs on slow debug builds on
+ // slow machines.
+ await BrowserTestUtils.withNewTab("http://example.com/", async () => {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ // Do an initial search. There should be 4 results: heuristic, open tab,
+ // and the two suggestions.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "amp",
+ });
+ await TestUtils.waitForCondition(() => {
+ return (
+ UrlbarTestUtils.getResultCount(window) ==
+ 2 + TEST_ENGINE_NUM_EXPECTED_RESULTS
+ );
+ });
+
+ // Type a character to start a new search. The new search should still
+ // match the open tab so that the open-tab result appears again.
+ EventUtils.synthesizeKey("l");
+
+ // There should be 2 results immediately: heuristic and open tab.
+ await TestUtils.waitForCondition(() => {
+ return UrlbarTestUtils.getResultCount(window) == 2;
+ });
+
+ // Before the search completes, change the selected result. Pressing only
+ // the down arrow key ends up selecting the first one-off on Linux debug
+ // builds on the infrastructure for some reason, so arrow back up to
+ // select the heuristic result again. The important thing is to change
+ // the selection. It doesn't matter which result ends up selected.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+
+ // Wait for the new search to complete. It should be canceled due to the
+ // selection change, but it should still complete.
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ // To make absolutely sure the suggestions don't appear after the search
+ // completes, wait a bit.
+ await new Promise(r =>
+ setTimeout(r, 1 + TEST_ENGINE_SUGGESTIONS_TIMEOUT)
+ );
+
+ // The heuristic result should reflect the new search, "ampl".
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "Should have the correct result type"
+ );
+ Assert.equal(
+ result.searchParams.query,
+ "ampl",
+ "Should have the correct query"
+ );
+
+ // None of the other results should be "ampl" suggestions, i.e., amplfoo
+ // and amplbar should not be in the results.
+ let count = UrlbarTestUtils.getResultCount(window);
+ for (let i = 1; i < count; i++) {
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ Assert.ok(
+ result.type != UrlbarUtils.RESULT_TYPE.SEARCH ||
+ !["amplfoo", "amplbar"].includes(result.searchParams.suggestion),
+ "Suggestions should not contain the typed l char"
+ );
+ }
+ });
+ });
+});
diff --git a/browser/components/urlbar/tests/browser/browser_stop_pending.js b/browser/components/urlbar/tests/browser/browser_stop_pending.js
new file mode 100644
index 0000000000..2e7056e972
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_stop_pending.js
@@ -0,0 +1,459 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+"use strict";
+
+const SLOW_PAGE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://www.example.com"
+ ) + "slow-page.sjs";
+const SLOW_PAGE2 =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://mochi.test:8888"
+ ) + "slow-page.sjs?faster";
+
+/**
+ * Check that if we:
+ * 1) have a loaded page
+ * 2) load a separate URL
+ * 3) before the URL for step 2 has finished loading, load a third URL
+ * we don't revert to the URL from (1).
+ */
+add_task(async function () {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com",
+ true,
+ true
+ );
+
+ let initialValue = gURLBar.untrimmedValue;
+ let expectedURLBarChange = SLOW_PAGE;
+ let sawChange = false;
+ let handler = () => {
+ isnot(
+ gURLBar.untrimmedValue,
+ initialValue,
+ "Should not revert URL bar value!"
+ );
+ if (gURLBar.getAttribute("pageproxystate") == "valid") {
+ sawChange = true;
+ is(
+ gURLBar.untrimmedValue,
+ expectedURLBarChange,
+ "Should set expected URL bar value!"
+ );
+ }
+ };
+
+ let obs = new MutationObserver(handler);
+
+ obs.observe(gURLBar.textbox, { attributes: true });
+ gURLBar.value = SLOW_PAGE;
+ gURLBar.handleCommand();
+
+ // If this ever starts going intermittent, we've broken this.
+ await new Promise(resolve => setTimeout(resolve, 200));
+ expectedURLBarChange = SLOW_PAGE2;
+ let pageLoadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ gURLBar.value = expectedURLBarChange;
+ gURLBar.handleCommand();
+ is(
+ gURLBar.untrimmedValue,
+ expectedURLBarChange,
+ "Should not have changed URL bar value synchronously."
+ );
+ await pageLoadPromise;
+ ok(
+ sawChange,
+ "The URL bar change handler should have been called by the time the page was loaded"
+ );
+ obs.disconnect();
+ obs = null;
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * Check that if we:
+ * 1) middle-click a link to a separate page whose server doesn't respond
+ * 2) we switch to that tab and stop the request
+ *
+ * The URL bar continues to contain the URL of the page we wanted to visit.
+ */
+add_task(async function () {
+ let socket = Cc["@mozilla.org/network/server-socket;1"].createInstance(
+ Ci.nsIServerSocket
+ );
+ socket.init(-1, true, -1);
+ const PORT = socket.port;
+ registerCleanupFunction(() => {
+ socket.close();
+ });
+
+ const BASE_PAGE = TEST_BASE_URL + "dummy_page.html";
+ const SLOW_HOST = `https://localhost:${PORT}/`;
+ info("Using URLs: " + SLOW_HOST);
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_PAGE);
+ info("opened tab");
+ await SpecialPowers.spawn(tab.linkedBrowser, [SLOW_HOST], URL => {
+ let link = content.document.createElement("a");
+ link.href = URL;
+ link.textContent = "click me to open a slow page";
+ link.id = "clickme";
+ content.document.body.appendChild(link);
+ });
+ info("added link");
+ let newTabPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabOpen"
+ );
+ // Middle click the link:
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#clickme",
+ { button: 1 },
+ tab.linkedBrowser
+ );
+ // get new tab, switch to it
+ let newTab = (await newTabPromise).target;
+ await BrowserTestUtils.switchTab(gBrowser, newTab);
+ is(gURLBar.untrimmedValue, SLOW_HOST, "Should have slow page in URL bar");
+ let browserStoppedPromise = BrowserTestUtils.browserStopped(
+ newTab.linkedBrowser,
+ null,
+ true
+ );
+ BrowserStop();
+ await browserStoppedPromise;
+
+ is(
+ gURLBar.untrimmedValue,
+ SLOW_HOST,
+ "Should still have slow page in URL bar after stop"
+ );
+ BrowserTestUtils.removeTab(newTab);
+ BrowserTestUtils.removeTab(tab);
+});
+/**
+ * Check that if we:
+ * 1) middle-click a link to a separate page whose server doesn't respond
+ * 2) we alter the URL on that page to some other server that doesn't respond
+ * 3) we stop the request
+ *
+ * The URL bar continues to contain the second URL.
+ */
+add_task(async function () {
+ let socket = Cc["@mozilla.org/network/server-socket;1"].createInstance(
+ Ci.nsIServerSocket
+ );
+ socket.init(-1, true, -1);
+ const PORT1 = socket.port;
+ let socket2 = Cc["@mozilla.org/network/server-socket;1"].createInstance(
+ Ci.nsIServerSocket
+ );
+ socket2.init(-1, true, -1);
+ const PORT2 = socket2.port;
+ registerCleanupFunction(() => {
+ socket.close();
+ socket2.close();
+ });
+
+ const BASE_PAGE = TEST_BASE_URL + "dummy_page.html";
+ const SLOW_HOST1 = `https://localhost:${PORT1}/`;
+ const SLOW_HOST2 = `https://localhost:${PORT2}/`;
+ info("Using URLs: " + SLOW_HOST1 + " and " + SLOW_HOST2);
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_PAGE);
+ info("opened tab");
+ await SpecialPowers.spawn(tab.linkedBrowser, [SLOW_HOST1], URL => {
+ let link = content.document.createElement("a");
+ link.href = URL;
+ link.textContent = "click me to open a slow page";
+ link.id = "clickme";
+ content.document.body.appendChild(link);
+ });
+ info("added link");
+ let newTabPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabOpen"
+ );
+ // Middle click the link:
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#clickme",
+ { button: 1 },
+ tab.linkedBrowser
+ );
+ // get new tab, switch to it
+ let newTab = (await newTabPromise).target;
+ await BrowserTestUtils.switchTab(gBrowser, newTab);
+ is(gURLBar.untrimmedValue, SLOW_HOST1, "Should have slow page in URL bar");
+ let browserStoppedPromise = BrowserTestUtils.browserStopped(
+ newTab.linkedBrowser,
+ null,
+ true
+ );
+ gURLBar.value = SLOW_HOST2;
+ gURLBar.handleCommand();
+ await browserStoppedPromise;
+
+ is(
+ gURLBar.untrimmedValue,
+ SLOW_HOST2,
+ "Should have second slow page in URL bar"
+ );
+ browserStoppedPromise = BrowserTestUtils.browserStopped(
+ newTab.linkedBrowser,
+ null,
+ true
+ );
+ BrowserStop();
+ await browserStoppedPromise;
+
+ is(
+ gURLBar.untrimmedValue,
+ SLOW_HOST2,
+ "Should still have second slow page in URL bar after stop"
+ );
+ BrowserTestUtils.removeTab(newTab);
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * 1) Try to load page 0 and wait for it to finish loading.
+ * 2) Try to load page 1 and wait for it to finish loading.
+ * 3) Try to load SLOW_PAGE, and then before it finishes loading, navigate back.
+ * - We should be taken to page 0.
+ */
+add_task(async function testCorrectUrlBarAfterGoingBackDuringAnotherLoad() {
+ // Load example.org
+ let page0 = "http://example.org/";
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ page0,
+ true,
+ true
+ );
+
+ // Load example.com in the same browser
+ let page1 = "http://example.com/";
+ let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, page1);
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, page1);
+ await loaded;
+
+ let initialValue = gURLBar.untrimmedValue;
+ let expectedURLBarChange = SLOW_PAGE;
+ let sawChange = false;
+ let goneBack = false;
+ let handler = () => {
+ if (!goneBack) {
+ isnot(
+ gURLBar.untrimmedValue,
+ initialValue,
+ `Should not revert URL bar value to ${initialValue}`
+ );
+ }
+
+ if (gURLBar.getAttribute("pageproxystate") == "valid") {
+ sawChange = true;
+ is(
+ gURLBar.untrimmedValue,
+ expectedURLBarChange,
+ `Should set expected URL bar value - ${expectedURLBarChange}`
+ );
+ }
+ };
+
+ let obs = new MutationObserver(handler);
+
+ obs.observe(gURLBar.textbox, { attributes: true });
+ // Set the value of url bar to SLOW_PAGE
+ gURLBar.value = SLOW_PAGE;
+ gURLBar.handleCommand();
+
+ // Copied from the first test case:
+ // If this ever starts going intermittent, we've broken this.
+ await new Promise(resolve => setTimeout(resolve, 200));
+
+ expectedURLBarChange = page0;
+ let pageLoadPromise = BrowserTestUtils.browserStopped(
+ tab.linkedBrowser,
+ page0
+ );
+
+ // Wait until we can go back
+ await TestUtils.waitForCondition(() => tab.linkedBrowser.canGoBack);
+ ok(tab.linkedBrowser.canGoBack, "can go back");
+
+ // Navigate back from SLOW_PAGE. We should be taken to page 0 now.
+ tab.linkedBrowser.goBack();
+ goneBack = true;
+ is(
+ gURLBar.untrimmedValue,
+ SLOW_PAGE,
+ "Should not have changed URL bar value synchronously."
+ );
+ // Wait until page 0 have finished loading.
+ await pageLoadPromise;
+ is(
+ gURLBar.untrimmedValue,
+ page0,
+ "Should not have changed URL bar value synchronously."
+ );
+ ok(
+ sawChange,
+ "The URL bar change handler should have been called by the time the page was loaded"
+ );
+ obs.disconnect();
+ obs = null;
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * 1) Try to load page 1 and wait for it to finish loading.
+ * 2) Start loading SLOW_PAGE (it won't finish loading)
+ * 3) Reload the page. We should have loaded page 1 now.
+ */
+add_task(async function testCorrectUrlBarAfterReloadingDuringSlowPageLoad() {
+ // Load page 1 - example.com
+ let page1 = "http://example.com/";
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ page1,
+ true,
+ true
+ );
+
+ let initialValue = gURLBar.untrimmedValue;
+ let expectedURLBarChange = SLOW_PAGE;
+ let sawChange = false;
+ let hasReloaded = false;
+ let handler = () => {
+ if (!hasReloaded) {
+ isnot(
+ gURLBar.untrimmedValue,
+ initialValue,
+ "Should not revert URL bar value!"
+ );
+ }
+ if (gURLBar.getAttribute("pageproxystate") == "valid") {
+ sawChange = true;
+ is(
+ gURLBar.untrimmedValue,
+ expectedURLBarChange,
+ "Should set expected URL bar value!"
+ );
+ }
+ };
+
+ let obs = new MutationObserver(handler);
+
+ obs.observe(gURLBar.textbox, { attributes: true });
+ // Start loading SLOW_PAGE
+ gURLBar.value = SLOW_PAGE;
+ gURLBar.handleCommand();
+
+ // Copied from the first test: If this ever starts going intermittent,
+ // we've broken this.
+ await new Promise(resolve => setTimeout(resolve, 200));
+
+ expectedURLBarChange = page1;
+ let pageLoadPromise = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ page1
+ );
+ // Reload the page
+ tab.linkedBrowser.reload();
+ hasReloaded = true;
+ is(
+ gURLBar.untrimmedValue,
+ SLOW_PAGE,
+ "Should not have changed URL bar value synchronously."
+ );
+ // Wait for page1 to be loaded due to a reload while the slow page was still loading
+ await pageLoadPromise;
+ ok(
+ sawChange,
+ "The URL bar change handler should have been called by the time the page was loaded"
+ );
+ obs.disconnect();
+ obs = null;
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * 1) Try to load example.com and wait for it to finish loading.
+ * 2) Start loading SLOW_PAGE and then stop the load before the load completes
+ * 3) Check that example.com has been loaded as a result of stopping SLOW_PAGE
+ * load.
+ */
+add_task(async function testCorrectUrlBarAfterStoppingTheLoad() {
+ // Load page 1
+ let page1 = "http://example.com/";
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ page1,
+ true,
+ true
+ );
+
+ let initialValue = gURLBar.untrimmedValue;
+ let expectedURLBarChange = SLOW_PAGE;
+ let sawChange = false;
+ let hasStopped = false;
+ let handler = () => {
+ if (!hasStopped) {
+ isnot(
+ gURLBar.untrimmedValue,
+ initialValue,
+ "Should not revert URL bar value!"
+ );
+ }
+ if (gURLBar.getAttribute("pageproxystate") == "valid") {
+ sawChange = true;
+ is(
+ gURLBar.untrimmedValue,
+ expectedURLBarChange,
+ "Should set expected URL bar value!"
+ );
+ }
+ };
+
+ let obs = new MutationObserver(handler);
+
+ obs.observe(gURLBar.textbox, { attributes: true });
+ // Start loading SLOW_PAGE
+ gURLBar.value = SLOW_PAGE;
+ gURLBar.handleCommand();
+
+ // Copied from the first test case:
+ // If this ever starts going intermittent, we've broken this.
+ await new Promise(resolve => setTimeout(resolve, 200));
+
+ // We expect page 1 to be loaded after the SLOW_PAGE load is stopped.
+ expectedURLBarChange = page1;
+ let pageLoadPromise = BrowserTestUtils.browserStopped(
+ tab.linkedBrowser,
+ SLOW_PAGE,
+ true
+ );
+ // Stop the SLOW_PAGE load
+ tab.linkedBrowser.stop();
+ hasStopped = true;
+ is(
+ gURLBar.untrimmedValue,
+ SLOW_PAGE,
+ "Should not have changed URL bar value synchronously."
+ );
+ // Wait for SLOW_PAGE load to stop
+ await pageLoadPromise;
+
+ ok(
+ sawChange,
+ "The URL bar change handler should have been called by the time the page was loaded"
+ );
+ obs.disconnect();
+ obs = null;
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_strip_on_share.js b/browser/components/urlbar/tests/browser/browser_strip_on_share.js
new file mode 100644
index 0000000000..39a3e23aa7
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_strip_on_share.js
@@ -0,0 +1,125 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let listService;
+
+// Tests for the strip on share functionality of the urlbar.
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.query_stripping.strip_list", "stripParam"],
+ ["privacy.query_stripping.enabled", false],
+ ],
+ });
+
+ // Get the list service so we can wait for it to be fully initialized before running tests.
+ listService = Cc["@mozilla.org/query-stripping-list-service;1"].getService(
+ Ci.nsIURLQueryStrippingListService
+ );
+
+ await listService.testWaitForInit();
+});
+
+// Menu item should be visible, the whole url is copied without a selection, url should be stripped.
+add_task(async function testQueryParamIsStripped() {
+ await testMenuItemEnabled(false);
+});
+
+// Menu item should be visible, selecting the whole url, url should be stripped.
+add_task(async function testQueryParamIsStrippedSelectURL() {
+ await testMenuItemEnabled(true);
+});
+
+// We cannot strip anything, menu item should be hidden
+add_task(async function testUnknownQueryParam() {
+ await testMenuItemDisabled(
+ "https://www.example.com/?noStripParam=1234",
+ true,
+ false
+ );
+});
+
+// Selection is not a valid URI, menu item should be hidden
+add_task(async function testInvalidURI() {
+ await testMenuItemDisabled(
+ "https://www.example.com/?stripParam=1234",
+ true,
+ true
+ );
+});
+
+// Pref is not enabled, menu item should be hidden
+add_task(async function testPrefDisabled() {
+ await testMenuItemDisabled(
+ "https://www.example.com/?stripParam=1234",
+ false,
+ false
+ );
+});
+
+/**
+ * Opens a new tab, opens the ulr bar context menu and checks that the strip-on-share menu item is not visible
+ *
+ * @param {string} url - The url to be loaded
+ * @param {boolean} prefEnabled - Whether privacy.query_stripping.strip_on_share.enabled should be enabled for the test
+ * @param {boolean} selection - True: The whole url will be selected, false: Only part of the url will be selected
+ */
+async function testMenuItemDisabled(url, prefEnabled, selection) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.query_stripping.strip_on_share.enabled", prefEnabled]],
+ });
+ await BrowserTestUtils.withNewTab(url, async function (browser) {
+ gURLBar.focus();
+ if (selection) {
+ //select only part of the url
+ gURLBar.selectionStart = url.indexOf("example");
+ gURLBar.selectionEnd = url.indexOf("4");
+ }
+ let menuitem = await promiseContextualMenuitem("strip-on-share");
+ Assert.ok(
+ !BrowserTestUtils.is_visible(menuitem),
+ "Menu item is not visible"
+ );
+ let hidePromise = BrowserTestUtils.waitForEvent(
+ menuitem.parentElement,
+ "popuphidden"
+ );
+ menuitem.parentElement.hidePopup();
+ await hidePromise;
+ });
+}
+
+/**
+ * Opens a new tab, opens the url bar context menu and checks that the strip-on-share menu item is visible.
+ * Checks that the stripped version of the url is copied to the clipboard.
+ *
+ * @param {boolean} selectWholeUrl - Whether the whole url should be explicitely selected
+ */
+async function testMenuItemEnabled(selectWholeUrl) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.query_stripping.strip_on_share.enabled", true]],
+ });
+ let validUrl = "https://www.example.com/?stripParam=1234";
+ let strippedUrl = "https://www.example.com/";
+ await BrowserTestUtils.withNewTab(validUrl, async function (browser) {
+ gURLBar.focus();
+ if (selectWholeUrl) {
+ //select the whole url
+ gURLBar.select();
+ }
+ let menuitem = await promiseContextualMenuitem("strip-on-share");
+ Assert.ok(BrowserTestUtils.is_visible(menuitem), "Menu item is visible");
+ let hidePromise = BrowserTestUtils.waitForEvent(
+ menuitem.parentElement,
+ "popuphidden"
+ );
+ // Make sure the clean copy of the link will be copied to the clipboard
+ await SimpleTest.promiseClipboardChange(strippedUrl, () => {
+ menuitem.closest("menupopup").activateItem(menuitem);
+ });
+ await hidePromise;
+ });
+}
diff --git a/browser/components/urlbar/tests/browser/browser_suggestedIndex.js b/browser/components/urlbar/tests/browser/browser_suggestedIndex.js
new file mode 100644
index 0000000000..563202036a
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_suggestedIndex.js
@@ -0,0 +1,120 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that results with a suggestedIndex property end up in the expected
+// position.
+
+add_task(async function suggestedIndex() {
+ let result1 = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/1" }
+ );
+ result1.suggestedIndex = 2;
+ let result2 = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/2" }
+ );
+ result2.suggestedIndex = 6;
+
+ let provider = new UrlbarTestUtils.TestProvider({
+ results: [result1, result2],
+ });
+ UrlbarProvidersManager.registerProvider(provider);
+ async function clean() {
+ UrlbarProvidersManager.unregisterProvider(provider);
+ await PlacesUtils.history.clear();
+ }
+ registerCleanupFunction(clean);
+
+ let urls = [];
+ let maxResults = UrlbarPrefs.get("maxRichResults");
+ // Add more results, so that the sum of these results plus the above ones,
+ // will be greater than maxResults.
+ for (let i = 0; i < maxResults; ++i) {
+ urls.push("http://example.com/foo" + i);
+ }
+ await PlacesTestUtils.addVisits(urls);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ });
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ maxResults,
+ `There should be ${maxResults} results in the view.`
+ );
+
+ urls.reverse();
+ urls.unshift(
+ (await Services.search.getDefault()).getSubmission("foo").uri.spec
+ );
+ urls.splice(result1.suggestedIndex, 0, result1.payload.url);
+ urls.splice(result2.suggestedIndex, 0, result2.payload.url);
+ urls = urls.slice(0, maxResults);
+
+ let expected = [];
+ for (let i = 0; i < maxResults; ++i) {
+ let url = (await UrlbarTestUtils.getDetailsOfResultAt(window, i)).url;
+ expected.push(url);
+ }
+ // Check all the results.
+ Assert.deepEqual(expected, urls);
+
+ await clean();
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+add_task(async function suggestedIndex_append() {
+ // When suggestedIndex is greater than the number of results the result is
+ // appended.
+ let result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/append/" }
+ );
+ result.suggestedIndex = 4;
+
+ let provider = new UrlbarTestUtils.TestProvider({ results: [result] });
+ UrlbarProvidersManager.registerProvider(provider);
+ async function clean() {
+ UrlbarProvidersManager.unregisterProvider(provider);
+ await PlacesUtils.history.clear();
+ }
+ registerCleanupFunction(clean);
+
+ await PlacesTestUtils.addVisits("http://example.com/bar");
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "bar",
+ });
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 3,
+ `There should be 3 results in the view.`
+ );
+
+ let urls = [
+ (await Services.search.getDefault()).getSubmission("bar").uri.spec,
+ "http://example.com/bar",
+ "http://mozilla.org/append/",
+ ];
+
+ let expected = [];
+ for (let i = 0; i < 3; ++i) {
+ let url = (await UrlbarTestUtils.getDetailsOfResultAt(window, i)).url;
+ expected.push(url);
+ }
+ // Check all the results.
+ Assert.deepEqual(expected, urls);
+
+ await clean();
+ await UrlbarTestUtils.promisePopupClose(window);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_suppressFocusBorder.js b/browser/components/urlbar/tests/browser/browser_suppressFocusBorder.js
new file mode 100644
index 0000000000..c8d84e5c4c
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_suppressFocusBorder.js
@@ -0,0 +1,391 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the suppress-focus-border attribute is applied to the Urlbar
+ * correctly. Its purpose is to hide the focus border after the panel is closed.
+ * It also ensures we don't flash the border at the user after they click the
+ * Urlbar but before we decide we're opening the view.
+ */
+
+let TEST_RESULT = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/" }
+);
+
+/**
+ * A test provider that awaits a promise before returning results.
+ */
+class AwaitPromiseProvider extends UrlbarTestUtils.TestProvider {
+ /**
+ * @param {object} args
+ * The constructor arguments for UrlbarTestUtils.TestProvider.
+ * @param {Promise} promise
+ * The promise that will be awaited before returning results.
+ */
+ constructor(args, promise) {
+ super(args);
+ this._promise = promise;
+ }
+
+ async startQuery(context, add) {
+ await this._promise;
+ for (let result of this._results) {
+ add(this, result);
+ }
+ }
+}
+
+add_setup(async function () {
+ await SearchTestUtils.installSearchExtension({}, { setAsDefault: true });
+
+ registerCleanupFunction(function () {
+ SpecialPowers.clipboardCopyString("");
+ });
+});
+
+add_task(async function afterMousedown_topSites() {
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+ win.gURLBar.blur();
+
+ await withAwaitProvider(
+ { results: [TEST_RESULT], priority: Infinity },
+ getSuppressFocusPromise(win),
+ async () => {
+ Assert.ok(
+ !win.gURLBar.hasAttribute("suppress-focus-border"),
+ "Sanity check: the Urlbar does not have the supress-focus-border attribute."
+ );
+
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ if (win.gURLBar.getAttribute("pageproxystate") == "invalid") {
+ gURLBar.handleRevert();
+ }
+ EventUtils.synthesizeMouseAtCenter(win.gURLBar.inputField, {}, win);
+ });
+
+ let result = await UrlbarTestUtils.waitForAutocompleteResultAt(win, 0);
+ Assert.ok(
+ result,
+ "The provider returned a result after waiting for the suppress-focus-border attribute."
+ );
+
+ await UrlbarTestUtils.promisePopupClose(win);
+ Assert.ok(
+ !gURLBar.hasAttribute("suppress-focus-border"),
+ "The Urlbar no longer has the supress-focus-border attribute after close."
+ );
+ }
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function openLocation_topSites() {
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+
+ await withAwaitProvider(
+ { results: [TEST_RESULT], priority: Infinity },
+ getSuppressFocusPromise(win),
+ async () => {
+ Assert.ok(
+ !win.gURLBar.hasAttribute("suppress-focus-border"),
+ "Sanity check: the Urlbar does not have the supress-focus-border attribute."
+ );
+
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ EventUtils.synthesizeKey("l", { accelKey: true }, win);
+ });
+
+ let result = await UrlbarTestUtils.waitForAutocompleteResultAt(win, 0);
+ Assert.ok(
+ result,
+ "The provider returned a result after waiting for the suppress-focus-border attribute."
+ );
+
+ await UrlbarTestUtils.promisePopupClose(win);
+ Assert.ok(
+ !win.gURLBar.hasAttribute("suppress-focus-border"),
+ "The Urlbar no longer has the supress-focus-border attribute after close."
+ );
+ }
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+// Tests that the address bar loses the suppress-focus-border attribute if no
+// results are returned by a query. This simulates the user disabling Top Sites
+// then clicking the address bar.
+add_task(async function afterMousedown_noTopSites() {
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+
+ await withAwaitProvider(
+ // Note that the provider returns no results.
+ { results: [], priority: Infinity },
+ getSuppressFocusPromise(win),
+ async () => {
+ Assert.ok(
+ !win.gURLBar.hasAttribute("suppress-focus-border"),
+ "Sanity check: the Urlbar does not have the supress-focus-border attribute."
+ );
+
+ EventUtils.synthesizeMouseAtCenter(win.gURLBar.inputField, {}, win);
+ // Because the panel opening may not be immediate, we must wait a bit.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 500));
+ Assert.ok(!UrlbarTestUtils.isPopupOpen(win), "The popup is not open.");
+
+ Assert.ok(
+ !win.gURLBar.hasAttribute("suppress-focus-border"),
+ "The Urlbar no longer has the supress-focus-border attribute."
+ );
+ }
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+// Tests that we show the focus border when new tabs are opened.
+add_task(async function newTab() {
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+
+ // Tabs opened with withNewTab don't focus the Urlbar, so we have to open one
+ // manually.
+ let tab = await openAboutNewTab(win);
+ await BrowserTestUtils.waitForCondition(
+ () => win.gURLBar.hasAttribute("focused"),
+ "Waiting for the Urlbar to become focused."
+ );
+ Assert.ok(
+ !win.gURLBar.hasAttribute(
+ "suppress-focus-border",
+ "The Urlbar does not have the suppress-focus-border attribute."
+ )
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ await BrowserTestUtils.closeWindow(win);
+});
+
+// Tests that we show the focus border when a new tab is opened and the address
+// bar panel is already open.
+add_task(async function newTab_alreadyOpen() {
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+
+ await withAwaitProvider(
+ { results: [TEST_RESULT], priority: Infinity },
+ getSuppressFocusPromise(win),
+ async () => {
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ EventUtils.synthesizeKey("l", { accelKey: true }, win);
+ });
+
+ let tab = await openAboutNewTab(win);
+ await BrowserTestUtils.waitForCondition(
+ () => !UrlbarTestUtils.isPopupOpen(win),
+ "Waiting for the Urlbar panel to close."
+ );
+ Assert.ok(
+ !win.gURLBar.hasAttribute(
+ "suppress-focus-border",
+ "The Urlbar does not have the suppress-focus-border attribute."
+ )
+ );
+ BrowserTestUtils.removeTab(tab);
+ }
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function searchTip() {
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+
+ info("Set a pref to show a search tip button.");
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.searchTips.test.ignoreShowLimits", true]],
+ });
+
+ info("Open new tab.");
+ const tab = await openAboutNewTab(win);
+
+ info("Click the tip button.");
+ const result = await UrlbarTestUtils.getDetailsOfResultAt(win, 0);
+ const button = result.element.row._buttons.get("0");
+ await UrlbarTestUtils.promisePopupClose(win, () => {
+ EventUtils.synthesizeMouseAtCenter(button, {}, win);
+ });
+
+ Assert.ok(
+ !win.gURLBar.hasAttribute(
+ "suppress-focus-border",
+ "The Urlbar does not have the suppress-focus-border attribute."
+ )
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ await BrowserTestUtils.closeWindow(win);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function interactionOnNewTab() {
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+
+ info("Open about:newtab in new tab");
+ const tab = await openAboutNewTab(win);
+ await BrowserTestUtils.waitForCondition(
+ () => win.gBrowser.selectedTab === tab
+ );
+
+ await testInteractionsOnAboutNewTab(win);
+
+ BrowserTestUtils.removeTab(tab);
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function interactionOnNewTabInPrivateWindow() {
+ const win = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ waitForTabURL: "about:privatebrowsing",
+ });
+ await testInteractionsOnAboutNewTab(win);
+ await BrowserTestUtils.closeWindow(win);
+ await SimpleTest.promiseFocus(window);
+});
+
+add_task(async function clickOnEdgeOfURLBar() {
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+ win.gURLBar.blur();
+
+ Assert.ok(
+ !win.gURLBar.hasAttribute("suppress-focus-border"),
+ "URLBar does not have suppress-focus-border attribute"
+ );
+
+ const onHiddenFocusRemoved = BrowserTestUtils.waitForCondition(
+ () => !win.gURLBar._hideFocus
+ );
+
+ const container = win.document.getElementById("urlbar-input-container");
+ container.click();
+
+ await onHiddenFocusRemoved;
+ Assert.ok(
+ win.gURLBar.hasAttribute("suppress-focus-border"),
+ "suppress-focus-border is set from the beginning"
+ );
+
+ await UrlbarTestUtils.promisePopupClose(win.window);
+ await BrowserTestUtils.closeWindow(win);
+});
+
+async function testInteractionsOnAboutNewTab(win) {
+ info("Test for clicking on URLBar while showing about:newtab");
+ await testInteractionFeature(() => {
+ info("Click on URLBar");
+ EventUtils.synthesizeMouseAtCenter(win.gURLBar.inputField, {}, win);
+ }, win);
+
+ info("Test for typing on .fake-editable while showing about:newtab");
+ await testInteractionFeature(() => {
+ info("Type a character on .fake-editable");
+ EventUtils.synthesizeKey("v", {}, win);
+ }, win);
+ Assert.equal(win.gURLBar.value, "v", "URLBar value is correct");
+
+ info("Test for typing on .fake-editable while showing about:newtab");
+ await testInteractionFeature(() => {
+ info("Paste some words on .fake-editable");
+ SpecialPowers.clipboardCopyString("paste test");
+ win.document.commandDispatcher
+ .getControllerForCommand("cmd_paste")
+ .doCommand("cmd_paste");
+ SpecialPowers.clipboardCopyString("");
+ }, win);
+ Assert.equal(win.gURLBar.value, "paste test", "URLBar value is correct");
+}
+
+async function testInteractionFeature(interaction, win) {
+ info("Focus on URLBar");
+ win.gURLBar.value = "";
+ win.gURLBar.focus();
+ Assert.ok(
+ !win.gURLBar.hasAttribute("suppress-focus-border"),
+ "URLBar does not have suppress-focus-border attribute"
+ );
+
+ info("Click on search-handoff-button in newtab page");
+ await ContentTask.spawn(win.gBrowser.selectedBrowser, null, async () => {
+ await ContentTaskUtils.waitForCondition(() =>
+ content.document.querySelector(".search-handoff-button")
+ );
+ content.document.querySelector(".search-handoff-button").click();
+ });
+
+ await BrowserTestUtils.waitForCondition(
+ () => win.gURLBar._hideFocus,
+ "Wait until _hideFocus will be true"
+ );
+
+ const onHiddenFocusRemoved = BrowserTestUtils.waitForCondition(
+ () => !win.gURLBar._hideFocus
+ );
+
+ await interaction();
+
+ await onHiddenFocusRemoved;
+ Assert.ok(
+ win.gURLBar.hasAttribute("suppress-focus-border"),
+ "suppress-focus-border is set from the beginning"
+ );
+
+ const result = await UrlbarTestUtils.waitForAutocompleteResultAt(win, 0);
+ Assert.ok(result, "The provider returned a result");
+ await UrlbarTestUtils.promisePopupClose(win);
+}
+
+function getSuppressFocusPromise(win = window) {
+ return new Promise(resolve => {
+ let observer = new MutationObserver(() => {
+ if (
+ win.gURLBar.hasAttribute("suppress-focus-border") &&
+ !UrlbarTestUtils.isPopupOpen(win)
+ ) {
+ resolve();
+ observer.disconnect();
+ }
+ });
+ observer.observe(win.gURLBar.textbox, {
+ attributes: true,
+ attributeFilter: ["suppress-focus-border"],
+ });
+ });
+}
+
+async function withAwaitProvider(args, promise, callback) {
+ let provider = new AwaitPromiseProvider(args, promise);
+ UrlbarProvidersManager.registerProvider(provider);
+ try {
+ await callback();
+ } catch (ex) {
+ console.error(ex);
+ } finally {
+ UrlbarProvidersManager.unregisterProvider(provider);
+ }
+}
+
+async function openAboutNewTab(win = window) {
+ // We have to listen for the new tab using this brute force method.
+ // about:newtab is preloaded in the background. When about:newtab is opened,
+ // the cached version is shown. Since the page is already loaded,
+ // waitForNewTab does not detect it. It also doesn't fire the TabOpen event.
+ const tabCount = win.gBrowser.tabs.length;
+ EventUtils.synthesizeKey("t", { accelKey: true }, win);
+ await TestUtils.waitForCondition(
+ () => win.gBrowser.tabs.length === tabCount + 1,
+ "Waiting for background about:newtab to open."
+ );
+ return win.gBrowser.tabs[win.gBrowser.tabs.length - 1];
+}
diff --git a/browser/components/urlbar/tests/browser/browser_switchTab_closesUrlbarPopup.js b/browser/components/urlbar/tests/browser/browser_switchTab_closesUrlbarPopup.js
new file mode 100644
index 0000000000..a9b0eb7b1a
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_switchTab_closesUrlbarPopup.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Checks that switching tabs closes the urlbar popup.
+ */
+
+"use strict";
+
+add_task(async function () {
+ let tab1 = BrowserTestUtils.addTab(gBrowser);
+ let tab2 = BrowserTestUtils.addTab(gBrowser);
+
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+ });
+
+ // Add a couple of dummy entries to ensure the history popup will open.
+ await PlacesTestUtils.addVisits([
+ { uri: makeURI("http://example.com/foo") },
+ { uri: makeURI("http://example.com/foo/bar") },
+ ]);
+
+ // When urlbar in a new tab is focused, and a tab switch occurs,
+ // the urlbar popup should be closed
+ await BrowserTestUtils.switchTab(gBrowser, tab2);
+ gURLBar.focus(); // focus the urlbar in the tab we will switch to
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ // Now open the popup.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ // Check that the popup closes when we switch tab.
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ return BrowserTestUtils.switchTab(gBrowser, tab2);
+ });
+ Assert.ok(true, "Popup was successfully closed");
+});
diff --git a/browser/components/urlbar/tests/browser/browser_switchTab_currentTab.js b/browser/components/urlbar/tests/browser/browser_switchTab_currentTab.js
new file mode 100644
index 0000000000..eccee800e3
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_switchTab_currentTab.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This test ensures that switch to tab still works when the URI contains an
+ * encoded part.
+ */
+
+"use strict";
+
+add_task(async function test_switchTab_currentTab() {
+ registerCleanupFunction(PlacesUtils.history.clear);
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:robots#1" },
+ async () => {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:robots#2" },
+ async () => {
+ let context = await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "robot",
+ });
+ Assert.ok(
+ context.results.some(
+ result =>
+ result.type == UrlbarUtils.RESULT_TYPE.TAB_SWITCH &&
+ result.payload.url == "about:robots#1"
+ )
+ );
+ Assert.ok(
+ !context.results.some(
+ result =>
+ result.type == UrlbarUtils.RESULT_TYPE.TAB_SWITCH &&
+ result.payload.url == "about:robots#2"
+ )
+ );
+ }
+ );
+ }
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_switchTab_decodeuri.js b/browser/components/urlbar/tests/browser/browser_switchTab_decodeuri.js
new file mode 100644
index 0000000000..fe23eceaf9
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_switchTab_decodeuri.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This test ensures that switch to tab still works when the URI contains an
+ * encoded part.
+ */
+
+"use strict";
+
+const TEST_URL = `${TEST_BASE_URL}dummy_page.html#test%7C1`;
+
+add_task(async function test_switchtab_decodeuri() {
+ info("Opening first tab");
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+ info("Opening and selecting second tab");
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+
+ info("Wait for autocomplete");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "dummy_page",
+ });
+
+ info("Select autocomplete popup entry");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ UrlbarTestUtils.getSelectedRowIndex(window)
+ );
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.TAB_SWITCH);
+
+ info("switch-to-tab");
+ let tabSelectPromise = BrowserTestUtils.waitForEvent(
+ window,
+ "TabSelect",
+ false
+ );
+ EventUtils.synthesizeKey("KEY_Enter");
+ await tabSelectPromise;
+
+ Assert.equal(
+ gBrowser.selectedTab,
+ tab,
+ "Should have switched to the right tab"
+ );
+
+ gBrowser.removeCurrentTab();
+ await PlacesUtils.history.clear();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_switchTab_inputHistory.js b/browser/components/urlbar/tests/browser/browser_switchTab_inputHistory.js
new file mode 100644
index 0000000000..0da3161d0e
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_switchTab_inputHistory.js
@@ -0,0 +1,91 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This tests ensures that the urlbar adaptive behavior updates
+ * when using switch to tab in the address bar dropdown.
+ */
+
+"use strict";
+
+add_setup(async function () {
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ });
+});
+
+add_task(async function test_adaptive_with_search_term_and_switch_tab() {
+ await PlacesUtils.history.clear();
+ let urls = [
+ "https://example.com/",
+ "https://example.com/#cat",
+ "https://example.com/#cake",
+ "https://example.com/#car",
+ ];
+
+ info(`Load tabs in same order as urls`);
+ let tabs = [];
+ for (let url of urls) {
+ let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, url, false, true);
+ gBrowser.loadTabs([url], {
+ inBackground: true,
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+
+ let tab = await tabPromise;
+ tabs.push(tab);
+ }
+
+ info(`Switch to tab 0`);
+ await BrowserTestUtils.switchTab(gBrowser, tabs[0]);
+
+ info("Wait for autocomplete");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "ca",
+ });
+
+ let result1 = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.notEqual(result1.url, urls[1], `${urls[1]} url should not be first`);
+
+ info(`Scroll down to select the ${urls[1]} entry using keyboard`);
+ let result2 = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ UrlbarTestUtils.getSelectedRowIndex(window)
+ );
+
+ while (result2.url != urls[1]) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ result2 = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ UrlbarTestUtils.getSelectedRowIndex(window)
+ );
+ }
+
+ Assert.equal(
+ result2.type,
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ "Selected entry should be tab switch"
+ );
+ Assert.equal(result2.url, urls[1]);
+
+ info("Visiting tab 1");
+ EventUtils.synthesizeKey("KEY_Enter");
+ Assert.equal(gBrowser.selectedTab, tabs[1], "Should have switched to tab 1");
+
+ info("Switch back to tab 0");
+ await BrowserTestUtils.switchTab(gBrowser, tabs[0]);
+
+ info("Wait for autocomplete");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "ca",
+ });
+
+ let result3 = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result3.url, urls[1], `${urls[1]} url should be first`);
+
+ for (let tab of tabs) {
+ BrowserTestUtils.removeTab(tab);
+ }
+});
diff --git a/browser/components/urlbar/tests/browser/browser_switchTab_override.js b/browser/components/urlbar/tests/browser/browser_switchTab_override.js
new file mode 100644
index 0000000000..dabce43612
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_switchTab_override.js
@@ -0,0 +1,100 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This test ensures that overriding switch-to-tab correctly loads the page
+ * rather than switching to it.
+ */
+
+"use strict";
+
+const TEST_URL = `${TEST_BASE_URL}dummy_page.html`;
+
+add_task(async function test_switchtab_override() {
+ info("Opening first tab");
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+ info("Opening and selecting second tab");
+ let secondTab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ registerCleanupFunction(() => {
+ try {
+ gBrowser.removeTab(tab);
+ gBrowser.removeTab(secondTab);
+ } catch (ex) {
+ /* tabs may have already been closed in case of failure */
+ }
+ });
+
+ info("Wait for autocomplete");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "dummy_page",
+ });
+
+ info("Select second autocomplete popup entry");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ UrlbarTestUtils.getSelectedRowIndex(window)
+ );
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.TAB_SWITCH);
+
+ // Check to see if the switchtab label is visible and
+ // all other labels are hidden
+ const allLabels = document.getElementById("urlbar-label-box").children;
+ for (let label of allLabels) {
+ if (label.id == "urlbar-label-switchtab") {
+ Assert.ok(BrowserTestUtils.is_visible(label));
+ } else {
+ Assert.ok(BrowserTestUtils.is_hidden(label));
+ }
+ }
+
+ info("Override switch-to-tab");
+ let deferred = PromiseUtils.defer();
+ // In case of failure this would switch tab.
+ let onTabSelect = event => {
+ deferred.reject(new Error("Should have overridden switch to tab"));
+ };
+ gBrowser.tabContainer.addEventListener("TabSelect", onTabSelect);
+ registerCleanupFunction(() => {
+ gBrowser.tabContainer.removeEventListener("TabSelect", onTabSelect);
+ });
+ // Otherwise it would load the page.
+ BrowserTestUtils.browserLoaded(secondTab.linkedBrowser).then(
+ deferred.resolve
+ );
+
+ EventUtils.synthesizeKey("KEY_Shift", { type: "keydown" });
+
+ // Checks that all labels are hidden when Shift is held down on the SwitchToTab result
+ for (let label of allLabels) {
+ Assert.ok(BrowserTestUtils.is_hidden(label));
+ }
+
+ registerCleanupFunction(() => {
+ // Avoid confusing next tests by leaving a pending keydown.
+ EventUtils.synthesizeKey("KEY_Shift", { type: "keyup" });
+ });
+
+ let attribute = "actionoverride";
+ Assert.ok(
+ gURLBar.view.panel.hasAttribute(attribute),
+ "We should be overriding"
+ );
+
+ EventUtils.synthesizeKey("KEY_Enter");
+ info(`gURLBar.value = ${gURLBar.value}`);
+ await deferred.promise;
+
+ // Blurring the urlbar should have cleared the override.
+ Assert.ok(
+ !gURLBar.view.panel.hasAttribute(attribute),
+ "We should not be overriding anymore"
+ );
+
+ await PlacesUtils.history.clear();
+ gBrowser.removeTab(tab);
+ gBrowser.removeTab(secondTab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_switchToTabHavingURI_aOpenParams.js b/browser/components/urlbar/tests/browser/browser_switchToTabHavingURI_aOpenParams.js
new file mode 100644
index 0000000000..1a0d2eef70
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_switchToTabHavingURI_aOpenParams.js
@@ -0,0 +1,217 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+add_task(async function test_ignoreFragment() {
+ let tabRefAboutHome = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:home#1"
+ );
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla");
+ let numTabsAtStart = gBrowser.tabs.length;
+
+ switchTab("about:home#1", true);
+ switchTab("about:mozilla", true);
+
+ let hashChangePromise = ContentTask.spawn(
+ tabRefAboutHome.linkedBrowser,
+ [],
+ async function () {
+ await ContentTaskUtils.waitForEvent(this, "hashchange", true);
+ }
+ );
+ switchTab("about:home#2", true, {
+ ignoreFragment: "whenComparingAndReplace",
+ });
+ is(
+ tabRefAboutHome,
+ gBrowser.selectedTab,
+ "The same about:home tab should be switched to"
+ );
+ await hashChangePromise;
+ is(gBrowser.currentURI.ref, "2", "The ref should be updated to the new ref");
+ switchTab("about:mozilla", true);
+ switchTab("about:home#3", true, { ignoreFragment: "whenComparing" });
+ is(
+ tabRefAboutHome,
+ gBrowser.selectedTab,
+ "The same about:home tab should be switched to"
+ );
+ is(
+ gBrowser.currentURI.ref,
+ "2",
+ "The ref should be unchanged since the fragment is only ignored when comparing"
+ );
+ switchTab("about:mozilla", true);
+ switchTab("about:home#1", false);
+ isnot(
+ tabRefAboutHome,
+ gBrowser.selectedTab,
+ "Selected tab should not be initial about:blank tab"
+ );
+ is(
+ gBrowser.tabs.length,
+ numTabsAtStart + 1,
+ "Should have one new tab opened"
+ );
+ switchTab("about:mozilla", true);
+ switchTab("about:home", true, { ignoreFragment: "whenComparingAndReplace" });
+ await BrowserTestUtils.waitForCondition(function () {
+ return tabRefAboutHome.linkedBrowser.currentURI.spec == "about:home";
+ });
+ is(
+ tabRefAboutHome.linkedBrowser.currentURI.spec,
+ "about:home",
+ "about:home shouldn't have hash"
+ );
+ switchTab("about:about", false, {
+ ignoreFragment: "whenComparingAndReplace",
+ });
+ cleanupTestTabs();
+});
+
+add_task(async function test_ignoreQueryString() {
+ let tabRefAboutHome = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:home?hello=firefox"
+ );
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla");
+
+ switchTab("about:home?hello=firefox", true);
+ switchTab("about:home?hello=firefoxos", false);
+ // Remove the last opened tab to test ignoreQueryString option.
+ gBrowser.removeCurrentTab();
+ switchTab("about:home?hello=firefoxos", true, { ignoreQueryString: true });
+ is(
+ tabRefAboutHome,
+ gBrowser.selectedTab,
+ "Selected tab should be the initial about:home tab"
+ );
+ is(
+ gBrowser.currentURI.spec,
+ "about:home?hello=firefox",
+ "The spec should NOT be updated to the new query string"
+ );
+ cleanupTestTabs();
+});
+
+add_task(async function test_replaceQueryString() {
+ let tabRefAboutHome = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:home?hello=firefox"
+ );
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla");
+
+ switchTab("about:home", false);
+ switchTab("about:home?hello=firefox", true);
+ switchTab("about:home?hello=firefoxos", false);
+ // Remove the last opened tab to test replaceQueryString option.
+ gBrowser.removeCurrentTab();
+ switchTab("about:home?hello=firefoxos", true, { replaceQueryString: true });
+ is(
+ tabRefAboutHome,
+ gBrowser.selectedTab,
+ "Selected tab should be the initial about:home tab"
+ );
+ // Wait for the tab to load the new URI spec.
+ await BrowserTestUtils.browserLoaded(tabRefAboutHome.linkedBrowser);
+ is(
+ gBrowser.currentURI.spec,
+ "about:home?hello=firefoxos",
+ "The spec should be updated to the new spec"
+ );
+ cleanupTestTabs();
+});
+
+add_task(async function test_replaceQueryStringAndFragment() {
+ let tabRefAboutHome = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:home?hello=firefox#aaa"
+ );
+ let tabRefAboutMozilla = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:mozilla?hello=firefoxos#aaa"
+ );
+
+ switchTab("about:home", false);
+ gBrowser.removeCurrentTab();
+ switchTab("about:home?hello=firefox#aaa", true);
+ is(
+ tabRefAboutHome,
+ gBrowser.selectedTab,
+ "Selected tab should be the initial about:home tab"
+ );
+ switchTab("about:mozilla?hello=firefox#bbb", true, {
+ replaceQueryString: true,
+ ignoreFragment: "whenComparingAndReplace",
+ });
+ is(
+ tabRefAboutMozilla,
+ gBrowser.selectedTab,
+ "Selected tab should be the initial about:mozilla tab"
+ );
+ switchTab("about:home?hello=firefoxos#bbb", true, {
+ ignoreQueryString: true,
+ ignoreFragment: "whenComparingAndReplace",
+ });
+ is(
+ tabRefAboutHome,
+ gBrowser.selectedTab,
+ "Selected tab should be the initial about:home tab"
+ );
+ cleanupTestTabs();
+});
+
+add_task(async function test_ignoreQueryStringIgnoresFragment() {
+ let tabRefAboutHome = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:home?hello=firefox#aaa"
+ );
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:mozilla?hello=firefoxos#aaa"
+ );
+
+ switchTab("about:home?hello=firefox#bbb", false, { ignoreQueryString: true });
+ gBrowser.removeCurrentTab();
+ switchTab("about:home?hello=firefoxos#aaa", true, {
+ ignoreQueryString: true,
+ });
+ is(
+ tabRefAboutHome,
+ gBrowser.selectedTab,
+ "Selected tab should be the initial about:home tab"
+ );
+ cleanupTestTabs();
+});
+
+// Begin helpers
+
+function cleanupTestTabs() {
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+}
+
+function switchTab(aURI, aShouldFindExistingTab, aOpenParams = {}) {
+ // Build the description before switchToTabHavingURI deletes the object properties.
+ let msg =
+ `Should switch to existing ${aURI} tab if one existed, ` +
+ `${
+ aOpenParams.ignoreFragment ? "ignoring" : "including"
+ } fragment portion, `;
+ if (aOpenParams.replaceQueryString) {
+ msg += "replacing";
+ } else if (aOpenParams.ignoreQueryString) {
+ msg += "ignoring";
+ } else {
+ msg += "including";
+ }
+ msg += " query string.";
+ aOpenParams.triggeringPrincipal =
+ Services.scriptSecurityManager.getSystemPrincipal();
+ let tabFound = switchToTabHavingURI(aURI, true, aOpenParams);
+ is(tabFound, aShouldFindExistingTab, msg);
+}
+
+registerCleanupFunction(cleanupTestTabs);
diff --git a/browser/components/urlbar/tests/browser/browser_switchToTab_chiclet.js b/browser/components/urlbar/tests/browser/browser_switchToTab_chiclet.js
new file mode 100644
index 0000000000..6702ce340a
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_switchToTab_chiclet.js
@@ -0,0 +1,110 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test for chiclet upon switching tab mode.
+ */
+
+"use strict";
+
+const TEST_URL = `${TEST_BASE_URL}dummy_page.html`;
+
+add_task(async function test_with_oneoff_button() {
+ info("Loading test page into first tab");
+ await BrowserTestUtils.loadURIString(gBrowser, TEST_URL);
+
+ info("Opening a new tab");
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ info("Wait for autocomplete");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+
+ info("Enter Tabs mode");
+ await UrlbarTestUtils.enterSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.TABS,
+ });
+
+ info("Select first popup entry");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "dummy",
+ });
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ const result = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ UrlbarTestUtils.getSelectedRowIndex(window)
+ );
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.TAB_SWITCH);
+
+ info("Enter escape key");
+ EventUtils.synthesizeKey("KEY_Escape");
+
+ info("Check label visibility");
+ const searchModeTitle = document.getElementById(
+ "urlbar-search-mode-indicator-title"
+ );
+ const switchTabLabel = document.getElementById("urlbar-label-switchtab");
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ BrowserTestUtils.is_visible(searchModeTitle) &&
+ searchModeTitle.textContent === "Tabs",
+ "Waiting until the search mode title will be visible"
+ );
+ await BrowserTestUtils.waitForCondition(
+ () => BrowserTestUtils.is_hidden(switchTabLabel),
+ "Waiting until the switch tab label will be hidden"
+ );
+
+ await PlacesUtils.history.clear();
+ gBrowser.removeTab(tab);
+});
+
+add_task(async function test_with_keytype() {
+ info("Loading test page into first tab");
+ await BrowserTestUtils.loadURIString(gBrowser, TEST_URL);
+
+ info("Opening a new tab");
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ info("Enter Tabs mode with keytype");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "%",
+ });
+
+ info("Select second popup entry");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "dummy",
+ });
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ const result = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ UrlbarTestUtils.getSelectedRowIndex(window)
+ );
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.TAB_SWITCH);
+
+ info("Enter escape key");
+ EventUtils.synthesizeKey("KEY_Escape");
+
+ info("Check label visibility");
+ const searchModeTitle = document.getElementById(
+ "urlbar-search-mode-indicator-title"
+ );
+ const switchTabLabel = document.getElementById("urlbar-label-switchtab");
+ await BrowserTestUtils.waitForCondition(
+ () => BrowserTestUtils.is_hidden(searchModeTitle),
+ "Waiting until the search mode title will be hidden"
+ );
+ await BrowserTestUtils.waitForCondition(
+ () => BrowserTestUtils.is_visible(switchTabLabel),
+ "Waiting until the switch tab label will be visible"
+ );
+
+ await PlacesUtils.history.clear();
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_switchToTab_closes_newtab.js b/browser/components/urlbar/tests/browser/browser_switchToTab_closes_newtab.js
new file mode 100644
index 0000000000..5031491d7e
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_switchToTab_closes_newtab.js
@@ -0,0 +1,63 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This tests that switch to tab from a blank tab switches and then closes
+ * the blank tab.
+ */
+
+"use strict";
+
+add_task(async function test_switchToTab_closes() {
+ let testURL =
+ "http://example.org/browser/browser/components/urlbar/tests/browser/dummy_page.html";
+
+ // Open the base tab
+ let baseTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, testURL);
+
+ if (baseTab.linkedBrowser.currentURI.spec == "about:blank") {
+ return;
+ }
+
+ // Open a blank tab to start the test from.
+ let testTab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ // Functions for TabClose and TabSelect
+ let tabClosePromise = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabClose",
+ false,
+ event => {
+ return event.originalTarget == testTab;
+ }
+ );
+ let tabSelectPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabSelect",
+ false,
+ event => {
+ return event.originalTarget == baseTab;
+ }
+ );
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "dummy",
+ });
+
+ // The first result is the heuristic, the second will be the switch to tab.
+ let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1);
+ EventUtils.synthesizeMouseAtCenter(element, {}, window);
+
+ await Promise.all([tabSelectPromise, tabClosePromise]);
+
+ // Confirm that the selected tab is now the base tab
+ Assert.equal(
+ gBrowser.selectedTab,
+ baseTab,
+ "Should have switched to the correct tab"
+ );
+
+ gBrowser.removeTab(baseTab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_switchToTab_fullUrl_repeatedKeydown.js b/browser/components/urlbar/tests/browser/browser_switchToTab_fullUrl_repeatedKeydown.js
new file mode 100644
index 0000000000..8f80ac5841
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_switchToTab_fullUrl_repeatedKeydown.js
@@ -0,0 +1,60 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This tests that typing a url and picking a switch to tab actually switches
+ * to the right tab. Also tests repeated keydown/keyup events don't confuse
+ * override.
+ */
+
+"use strict";
+
+add_task(async function test_switchToTab_url() {
+ const TEST_URL = "https://example.org/browser/";
+
+ let baseTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+ let testTab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ // Functions for TabClose and TabSelect
+ let tabClosePromise = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabClose",
+ false,
+ event => event.target == testTab
+ );
+ let tabSelectPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabSelect",
+ false,
+ event => event.target == baseTab
+ );
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_URL,
+ fireInputEvent: true,
+ });
+ // The first result is the heuristic, the second will be the switch to tab.
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1);
+
+ // Simulate a long press, on some platforms (Windows) it can generate multiple
+ // keydown events.
+ EventUtils.synthesizeKey("VK_SHIFT", { type: "keydown", repeat: 3 });
+ EventUtils.synthesizeKey("VK_SHIFT", { type: "keyup" });
+
+ // Pick the switch to tab result.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_Enter");
+
+ await Promise.all([tabSelectPromise, tabClosePromise]);
+
+ // Confirm that the selected tab is now the base tab
+ Assert.equal(
+ gBrowser.selectedTab,
+ baseTab,
+ "Should have switched to the correct tab"
+ );
+
+ gBrowser.removeTab(baseTab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_tabKeyBehavior.js b/browser/components/urlbar/tests/browser/browser_tabKeyBehavior.js
new file mode 100644
index 0000000000..e326939581
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_tabKeyBehavior.js
@@ -0,0 +1,378 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test makes sure that the tab key properly adjusts the selection or moves
+// through toolbar items, depending on the urlbar state.
+// When the view is open, tab should go through results if the urlbar was
+// focused with the mouse, or has a typed string.
+
+"use strict";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.quickactions", false]],
+ });
+
+ for (let i = 0; i < UrlbarPrefs.get("maxRichResults"); i++) {
+ await PlacesTestUtils.addVisits("http://example.com/" + i);
+ }
+
+ registerCleanupFunction(PlacesUtils.history.clear);
+
+ CustomizableUI.addWidgetToArea("home-button", "nav-bar", 0);
+ CustomizableUI.addWidgetToArea("sidebar-button", "nav-bar");
+ registerCleanupFunction(() => {
+ CustomizableUI.removeWidgetFromArea("home-button");
+ CustomizableUI.removeWidgetFromArea("sidebar-button");
+ });
+});
+
+add_task(async function tabWithSearchString() {
+ info("Tab with a search string");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ fireInputEvent: true,
+ });
+ await expectTabThroughResults();
+ info("Reverse Tab with a search string");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ fireInputEvent: true,
+ });
+ await expectTabThroughResults({ reverse: true });
+});
+
+add_task(async function tabNoSearchString() {
+ info("Tab without a search string");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ fireInputEvent: true,
+ });
+ await expectTabThroughToolbar();
+ info("Reverse Tab without a search string");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ fireInputEvent: true,
+ });
+ await expectTabThroughToolbar({ reverse: true });
+});
+
+add_task(async function tabAfterBlur() {
+ info("Tab after closing the view");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+ await expectTabThroughToolbar();
+});
+
+add_task(async function tabNoSearchStringMouseFocus() {
+ info("Tab in a new blank tab after mouse focus");
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await expectTabThroughResults();
+ });
+ info("Tab in a loaded tab after mouse focus");
+ await BrowserTestUtils.withNewTab("example.com", async () => {
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await expectTabThroughResults();
+ });
+});
+
+add_task(async function tabNoSearchStringKeyboardFocus() {
+ info("Tab in a new blank tab after keyboard focus");
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeKey("l", { accelKey: true });
+ });
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await expectTabThroughToolbar();
+ });
+ info("Tab in a loaded tab after keyboard focus");
+ await BrowserTestUtils.withNewTab("example.com", async () => {
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeKey("l", { accelKey: true });
+ });
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await expectTabThroughToolbar();
+ });
+});
+
+add_task(async function tabRetainedResultMouseFocus() {
+ info("Tab after retained results with mouse focus");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await expectTabThroughResults();
+});
+
+add_task(async function tabRetainedResultsKeyboardFocus() {
+ info("Tab after retained results with keyboard focus");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeKey("l", { accelKey: true });
+ });
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await expectTabThroughResults();
+});
+
+add_task(async function tabRetainedResults() {
+ info("Tab with a search string after mouse focus.");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ await expectTabThroughResults();
+});
+
+add_task(async function tabSearchModePreview() {
+ info(
+ "Tab past a search mode preview keywordoffer after focusing with the keyboard."
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@",
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeKey("l", { accelKey: true });
+ });
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(
+ result.searchParams.keyword,
+ "The first result is a keyword offer."
+ );
+
+ // Sanity check: the Urlbar value is cleared when keywordoffer results are
+ // selected.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.ok(!gURLBar.value, "The Urlbar should have no value.");
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+
+ await expectTabThroughResults();
+
+ await UrlbarTestUtils.promisePopupClose(window, async () => {
+ gURLBar.blur();
+ // Verify that blur closes search mode preview.
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ });
+});
+
+add_task(async function tabTabToSearch() {
+ info("Tab past a tab-to-search result after focusing with the keyboard.");
+ await SearchTestUtils.installSearchExtension();
+
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits(["https://example.com/"]);
+ }
+
+ // Search for a tab-to-search result.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "exam",
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeKey("l", { accelKey: true });
+ });
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ let tabToSearchResult = (
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)
+ ).result;
+ Assert.equal(
+ tabToSearchResult.providerName,
+ "TabToSearch",
+ "The second result is a tab-to-search result."
+ );
+
+ await expectTabThroughResults();
+
+ await UrlbarTestUtils.promisePopupClose(window, async () => {
+ gURLBar.blur();
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ });
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function tabNoSearchStringSearchMode() {
+ info(
+ "Tab through the toolbar when refocusing a Urlbar in search mode with the keyboard."
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ fireInputEvent: true,
+ });
+ // Enter history search mode to avoid hitting the network.
+ await UrlbarTestUtils.enterSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeKey("l", { accelKey: true });
+ });
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ await expectTabThroughToolbar();
+
+ // We have to reopen the view to exit search mode.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+add_task(async function tabOnTopSites() {
+ info("Tab through the toolbar when focusing the Address Bar on top sites.");
+ for (let val of [true, false]) {
+ info(`Test with keyboard_navigation set to "${val}"`);
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.toolbars.keyboard_navigation", val]],
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ fireInputEvent: true,
+ });
+ Assert.ok(
+ UrlbarTestUtils.getResultCount(window) > 0,
+ "There should be some results"
+ );
+ Assert.deepEqual(
+ UrlbarTestUtils.getSelectedElement(window),
+ null,
+ "There should be no selection"
+ );
+
+ await expectTabThroughToolbar();
+ await UrlbarTestUtils.promisePopupClose(window);
+ await SpecialPowers.popPrefEnv();
+ }
+});
+
+async function expectTabThroughResults(options = { reverse: false }) {
+ let resultCount = UrlbarTestUtils.getResultCount(window);
+ Assert.ok(resultCount > 0, "There should be results");
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+
+ let initiallySelectedIndex = result.heuristic ? 0 : -1;
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ initiallySelectedIndex,
+ "Check the initial selection."
+ );
+
+ for (let i = initiallySelectedIndex + 1; i < resultCount; i++) {
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: options.reverse });
+ if (
+ UrlbarTestUtils.getButtonForResultIndex(
+ window,
+ "menu",
+ UrlbarTestUtils.getSelectedRowIndex(window)
+ )
+ ) {
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: options.reverse });
+ }
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ options.reverse ? resultCount - i : i
+ );
+ }
+
+ EventUtils.synthesizeKey("KEY_Tab");
+
+ if (!options.reverse) {
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ initiallySelectedIndex,
+ "Should be back at the initial selection."
+ );
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ gURLBar.blur();
+ });
+}
+
+async function expectTabThroughToolbar(options = { reverse: false }) {
+ if (gURLBar.getAttribute("pageproxystate") == "valid") {
+ Assert.equal(document.activeElement, gURLBar.inputField);
+ EventUtils.synthesizeKey("KEY_Tab");
+ Assert.notEqual(document.activeElement, gURLBar.inputField);
+ } else {
+ let focusPromise = waitForFocusOnNextFocusableElement(options.reverse);
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: options.reverse });
+ await focusPromise;
+ }
+ Assert.ok(!gURLBar.view.isOpen, "The urlbar view should be closed.");
+}
+
+async function waitForFocusOnNextFocusableElement(reverse = false) {
+ if (
+ !Services.prefs.getBoolPref("browser.toolbars.keyboard_navigation", true)
+ ) {
+ return BrowserTestUtils.waitForCondition(
+ () => document.activeElement == gBrowser.selectedBrowser
+ );
+ }
+ let urlbar = document.getElementById("urlbar-container");
+ let nextFocusableElement = reverse
+ ? urlbar.previousElementSibling
+ : urlbar.nextElementSibling;
+ while (
+ nextFocusableElement &&
+ (!nextFocusableElement.classList.contains("toolbarbutton-1") ||
+ nextFocusableElement.hasAttribute("hidden") ||
+ nextFocusableElement.hasAttribute("disabled") ||
+ BrowserTestUtils.is_hidden(nextFocusableElement))
+ ) {
+ nextFocusableElement = reverse
+ ? nextFocusableElement.previousElementSibling
+ : nextFocusableElement.nextElementSibling;
+ }
+ info(
+ `Next focusable element: ${nextFocusableElement.localName}.#${nextFocusableElement.id}`
+ );
+
+ Assert.ok(
+ nextFocusableElement.classList.contains("toolbarbutton-1"),
+ "We should have a reference to the next focusable element after the Urlbar."
+ );
+
+ return BrowserTestUtils.waitForCondition(
+ () => nextFocusableElement.tabIndex == -1
+ );
+}
diff --git a/browser/components/urlbar/tests/browser/browser_tabMatchesInAwesomebar.js b/browser/components/urlbar/tests/browser/browser_tabMatchesInAwesomebar.js
new file mode 100644
index 0000000000..e186681907
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_tabMatchesInAwesomebar.js
@@ -0,0 +1,224 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim:set ts=2 sw=2 sts=2 et:
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Tests for ensuring that the tab switch results correctly match what is
+ * currently available.
+ */
+
+requestLongerTimeout(2);
+
+const TEST_URL_BASES = [
+ `${TEST_BASE_URL}dummy_page.html#tabmatch`,
+ `${TEST_BASE_URL}moz.png#tabmatch`,
+];
+
+const RESTRICT_TOKEN_OPENPAGE = "%";
+
+var gTabCounter = 0;
+
+add_task(async function step_1() {
+ info("Running step 1");
+ let maxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults");
+ let promises = [];
+ for (let i = 0; i < maxResults - 1; i++) {
+ let tab = BrowserTestUtils.addTab(gBrowser);
+ promises.push(loadTab(tab, TEST_URL_BASES[0] + ++gTabCounter));
+ }
+
+ await Promise.all(promises);
+ await ensure_opentabs_match_db();
+});
+
+add_task(async function step_2() {
+ info("Running step 2");
+ gBrowser.selectTabAtIndex(1);
+ gBrowser.removeCurrentTab();
+ gBrowser.selectTabAtIndex(1);
+ gBrowser.removeCurrentTab();
+ gBrowser.selectTabAtIndex(0);
+
+ let promises = [];
+ for (let i = 1; i < gBrowser.tabs.length; i++) {
+ promises.push(loadTab(gBrowser.tabs[i], TEST_URL_BASES[1] + ++gTabCounter));
+ }
+
+ await Promise.all(promises);
+ await ensure_opentabs_match_db();
+});
+
+add_task(async function step_3() {
+ info("Running step 3");
+ let promises = [];
+ for (let i = 1; i < gBrowser.tabs.length; i++) {
+ promises.push(loadTab(gBrowser.tabs[i], TEST_URL_BASES[0] + gTabCounter));
+ }
+
+ await Promise.all(promises);
+ await ensure_opentabs_match_db();
+});
+
+add_task(async function step_4() {
+ info("Running step 4 - ensure we don't register subframes as open pages");
+ let tab = BrowserTestUtils.addTab(
+ gBrowser,
+ 'data:text/html,<body><iframe src=""></iframe></body>'
+ );
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ let iframe_loaded = ContentTaskUtils.waitForEvent(
+ content.document,
+ "load",
+ true
+ );
+ content.document.querySelector("iframe").src = "http://test2.example.org/";
+ await iframe_loaded;
+ });
+
+ await ensure_opentabs_match_db();
+});
+
+add_task(async function step_5() {
+ info("Running step 5 - remove tab immediately");
+ let tab = BrowserTestUtils.addTab(gBrowser, "about:logo");
+ BrowserTestUtils.removeTab(tab);
+ await ensure_opentabs_match_db();
+});
+
+add_task(async function step_6() {
+ info(
+ "Running step 6 - check swapBrowsersAndCloseOther preserves registered switch-to-tab result"
+ );
+ let tabToKeep = BrowserTestUtils.addTab(gBrowser);
+ let tab = BrowserTestUtils.addTab(gBrowser, "about:mozilla");
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ gBrowser.updateBrowserRemoteness(tabToKeep.linkedBrowser, {
+ remoteType: tab.linkedBrowser.isRemoteBrowser
+ ? E10SUtils.DEFAULT_REMOTE_TYPE
+ : E10SUtils.NOT_REMOTE,
+ });
+ gBrowser.swapBrowsersAndCloseOther(tabToKeep, tab);
+
+ await ensure_opentabs_match_db();
+
+ BrowserTestUtils.removeTab(tabToKeep);
+
+ await ensure_opentabs_match_db();
+});
+
+add_task(async function step_7() {
+ info("Running step 7 - close all tabs");
+
+ Services.prefs.clearUserPref("browser.sessionstore.restore_on_demand");
+
+ BrowserTestUtils.addTab(gBrowser, "about:blank", { skipAnimation: true });
+ while (gBrowser.tabs.length > 1) {
+ info("Removing tab: " + gBrowser.tabs[0].linkedBrowser.currentURI.spec);
+ gBrowser.selectTabAtIndex(0);
+ gBrowser.removeCurrentTab();
+ }
+
+ await ensure_opentabs_match_db();
+});
+
+add_task(async function cleanup() {
+ info("Cleaning up");
+
+ await PlacesUtils.history.clear();
+});
+
+function loadTab(tab, url) {
+ // Because adding visits is async, we will not be notified immediately.
+ let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ let visited = new Promise(resolve => {
+ Services.obs.addObserver(function observer(aSubject, aTopic, aData) {
+ if (url != aSubject.QueryInterface(Ci.nsIURI).spec) {
+ return;
+ }
+ Services.obs.removeObserver(observer, aTopic);
+ resolve();
+ }, "uri-visit-saved");
+ });
+
+ info("Loading page: " + url);
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, url);
+ return Promise.all([loaded, visited]);
+}
+
+function ensure_opentabs_match_db() {
+ let tabs = {};
+
+ for (let browserWin of Services.wm.getEnumerator("navigator:browser")) {
+ // skip closed-but-not-destroyed windows
+ if (browserWin.closed) {
+ continue;
+ }
+
+ for (let i = 0; i < browserWin.gBrowser.tabs.length; i++) {
+ let browser = browserWin.gBrowser.getBrowserAtIndex(i);
+ let url = browser.currentURI.spec;
+ if (browserWin.isBlankPageURL(url)) {
+ continue;
+ }
+ if (!(url in tabs)) {
+ tabs[url] = 1;
+ } else {
+ tabs[url]++;
+ }
+ }
+ }
+
+ return checkAutocompleteResults(tabs);
+}
+
+async function checkAutocompleteResults(expected) {
+ info("Searching open pages.");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: RESTRICT_TOKEN_OPENPAGE,
+ });
+
+ let resultCount = UrlbarTestUtils.getResultCount(window);
+ for (let i = 0; i < resultCount; i++) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ if (result.heuristic) {
+ info("Skip heuristic match");
+ continue;
+ }
+
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ "Should have a tab switch result"
+ );
+
+ let url = result.url;
+
+ info(`Search for ${url} in open tabs.`);
+ let inExpected = url in expected;
+ Assert.ok(
+ inExpected,
+ `${url} was found in autocomplete, was ${
+ inExpected ? "" : "not "
+ } expected`
+ );
+ // Remove the found entry from expected results.
+ delete expected[url];
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+
+ // Make sure there is no reported open page that is not open.
+ for (let entry in expected) {
+ Assert.ok(!entry, `Should have been found in autocomplete`);
+ }
+}
diff --git a/browser/components/urlbar/tests/browser/browser_tabMatchesInAwesomebar_perwindowpb.js b/browser/components/urlbar/tests/browser/browser_tabMatchesInAwesomebar_perwindowpb.js
new file mode 100644
index 0000000000..b7b13eecf8
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_tabMatchesInAwesomebar_perwindowpb.js
@@ -0,0 +1,120 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test ensures that we don't switch between tabs from normal window to
+ * private browsing window or opposite.
+ */
+
+const TEST_URL = `${TEST_BASE_URL}dummy_page.html`;
+
+add_task(async function () {
+ let normalWindow = await BrowserTestUtils.openNewBrowserWindow();
+ let privateWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ await runTest(normalWindow, privateWindow, false);
+ await BrowserTestUtils.closeWindow(normalWindow);
+ await BrowserTestUtils.closeWindow(privateWindow);
+
+ normalWindow = await BrowserTestUtils.openNewBrowserWindow();
+ privateWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ await runTest(privateWindow, normalWindow, false);
+ await BrowserTestUtils.closeWindow(normalWindow);
+ await BrowserTestUtils.closeWindow(privateWindow);
+
+ privateWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ await runTest(privateWindow, privateWindow, true);
+ await BrowserTestUtils.closeWindow(privateWindow);
+
+ normalWindow = await BrowserTestUtils.openNewBrowserWindow();
+ await runTest(normalWindow, normalWindow, true);
+ await BrowserTestUtils.closeWindow(normalWindow);
+});
+
+async function runTest(aSourceWindow, aDestWindow, aExpectSwitch, aCallback) {
+ await BrowserTestUtils.openNewForegroundTab(aSourceWindow.gBrowser, TEST_URL);
+ let testTab = await BrowserTestUtils.openNewForegroundTab(
+ aDestWindow.gBrowser
+ );
+
+ info("waiting for focus on the window");
+ await SimpleTest.promiseFocus(aDestWindow);
+ info("got focus on the window");
+
+ // Select the testTab
+ aDestWindow.gBrowser.selectedTab = testTab;
+
+ // Ensure that this tab has no history entries
+ let sessionHistoryCount = await new Promise(resolve => {
+ SessionStore.getSessionHistory(
+ gBrowser.selectedTab,
+ function (sessionHistory) {
+ resolve(sessionHistory.entries.length);
+ }
+ );
+ });
+
+ ok(
+ sessionHistoryCount < 2,
+ `The test tab has 1 or fewer history entries. sessionHistoryCount=${sessionHistoryCount}`
+ );
+ // Ensure that this tab is on about:blank
+ is(
+ testTab.linkedBrowser.currentURI.spec,
+ "about:blank",
+ "The test tab is on about:blank"
+ );
+ // Ensure that this tab's document has no child nodes
+ await SpecialPowers.spawn(testTab.linkedBrowser, [], async function () {
+ ok(
+ !content.document.body.hasChildNodes(),
+ "The test tab has no child nodes"
+ );
+ });
+ ok(
+ !testTab.hasAttribute("busy"),
+ "The test tab doesn't have the busy attribute"
+ );
+
+ // Wait for the Awesomebar popup to appear.
+ let searchString = TEST_URL;
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: aDestWindow,
+ value: searchString,
+ });
+
+ info(`awesomebar popup appeared. aExpectSwitch: ${aExpectSwitch}`);
+ // Make sure the last match is selected.
+ while (
+ UrlbarTestUtils.getSelectedRowIndex(aDestWindow) <
+ UrlbarTestUtils.getResultCount(aDestWindow) - 1
+ ) {
+ info("handling key navigation for DOM_VK_DOWN key");
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, aDestWindow);
+ }
+
+ let awaitTabSwitch;
+ if (aExpectSwitch) {
+ awaitTabSwitch = BrowserTestUtils.waitForTabClosing(testTab);
+ }
+
+ // Execute the selected action.
+ EventUtils.synthesizeKey("KEY_Enter", {}, aDestWindow);
+ info("sent Enter command to the controller");
+
+ if (aExpectSwitch) {
+ // If we expect a tab switch then the current tab
+ // will be closed and we switch to the other tab.
+ await awaitTabSwitch;
+ } else {
+ // If we don't expect a tab switch then wait for the tab to load.
+ await BrowserTestUtils.browserLoaded(testTab.linkedBrowser);
+ }
+}
diff --git a/browser/components/urlbar/tests/browser/browser_tabToSearch.js b/browser/components/urlbar/tests/browser/browser_tabToSearch.js
new file mode 100644
index 0000000000..b029682eda
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_tabToSearch.js
@@ -0,0 +1,641 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests tab-to-search results. See also
+ * browser/components/urlbar/tests/unit/test_providerTabToSearch.js.
+ */
+
+"use strict";
+
+const TEST_ENGINE_NAME = "Test";
+const TEST_ENGINE_DOMAIN = "example.com";
+
+const DYNAMIC_RESULT_TYPE = "onboardTabToSearch";
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarProviderTabToSearch:
+ "resource:///modules/UrlbarProviderTabToSearch.sys.mjs",
+});
+
+add_setup(async function () {
+ await PlacesUtils.history.clear();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Disable onboarding results for general tests. They are enabled in tests
+ // that specifically address onboarding.
+ ["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0],
+ ],
+ });
+
+ await SearchTestUtils.installSearchExtension({
+ name: TEST_ENGINE_NAME,
+ search_url: `https://${TEST_ENGINE_DOMAIN}/`,
+ });
+
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits([`https://${TEST_ENGINE_DOMAIN}/`]);
+ }
+
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ });
+});
+
+// Tests that tab-to-search results preview search mode when highlighted. These
+// results are worth testing separately since they do not set the
+// payload.keyword parameter.
+add_task(async function basic() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_ENGINE_DOMAIN.slice(0, 4),
+ fireInputEvent: true,
+ });
+ let autofillResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(autofillResult.autofill);
+ Assert.equal(
+ autofillResult.url,
+ `https://${TEST_ENGINE_DOMAIN}/`,
+ "The autofilled URL matches the engine domain."
+ );
+
+ let tabToSearchResult = (
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)
+ ).result;
+ Assert.equal(
+ tabToSearchResult.providerName,
+ "TabToSearch",
+ "The second result is a tab-to-search result."
+ );
+ Assert.equal(
+ tabToSearchResult.payload.engine,
+ TEST_ENGINE_NAME,
+ "The tab-to-search result is for the correct engine."
+ );
+ let tabToSearchDetails = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ 1
+ );
+ let [actionTabToSearch] = await document.l10n.formatValues([
+ {
+ id: Services.search.getEngineByName(
+ tabToSearchDetails.searchParams.engine
+ ).isGeneralPurposeEngine
+ ? "urlbar-result-action-tabtosearch-web"
+ : "urlbar-result-action-tabtosearch-other-engine",
+ args: { engine: tabToSearchDetails.searchParams.engine },
+ },
+ ]);
+ Assert.equal(
+ tabToSearchDetails.displayed.title,
+ `Search with ${tabToSearchDetails.searchParams.engine}`,
+ "The result's title is set correctly."
+ );
+ Assert.equal(
+ tabToSearchDetails.displayed.action,
+ actionTabToSearch,
+ "The correct action text is displayed in the tab-to-search result."
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 1,
+ "Sanity check: The second result is selected."
+ );
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ENGINE_NAME,
+ entry: "tabtosearch",
+ isPreview: true,
+ });
+
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+});
+
+// Tests that we do not set aria-activedescendant after tabbing to a
+// tab-to-search result when the pref
+// browser.urlbar.accessibility.tabToSearch.announceResults is true. If that
+// pref is true, the result was already announced while the user was typing.
+add_task(async function activedescendant_tab() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.accessibility.tabToSearch.announceResults", true]],
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_ENGINE_DOMAIN.slice(0, 4),
+ fireInputEvent: true,
+ });
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 2,
+ "There should be two results."
+ );
+ let tabToSearchRow = await UrlbarTestUtils.waitForAutocompleteResultAt(
+ window,
+ 1
+ );
+ Assert.equal(
+ tabToSearchRow.result.providerName,
+ "TabToSearch",
+ "The second result is a tab-to-search result."
+ );
+
+ EventUtils.synthesizeKey("KEY_Tab");
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ENGINE_NAME,
+ entry: "tabtosearch",
+ isPreview: true,
+ });
+ let aadID = gURLBar.inputField.getAttribute("aria-activedescendant");
+ Assert.equal(aadID, null, "aria-activedescendant was not set.");
+
+ // Cycle through all the results then return to the tab-to-search result. It
+ // should be announced.
+ EventUtils.synthesizeKey("KEY_Tab");
+ aadID = gURLBar.inputField.getAttribute("aria-activedescendant");
+ let firstRow = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
+ Assert.equal(
+ aadID,
+ firstRow._content.id,
+ "aria-activedescendant was set to the row after the tab-to-search result."
+ );
+ EventUtils.synthesizeKey("KEY_Tab");
+ aadID = gURLBar.inputField.getAttribute("aria-activedescendant");
+ Assert.equal(
+ aadID,
+ tabToSearchRow._content.id,
+ "aria-activedescendant was set to the tab-to-search result."
+ );
+
+ // Now close and reopen the view, then do another search that yields a
+ // tab-to-search result. aria-activedescendant should not be set when it is
+ // selected.
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_ENGINE_DOMAIN.slice(0, 4),
+ fireInputEvent: true,
+ });
+ tabToSearchRow = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1);
+ Assert.equal(
+ tabToSearchRow.result.providerName,
+ "TabToSearch",
+ "The second result is a tab-to-search result."
+ );
+
+ EventUtils.synthesizeKey("KEY_Tab");
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ENGINE_NAME,
+ entry: "tabtosearch",
+ isPreview: true,
+ });
+ aadID = gURLBar.inputField.getAttribute("aria-activedescendant");
+ Assert.equal(aadID, null, "aria-activedescendant was not set.");
+
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+ await SpecialPowers.popPrefEnv();
+});
+
+// Tests that we set aria-activedescendant after accessing a tab-to-search
+// result with the arrow keys.
+add_task(async function activedescendant_arrow() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_ENGINE_DOMAIN.slice(0, 4),
+ fireInputEvent: true,
+ });
+ let tabToSearchRow = await UrlbarTestUtils.waitForAutocompleteResultAt(
+ window,
+ 1
+ );
+ Assert.equal(
+ tabToSearchRow.result.providerName,
+ "TabToSearch",
+ "The second result is a tab-to-search result."
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ENGINE_NAME,
+ entry: "tabtosearch",
+ isPreview: true,
+ });
+ let aadID = gURLBar.inputField.getAttribute("aria-activedescendant");
+ Assert.equal(
+ aadID,
+ tabToSearchRow._content.id,
+ "aria-activedescendant was set to the tab-to-search result."
+ );
+
+ // Move selection away from the tab-to-search result then return. It should
+ // be announced.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ aadID = gURLBar.inputField.getAttribute("aria-activedescendant");
+ Assert.equal(
+ aadID,
+ UrlbarTestUtils.getOneOffSearchButtons(window).selectedButton.id,
+ "aria-activedescendant was moved to the first one-off."
+ );
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ aadID = gURLBar.inputField.getAttribute("aria-activedescendant");
+ Assert.equal(
+ aadID,
+ tabToSearchRow._content.id,
+ "aria-activedescendant was set to the tab-to-search result."
+ );
+
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+});
+
+add_task(async function tab_key_race() {
+ // Mac Debug tinderboxes are just too slow and fail intermittently
+ // even if the EventBufferer timeout is set to an high value.
+ if (AppConstants.platform == "macosx" && AppConstants.DEBUG) {
+ return;
+ }
+ info(
+ "Test typing a letter followed shortly by down arrow consistently selects a tab-to-search result"
+ );
+ Assert.equal(gURLBar.value, "", "Sanity check urlbar is empty");
+ let promiseQueryStarted = new Promise(resolve => {
+ /**
+ * A no-op test provider.
+ * We use this to wait for the query to start, because otherwise TAB will
+ * move to the next widget since the panel is closed and there's no running
+ * query. This means waiting for the UrlbarProvidersManager to at least
+ * evaluate the isActive status of providers.
+ * In the future we should try to reduce this latency, to defer user events
+ * even more efficiently.
+ */
+ class ListeningTestProvider extends UrlbarProvider {
+ constructor() {
+ super();
+ }
+ get name() {
+ return "ListeningTestProvider";
+ }
+ get type() {
+ return UrlbarUtils.PROVIDER_TYPE.PROFILE;
+ }
+ isActive(context) {
+ executeSoon(resolve);
+ return false;
+ }
+ isRestricting(context) {
+ return false;
+ }
+ async startQuery(context, addCallback) {
+ // Nothing to do.
+ }
+ }
+ let provider = new ListeningTestProvider();
+ UrlbarProvidersManager.registerProvider(provider);
+ registerCleanupFunction(async function () {
+ UrlbarProvidersManager.unregisterProvider(provider);
+ });
+ });
+ gURLBar.focus();
+ info("Type the beginning of the search string to get tab-to-search");
+ EventUtils.synthesizeKey(TEST_ENGINE_DOMAIN.slice(0, 1));
+ info("Awaiting for the query to start");
+ await promiseQueryStarted;
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await TestUtils.waitForCondition(
+ () => UrlbarTestUtils.getSelectedRowIndex(window) == 1,
+ "Wait for down arrow key to be handled"
+ );
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ENGINE_NAME,
+ entry: "tabtosearch",
+ isPreview: true,
+ });
+
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+});
+
+// Test that large-style onboarding results appear and have the correct
+// properties.
+add_task(async function onboard() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 3]],
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_ENGINE_DOMAIN.slice(0, 4),
+ fireInputEvent: true,
+ });
+ let autofillResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(autofillResult.autofill);
+ Assert.equal(
+ autofillResult.url,
+ `https://${TEST_ENGINE_DOMAIN}/`,
+ "The autofilled URL matches the engine domain."
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 1,
+ "Sanity check: The second result is selected."
+ );
+
+ // Now check the properties of the onboarding result.
+ let onboardingElement = await UrlbarTestUtils.waitForAutocompleteResultAt(
+ window,
+ 1
+ );
+ Assert.equal(
+ onboardingElement.result.payload.dynamicType,
+ DYNAMIC_RESULT_TYPE,
+ "The tab-to-search result is an onboarding result."
+ );
+ Assert.equal(
+ onboardingElement.result.resultSpan,
+ 2,
+ "The correct resultSpan was set."
+ );
+ Assert.ok(
+ onboardingElement
+ .querySelector(".urlbarView-row-inner")
+ .hasAttribute("selected"),
+ "The onboarding element set the selected attribute."
+ );
+
+ let [titleOnboarding, actionOnboarding, descriptionOnboarding] =
+ await document.l10n.formatValues([
+ {
+ id: "urlbar-result-action-search-w-engine",
+ args: {
+ engine: onboardingElement.result.payload.engine,
+ },
+ },
+ {
+ id: Services.search.getEngineByName(
+ onboardingElement.result.payload.engine
+ ).isGeneralPurposeEngine
+ ? "urlbar-result-action-tabtosearch-web"
+ : "urlbar-result-action-tabtosearch-other-engine",
+ args: { engine: onboardingElement.result.payload.engine },
+ },
+ {
+ id: "urlbar-tabtosearch-onboard",
+ },
+ ]);
+ let onboardingDetails = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(
+ onboardingDetails.displayed.title,
+ titleOnboarding,
+ "The correct title was set."
+ );
+ Assert.equal(
+ onboardingDetails.displayed.action,
+ actionOnboarding,
+ "The correct action text was set."
+ );
+ Assert.equal(
+ onboardingDetails.element.row.querySelector(
+ ".urlbarView-dynamic-onboardTabToSearch-description"
+ ).textContent,
+ descriptionOnboarding,
+ "The correct description was set."
+ );
+
+ // Check that the onboarding result enters search mode.
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ENGINE_NAME,
+ entry: "tabtosearch_onboard",
+ isPreview: true,
+ });
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ENGINE_NAME,
+ entry: "tabtosearch_onboard",
+ });
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+ UrlbarPrefs.set("tabToSearch.onboard.interactionsLeft", 3);
+ delete UrlbarProviderTabToSearch.onboardingInteractionAtTime;
+ await SpecialPowers.popPrefEnv();
+});
+
+// Tests that we show the onboarding result until the user interacts with it
+// `browser.urlbar.tabToSearch.onboard.interactionsLeft` times.
+add_task(async function onboard_limit() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 3]],
+ });
+
+ Assert.equal(
+ UrlbarPrefs.get("tabToSearch.onboard.interactionsLeft"),
+ 3,
+ "Sanity check: interactionsLeft is 3."
+ );
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_ENGINE_DOMAIN.slice(0, 4),
+ fireInputEvent: true,
+ });
+ let onboardingResult = (
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)
+ ).result;
+ Assert.equal(
+ onboardingResult.payload.dynamicType,
+ DYNAMIC_RESULT_TYPE,
+ "The second result is an onboarding result."
+ );
+ await EventUtils.synthesizeKey("KEY_ArrowDown");
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ENGINE_NAME,
+ entry: "tabtosearch_onboard",
+ isPreview: true,
+ });
+ Assert.equal(UrlbarPrefs.get("tabToSearch.onboard.interactionsLeft"), 2);
+ await UrlbarTestUtils.exitSearchMode(window);
+
+ // We don't increment the counter if we showed the onboarding result less than
+ // 5 minutes ago.
+ for (let i = 0; i < 5; i++) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_ENGINE_DOMAIN.slice(0, 4),
+ fireInputEvent: true,
+ });
+ onboardingResult = (
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)
+ ).result;
+ Assert.equal(
+ onboardingResult.payload.dynamicType,
+ DYNAMIC_RESULT_TYPE,
+ "The second result is an onboarding result."
+ );
+ await EventUtils.synthesizeKey("KEY_ArrowDown");
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ENGINE_NAME,
+ entry: "tabtosearch_onboard",
+ isPreview: true,
+ });
+ Assert.equal(
+ UrlbarPrefs.get("tabToSearch.onboard.interactionsLeft"),
+ 2,
+ "We shouldn't decrement interactionsLeft if an onboarding result was just shown."
+ );
+ await UrlbarTestUtils.exitSearchMode(window);
+ }
+
+ // If the user doesn't interact with the result, we don't increment the
+ // counter.
+ for (let i = 0; i < 5; i++) {
+ delete UrlbarProviderTabToSearch.onboardingInteractionAtTime;
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_ENGINE_DOMAIN.slice(0, 4),
+ fireInputEvent: true,
+ });
+ let tabToSearchResult = (
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)
+ ).result;
+ Assert.equal(
+ tabToSearchResult.providerName,
+ "TabToSearch",
+ "The second result is a tab-to-search result."
+ );
+ Assert.equal(
+ tabToSearchResult.type,
+ UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ "The tab-to-search result is an onboarding result."
+ );
+ Assert.equal(
+ UrlbarPrefs.get("tabToSearch.onboard.interactionsLeft"),
+ 2,
+ "We shouldn't decrement interactionsLeft if the user doesn't interact with onboarding."
+ );
+ }
+
+ // Test that we increment the counter if the user interacts with the result
+ // and it's been 5+ minutes since they last interacted with it.
+ for (let i = 1; i >= 0; i--) {
+ delete UrlbarProviderTabToSearch.onboardingInteractionAtTime;
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_ENGINE_DOMAIN.slice(0, 4),
+ fireInputEvent: true,
+ });
+ let tabToSearchResult = (
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)
+ ).result;
+ Assert.equal(
+ tabToSearchResult.providerName,
+ "TabToSearch",
+ "The second result is a tab-to-search result."
+ );
+ Assert.equal(
+ onboardingResult.payload.dynamicType,
+ DYNAMIC_RESULT_TYPE,
+ "The second result is an onboarding result."
+ );
+ await EventUtils.synthesizeKey("KEY_ArrowDown");
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ENGINE_NAME,
+ entry: "tabtosearch_onboard",
+ isPreview: true,
+ });
+ Assert.equal(
+ UrlbarPrefs.get("tabToSearch.onboard.interactionsLeft"),
+ i,
+ "We decremented interactionsLeft."
+ );
+ await UrlbarTestUtils.exitSearchMode(window);
+ }
+
+ delete UrlbarProviderTabToSearch.onboardingInteractionAtTime;
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_ENGINE_DOMAIN.slice(0, 4),
+ fireInputEvent: true,
+ });
+ let tabToSearchResult = (
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)
+ ).result;
+ Assert.equal(
+ tabToSearchResult.providerName,
+ "TabToSearch",
+ "The second result is a tab-to-search result."
+ );
+ Assert.notEqual(
+ tabToSearchResult.type,
+ UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ "Now that interactionsLeft is 0, we don't show onboarding results."
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+ UrlbarPrefs.set("tabToSearch.onboard.interactionsLeft", 3);
+ delete UrlbarProviderTabToSearch.onboardingInteractionAtTime;
+ await SpecialPowers.popPrefEnv();
+});
+
+// Tests that we show at most one onboarding result at a time. See
+// tests/unit/test_providerTabToSearch.js:multipleEnginesForHostname for a test
+// that ensures only one normal tab-to-search result is shown in this scenario.
+add_task(async function onboard_multipleEnginesForHostname() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 3]],
+ });
+
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: `${TEST_ENGINE_NAME}Maps`,
+ search_url: `https://${TEST_ENGINE_DOMAIN}/maps/`,
+ },
+ { skipUnload: true }
+ );
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_ENGINE_DOMAIN.slice(0, 4),
+ fireInputEvent: true,
+ });
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 2,
+ "Only two results are shown."
+ );
+ let firstResult = (
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0)
+ ).result;
+ Assert.notEqual(
+ firstResult.providerName,
+ "TabToSearch",
+ "The first result is not from TabToSearch."
+ );
+ let secondResult = (
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)
+ ).result;
+ Assert.equal(
+ secondResult.providerName,
+ "TabToSearch",
+ "The second result is from TabToSearch."
+ );
+ Assert.equal(
+ secondResult.type,
+ UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ "The tab-to-search result is the only onboarding result."
+ );
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+ await extension.unload();
+ UrlbarPrefs.set("tabToSearch.onboard.interactionsLeft", 3);
+ delete UrlbarProviderTabToSearch.onboardingInteractionAtTime;
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_textruns.js b/browser/components/urlbar/tests/browser/browser_textruns.js
new file mode 100644
index 0000000000..ed7a61e6b0
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_textruns.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test ensures that we limit textruns in case of very long urls or titles.
+ */
+
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.searches", true]],
+ });
+ await SearchTestUtils.installSearchExtension(
+ { name: "Test" },
+ { setAsDefault: true }
+ );
+
+ let lotsOfSpaces = "%20".repeat(300);
+ await PlacesTestUtils.addVisits({
+ uri: `https://textruns.mozilla.org/${lotsOfSpaces}/test/`,
+ title: `A long ${lotsOfSpaces} title`,
+ });
+ await UrlbarTestUtils.formHistory.add([
+ { value: `A long ${lotsOfSpaces} textruns suggestion` },
+ ]);
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.formHistory.clear();
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "textruns",
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.searchParams.engine, "Test", "Sanity check engine");
+ Assert.equal(
+ result.displayed.title.length,
+ UrlbarUtils.MAX_TEXT_LENGTH,
+ "Result title should be limited"
+ );
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2);
+ Assert.equal(
+ result.displayed.title.length,
+ UrlbarUtils.MAX_TEXT_LENGTH,
+ "Result title should be limited"
+ );
+ Assert.equal(
+ result.displayed.url.length,
+ UrlbarUtils.MAX_TEXT_LENGTH,
+ "Result url should be limited"
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_tokenAlias.js b/browser/components/urlbar/tests/browser/browser_tokenAlias.js
new file mode 100644
index 0000000000..d215c2536f
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_tokenAlias.js
@@ -0,0 +1,861 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test checks "@" search engine aliases ("token aliases") in the urlbar.
+
+"use strict";
+
+const TEST_ALIAS_ENGINE_NAME = "Test";
+const ALIAS = "@test";
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+
+// We make sure that aliases and search terms are correctly recognized when they
+// are separated by each of these different types of spaces and combinations of
+// spaces. U+3000 is the ideographic space in CJK and is commonly used by CJK
+// speakers.
+const TEST_SPACES = [" ", "\u3000", " \u3000", "\u3000 "];
+
+// Allow more time for Mac machines so they don't time out in verify mode. See
+// bug 1673062.
+if (AppConstants.platform == "macosx") {
+ requestLongerTimeout(5);
+}
+
+add_setup(async function () {
+ // Add a default engine with suggestions, to avoid hitting the network when
+ // fetching them.
+ let defaultEngine = await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME,
+ setAsDefault: true,
+ });
+ defaultEngine.alias = "@default";
+ await SearchTestUtils.installSearchExtension({
+ name: TEST_ALIAS_ENGINE_NAME,
+ keyword: ALIAS,
+ });
+
+ // Search results aren't shown in quantumbar unless search suggestions are
+ // enabled.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.searches", true]],
+ });
+});
+
+// Simple test that tries different variations of an alias, without reverting
+// the urlbar value in between.
+add_task(async function testNoRevert() {
+ await doSimpleTest(false);
+});
+
+// Simple test that tries different variations of an alias, reverting the urlbar
+// value in between.
+add_task(async function testRevert() {
+ await doSimpleTest(true);
+});
+
+async function doSimpleTest(revertBetweenSteps) {
+ // When autofill is enabled, searching for "@tes" will autofill to "@test",
+ // which gets in the way of this test task, so temporarily disable it.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.autoFill", false]],
+ });
+
+ // "@tes" -- not an alias, no search mode
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: ALIAS.substr(0, ALIAS.length - 1),
+ });
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ Assert.equal(
+ gURLBar.value,
+ ALIAS.substr(0, ALIAS.length - 1),
+ "value should be alias substring"
+ );
+
+ if (revertBetweenSteps) {
+ gURLBar.handleRevert();
+ gURLBar.blur();
+ }
+
+ // "@test" -- alias but no trailing space, no search mode
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: ALIAS,
+ });
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ Assert.equal(gURLBar.value, ALIAS, "value should be alias");
+
+ if (revertBetweenSteps) {
+ gURLBar.handleRevert();
+ gURLBar.blur();
+ }
+
+ // "@test " -- alias with trailing space, search mode
+ for (let spaces of TEST_SPACES) {
+ info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) }));
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: ALIAS + spaces,
+ });
+ // Wait for the second new search that starts when search mode is entered.
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ALIAS_ENGINE_NAME,
+ entry: "typed",
+ });
+ Assert.equal(gURLBar.value, "", "value should be empty");
+ await UrlbarTestUtils.exitSearchMode(window);
+
+ if (revertBetweenSteps) {
+ gURLBar.handleRevert();
+ gURLBar.blur();
+ }
+ }
+
+ // "@test foo" -- alias, search mode
+ for (let spaces of TEST_SPACES) {
+ info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) }));
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: ALIAS + spaces + "foo",
+ });
+ // Wait for the second new search that starts when search mode is entered.
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ALIAS_ENGINE_NAME,
+ entry: "typed",
+ });
+ Assert.equal(gURLBar.value, "foo", "value should be query");
+ await UrlbarTestUtils.exitSearchMode(window);
+
+ if (revertBetweenSteps) {
+ gURLBar.handleRevert();
+ gURLBar.blur();
+ }
+ }
+
+ // "@test " -- alias with trailing space, search mode
+ for (let spaces of TEST_SPACES) {
+ info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) }));
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: ALIAS + spaces,
+ });
+ // Wait for the second new search that starts when search mode is entered.
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ALIAS_ENGINE_NAME,
+ entry: "typed",
+ });
+ Assert.equal(gURLBar.value, "", "value should be empty");
+ await UrlbarTestUtils.exitSearchMode(window);
+
+ if (revertBetweenSteps) {
+ gURLBar.handleRevert();
+ gURLBar.blur();
+ }
+ }
+
+ // "@test" -- alias but no trailing space, no highlight
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: ALIAS,
+ });
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ Assert.equal(gURLBar.value, ALIAS, "value should be alias");
+
+ if (revertBetweenSteps) {
+ gURLBar.handleRevert();
+ gURLBar.blur();
+ }
+
+ // "@tes" -- not an alias, no highlight
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: ALIAS.substr(0, ALIAS.length - 1),
+ });
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ Assert.equal(
+ gURLBar.value,
+ ALIAS.substr(0, ALIAS.length - 1),
+ "value should be alias substring"
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+
+ await SpecialPowers.popPrefEnv();
+}
+
+// An alias should be recognized even when there are spaces before it, and
+// search mode should be entered.
+add_task(async function spacesBeforeAlias() {
+ for (let spaces of TEST_SPACES) {
+ info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) }));
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: spaces + ALIAS + spaces,
+ });
+ // Wait for the second new search that starts when search mode is entered.
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ALIAS_ENGINE_NAME,
+ entry: "typed",
+ });
+ Assert.equal(gURLBar.value, "", "value should be empty");
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+ }
+});
+
+// An alias in the middle of a string should not be recognized and search mode
+// should not be entered.
+add_task(async function charsBeforeAlias() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "not an alias " + ALIAS + " ",
+ });
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ Assert.equal(
+ gURLBar.value,
+ "not an alias " + ALIAS + " ",
+ "value should be unchanged"
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+});
+
+// While already in search mode, an alias should not be recognized.
+add_task(async function alreadyInSearchMode() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ await UrlbarTestUtils.enterSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: ALIAS + " ",
+ });
+
+ // Search mode source should still be bookmarks.
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ entry: "oneoff",
+ });
+ Assert.equal(gURLBar.value, ALIAS + " ", "value should be unchanged");
+
+ // Exit search mode, but first remove the value in the input. Since the value
+ // is "alias ", we'd actually immediately re-enter search mode otherwise.
+ gURLBar.value = "";
+
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+});
+
+// Types a space while typing an alias to ensure we stop autofilling.
+add_task(async function spaceWhileTypingAlias() {
+ for (let spaces of TEST_SPACES) {
+ if (spaces.length != 1) {
+ continue;
+ }
+ info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) }));
+
+ let value = ALIAS.substring(0, ALIAS.length - 1);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value,
+ selectionStart: value.length,
+ selectionEnd: value.length,
+ });
+ Assert.equal(gURLBar.value, ALIAS + " ", "Alias should be autofilled");
+
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey(spaces);
+ await searchPromise;
+
+ Assert.equal(
+ gURLBar.value,
+ value + spaces,
+ "Alias should not be autofilled"
+ );
+ await UrlbarTestUtils.assertSearchMode(window, null);
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ }
+});
+
+// Aliases are case insensitive. Make sure that searching with an alias using a
+// weird case still causes the alias to be recognized and search mode entered.
+add_task(async function aliasCase() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@TeSt ",
+ });
+ // Wait for the second new search that starts when search mode is entered.
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ALIAS_ENGINE_NAME,
+ entry: "typed",
+ });
+ Assert.equal(gURLBar.value, "", "value should be empty");
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+});
+
+// Same as previous but with a query.
+add_task(async function aliasCase_query() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@tEsT query",
+ });
+ // Wait for the second new search that starts when search mode is entered.
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ALIAS_ENGINE_NAME,
+ entry: "typed",
+ });
+ Assert.equal(gURLBar.value, "query", "value should be query");
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+});
+
+// Selecting a non-heuristic (non-first) search engine result with an alias and
+// empty query should put the alias in the urlbar and highlight it.
+// Also checks that internal aliases appear with the "@" keyword.
+add_task(async function nonHeuristicAliases() {
+ // Get the list of token alias engines (those with aliases that start with
+ // "@").
+ let tokenEngines = [];
+ for (let engine of await Services.search.getEngines()) {
+ let aliases = [];
+ if (engine.alias) {
+ aliases.push(engine.alias);
+ }
+ aliases.push(...engine.aliases);
+ let tokenAliases = aliases.filter(a => a.startsWith("@"));
+ if (tokenAliases.length) {
+ tokenEngines.push({ engine, tokenAliases });
+ }
+ }
+ if (!tokenEngines.length) {
+ Assert.ok(true, "No token alias engines, skipping task.");
+ return;
+ }
+ info(
+ "Got token alias engines: " + tokenEngines.map(({ engine }) => engine.name)
+ );
+
+ // Populate the results with the list of token alias engines by searching for
+ // "@".
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@",
+ });
+ await UrlbarTestUtils.waitForAutocompleteResultAt(
+ window,
+ tokenEngines.length - 1
+ );
+ // Key down to select each result in turn. The urlbar should preview search
+ // mode for each engine.
+ for (let { tokenAliases } of tokenEngines) {
+ let alias = tokenAliases[0];
+ let engineName = (await UrlbarSearchUtils.engineForAlias(alias)).name;
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ let expectedSearchMode = {
+ engineName,
+ entry: "keywordoffer",
+ isPreview: true,
+ };
+ if (Services.search.getEngineByName(engineName).isGeneralPurposeEngine) {
+ expectedSearchMode.source = UrlbarUtils.RESULT_SOURCE.SEARCH;
+ }
+ await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode);
+ Assert.ok(!gURLBar.value, "The Urlbar should be empty.");
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+});
+
+// Clicking on an @ alias offer (an @ alias with an empty search string) in the
+// view should enter search mode.
+add_task(async function clickAndFillAlias() {
+ // Do a search for "@" to show all the @ aliases.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@",
+ });
+
+ // Find our test engine in the results. It's probably last, but for
+ // robustness don't assume it is.
+ let testEngineItem;
+ for (let i = 0; !testEngineItem; i++) {
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ Assert.equal(
+ details.displayed.title,
+ `Search with ${details.searchParams.engine}`,
+ "The result's title is set correctly."
+ );
+ Assert.ok(!details.action, "The result should have no action text.");
+ if (details.searchParams && details.searchParams.keyword == ALIAS) {
+ testEngineItem = await UrlbarTestUtils.waitForAutocompleteResultAt(
+ window,
+ i
+ );
+ }
+ }
+
+ // Click it.
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeMouseAtCenter(testEngineItem, {});
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: testEngineItem.result.payload.engine,
+ entry: "keywordoffer",
+ });
+
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+});
+
+// Pressing enter on an @ alias offer (an @ alias with an empty search string)
+// in the view should enter search mode.
+add_task(async function enterAndFillAlias() {
+ // Do a search for "@" to show all the @ aliases.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@",
+ });
+
+ // Find our test engine in the results. It's probably last, but for
+ // robustness don't assume it is.
+ let details;
+ let index = 0;
+ for (; ; index++) {
+ details = await UrlbarTestUtils.getDetailsOfResultAt(window, index);
+ if (details.searchParams && details.searchParams.keyword == ALIAS) {
+ index++;
+ break;
+ }
+ }
+
+ // Key down to it and press enter.
+ EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: index });
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: details.searchParams.engine,
+ entry: "keywordoffer",
+ });
+
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+});
+
+// Pressing Enter on an @ alias autofill should enter search mode.
+add_task(async function enterAutofillsAlias() {
+ for (let value of [ALIAS.substring(0, ALIAS.length - 1), ALIAS]) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value,
+ selectionStart: value.length,
+ selectionEnd: value.length,
+ });
+
+ // Press Enter.
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ALIAS_ENGINE_NAME,
+ entry: "keywordoffer",
+ });
+
+ await UrlbarTestUtils.exitSearchMode(window);
+ }
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+});
+
+// Pressing Right on an @ alias autofill should enter search mode.
+add_task(async function rightEntersSearchMode() {
+ for (let value of [ALIAS.substring(0, ALIAS.length - 1), ALIAS]) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value,
+ selectionStart: value.length,
+ selectionEnd: value.length,
+ });
+
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ALIAS_ENGINE_NAME,
+ entry: "typed",
+ });
+ Assert.equal(gURLBar.value, "", "value should be empty");
+ await UrlbarTestUtils.exitSearchMode(window);
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+});
+
+// Pressing Tab when an @ alias is autofilled should enter search mode preview.
+add_task(async function rightEntersSearchMode() {
+ for (let value of [ALIAS.substring(0, ALIAS.length - 1), ALIAS]) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value,
+ selectionStart: value.length,
+ selectionEnd: value.length,
+ });
+
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ -1,
+ "There is no selected result."
+ );
+
+ EventUtils.synthesizeKey("KEY_Tab");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 0,
+ "The first result is selected."
+ );
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ALIAS_ENGINE_NAME,
+ entry: "keywordoffer",
+ isPreview: true,
+ });
+ Assert.equal(gURLBar.value, "", "value should be empty");
+
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: TEST_ALIAS_ENGINE_NAME,
+ entry: "keywordoffer",
+ isPreview: false,
+ });
+ await UrlbarTestUtils.exitSearchMode(window);
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+});
+
+/**
+ * This test checks that if an engine is marked as hidden then
+ * it should not appear in the popup when using the "@" token alias in the search bar.
+ */
+add_task(async function hiddenEngine() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@",
+ fireInputEvent: true,
+ });
+
+ const defaultEngine = await Services.search.getDefault();
+
+ let foundDefaultEngineInPopup = false;
+
+ // Checks that the default engine appears in the urlbar's popup.
+ for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) {
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ if (defaultEngine.name == details.searchParams.engine) {
+ foundDefaultEngineInPopup = true;
+ break;
+ }
+ }
+ Assert.ok(foundDefaultEngineInPopup, "Default engine appears in the popup.");
+
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+
+ // Checks that a hidden default engine (i.e. an engine removed from
+ // a user's search settings) does not appear in the urlbar's popup.
+ defaultEngine.hidden = true;
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@",
+ fireInputEvent: true,
+ });
+ foundDefaultEngineInPopup = false;
+ for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) {
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ if (defaultEngine.name == details.searchParams.engine) {
+ foundDefaultEngineInPopup = true;
+ break;
+ }
+ }
+ Assert.ok(
+ !foundDefaultEngineInPopup,
+ "Hidden default engine does not appear in the popup"
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Escape")
+ );
+
+ defaultEngine.hidden = false;
+});
+
+/**
+ * This test checks that if an engines alias is not prefixed with
+ * @ it still appears in the popup when using the "@" token
+ * alias in the search bar.
+ */
+add_task(async function nonPrefixedKeyword() {
+ let name = "Custom";
+ let alias = "customkeyword";
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ name,
+ keyword: alias,
+ },
+ { skipUnload: true }
+ );
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@",
+ });
+
+ let foundEngineInPopup = false;
+
+ // Checks that the default engine appears in the urlbar's popup.
+ for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) {
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ if (details.searchParams.engine === name) {
+ foundEngineInPopup = true;
+ break;
+ }
+ }
+ Assert.ok(foundEngineInPopup, "Custom engine appears in the popup.");
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@" + alias,
+ });
+
+ let keywordOfferResult = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ 0
+ );
+
+ Assert.equal(
+ keywordOfferResult.searchParams.engine,
+ name,
+ "The first result should be a keyword search result with the correct engine."
+ );
+
+ await extension.unload();
+});
+
+// Tests that we show all engines with a token alias that match the search
+// string.
+add_task(async function multipleMatchingEngines() {
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: "TestFoo",
+ keyword: `${ALIAS}foo`,
+ },
+ { skipUnload: true }
+ );
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@te",
+ fireInputEvent: true,
+ });
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 2,
+ "Two results are shown."
+ );
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ -1,
+ "Neither result is selected."
+ );
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(result.autofill, "The first result is autofilling.");
+ Assert.equal(
+ result.searchParams.keyword,
+ ALIAS,
+ "The autofilled engine is shown first."
+ );
+
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(
+ result.searchParams.keyword,
+ `${ALIAS}foo`,
+ "The other engine is shown second."
+ );
+
+ EventUtils.synthesizeKey("KEY_Tab");
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), 0);
+ Assert.equal(gURLBar.value, "", "Urlbar should be empty.");
+ EventUtils.synthesizeKey("KEY_Tab");
+ Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), 1);
+ Assert.equal(gURLBar.value, "", "Urlbar should be empty.");
+ EventUtils.synthesizeKey("KEY_Tab");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ -1,
+ "Tabbing all the way through the matching engines should return to the input."
+ );
+ Assert.equal(
+ gURLBar.value,
+ "@te",
+ "Urlbar should contain the search string."
+ );
+
+ await extension.unload();
+});
+
+// Tests that UrlbarProviderTokenAliasEngines is disabled in search mode.
+add_task(async function doNotShowInSearchMode() {
+ // Do a search for "@" to show all the @ aliases.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@",
+ });
+
+ // Find our test engine in the results. It's probably last, but for
+ // robustness don't assume it is.
+ let testEngineItem;
+ for (let i = 0; !testEngineItem; i++) {
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ if (details.searchParams && details.searchParams.keyword == ALIAS) {
+ testEngineItem = await UrlbarTestUtils.waitForAutocompleteResultAt(
+ window,
+ i
+ );
+ }
+ }
+
+ Assert.equal(
+ testEngineItem.result.payload.keyword,
+ ALIAS,
+ "Sanity check: we found our engine."
+ );
+
+ // Click it.
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeMouseAtCenter(testEngineItem, {});
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: testEngineItem.result.payload.engine,
+ entry: "keywordoffer",
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "@",
+ fireInputEvent: true,
+ });
+
+ let resultCount = UrlbarTestUtils.getResultCount(window);
+ for (let i = 0; i < resultCount; i++) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ Assert.ok(
+ !result.searchParams.keyword,
+ `Result at index ${i} is not a keywordoffer.`
+ );
+ }
+});
+
+async function assertFirstResultIsAlias(isAlias, expectedAlias) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "Should have the correct type"
+ );
+
+ if (isAlias) {
+ Assert.equal(
+ result.searchParams.keyword,
+ expectedAlias,
+ "Payload keyword should be the alias"
+ );
+ } else {
+ Assert.notEqual(
+ result.searchParams.keyword,
+ expectedAlias,
+ "Payload keyword should be absent or not the alias"
+ );
+ }
+}
+
+function assertHighlighted(highlighted, expectedAlias) {
+ let selection = gURLBar.editor.selectionController.getSelection(
+ Ci.nsISelectionController.SELECTION_FIND
+ );
+ Assert.ok(selection);
+ if (!highlighted) {
+ Assert.equal(selection.rangeCount, 0);
+ return;
+ }
+ Assert.equal(selection.rangeCount, 1);
+ let index = gURLBar.value.indexOf(expectedAlias);
+ Assert.ok(
+ index >= 0,
+ `gURLBar.value="${gURLBar.value}" expectedAlias="${expectedAlias}"`
+ );
+ let range = selection.getRangeAt(0);
+ Assert.ok(range);
+ Assert.equal(range.startOffset, index);
+ Assert.equal(range.endOffset, index + expectedAlias.length);
+}
+
+/**
+ * Returns an array of code points in the given string. Each code point is
+ * returned as a hexidecimal string.
+ *
+ * @param {string} str
+ * The code points of this string will be returned.
+ * @returns {Array}
+ * Array of code points in the string, where each is a hexidecimal string.
+ */
+function codePoints(str) {
+ return str.split("").map(s => s.charCodeAt(0).toString(16));
+}
diff --git a/browser/components/urlbar/tests/browser/browser_top_sites.js b/browser/components/urlbar/tests/browser/browser_top_sites.js
new file mode 100644
index 0000000000..cb6502d2b2
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_top_sites.js
@@ -0,0 +1,481 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AboutNewTab: "resource:///modules/AboutNewTab.jsm",
+});
+
+const EN_US_TOPSITES =
+ "https://www.youtube.com/,https://www.facebook.com/,https://www.amazon.com/,https://www.reddit.com/,https://www.wikipedia.org/,https://twitter.com/";
+
+async function addTestVisits() {
+ // Add some visits to a URL.
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits("http://example.com/");
+ }
+
+ // Wait for example.com to be listed first.
+ await updateTopSites(sites => {
+ return sites && sites[0] && sites[0].url == "http://example.com/";
+ });
+
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "https://www.youtube.com/",
+ title: "YouTube",
+ });
+}
+
+async function checkDoesNotOpenOnFocus(win = window) {
+ // The view should not open when the input is focused programmatically.
+ win.gURLBar.blur();
+ win.gURLBar.focus();
+ Assert.ok(!win.gURLBar.view.isOpen, "check urlbar panel is not open");
+ win.gURLBar.blur();
+
+ // Check the keyboard shortcut.
+ win.document.getElementById("Browser:OpenLocation").doCommand();
+ // Because the panel opening may not be immediate, we must wait a bit.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 500));
+ Assert.ok(!win.gURLBar.view.isOpen, "check urlbar panel is not open");
+ win.gURLBar.blur();
+
+ // Focus with the mouse.
+ EventUtils.synthesizeMouseAtCenter(win.gURLBar.inputField, {}, win);
+ // Because the panel opening may not be immediate, we must wait a bit.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 500));
+ Assert.ok(!win.gURLBar.view.isOpen, "check urlbar panel is not open");
+ win.gURLBar.blur();
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.topsites", true],
+ ["browser.urlbar.suggest.quickactions", false],
+ ["browser.newtabpage.activity-stream.default.sites", EN_US_TOPSITES],
+ ],
+ });
+
+ await updateTopSites(
+ sites => sites && sites.length == EN_US_TOPSITES.split(",").length
+ );
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/"
+ );
+
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(tab);
+ });
+});
+
+add_task(async function topSitesShown() {
+ let sites = AboutNewTab.getTopSites();
+
+ for (let prefVal of [true, false]) {
+ // This test should work regardless of whether Top Sites are enabled on
+ // about:newtab.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.newtabpage.activity-stream.feeds.topsites", prefVal]],
+ });
+ // We don't expect this to change, but we run updateTopSites just in case
+ // feeds.topsites were to have an effect on the composition of Top Sites.
+ await updateTopSites(siteList => siteList.length == 6);
+
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ if (gURLBar.getAttribute("pageproxystate") == "invalid") {
+ gURLBar.handleRevert();
+ }
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ Assert.ok(gURLBar.view.isOpen, "UrlbarView should be open.");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ sites.length,
+ "The number of results should be the same as the number of Top Sites (6)."
+ );
+
+ for (let i = 0; i < sites.length; i++) {
+ let site = sites[i];
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ if (site.searchTopSite) {
+ Assert.equal(
+ result.searchParams.keyword,
+ site.label,
+ "The search Top Site should have an alias."
+ );
+ continue;
+ }
+
+ Assert.equal(
+ site.url,
+ result.url,
+ "The Top Site URL and the result URL shoud match."
+ );
+ Assert.equal(
+ site.label || site.title || site.hostname,
+ result.title,
+ "The Top Site title and the result title shoud match."
+ );
+ }
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ gURLBar.blur();
+ });
+ // This pops updateTopSites changes.
+ await SpecialPowers.popPrefEnv();
+ await SpecialPowers.popPrefEnv();
+ }
+});
+
+add_task(async function selectSearchTopSite() {
+ await updateTopSites(
+ sites => sites && sites[0] && sites[0].searchTopSite,
+ true
+ );
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ if (gURLBar.getAttribute("pageproxystate") == "invalid") {
+ gURLBar.handleRevert();
+ }
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ let amazonSearch = await UrlbarTestUtils.waitForAutocompleteResultAt(
+ window,
+ 0
+ );
+
+ Assert.equal(
+ amazonSearch.result.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "First result should have SEARCH type."
+ );
+
+ Assert.equal(
+ amazonSearch.result.payload.keyword,
+ "@amazon",
+ "First result should have the Amazon keyword."
+ );
+
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeMouseAtCenter(amazonSearch, {});
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: amazonSearch.result.payload.engine,
+ entry: "topsites_urlbar",
+ });
+ await UrlbarTestUtils.exitSearchMode(window, { backspace: true });
+
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ gURLBar.blur();
+ });
+});
+
+add_task(async function topSitesBookmarksAndTabs() {
+ await addTestVisits();
+ let sites = AboutNewTab.getTopSites();
+ Assert.equal(
+ sites.length,
+ 7,
+ "The test suite browser should have 7 Top Sites."
+ );
+
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ if (gURLBar.getAttribute("pageproxystate") == "invalid") {
+ gURLBar.handleRevert();
+ }
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ Assert.ok(gURLBar.view.isOpen, "UrlbarView should be open.");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 7,
+ "The number of results should be the same as the number of Top Sites (7)."
+ );
+
+ let exampleResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ exampleResult.url,
+ "http://example.com/",
+ "The example.com Top Site should be the first result."
+ );
+ Assert.equal(
+ exampleResult.source,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ "The example.com Top Site should appear in the view as an open tab result."
+ );
+
+ let youtubeResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(
+ youtubeResult.url,
+ "https://www.youtube.com/",
+ "The YouTube Top Site should be the second result."
+ );
+ Assert.equal(
+ youtubeResult.source,
+ UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ "The YouTube Top Site should appear in the view as a bookmark result."
+ );
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ gURLBar.blur();
+ });
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function topSitesKeywordNavigationPageproxystate() {
+ await addTestVisits();
+ Assert.equal(
+ gURLBar.getAttribute("pageproxystate"),
+ "valid",
+ "Sanity check initial state"
+ );
+
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ if (gURLBar.getAttribute("pageproxystate") == "invalid") {
+ gURLBar.handleRevert();
+ }
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ Assert.ok(gURLBar.view.isOpen, "UrlbarView should be open.");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ let count = UrlbarTestUtils.getResultCount(window);
+ Assert.equal(count, 7, "The number of results should be the expected one.");
+
+ for (let i = 0; i < count; ++i) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.equal(
+ gURLBar.getAttribute("pageproxystate"),
+ "invalid",
+ "Moving through results"
+ );
+ }
+ for (let i = 0; i < count; ++i) {
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ Assert.equal(
+ gURLBar.getAttribute("pageproxystate"),
+ "invalid",
+ "Moving through results"
+ );
+ }
+
+ // Double ESC should restore state.
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeKey("KEY_Escape");
+ });
+ EventUtils.synthesizeKey("KEY_Escape");
+ Assert.equal(
+ gURLBar.getAttribute("pageproxystate"),
+ "valid",
+ "Double ESC should restore state"
+ );
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function topSitesPinned() {
+ await addTestVisits();
+ let info = { url: "http://example.com/" };
+ NewTabUtils.pinnedLinks.pin(info, 0);
+
+ await updateTopSites(sites => sites && sites[0] && sites[0].isPinned);
+
+ let sites = AboutNewTab.getTopSites();
+ Assert.equal(
+ sites.length,
+ 7,
+ "The test suite browser should have 7 Top Sites."
+ );
+
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ Assert.ok(gURLBar.view.isOpen, "UrlbarView should be open.");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 7,
+ "The number of results should be the same as the number of Top Sites (7)."
+ );
+
+ let exampleResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ exampleResult.url,
+ "http://example.com/",
+ "The example.com Top Site should be the first result."
+ );
+
+ Assert.equal(
+ exampleResult.source,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ "The example.com Top Site should be an open tab result."
+ );
+
+ Assert.ok(
+ exampleResult.element.row.hasAttribute("pinned"),
+ "The example.com Top Site should have the pinned property."
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ gURLBar.blur();
+ });
+ NewTabUtils.pinnedLinks.unpin(info);
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function topSitesBookmarksAndTabsDisabled() {
+ await addTestVisits();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.openpage", false],
+ ["browser.urlbar.suggest.bookmark", false],
+ ],
+ });
+
+ let sites = AboutNewTab.getTopSites();
+ Assert.equal(
+ sites.length,
+ 7,
+ "The test suite browser should have 7 Top Sites."
+ );
+
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ if (gURLBar.getAttribute("pageproxystate") == "invalid") {
+ gURLBar.handleRevert();
+ }
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ Assert.ok(gURLBar.view.isOpen, "UrlbarView should be open.");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 7,
+ "The number of results should be the same as the number of Top Sites (7)."
+ );
+
+ let exampleResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(
+ exampleResult.url,
+ "http://example.com/",
+ "The example.com Top Site should be the second result."
+ );
+ Assert.equal(
+ exampleResult.source,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ "The example.com Top Site should appear as a normal result even though it's open in a tab."
+ );
+
+ let youtubeResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(
+ youtubeResult.url,
+ "https://www.youtube.com/",
+ "The YouTube Top Site should be the third result."
+ );
+ Assert.equal(
+ youtubeResult.source,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ "The YouTube Top Site should appear as a normal result even though it's bookmarked."
+ );
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ gURLBar.blur();
+ });
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function topSitesDisabled() {
+ // Disable Top Sites feed.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.newtabpage.activity-stream.feeds.system.topsites", false]],
+ });
+ await checkDoesNotOpenOnFocus();
+ await SpecialPowers.popPrefEnv();
+
+ // Top Sites should also not be shown when Urlbar Top Sites are disabled.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.topsites", false]],
+ });
+ await checkDoesNotOpenOnFocus();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function topSitesNumber() {
+ // Add some visits
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits([
+ "http://example-a.com/",
+ "http://example-b.com/",
+ "http://example-c.com/",
+ "http://example-d.com/",
+ "http://example-e.com/",
+ ]);
+ }
+
+ // Wait for the expected number of Top sites.
+ await updateTopSites(sites => sites && sites.length == 8);
+ Assert.equal(
+ AboutNewTab.getTopSites().length,
+ 8,
+ "The test suite browser should have 8 Top Sites."
+ );
+
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ Assert.ok(gURLBar.view.isOpen, "UrlbarView should be open.");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 8,
+ "The number of results should be the default (8)."
+ );
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.newtabpage.activity-stream.topSitesRows", 2]],
+ });
+ // Wait for the expected number of Top sites.
+ await updateTopSites(sites => sites && sites.length == 11);
+ Assert.equal(
+ AboutNewTab.getTopSites().length,
+ 11,
+ "The test suite browser should have 11 Top Sites."
+ );
+
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ Assert.ok(gURLBar.view.isOpen, "UrlbarView should be open.");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 10,
+ "The number of results should be maxRichResults (10)."
+ );
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ await SpecialPowers.popPrefEnv();
+ await PlacesUtils.history.clear();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_top_sites_private.js b/browser/components/urlbar/tests/browser/browser_top_sites_private.js
new file mode 100644
index 0000000000..bcc6a70d88
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_top_sites_private.js
@@ -0,0 +1,174 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AboutNewTab: "resource:///modules/AboutNewTab.jsm",
+});
+
+const EN_US_TOPSITES =
+ "https://www.youtube.com/,https://www.facebook.com/,https://www.amazon.com/,https://www.reddit.com/,https://www.wikipedia.org/,https://twitter.com/";
+
+async function addTestVisits() {
+ // Add some visits to a URL.
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits("http://example.com/");
+ }
+
+ // Wait for example.com to be listed first.
+ await updateTopSites(sites => {
+ return sites && sites[0] && sites[0].url == "http://example.com/";
+ });
+
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "https://www.youtube.com/",
+ title: "YouTube",
+ });
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.topsites", true],
+ ["browser.urlbar.suggest.quickactions", false],
+ ["browser.newtabpage.activity-stream.default.sites", EN_US_TOPSITES],
+ ],
+ });
+
+ await updateTopSites(
+ sites => sites && sites.length == EN_US_TOPSITES.split(",").length
+ );
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/"
+ );
+
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(tab);
+ });
+});
+
+add_task(async function topSitesPrivateWindow() {
+ // Top Sites should also be shown in private windows.
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ await addTestVisits();
+ let sites = AboutNewTab.getTopSites();
+ Assert.equal(
+ sites.length,
+ 7,
+ "The test suite browser should have 7 Top Sites."
+ );
+ let urlbar = privateWin.gURLBar;
+ await UrlbarTestUtils.promisePopupOpen(privateWin, () => {
+ if (urlbar.getAttribute("pageproxystate") == "invalid") {
+ urlbar.handleRevert();
+ }
+ EventUtils.synthesizeMouseAtCenter(urlbar.inputField, {}, privateWin);
+ });
+ Assert.ok(urlbar.view.isOpen, "UrlbarView should be open.");
+ await UrlbarTestUtils.promiseSearchComplete(privateWin);
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(privateWin),
+ 7,
+ "The number of results should be the same as the number of Top Sites (7)."
+ );
+
+ // Top sites should also be shown in a private window if the search string
+ // gets cleared.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: privateWin,
+ value: "example",
+ });
+ urlbar.select();
+ EventUtils.synthesizeKey("KEY_Backspace", {}, privateWin);
+ Assert.ok(urlbar.view.isOpen, "UrlbarView should be open.");
+ await UrlbarTestUtils.promiseSearchComplete(privateWin);
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(privateWin),
+ 7,
+ "The number of results should be the same as the number of Top Sites (7)."
+ );
+
+ await BrowserTestUtils.closeWindow(privateWin);
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ gURLBar.blur();
+ });
+});
+
+add_task(async function topSitesTabSwitch() {
+ // Add some visits
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits(["http://example.com/"]);
+ }
+
+ // Switch to the originating tab, to check for switch to the current tab.
+ gBrowser.selectedTab = gBrowser.tabs[0];
+
+ // Wait for the expected number of Top sites.
+ await updateTopSites(sites => sites?.length == 7);
+ Assert.equal(
+ AboutNewTab.getTopSites().length,
+ 7,
+ "The test suite browser should have 7 Top Sites."
+ );
+
+ async function checkResults(win, expectedResultType) {
+ let resultCount = UrlbarTestUtils.getResultCount(win);
+ let result;
+ for (let i = 0; i < resultCount; ++i) {
+ result = await UrlbarTestUtils.getDetailsOfResultAt(win, i);
+ if (result.url == "http://example.com/") {
+ break;
+ }
+ }
+ Assert.equal(
+ result.type,
+ expectedResultType,
+ `Should provide a result of type ${expectedResultType}.`
+ );
+ }
+
+ info("Test in a non-private window");
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ Assert.ok(gURLBar.view.isOpen, "UrlbarView should be open.");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await checkResults(window, UrlbarUtils.RESULT_TYPE.TAB_SWITCH);
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ info("Test in a private window, switch to tab should not be offered");
+ // Top Sites should also be shown in private windows.
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ let urlbar = privateWin.gURLBar;
+ await UrlbarTestUtils.promisePopupOpen(privateWin, () => {
+ if (urlbar.getAttribute("pageproxystate") == "invalid") {
+ urlbar.handleRevert();
+ }
+ EventUtils.synthesizeMouseAtCenter(urlbar.inputField, {}, privateWin);
+ });
+
+ Assert.ok(urlbar.view.isOpen, "UrlbarView should be open.");
+ await UrlbarTestUtils.promiseSearchComplete(privateWin);
+ await checkResults(privateWin, UrlbarUtils.RESULT_TYPE.URL);
+ await UrlbarTestUtils.promisePopupClose(privateWin);
+ await BrowserTestUtils.closeWindow(privateWin);
+
+ await PlacesUtils.history.clear();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_typed_value.js b/browser/components/urlbar/tests/browser/browser_typed_value.js
new file mode 100644
index 0000000000..ca4566d172
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_typed_value.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test ensures that the urlbar is restored to the typed value on blur.
+
+"use strict";
+
+add_setup(async function () {
+ registerCleanupFunction(async function () {
+ Services.prefs.clearUserPref("browser.urlbar.autoFill");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions");
+ gURLBar.handleRevert();
+ await PlacesUtils.history.clear();
+ });
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false);
+
+ await PlacesTestUtils.addVisits([
+ "http://example.com/",
+ "http://example.com/foo",
+ ]);
+});
+
+add_task(async function test_autofill() {
+ let typed = "ex";
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: typed,
+ fireInputEvent: true,
+ });
+ Assert.equal(gURLBar.value, "example.com/", "autofilled value as expected");
+ Assert.equal(gURLBar.selectionStart, typed.length);
+ Assert.equal(gURLBar.selectionEnd, gURLBar.value.length);
+
+ gURLBar.blur();
+ Assert.equal(gURLBar.value, typed, "Value should have been restored");
+ gURLBar.focus();
+ Assert.equal(gURLBar.value, typed, "Value should not have changed");
+ Assert.equal(gURLBar.selectionStart, typed.length);
+ Assert.equal(gURLBar.selectionEnd, typed.length);
+});
+
+add_task(async function test_complete_selection() {
+ let typed = "ex";
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: typed,
+ fireInputEvent: true,
+ });
+ Assert.equal(gURLBar.value, "example.com/", "autofilled value as expected");
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 2,
+ "Should have the correct number of matches"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.equal(
+ gURLBar.value,
+ "example.com/foo",
+ "Value should have been completed"
+ );
+
+ gURLBar.blur();
+ Assert.equal(gURLBar.value, typed, "Value should have been restored");
+ gURLBar.focus();
+ Assert.equal(gURLBar.value, typed, "Value should not have changed");
+ Assert.equal(gURLBar.selectionStart, typed.length);
+ Assert.equal(gURLBar.selectionEnd, typed.length);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_unitConversion.js b/browser/components/urlbar/tests/browser/browser_unitConversion.js
new file mode 100644
index 0000000000..566300b7d4
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_unitConversion.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests unit conversion on browser.
+ */
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.unitConversion.enabled", true]],
+ });
+
+ registerCleanupFunction(function () {
+ SpecialPowers.clipboardCopyString("");
+ });
+});
+
+add_task(async function test_selectByMouse() {
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+
+ // Clear clipboard content.
+ SpecialPowers.clipboardCopyString("");
+
+ const row = await doUnitConversion(win);
+
+ info("Check if the result is copied to clipboard when selecting by mouse");
+ EventUtils.synthesizeMouseAtCenter(
+ row.querySelector(".urlbarView-no-wrap"),
+ {},
+ win
+ );
+ assertClipboard();
+
+ await UrlbarTestUtils.promisePopupClose(win);
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_selectByKey() {
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+
+ // Clear clipboard content.
+ SpecialPowers.clipboardCopyString("");
+
+ await doUnitConversion(win);
+
+ // As gURLBar might lost focus,
+ // give focus again in order to enable key event on the result.
+ win.gURLBar.focus();
+
+ info("Check if the result is copied to clipboard when selecting by key");
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ EventUtils.synthesizeKey("KEY_Enter", {}, win);
+ assertClipboard();
+
+ await UrlbarTestUtils.promisePopupClose(win);
+ await BrowserTestUtils.closeWindow(win);
+});
+
+function assertClipboard() {
+ Assert.equal(
+ SpecialPowers.getClipboardData("text/plain"),
+ "100 cm",
+ "The result of conversion is copied to clipboard"
+ );
+}
+
+async function doUnitConversion(win) {
+ info("Do unit conversion then wait the result");
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "1m to cm",
+ waitForFocus: SimpleTest.waitForFocus,
+ });
+
+ const row = await UrlbarTestUtils.waitForAutocompleteResultAt(win, 1);
+
+ Assert.ok(row.querySelector(".urlbarView-favicon"), "The icon is displayed");
+ Assert.equal(
+ row.querySelector(".urlbarView-dynamic-unitConversion-output").textContent,
+ "100 cm",
+ "The unit is converted"
+ );
+
+ return row;
+}
diff --git a/browser/components/urlbar/tests/browser/browser_updateForDomainCompletion.js b/browser/components/urlbar/tests/browser/browser_updateForDomainCompletion.js
new file mode 100644
index 0000000000..4f34b5d52a
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_updateForDomainCompletion.js
@@ -0,0 +1,51 @@
+"use strict";
+
+/**
+ * Disable keyword.enabled (so no keyword search), and check that when
+ * you type in "example" and hit enter, the browser shows an error page.
+ */
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["keyword.enabled", false]],
+ });
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async function (browser) {
+ gURLBar.value = "example";
+ gURLBar.select();
+ const loadPromise = BrowserTestUtils.waitForErrorPage(browser);
+ EventUtils.sendKey("return");
+ await loadPromise;
+ ok(true, "error page is loaded correctly");
+ }
+ );
+});
+
+/**
+ * Disable keyword.enabled (so no keyword search) and enable fixup.alternate, and check
+ * that when you type in "example" and hit enter, the browser loads and the URL bar
+ * is updated accordingly.
+ */
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["keyword.enabled", false],
+ ["browser.fixup.alternate.enabled", true],
+ ],
+ });
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async function (browser) {
+ gURLBar.value = "example";
+ gURLBar.select();
+ const loadPromise = BrowserTestUtils.waitForDocLoadAndStopIt(
+ "https://www.example.com/",
+ gBrowser.selectedBrowser
+ );
+
+ EventUtils.sendKey("return");
+ await loadPromise;
+ ok(true, "https://www.example.com is loaded correctly");
+ }
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_annotation.js b/browser/components/urlbar/tests/browser/browser_urlbar_annotation.js
new file mode 100644
index 0000000000..9f736ea6af
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_urlbar_annotation.js
@@ -0,0 +1,333 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test whether a visit information is annotated correctly when picking a result.
+
+if (AppConstants.platform === "macosx") {
+ requestLongerTimeout(2);
+}
+
+const FRECENCY = {
+ ORGANIC: 2000,
+ SPONSORED: -1,
+ BOOKMARKED: 2075,
+ SEARCHED: 100,
+};
+
+const {
+ VISIT_SOURCE_ORGANIC,
+ VISIT_SOURCE_SPONSORED,
+ VISIT_SOURCE_BOOKMARKED,
+ VISIT_SOURCE_SEARCHED,
+} = PlacesUtils.history;
+
+/**
+ * To be used before checking database contents when they depend on a visit
+ * being added to History.
+ *
+ * @param {string} href the page to await notifications for.
+ */
+async function waitForVisitNotification(href) {
+ await PlacesTestUtils.waitForNotification("page-visited", events =>
+ events.some(e => e.url === href)
+ );
+}
+
+async function assertDatabase({ targetURL, expected }) {
+ const frecency = await PlacesTestUtils.getDatabaseValue(
+ "moz_places",
+ "frecency",
+ { url: targetURL }
+ );
+ Assert.equal(frecency, expected.frecency, "Frecency is correct");
+
+ const placesId = await PlacesTestUtils.getDatabaseValue("moz_places", "id", {
+ url: targetURL,
+ });
+ const expectedTriggeringPlaceId = expected.triggerURL
+ ? await PlacesTestUtils.getDatabaseValue("moz_places", "id", {
+ url: expected.triggerURL,
+ })
+ : null;
+ const db = await PlacesUtils.promiseDBConnection();
+ const rows = await db.execute(
+ "SELECT source, triggeringPlaceId FROM moz_historyvisits WHERE place_id = :place_id AND source = :source",
+ {
+ place_id: placesId,
+ source: expected.source,
+ }
+ );
+ Assert.equal(rows.length, 1);
+ Assert.equal(
+ rows[0].getResultByName("triggeringPlaceId"),
+ expectedTriggeringPlaceId,
+ `The triggeringPlaceId in database is correct for ${targetURL}`
+ );
+}
+
+function registerProvider(payload) {
+ const provider = new UrlbarTestUtils.TestProvider({
+ results: [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ ...UrlbarResult.payloadAndSimpleHighlights([], {
+ ...payload,
+ })
+ ),
+ ],
+ priority: Infinity,
+ });
+ UrlbarProvidersManager.registerProvider(provider);
+ return provider;
+}
+
+async function pickResult({ input, payloadURL, redirectTo }) {
+ const destinationURL = redirectTo || payloadURL;
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: input,
+ fireInputEvent: true,
+ });
+
+ const result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(result.url, payloadURL);
+ UrlbarTestUtils.setSelectedRowIndex(window, 0);
+
+ info("Show result and wait for loading");
+ const onLoad = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ destinationURL
+ );
+ EventUtils.synthesizeKey("KEY_Enter");
+ await onLoad;
+}
+
+add_setup(async function () {
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+});
+
+add_task(async function basic() {
+ const testData = [
+ {
+ description: "Sponsored result",
+ input: "exa",
+ payload: {
+ url: "http://example.com/",
+ isSponsored: true,
+ },
+ expected: {
+ source: VISIT_SOURCE_SPONSORED,
+ frecency: FRECENCY.SPONSORED,
+ },
+ },
+ {
+ description: "Bookmarked result",
+ input: "exa",
+ payload: {
+ url: "http://example.com/",
+ },
+ bookmarks: [
+ {
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: Services.io.newURI("http://example.com/"),
+ title: "test bookmark",
+ },
+ ],
+ expected: {
+ source: VISIT_SOURCE_BOOKMARKED,
+ frecency: FRECENCY.BOOKMARKED,
+ },
+ },
+ {
+ description: "Sponsored and bookmarked result",
+ input: "exa",
+ payload: {
+ url: "http://example.com/",
+ isSponsored: true,
+ },
+ bookmarks: [
+ {
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: Services.io.newURI("http://example.com/"),
+ title: "test bookmark",
+ },
+ ],
+ expected: {
+ source: VISIT_SOURCE_SPONSORED,
+ frecency: FRECENCY.BOOKMARKED,
+ },
+ },
+ {
+ description: "Organic result",
+ input: "exa",
+ payload: {
+ url: "http://example.com/",
+ },
+ expected: {
+ source: VISIT_SOURCE_ORGANIC,
+ frecency: FRECENCY.ORGANIC,
+ },
+ },
+ ];
+
+ for (const { description, input, payload, bookmarks, expected } of testData) {
+ info(description);
+ const provider = registerProvider(payload);
+
+ for (const bookmark of bookmarks || []) {
+ await PlacesUtils.bookmarks.insert(bookmark);
+ }
+
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ info("Pick result");
+ let promiseVisited = waitForVisitNotification(payload.url);
+ await pickResult({ input, payloadURL: payload.url });
+ await promiseVisited;
+ info("Check database");
+ await assertDatabase({ targetURL: payload.url, expected });
+ });
+
+ UrlbarProvidersManager.unregisterProvider(provider);
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ }
+});
+
+add_task(async function redirection() {
+ const redirectTo = "http://example.com/";
+ const payload = {
+ url: "http://example.com/browser/browser/components/urlbar/tests/browser/redirect_to.sjs?/",
+ isSponsored: true,
+ };
+ const input = "exa";
+ const provider = registerProvider(payload);
+
+ await BrowserTestUtils.withNewTab("about:home", async () => {
+ info("Pick result");
+ let promises = [
+ waitForVisitNotification(payload.url),
+ waitForVisitNotification(redirectTo),
+ ];
+ await pickResult({ input, payloadURL: payload.url, redirectTo });
+ await Promise.all(promises);
+
+ info("Check database");
+ await assertDatabase({
+ targetURL: payload.url,
+ expected: {
+ source: VISIT_SOURCE_SPONSORED,
+ frecency: FRECENCY.SPONSORED,
+ },
+ });
+ await assertDatabase({
+ targetURL: redirectTo,
+ expected: {
+ source: VISIT_SOURCE_SPONSORED,
+ triggerURL: payload.url,
+ frecency: FRECENCY.SPONSORED,
+ },
+ });
+ });
+
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ UrlbarProvidersManager.unregisterProvider(provider);
+});
+
+add_task(async function search() {
+ const originalDefaultEngine = await Services.search.getDefault();
+ await SearchTestUtils.installSearchExtension({
+ name: "test engine",
+ keyword: "@test",
+ });
+
+ const testData = [
+ {
+ description: "Searched result",
+ input: "@test abc",
+ resultURL: "https://example.com/?q=abc",
+ expected: {
+ source: VISIT_SOURCE_SEARCHED,
+ frecency: FRECENCY.SEARCHED,
+ },
+ },
+ {
+ description: "Searched bookmarked result",
+ input: "@test abc",
+ resultURL: "https://example.com/?q=abc",
+ bookmarks: [
+ {
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: Services.io.newURI("https://example.com/?q=abc"),
+ title: "test bookmark",
+ },
+ ],
+ expected: {
+ source: VISIT_SOURCE_BOOKMARKED,
+ frecency: FRECENCY.BOOKMARKED,
+ },
+ },
+ ];
+
+ for (const {
+ description,
+ input,
+ resultURL,
+ bookmarks,
+ expected,
+ } of testData) {
+ info(description);
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ for (const bookmark of bookmarks || []) {
+ await PlacesUtils.bookmarks.insert(bookmark);
+ }
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: input,
+ });
+ const onLoad = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ resultURL
+ );
+ let promiseVisited = waitForVisitNotification(resultURL);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await onLoad;
+ await promiseVisited;
+ await assertDatabase({ targetURL: resultURL, expected });
+
+ // Open another URL to check whther the source is not inherited.
+ const payload = { url: "http://example.com/" };
+ const provider = registerProvider(payload);
+ promiseVisited = waitForVisitNotification(payload.url);
+ await pickResult({ input, payloadURL: payload.url });
+ await promiseVisited;
+ await assertDatabase({
+ targetURL: payload.url,
+ expected: {
+ source: VISIT_SOURCE_ORGANIC,
+ frecency: FRECENCY.ORGANIC,
+ },
+ });
+ UrlbarProvidersManager.unregisterProvider(provider);
+
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+ }
+
+ await Services.search.setDefault(
+ originalDefaultEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry_abandonment.js b/browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry_abandonment.js
new file mode 100644
index 0000000000..6f30392e48
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry_abandonment.js
@@ -0,0 +1,357 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
+});
+
+const TEST_ENGINE_NAME = "Test";
+const TEST_ENGINE_ALIAS = "@test";
+const TEST_ENGINE_DOMAIN = "example.com";
+
+// Each test is a function that executes an urlbar action and returns the
+// expected event object.
+const tests = [
+ async function (win) {
+ info("Type something, blur.");
+ win.gURLBar.select();
+ EventUtils.synthesizeKey("x", {}, win);
+ win.gURLBar.blur();
+ return {
+ category: "urlbar",
+ method: "abandonment",
+ object: "blur",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "1",
+ numWords: "1",
+ },
+ };
+ },
+
+ async function (win) {
+ info("Open the panel with DOWN, don't type, blur it.");
+ await addTopSite("http://example.org/");
+ win.gURLBar.value = "";
+ win.gURLBar.select();
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ });
+ win.gURLBar.blur();
+ return {
+ category: "urlbar",
+ method: "abandonment",
+ object: "blur",
+ value: "topsites",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "0",
+ numWords: "0",
+ },
+ };
+ },
+
+ async function (win) {
+ info("With pageproxystate=valid, autoopen the panel, don't type, blur it.");
+ win.gURLBar.value = "";
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ win.document.getElementById("Browser:OpenLocation").doCommand();
+ });
+ win.gURLBar.blur();
+ return {
+ category: "urlbar",
+ method: "abandonment",
+ object: "blur",
+ value: "topsites",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "0",
+ numWords: "0",
+ },
+ };
+ },
+
+ async function (win) {
+ info("Enter search mode from Top Sites.");
+ await updateTopSites(sites => true, /* enableSearchShorcuts */ true);
+
+ win.gURLBar.value = "";
+ win.gURLBar.select();
+
+ await BrowserTestUtils.waitForCondition(async () => {
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ });
+
+ if (UrlbarTestUtils.getResultCount(win) > 1) {
+ return true;
+ }
+
+ win.gURLBar.view.close();
+ return false;
+ });
+
+ while (win.gURLBar.searchMode?.engineName != "Google") {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ }
+
+ let element = UrlbarTestUtils.getSelectedRow(win);
+ Assert.ok(
+ element.result.source == UrlbarUtils.RESULT_SOURCE.SEARCH,
+ "The selected result is a search Top Site."
+ );
+
+ let engine = element.result.payload.engine;
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(win);
+ EventUtils.synthesizeMouseAtCenter(element, {}, win);
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(win, {
+ engineName: engine,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ entry: "topsites_urlbar",
+ });
+
+ await UrlbarTestUtils.exitSearchMode(win);
+
+ // To avoid needing to add a custom search shortcut Top Site, we just
+ // abandon this interaction.
+ await UrlbarTestUtils.promisePopupClose(win, () => {
+ win.gURLBar.blur();
+ });
+
+ return [
+ // engagement on the top sites search engine to enter search mode
+ {
+ category: "urlbar",
+ method: "engagement",
+ object: "click",
+ value: "topsites",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "0",
+ numWords: "0",
+ selIndex: "0",
+ selType: "searchengine",
+ provider: "UrlbarProviderTopSites",
+ },
+ },
+ // abandonment
+ {
+ category: "urlbar",
+ method: "abandonment",
+ object: "blur",
+ value: "topsites",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "0",
+ numWords: "0",
+ },
+ },
+ ];
+ },
+
+ async function (win) {
+ info("Open search mode from a tab-to-search result.");
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0]],
+ });
+
+ await PlacesUtils.history.clear();
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits([`https://${TEST_ENGINE_DOMAIN}/`]);
+ }
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: TEST_ENGINE_DOMAIN.slice(0, 4),
+ });
+
+ let tabToSearchResult = (
+ await UrlbarTestUtils.waitForAutocompleteResultAt(win, 1)
+ ).result;
+ Assert.equal(
+ tabToSearchResult.providerName,
+ "TabToSearch",
+ "The second result is a tab-to-search result."
+ );
+
+ // Select the tab-to-search result.
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(win);
+ EventUtils.synthesizeKey("KEY_Enter", {}, win);
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(win, {
+ engineName: TEST_ENGINE_NAME,
+ entry: "tabtosearch",
+ });
+
+ // Abandon the interaction since simply entering search mode is not
+ // considered the end of an engagement.
+ await UrlbarTestUtils.promisePopupClose(win, () => {
+ win.gURLBar.blur();
+ });
+
+ await PlacesUtils.history.clear();
+ await SpecialPowers.popPrefEnv();
+
+ return [
+ // engagement on the tab-to-search to enter search mode
+ {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "4",
+ numWords: "1",
+ selIndex: "1",
+ selType: "tabtosearch",
+ provider: "TabToSearch",
+ },
+ },
+ // abandonment
+ {
+ category: "urlbar",
+ method: "abandonment",
+ object: "blur",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "0",
+ numWords: "0",
+ },
+ },
+ ];
+ },
+
+ async function (win) {
+ info(
+ "With pageproxystate=invalid, open retained results, don't type, blur it."
+ );
+ win.gURLBar.value = "mochi.test";
+ win.gURLBar.setPageProxyState("invalid");
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ win.document.getElementById("Browser:OpenLocation").doCommand();
+ });
+ win.gURLBar.blur();
+ return {
+ category: "urlbar",
+ method: "abandonment",
+ object: "blur",
+ value: "returned",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "10",
+ numWords: "1",
+ },
+ };
+ },
+];
+
+add_setup(async function () {
+ await PlacesUtils.history.clear();
+
+ // Create a new search engine and mark it as default
+ let engine = await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + "searchSuggestionEngine.xml",
+ setAsDefault: true,
+ });
+ await Services.search.moveEngine(engine, 0);
+
+ await SearchTestUtils.installSearchExtension({
+ name: TEST_ENGINE_NAME,
+ keyword: TEST_ENGINE_ALIAS,
+ search_url: `https://${TEST_ENGINE_DOMAIN}/`,
+ });
+
+ // This test used to rely on the initial timer of
+ // TestUtils.waitForCondition. See bug 1667216.
+ let originalWaitForCondition = TestUtils.waitForCondition;
+ TestUtils.waitForCondition = async function (
+ condition,
+ msg,
+ interval = 100,
+ maxTries = 50
+ ) {
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 100));
+
+ return originalWaitForCondition(condition, msg, interval, maxTries);
+ };
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.history.clear();
+ TestUtils.waitForCondition = originalWaitForCondition;
+ });
+});
+
+async function doTest(eventTelemetryEnabled) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.eventTelemetry.enabled", eventTelemetryEnabled]],
+ });
+
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+
+ // This is not necessary after each loop, because assertEvents does it.
+ Services.telemetry.clearEvents();
+ Services.telemetry.clearScalars();
+
+ for (let i = 0; i < tests.length; i++) {
+ info(`Running test at index ${i}`);
+ let events = await tests[i](win);
+ if (!Array.isArray(events)) {
+ events = [events];
+ }
+ // Always blur to ensure it's not accounted as an additional abandonment.
+ win.gURLBar.setSearchMode({});
+ win.gURLBar.blur();
+ TelemetryTestUtils.assertEvents(eventTelemetryEnabled ? events : [], {
+ category: "urlbar",
+ });
+
+ // Scalars should be recorded regardless of `eventTelemetry.enabled`.
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "urlbar.engagement",
+ events.filter(e => e.method == "engagement").length || undefined
+ );
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "urlbar.abandonment",
+ events.filter(e => e.method == "abandonment").length || undefined
+ );
+
+ await UrlbarTestUtils.formHistory.clear(win);
+ }
+
+ await BrowserTestUtils.closeWindow(win);
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function enabled() {
+ await doTest(true);
+});
+
+add_task(async function disabled() {
+ await doTest(false);
+});
+
+/**
+ * Replaces the contents of Top Sites with the specified site.
+ *
+ * @param {string} site
+ * A site to add to Top Sites.
+ */
+async function addTopSite(site) {
+ await PlacesUtils.history.clear();
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits(site);
+ }
+
+ await updateTopSites(sites => sites && sites[0] && sites[0].url == site);
+}
diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry_engagement.js b/browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry_engagement.js
new file mode 100644
index 0000000000..c1fd36b452
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry_engagement.js
@@ -0,0 +1,1340 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarProvider: "resource:///modules/UrlbarUtils.sys.mjs",
+ UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
+});
+
+const TEST_ENGINE_NAME = "Test";
+const TEST_ENGINE_ALIAS = "@test";
+const TEST_ENGINE_DOMAIN = "example.com";
+
+// This test has many subtests and can time out in verify mode.
+requestLongerTimeout(5);
+
+// Each test is a function that executes an urlbar action and returns the
+// expected event object.
+const tests = [
+ async function (win) {
+ info("Type something, press Enter.");
+ win.gURLBar.select();
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "x",
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "1",
+ numWords: "1",
+ selIndex: "0",
+ selType: "searchengine",
+ provider: "HeuristicFallback",
+ },
+ };
+ },
+
+ async function (win) {
+ info("Type a multi-word query, press Enter.");
+ win.gURLBar.select();
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "multi word query ",
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "17",
+ numWords: "3",
+ selIndex: "0",
+ selType: "searchengine",
+ provider: "HeuristicFallback",
+ },
+ };
+ },
+
+ async function (win) {
+ info("Paste something, press Enter.");
+ win.gURLBar.select();
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await SimpleTest.promiseClipboardChange("test", () => {
+ clipboardHelper.copyString("test");
+ });
+ win.document.commandDispatcher
+ .getControllerForCommand("cmd_paste")
+ .doCommand("cmd_paste");
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "pasted",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "4",
+ numWords: "1",
+ selIndex: "0",
+ selType: "searchengine",
+ provider: "HeuristicFallback",
+ },
+ };
+ },
+
+ async function (win) {
+ info("Type something, click one-off and press enter.");
+ win.gURLBar.select();
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "test",
+ fireInputEvent: true,
+ });
+
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(win);
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }, win);
+ let selectedOneOff =
+ UrlbarTestUtils.getOneOffSearchButtons(win).selectedButton;
+ selectedOneOff.click();
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(win, {
+ engineName: selectedOneOff.engine.name,
+ entry: "oneoff",
+ });
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "4",
+ numWords: "1",
+ selIndex: "0",
+ selType: "searchengine",
+ provider: "HeuristicFallback",
+ },
+ };
+ },
+
+ async function (win) {
+ info(
+ "Type something, select one-off with enter, and select result with enter."
+ );
+ win.gURLBar.select();
+
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(win);
+
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "test",
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }, win);
+ let selectedOneOff =
+ UrlbarTestUtils.getOneOffSearchButtons(win).selectedButton;
+ Assert.ok(selectedOneOff);
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await searchPromise;
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "4",
+ numWords: "1",
+ selIndex: "0",
+ selType: "searchengine",
+ provider: "HeuristicFallback",
+ },
+ };
+ },
+
+ async function (win) {
+ info("Type something, ESC, type something else, press Enter.");
+ win.gURLBar.select();
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ EventUtils.synthesizeKey("x", {}, win);
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, win);
+ EventUtils.synthesizeKey("y", {}, win);
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "1",
+ numWords: "1",
+ selIndex: "0",
+ selType: "searchengine",
+ provider: "HeuristicFallback",
+ },
+ };
+ },
+
+ async function (win) {
+ info("Type a keyword, Enter.");
+ win.gURLBar.select();
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "kw test",
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "7",
+ numWords: "2",
+ selIndex: "0",
+ selType: "keyword",
+ provider: "BookmarkKeywords",
+ },
+ };
+ },
+
+ async function (win) {
+ let tipProvider = registerTipProvider();
+ info("Selecting a tip's main button, enter.");
+ win.gURLBar.search("x");
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ unregisterTipProvider(tipProvider);
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "1",
+ numWords: "1",
+ selIndex: "1",
+ selType: "tip",
+ provider: tipProvider.name,
+ },
+ };
+ },
+
+ async function (win) {
+ let tipProvider = registerTipProvider();
+ info("Selecting a tip's help option.");
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ win.gURLBar.search("x");
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ if (UrlbarPrefs.get("resultMenu")) {
+ await UrlbarTestUtils.openResultMenuAndPressAccesskey(win, "h");
+ } else {
+ EventUtils.synthesizeKey("KEY_Tab", {}, win);
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ }
+ await promise;
+ unregisterTipProvider(tipProvider);
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "1",
+ numWords: "1",
+ selIndex: "1",
+ selType: "tiphelp",
+ provider: tipProvider.name,
+ },
+ };
+ },
+
+ async function (win) {
+ info("Type something and canonize");
+ win.gURLBar.select();
+ const promise = BrowserTestUtils.waitForDocLoadAndStopIt(
+ "https://www.example.com/",
+ win.gBrowser.selectedBrowser
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "example",
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("VK_RETURN", { ctrlKey: true }, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "7",
+ numWords: "1",
+ selIndex: "0",
+ selType: "canonized",
+ provider: "Autofill",
+ },
+ };
+ },
+
+ async function (win) {
+ info("Type something, click on bookmark entry.");
+ // Add a clean bookmark.
+ const bookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.com/bookmark",
+ title: "bookmark",
+ });
+
+ win.gURLBar.select();
+ let url = "http://example.com/bookmark";
+ let promise = BrowserTestUtils.browserLoaded(
+ win.gBrowser.selectedBrowser,
+ false,
+ url
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "boo",
+ fireInputEvent: true,
+ });
+ while (win.gURLBar.untrimmedValue != url) {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ }
+ let element = UrlbarTestUtils.getSelectedRow(win);
+ EventUtils.synthesizeMouseAtCenter(element, {}, win);
+ await promise;
+ await PlacesUtils.bookmarks.remove(bookmark);
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "click",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "3",
+ numWords: "1",
+ selIndex: val => parseInt(val) > 0,
+ selType: "bookmark",
+ provider: "Places",
+ },
+ };
+ },
+
+ async function (win) {
+ info("Type an autofilled string, Enter.");
+ win.gURLBar.select();
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "exa",
+ fireInputEvent: true,
+ });
+ // Check it's autofilled.
+ Assert.equal(win.gURLBar.selectionStart, 3);
+ Assert.equal(win.gURLBar.selectionEnd, 12);
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "3",
+ numWords: "1",
+ selIndex: "0",
+ selType: "autofill_origin",
+ provider: "Autofill",
+ },
+ };
+ },
+
+ async function (win) {
+ info("Type something, select bookmark entry, Enter.");
+
+ // Add a clean bookmark and the input history in order to detect in InputHistory
+ // provider and to not show adaptive history autofill.
+ const bookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.com/bookmark",
+ title: "bookmark",
+ });
+ await UrlbarUtils.addToInputHistory(
+ "http://example.com/bookmark",
+ "bookmark"
+ );
+
+ win.gURLBar.select();
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "boo",
+ fireInputEvent: true,
+ });
+ while (win.gURLBar.untrimmedValue != "http://example.com/bookmark") {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ }
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ await PlacesUtils.bookmarks.remove(bookmark);
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "3",
+ numWords: "1",
+ selIndex: val => parseInt(val) > 0,
+ selType: "bookmark",
+ provider: "InputHistory",
+ },
+ };
+ },
+
+ async function (win) {
+ info("Type something, select remote search suggestion, Enter.");
+ win.gURLBar.select();
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "foo",
+ fireInputEvent: true,
+ });
+ while (win.gURLBar.untrimmedValue != "foofoo") {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ }
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "3",
+ numWords: "1",
+ selIndex: val => parseInt(val) > 0,
+ selType: "searchsuggestion",
+ provider: "SearchSuggestions",
+ },
+ };
+ },
+
+ async function (win) {
+ info("Type something, select form history, Enter.");
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.maxHistoricalSearchSuggestions", 2]],
+ });
+ await UrlbarTestUtils.formHistory.add(["foofoo", "foobar"]);
+ win.gURLBar.select();
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "foo",
+ fireInputEvent: true,
+ });
+ while (win.gURLBar.untrimmedValue != "foofoo") {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ }
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ await SpecialPowers.popPrefEnv();
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "3",
+ numWords: "1",
+ selIndex: val => parseInt(val) > 0,
+ selType: "formhistory",
+ provider: "SearchSuggestions",
+ },
+ };
+ },
+
+ async function (win) {
+ info("Type @, enter on a keywordoffer, then search and press enter.");
+ win.gURLBar.select();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "@",
+ fireInputEvent: true,
+ });
+
+ while (win.gURLBar.searchMode?.engineName != TEST_ENGINE_NAME) {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ }
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await UrlbarTestUtils.promiseSearchComplete(win);
+
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "moz",
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+
+ return [
+ // engagement on the keyword offer result to enter search mode
+ {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "1",
+ numWords: "1",
+ selIndex: "6",
+ selType: "searchengine",
+ provider: "TokenAliasEngines",
+ },
+ },
+ // engagement on the search heuristic
+ {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "3",
+ numWords: "1",
+ selIndex: "0",
+ selType: "searchengine",
+ provider: "HeuristicFallback",
+ },
+ },
+ ];
+ },
+
+ async function (win) {
+ info("Type an @alias, then space, then search and press enter.");
+ const alias = "testalias";
+ await SearchTestUtils.installSearchExtension({
+ name: "AliasTest",
+ keyword: alias,
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: `${alias} `,
+ });
+
+ await UrlbarTestUtils.assertSearchMode(win, {
+ engineName: "AliasTest",
+ entry: "typed",
+ });
+
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "moz",
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "3",
+ numWords: "1",
+ selIndex: "0",
+ selType: "searchengine",
+ provider: "HeuristicFallback",
+ },
+ };
+ },
+
+ async function (win) {
+ info("Drop something.");
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ EventUtils.synthesizeDrop(
+ win.document.getElementById("back-button"),
+ win.gURLBar.inputField,
+ [[{ type: "text/plain", data: "www.example.com" }]],
+ "copy",
+ win
+ );
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "drop_go",
+ value: "dropped",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "15",
+ numWords: "1",
+ selIndex: "-1",
+ selType: "none",
+ provider: "",
+ },
+ };
+ },
+
+ async function (win) {
+ info("Paste and Go something.");
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await SimpleTest.promiseClipboardChange("www.example.com", () => {
+ clipboardHelper.copyString("www.example.com");
+ });
+ let inputBox = win.gURLBar.querySelector("moz-input-box");
+ let cxmenu = inputBox.menupopup;
+ let cxmenuPromise = BrowserTestUtils.waitForEvent(cxmenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(
+ win.gURLBar.inputField,
+ {
+ type: "contextmenu",
+ button: 2,
+ },
+ win
+ );
+ await cxmenuPromise;
+ let menuitem = inputBox.getMenuItem("paste-and-go");
+ cxmenu.activateItem(menuitem);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "paste_go",
+ value: "pasted",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "15",
+ numWords: "1",
+ selIndex: "-1",
+ selType: "none",
+ provider: "",
+ },
+ };
+ },
+
+ // The URLs in the down arrow/autoOpen tests must vary from test to test,
+ // else the first Top Site results will be a switch-to-tab result and a page
+ // load will not occur.
+ async function (win) {
+ info("Open the panel with DOWN, select with DOWN, Enter.");
+ await addTopSite("http://example.org/");
+ win.gURLBar.value = "";
+ win.gURLBar.select();
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ });
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ while (win.gURLBar.untrimmedValue != "http://example.org/") {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ }
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "topsites",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "0",
+ numWords: "0",
+ selType: "history",
+ selIndex: val => parseInt(val) >= 0,
+ provider: "UrlbarProviderTopSites",
+ },
+ };
+ },
+
+ async function (win) {
+ info("Open the panel with DOWN, click on entry.");
+ await addTopSite("http://example.com/");
+ win.gURLBar.value = "";
+ win.gURLBar.select();
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ });
+ while (win.gURLBar.untrimmedValue != "http://example.com/") {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ }
+ let element = UrlbarTestUtils.getSelectedRow(win);
+ EventUtils.synthesizeMouseAtCenter(element, {}, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "click",
+ value: "topsites",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "0",
+ numWords: "0",
+ selType: "history",
+ selIndex: "0",
+ provider: "UrlbarProviderTopSites",
+ },
+ };
+ },
+
+ // The URLs in the autoOpen tests must vary from test to test, else
+ // the first Top Site results will be a switch-to-tab result and a page load
+ // will not occur.
+ async function (win) {
+ info(
+ "With pageproxystate=valid, autoopen the panel, select with DOWN, Enter."
+ );
+ await addTopSite("http://example.org/");
+ win.gURLBar.value = "";
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ win.document.getElementById("Browser:OpenLocation").doCommand();
+ });
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ while (win.gURLBar.untrimmedValue != "http://example.org/") {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ }
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "topsites",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "0",
+ numWords: "0",
+ selType: "history",
+ selIndex: val => parseInt(val) >= 0,
+ provider: "UrlbarProviderTopSites",
+ },
+ };
+ },
+
+ async function (win) {
+ info("With pageproxystate=valid, autoopen the panel, click on entry.");
+ await addTopSite("http://example.com/");
+ win.gURLBar.value = "";
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ win.document.getElementById("Browser:OpenLocation").doCommand();
+ });
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ while (win.gURLBar.untrimmedValue != "http://example.com/") {
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ }
+ let element = UrlbarTestUtils.getSelectedRow(win);
+ EventUtils.synthesizeMouseAtCenter(element, {}, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "click",
+ value: "topsites",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "0",
+ numWords: "0",
+ selType: "history",
+ selIndex: "0",
+ provider: "UrlbarProviderTopSites",
+ },
+ };
+ },
+
+ async function (win) {
+ info("With pageproxystate=invalid, open retained results, Enter.");
+ await addTopSite("http://example.org/");
+ win.gURLBar.value = "example.org";
+ win.gURLBar.setPageProxyState("invalid");
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ win.document.getElementById("Browser:OpenLocation").doCommand();
+ });
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "returned",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "11",
+ numWords: "1",
+ selType: "autofill_origin",
+ selIndex: "0",
+ provider: "Autofill",
+ },
+ };
+ },
+
+ async function (win) {
+ info("With pageproxystate=invalid, open retained results, click on entry.");
+ // This value must be different from the previous test, to avoid reopening
+ // the view.
+ win.gURLBar.value = "example.com";
+ win.gURLBar.setPageProxyState("invalid");
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ win.document.getElementById("Browser:OpenLocation").doCommand();
+ });
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ let element = UrlbarTestUtils.getSelectedRow(win);
+ EventUtils.synthesizeMouseAtCenter(element, {}, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "click",
+ value: "returned",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "11",
+ numWords: "1",
+ selType: "autofill_origin",
+ selIndex: "0",
+ provider: "Autofill",
+ },
+ };
+ },
+
+ async function (win) {
+ info("Reopen the view: type, blur, focus, confirm.");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "search",
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.promisePopupClose(win, () => {
+ win.gURLBar.blur();
+ });
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ win.document.getElementById("Browser:OpenLocation").doCommand();
+ });
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ return [
+ {
+ category: "urlbar",
+ method: "abandonment",
+ object: "blur",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "6",
+ numWords: "1",
+ },
+ },
+ {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "returned",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "6",
+ numWords: "1",
+ selType: "searchengine",
+ selIndex: "0",
+ provider: "HeuristicFallback",
+ },
+ },
+ ];
+ },
+
+ async function (win) {
+ info("Open search mode with a keyboard shortcut.");
+ // Bug 1797801: If the search mode used is the same as the default engine and
+ // showSearchTerms is enabled, the chiclet will remain in the urlbar on the search.
+ // Subsequent tests rely on search mode not already been selected.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.showSearchTerms.featureGate", false]],
+ });
+ let defaultEngine = await Services.search.getDefault();
+ win.gURLBar.select();
+ EventUtils.synthesizeKey("k", { accelKey: true }, win);
+ await UrlbarTestUtils.assertSearchMode(win, {
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ engineName: defaultEngine.name,
+ entry: "shortcut",
+ });
+
+ // Execute a search to finish the engagement.
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "moz",
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+
+ await SpecialPowers.popPrefEnv();
+
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "3",
+ numWords: "1",
+ selIndex: "0",
+ selType: "searchengine",
+ provider: "HeuristicFallback",
+ },
+ };
+ },
+
+ async function (win) {
+ info("Open search mode from a tab-to-search result.");
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0]],
+ });
+
+ await PlacesUtils.history.clear();
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits([`https://${TEST_ENGINE_DOMAIN}/`]);
+ }
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: TEST_ENGINE_DOMAIN.slice(0, 4),
+ });
+
+ let tabToSearchResult = (
+ await UrlbarTestUtils.waitForAutocompleteResultAt(win, 1)
+ ).result;
+ Assert.equal(
+ tabToSearchResult.providerName,
+ "TabToSearch",
+ "The second result is a tab-to-search result."
+ );
+
+ // Select the tab-to-search result.
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(win);
+ EventUtils.synthesizeKey("KEY_Enter", {}, win);
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(win, {
+ engineName: TEST_ENGINE_NAME,
+ entry: "tabtosearch",
+ });
+
+ // Execute a search to finish the engagement.
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "moz",
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+
+ await PlacesUtils.history.clear();
+ await SpecialPowers.popPrefEnv();
+
+ return [
+ // engagement on the tab-to-search to enter search mode
+ {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "4",
+ numWords: "1",
+ selIndex: "1",
+ selType: "tabtosearch",
+ provider: "TabToSearch",
+ },
+ },
+ // engagement on the search heuristic
+ {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "3",
+ numWords: "1",
+ selIndex: "0",
+ selType: "searchengine",
+ provider: "HeuristicFallback",
+ },
+ },
+ ];
+ },
+
+ async function (win) {
+ info("Sanity check we are not stuck on 'returned'");
+ win.gURLBar.select();
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "x",
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "1",
+ numWords: "1",
+ selIndex: "0",
+ selType: "searchengine",
+ provider: "HeuristicFallback",
+ },
+ };
+ },
+
+ async function (win) {
+ info("Reopen the view: type, blur, focus, backspace, type, confirm.");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "search",
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.promisePopupClose(win, () => {
+ win.gURLBar.blur();
+ });
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ win.document.getElementById("Browser:OpenLocation").doCommand();
+ });
+ EventUtils.synthesizeKey("VK_RIGHT", {}, win);
+ EventUtils.synthesizeKey("VK_BACK_SPACE", {}, win);
+ EventUtils.synthesizeKey("x", {}, win);
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ return [
+ {
+ category: "urlbar",
+ method: "abandonment",
+ object: "blur",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "6",
+ numWords: "1",
+ },
+ },
+ {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "returned",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "6",
+ numWords: "1",
+ selType: "searchengine",
+ selIndex: "0",
+ provider: "HeuristicFallback",
+ },
+ },
+ ];
+ },
+
+ async function (win) {
+ info("Reopen the view: type, blur, focus, type (overwrite), confirm.");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "search",
+ fireInputEvent: true,
+ });
+ await UrlbarTestUtils.promisePopupClose(win, () => {
+ win.gURLBar.blur();
+ });
+ await UrlbarTestUtils.promisePopupOpen(win, () => {
+ win.document.getElementById("Browser:OpenLocation").doCommand();
+ });
+ EventUtils.synthesizeKey("x", {}, win);
+ await UrlbarTestUtils.promiseSearchComplete(win);
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ return [
+ {
+ category: "urlbar",
+ method: "abandonment",
+ object: "blur",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "6",
+ numWords: "1",
+ },
+ },
+ {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "restarted",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "1",
+ numWords: "1",
+ selType: "searchengine",
+ selIndex: "0",
+ provider: "HeuristicFallback",
+ },
+ },
+ ];
+ },
+
+ async function (win) {
+ info("Sanity check we are not stuck on 'restarted'");
+ win.gURLBar.select();
+ let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "x",
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ return {
+ category: "urlbar",
+ method: "engagement",
+ object: "enter",
+ value: "typed",
+ extra: {
+ elapsed: val => parseInt(val) > 0,
+ numChars: "1",
+ numWords: "1",
+ selIndex: "0",
+ selType: "searchengine",
+ provider: "HeuristicFallback",
+ },
+ };
+ },
+];
+
+add_setup(async function () {
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ // Create a new search engine and mark it as default
+ let engine = await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + "searchSuggestionEngine.xml",
+ setAsDefault: true,
+ });
+ await Services.search.moveEngine(engine, 0);
+
+ await SearchTestUtils.installSearchExtension({
+ name: TEST_ENGINE_NAME,
+ keyword: TEST_ENGINE_ALIAS,
+ search_url: `https://${TEST_ENGINE_DOMAIN}/`,
+ });
+
+ // Add a bookmark and a keyword.
+ let bm = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.com/?q=%s",
+ title: "test",
+ });
+ await PlacesUtils.keywords.insert({
+ keyword: "kw",
+ url: "http://example.com/?q=%s",
+ });
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.keywords.remove("kw");
+ await PlacesUtils.bookmarks.remove(bm);
+ await PlacesUtils.history.clear();
+ });
+});
+
+async function doTest(eventTelemetryEnabled) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.eventTelemetry.enabled", eventTelemetryEnabled],
+ ["browser.urlbar.suggest.searches", true],
+ ],
+ });
+
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+
+ // This is not necessary after each loop, because assertEvents does it.
+ Services.telemetry.clearEvents();
+ Services.telemetry.clearScalars();
+
+ for (let i = 0; i < tests.length; i++) {
+ info(`Running test at index ${i}`);
+ let events = await tests[i](win);
+ if (events === null) {
+ info("Skipping test");
+ continue;
+ }
+ if (!Array.isArray(events)) {
+ events = [events];
+ }
+ // Always blur to ensure it's not accounted as an additional abandonment.
+ win.gURLBar.setSearchMode({});
+ win.gURLBar.blur();
+ TelemetryTestUtils.assertEvents(eventTelemetryEnabled ? events : [], {
+ category: "urlbar",
+ });
+
+ // Scalars should be recorded regardless of `eventTelemetry.enabled`.
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "urlbar.engagement",
+ events.filter(e => e.method == "engagement").length || undefined
+ );
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "urlbar.abandonment",
+ events.filter(e => e.method == "abandonment").length || undefined
+ );
+
+ await UrlbarTestUtils.formHistory.clear(win);
+ }
+
+ await BrowserTestUtils.closeWindow(win);
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function enabled() {
+ await doTest(true);
+});
+
+add_task(async function disabled() {
+ await doTest(false);
+});
+
+/**
+ * Replaces the contents of Top Sites with the specified site.
+ *
+ * @param {string} site
+ * A site to add to Top Sites.
+ */
+async function addTopSite(site) {
+ await PlacesUtils.history.clear();
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits(site);
+ }
+
+ await updateTopSites(sites => sites && sites[0] && sites[0].url == site);
+}
+
+function registerTipProvider() {
+ let provider = new UrlbarTestUtils.TestProvider({
+ results: tipMatches,
+ priority: 1,
+ });
+ UrlbarProvidersManager.registerProvider(provider);
+ return provider;
+}
+
+function unregisterTipProvider(provider) {
+ UrlbarProvidersManager.unregisterProvider(provider);
+}
+
+let tipMatches = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/a" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TIP,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ helpUrl: "http://example.com/",
+ helpL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-tip-get-help"
+ : "urlbar-tip-help-icon",
+ },
+ type: "test",
+ titleL10n: { id: "urlbar-search-tips-confirm" },
+ buttons: [
+ {
+ url: "http://example.com/",
+ l10n: { id: "urlbar-search-tips-confirm" },
+ },
+ ],
+ }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/b" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/c" }
+ ),
+];
diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry_noEvent.js b/browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry_noEvent.js
new file mode 100644
index 0000000000..bdba6888b7
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry_noEvent.js
@@ -0,0 +1,81 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const tests = [
+ async function (win) {
+ info("Type something, click on search settings.");
+ await BrowserTestUtils.withNewTab(
+ { gBrowser: win.gBrowser, url: "about:blank" },
+ async browser => {
+ win.gURLBar.select();
+ const promise = onSyncPaneLoaded();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "x",
+ fireInputEvent: true,
+ });
+ UrlbarTestUtils.getOneOffSearchButtons(win).settingsButton.click();
+ await promise;
+ }
+ );
+ return null;
+ },
+
+ async function (win) {
+ info("Type something, Up, Enter on search settings.");
+ await BrowserTestUtils.withNewTab(
+ { gBrowser: win.gBrowser, url: "about:blank" },
+ async browser => {
+ win.gURLBar.select();
+ const promise = onSyncPaneLoaded();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "x",
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("KEY_ArrowUp", {}, win);
+ Assert.ok(
+ UrlbarTestUtils.getOneOffSearchButtons(
+ win
+ ).selectedButton.classList.contains("search-setting-button"),
+ "Should have selected the settings button"
+ );
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ await promise;
+ }
+ );
+ return null;
+ },
+];
+
+function onSyncPaneLoaded() {
+ return new Promise(resolve => {
+ Services.obs.addObserver(function panesLoadedObs() {
+ Services.obs.removeObserver(panesLoadedObs, "sync-pane-loaded");
+ resolve();
+ }, "sync-pane-loaded");
+ });
+}
+
+add_task(async function test() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.eventTelemetry.enabled", true]],
+ });
+
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+
+ // This is not necessary after each loop, because assertEvents does it.
+ Services.telemetry.clearEvents();
+
+ for (let i = 0; i < tests.length; i++) {
+ info(`Running no event test at index ${i}`);
+ await tests[i](win);
+ // Always blur to ensure it's not accounted as an additional abandonment.
+ win.gURLBar.blur();
+ TelemetryTestUtils.assertEvents([], { category: "urlbar" });
+ }
+
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_selection.js b/browser/components/urlbar/tests/browser/browser_urlbar_selection.js
new file mode 100644
index 0000000000..3440c35e6f
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_urlbar_selection.js
@@ -0,0 +1,307 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const exampleSearch = "f oo bar";
+const exampleUrl = "https://example.com/1";
+
+function click(target) {
+ let promise = BrowserTestUtils.waitForEvent(target, "click");
+ EventUtils.synthesizeMouseAtCenter(target, {}, target.ownerGlobal);
+ return promise;
+}
+
+function openContextMenu(target) {
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ target.ownerGlobal,
+ "contextmenu"
+ );
+
+ EventUtils.synthesizeMouseAtCenter(
+ target,
+ {
+ type: "contextmenu",
+ button: 2,
+ },
+ target.ownerGlobal
+ );
+ return popupShownPromise;
+}
+
+function drag(target, fromX, fromY, toX, toY) {
+ let promise = BrowserTestUtils.waitForEvent(target, "mouseup");
+ EventUtils.synthesizeMouse(
+ target,
+ fromX,
+ fromY,
+ { type: "mousemove" },
+ target.ownerGlobal
+ );
+ EventUtils.synthesizeMouse(
+ target,
+ fromX,
+ fromY,
+ { type: "mousedown" },
+ target.ownerGlobal
+ );
+ EventUtils.synthesizeMouse(
+ target,
+ toX,
+ toY,
+ { type: "mousemove" },
+ target.ownerGlobal
+ );
+ EventUtils.synthesizeMouse(
+ target,
+ toX,
+ toY,
+ { type: "mouseup" },
+ target.ownerGlobal
+ );
+ return promise;
+}
+
+function resetPrimarySelection(val = "") {
+ if (
+ Services.clipboard.isClipboardTypeSupported(
+ Services.clipboard.kSelectionClipboard
+ )
+ ) {
+ // Reset the clipboard.
+ clipboardHelper.copyStringToClipboard(
+ val,
+ Services.clipboard.kSelectionClipboard
+ );
+ }
+}
+
+function checkPrimarySelection(expectedVal = "") {
+ if (
+ Services.clipboard.isClipboardTypeSupported(
+ Services.clipboard.kSelectionClipboard
+ )
+ ) {
+ let primaryAsText = SpecialPowers.getClipboardData(
+ "text/plain",
+ SpecialPowers.Ci.nsIClipboard.kSelectionClipboard
+ );
+ Assert.equal(primaryAsText, expectedVal);
+ }
+}
+
+add_setup(async function () {
+ // On macOS, we must "warm up" the Urlbar to get the first test to pass.
+ gURLBar.value = "";
+ await click(gURLBar.inputField);
+ gURLBar.blur();
+});
+
+add_task(async function leftClickSelectsAll() {
+ resetPrimarySelection();
+ gURLBar.value = exampleSearch;
+ await click(gURLBar.inputField);
+ Assert.equal(
+ gURLBar.selectionStart,
+ 0,
+ "The entire search term should be selected."
+ );
+ Assert.equal(
+ gURLBar.selectionEnd,
+ exampleSearch.length,
+ "The entire search term should be selected."
+ );
+ gURLBar.blur();
+ checkPrimarySelection();
+});
+
+add_task(async function leftClickSelectsUrl() {
+ resetPrimarySelection();
+ gURLBar.value = exampleUrl;
+ await click(gURLBar.inputField);
+ Assert.equal(gURLBar.selectionStart, 0, "The entire url should be selected.");
+ Assert.equal(
+ gURLBar.selectionEnd,
+ exampleUrl.length,
+ "The entire url should be selected."
+ );
+ gURLBar.blur();
+ checkPrimarySelection();
+});
+
+add_task(async function rightClickSelectsAll() {
+ gURLBar.inputField.focus();
+ gURLBar.value = exampleUrl;
+
+ // Remove the selection so the focus() call above doesn't influence the test.
+ gURLBar.selectionStart = gURLBar.selectionEnd = 0;
+
+ resetPrimarySelection();
+
+ await openContextMenu(gURLBar.inputField);
+
+ Assert.equal(gURLBar.selectionStart, 0, "The entire URL should be selected.");
+ Assert.equal(
+ gURLBar.selectionEnd,
+ exampleUrl.length,
+ "The entire URL should be selected."
+ );
+
+ checkPrimarySelection();
+
+ let contextMenu = gURLBar.querySelector("moz-input-box").menupopup;
+
+ // While the context menu is open, test the "Select All" button.
+ let contextMenuItem = contextMenu.firstElementChild;
+ while (
+ contextMenuItem.nextElementSibling &&
+ contextMenuItem.getAttribute("cmd") != "cmd_selectAll"
+ ) {
+ contextMenuItem = contextMenuItem.nextElementSibling;
+ }
+ Assert.ok(
+ contextMenuItem,
+ "The context menu should have the select all menu item."
+ );
+
+ let controller = document.commandDispatcher.getControllerForCommand(
+ contextMenuItem.getAttribute("cmd")
+ );
+ let enabled = controller.isCommandEnabled(
+ contextMenuItem.getAttribute("cmd")
+ );
+ Assert.ok(enabled, "The context menu select all item should be enabled.");
+
+ await click(contextMenuItem);
+ Assert.equal(
+ gURLBar.selectionStart,
+ 0,
+ "The entire URL should be selected after clicking selectAll button."
+ );
+ Assert.equal(
+ gURLBar.selectionEnd,
+ exampleUrl.length,
+ "The entire URL should be selected after clicking selectAll button."
+ );
+
+ gURLBar.querySelector("moz-input-box").menupopup.hidePopup();
+ gURLBar.blur();
+ checkPrimarySelection(gURLBar.value);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function contextMenuDoesNotCancelSelection() {
+ gURLBar.inputField.focus();
+ gURLBar.value = exampleUrl;
+
+ gURLBar.selectionStart = 3;
+ gURLBar.selectionEnd = 7;
+
+ resetPrimarySelection();
+
+ await openContextMenu(gURLBar.inputField);
+
+ Assert.equal(
+ gURLBar.selectionStart,
+ 3,
+ "The selection should not have changed."
+ );
+ Assert.equal(
+ gURLBar.selectionEnd,
+ 7,
+ "The selection should not have changed."
+ );
+
+ gURLBar.querySelector("moz-input-box").menupopup.hidePopup();
+ gURLBar.blur();
+ checkPrimarySelection();
+});
+
+add_task(async function dragSelect() {
+ resetPrimarySelection();
+ gURLBar.value = exampleSearch.repeat(10);
+ // Drags from an artibrary offset of 30 to test for bug 1562145: that the
+ // selection does not start at the beginning.
+ await drag(gURLBar.inputField, 30, 0, 60, 0);
+ Assert.greater(
+ gURLBar.selectionStart,
+ 0,
+ "Selection should not start at the beginning of the string."
+ );
+
+ let selectedVal = gURLBar.value.substring(
+ gURLBar.selectionStart,
+ gURLBar.selectionEnd
+ );
+ gURLBar.blur();
+ checkPrimarySelection(selectedVal);
+});
+
+/**
+ * Testing for bug 1571018: that the entire Urlbar isn't selected when the
+ * Urlbar is dragged following a selectsAll event then a blur.
+ */
+add_task(async function dragAfterSelectAll() {
+ resetPrimarySelection();
+ gURLBar.value = exampleSearch.repeat(10);
+ await click(gURLBar.inputField);
+ Assert.equal(
+ gURLBar.selectionStart,
+ 0,
+ "The entire search term should be selected."
+ );
+ Assert.equal(
+ gURLBar.selectionEnd,
+ exampleSearch.repeat(10).length,
+ "The entire search term should be selected."
+ );
+
+ gURLBar.blur();
+ checkPrimarySelection();
+
+ // The offset of 30 is arbitrary.
+ await drag(gURLBar.inputField, 30, 0, 60, 0);
+
+ Assert.notEqual(
+ gURLBar.selectionStart,
+ 0,
+ "Only part of the search term should be selected."
+ );
+ Assert.notEqual(
+ gURLBar.selectionEnd,
+ exampleSearch.repeat(10).length,
+ "Only part of the search term should be selected."
+ );
+
+ checkPrimarySelection(
+ gURLBar.value.substring(gURLBar.selectionStart, gURLBar.selectionEnd)
+ );
+});
+
+/**
+ * Testing for bug 1571018: that the entire Urlbar is selected when the Urlbar
+ * is refocused following a partial text selection then a blur.
+ */
+add_task(async function selectAllAfterDrag() {
+ gURLBar.value = exampleSearch;
+
+ gURLBar.selectionStart = 3;
+ gURLBar.selectionEnd = 7;
+
+ gURLBar.blur();
+
+ await click(gURLBar.inputField);
+
+ Assert.equal(
+ gURLBar.selectionStart,
+ 0,
+ "The entire search term should be selected."
+ );
+ Assert.equal(
+ gURLBar.selectionEnd,
+ exampleSearch.length,
+ "The entire search term should be selected."
+ );
+
+ gURLBar.blur();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry.js
new file mode 100644
index 0000000000..4e48a946d5
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry.js
@@ -0,0 +1,1218 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This file tests urlbar telemetry with search related actions.
+ */
+
+"use strict";
+
+const SCALAR_URLBAR = "browser.engagement.navigation.urlbar";
+const SCALAR_SEARCHMODE = "browser.engagement.navigation.urlbar_searchmode";
+
+// The preference to enable suggestions in the urlbar.
+const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches";
+
+ChromeUtils.defineESModuleGetters(this, {
+ SearchSERPTelemetry: "resource:///modules/SearchSERPTelemetry.sys.mjs",
+});
+
+function searchInAwesomebar(value, win = window) {
+ return UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ waitForFocus,
+ value,
+ fireInputEvent: true,
+ });
+}
+
+/**
+ * Click one of the entries in the urlbar suggestion popup.
+ *
+ * @param {string} resultTitle
+ * The title of the result to click on.
+ * @param {number} button [optional]
+ * which button to click.
+ * @returns {number}
+ * The index of the result that was clicked, or -1 if not found.
+ */
+async function clickURLBarSuggestion(resultTitle, button = 1) {
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ const count = UrlbarTestUtils.getResultCount(window);
+ for (let i = 0; i < count; i++) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ if (result.displayed.title == resultTitle) {
+ // This entry is the search suggestion we're looking for.
+ let element = await UrlbarTestUtils.waitForAutocompleteResultAt(
+ window,
+ i
+ );
+ if (button == 1) {
+ EventUtils.synthesizeMouseAtCenter(element, {});
+ } else if (button == 2) {
+ EventUtils.synthesizeMouseAtCenter(element, {
+ type: "mousedown",
+ button: 2,
+ });
+ }
+ return i;
+ }
+ }
+ return -1;
+}
+
+/**
+ * Create an engine to generate search suggestions and add it as default
+ * for this test.
+ *
+ * @param {Function} taskFn
+ * The function to run with the new search engine as default.
+ */
+async function withNewSearchEngine(taskFn) {
+ let suggestionEngine = await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + "urlbarTelemetrySearchSuggestions.xml",
+ });
+ let previousEngine = await Services.search.getDefault();
+ await Services.search.setDefault(
+ suggestionEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+
+ try {
+ await taskFn(suggestionEngine);
+ } finally {
+ await Services.search.setDefault(
+ previousEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ await Services.search.removeEngine(suggestionEngine);
+ }
+}
+
+add_setup(async function () {
+ await SearchTestUtils.installSearchExtension(
+ {
+ name: "MozSearch",
+ keyword: "mozalias",
+ search_url: "https://example.com/",
+ },
+ { setAsDefault: true }
+ );
+
+ // Make it the first one-off engine.
+ let engine = Services.search.getEngineByName("MozSearch");
+ await Services.search.moveEngine(engine, 0);
+
+ // Enable search suggestions in the urlbar.
+ let suggestionsEnabled = Services.prefs.getBoolPref(SUGGEST_URLBAR_PREF);
+ Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, true);
+
+ // Enable local telemetry recording for the duration of the tests.
+ let oldCanRecord = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+
+ // Enable event recording for the events tested here.
+ Services.telemetry.setEventRecordingEnabled("navigation", true);
+
+ // Clear history so that history added by previous tests doesn't mess up this
+ // test when it selects results in the urlbar.
+ await PlacesUtils.history.clear();
+
+ // Clear historical search suggestions to avoid interference from previous
+ // tests.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.maxHistoricalSearchSuggestions", 0]],
+ });
+
+ // This test assumes that general results are shown before suggestions.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.showSearchSuggestionsFirst", false]],
+ });
+
+ // Make sure to restore the engine once we're done.
+ registerCleanupFunction(async function () {
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, suggestionsEnabled);
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.formHistory.clear();
+ Services.telemetry.setEventRecordingEnabled("navigation", false);
+ });
+});
+
+add_task(async function test_simpleQuery() {
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ );
+ let search_hist =
+ TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS");
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Simulate entering a simple search.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("simple query");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ // Check if the scalars contain the expected values.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ SCALAR_URLBAR,
+ "search_enter",
+ 1
+ );
+ Assert.equal(
+ Object.keys(scalars[SCALAR_URLBAR]).length,
+ 1,
+ "This search must only increment one entry in the scalar."
+ );
+
+ // SEARCH_COUNTS should be incremented, but only the urlbar source since an
+ // internal @search keyword was not used.
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.urlbar",
+ 1
+ );
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.alias",
+ undefined
+ );
+
+ // Also check events.
+ TelemetryTestUtils.assertEvents(
+ [
+ [
+ "navigation",
+ "search",
+ "urlbar",
+ "enter",
+ { engine: "other-MozSearch" },
+ ],
+ ],
+ { category: "navigation", method: "search" }
+ );
+
+ TelemetryTestUtils.assertHistogram(
+ resultMethodHist,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.enter,
+ 1
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_searchMode_enter() {
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ );
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Enter search mode using an alias and a query.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("mozalias query");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ // Check if the scalars contain the expected values.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ SCALAR_SEARCHMODE,
+ "search_enter",
+ 1
+ );
+ Assert.equal(
+ Object.keys(scalars[SCALAR_SEARCHMODE]).length,
+ 1,
+ "This search must only increment one entry in the scalar."
+ );
+
+ // Also check events.
+ TelemetryTestUtils.assertEvents(
+ [
+ [
+ "navigation",
+ "search",
+ "urlbar_searchmode",
+ "enter",
+ { engine: "other-MozSearch" },
+ ],
+ ],
+ { category: "navigation", method: "search" }
+ );
+
+ TelemetryTestUtils.assertHistogram(
+ resultMethodHist,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.enter,
+ 1
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Performs a search using the first result, a one-off button, and the Return
+// (Enter) key.
+add_task(async function test_oneOff_enter() {
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ );
+ let search_hist =
+ TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS");
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Perform a one-off search using the first engine.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("query");
+
+ info("Pressing Alt+Down to take us to the first one-off engine.");
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ let engine =
+ UrlbarTestUtils.getOneOffSearchButtons(window).selectedButton.engine;
+ EventUtils.synthesizeKey("KEY_Enter");
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: engine.name,
+ entry: "oneoff",
+ });
+
+ // Now that we're in search mode, execute the search.
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ // Check if the scalars contain the expected values.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ SCALAR_SEARCHMODE,
+ "search_enter",
+ 1
+ );
+ Assert.equal(
+ Object.keys(scalars[SCALAR_SEARCHMODE]).length,
+ 1,
+ "This search must only increment one entry in the scalar."
+ );
+
+ // SEARCH_COUNTS should be incremented, but only the urlbar-searchmode source
+ // since aliases aren't counted separately in search mode.
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.urlbar-searchmode",
+ 1
+ );
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.alias",
+ undefined
+ );
+
+ // Also check events.
+ TelemetryTestUtils.assertEvents(
+ [
+ [
+ "navigation",
+ "search",
+ "urlbar_searchmode",
+ "enter",
+ { engine: "other-MozSearch" },
+ ],
+ ],
+ { category: "navigation", method: "search" }
+ );
+
+ TelemetryTestUtils.assertHistogram(
+ resultMethodHist,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.enter,
+ 1
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Performs a search using the second result, a one-off button, and the Return
+// (Enter) key. This only tests the FX_URLBAR_SELECTED_RESULT_METHOD histogram
+// since test_oneOff_enter covers everything else.
+add_task(async function test_oneOff_enterSelection() {
+ Services.telemetry.clearScalars();
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ );
+
+ await withNewSearchEngine(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.maxHistoricalSearchSuggestions", 1]],
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Type a query. Suggestions should be generated by the test engine.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("query");
+
+ info(
+ "Select the second result, press Alt+Down to take us to the first one-off engine."
+ );
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ let engine =
+ UrlbarTestUtils.getOneOffSearchButtons(window).selectedButton.engine;
+ EventUtils.synthesizeKey("KEY_Enter");
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: engine.name,
+ entry: "oneoff",
+ });
+
+ // Now that we're in search mode, execute the search.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ TelemetryTestUtils.assertHistogram(
+ resultMethodHist,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection,
+ 1
+ );
+
+ await SpecialPowers.popPrefEnv();
+ BrowserTestUtils.removeTab(tab);
+ });
+});
+
+// Performs a search using a click on a one-off button. This only tests the
+// FX_URLBAR_SELECTED_RESULT_METHOD histogram since test_oneOff_enter covers
+// everything else.
+add_task(async function test_oneOff_click() {
+ Services.telemetry.clearScalars();
+
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ );
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Type a query.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("query");
+
+ info("Click the first one-off button.");
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ let oneOffButton =
+ UrlbarTestUtils.getOneOffSearchButtons(window).getSelectableButtons(
+ false
+ )[0];
+ oneOffButton.click();
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: oneOffButton.engine.name,
+ entry: "oneoff",
+ });
+
+ // Now that we're in search mode, execute the search.
+ let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
+ Assert.ok(element, "Found result after entering search mode.");
+ EventUtils.synthesizeMouseAtCenter(element, {});
+ await p;
+
+ TelemetryTestUtils.assertHistogram(
+ resultMethodHist,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.click,
+ 1
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Clicks the first suggestion offered by the test search engine.
+add_task(async function test_suggestion_click() {
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+ await UrlbarTestUtils.formHistory.clear();
+
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ );
+ let search_hist =
+ TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS");
+
+ await withNewSearchEngine(async function (engine) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Type a query. Suggestions should be generated by the test engine.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("query");
+ info("Clicking the urlbar suggestion.");
+ await clickURLBarSuggestion("queryfoo");
+ await p;
+
+ // Check if the scalars contain the expected values.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ SCALAR_URLBAR,
+ "search_suggestion",
+ 1
+ );
+ Assert.equal(
+ Object.keys(scalars[SCALAR_URLBAR]).length,
+ 1,
+ "This search must only increment one entry in the scalar."
+ );
+
+ // SEARCH_COUNTS should be incremented.
+ let searchEngineId = "other-" + engine.name;
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ searchEngineId + ".urlbar",
+ 1
+ );
+
+ // Also check events.
+ TelemetryTestUtils.assertEvents(
+ [
+ [
+ "navigation",
+ "search",
+ "urlbar",
+ "suggestion",
+ { engine: searchEngineId },
+ ],
+ ],
+ { category: "navigation", method: "search" }
+ );
+
+ TelemetryTestUtils.assertHistogram(
+ resultMethodHist,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.click,
+ 1
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ });
+});
+
+// Selects and presses the Return (Enter) key on the first suggestion offered by
+// the test search engine. This only tests the FX_URLBAR_SELECTED_RESULT_METHOD
+// histogram since test_suggestion_click covers everything else.
+add_task(async function test_suggestion_arrowEnterSelection() {
+ Services.telemetry.clearScalars();
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ );
+
+ await withNewSearchEngine(async function () {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Type a query. Suggestions should be generated by the test engine.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("query");
+ info("Select the second result and press Return.");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ TelemetryTestUtils.assertHistogram(
+ resultMethodHist,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection,
+ 1
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ });
+});
+
+// Selects through tab and presses the Return (Enter) key on the first
+// suggestion offered by the test search engine.
+add_task(async function test_suggestion_tabEnterSelection() {
+ Services.telemetry.clearScalars();
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ );
+
+ await withNewSearchEngine(async function () {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Type a query. Suggestions should be generated by the test engine.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("query");
+ info("Select the second result and press Return.");
+ EventUtils.synthesizeKey("KEY_Tab");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ TelemetryTestUtils.assertHistogram(
+ resultMethodHist,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.tabEnterSelection,
+ 1
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ });
+});
+
+// Selects through code and presses the Return (Enter) key on the first
+// suggestion offered by the test search engine.
+add_task(async function test_suggestion_enterSelection() {
+ Services.telemetry.clearScalars();
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ );
+
+ await withNewSearchEngine(async function () {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Type a query. Suggestions should be generated by the test engine.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("query");
+ info("Select the second result and press Return.");
+ UrlbarTestUtils.setSelectedRowIndex(window, 1);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ TelemetryTestUtils.assertHistogram(
+ resultMethodHist,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.enterSelection,
+ 1
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ });
+});
+
+// Clicks the first suggestion offered by the test search engine when in search
+// mode.
+add_task(async function test_searchmode_suggestion_click() {
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ );
+ let search_hist =
+ TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS");
+
+ await withNewSearchEngine(async function (engine) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Type a query. Suggestions should be generated by the test engine.");
+ await searchInAwesomebar("query");
+ await UrlbarTestUtils.enterSearchMode(window, {
+ engineName: engine.name,
+ });
+ info("Clicking the urlbar suggestion.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await clickURLBarSuggestion("queryfoo");
+ await p;
+
+ // Check if the scalars contain the expected values.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ SCALAR_SEARCHMODE,
+ "search_suggestion",
+ 1
+ );
+ Assert.equal(
+ Object.keys(scalars[SCALAR_SEARCHMODE]).length,
+ 1,
+ "This search must only increment one entry in the scalar."
+ );
+
+ // SEARCH_COUNTS should be incremented.
+ let searchEngineId = "other-" + engine.name;
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ searchEngineId + ".urlbar-searchmode",
+ 1
+ );
+
+ // Also check events.
+ TelemetryTestUtils.assertEvents(
+ [
+ [
+ "navigation",
+ "search",
+ "urlbar_searchmode",
+ "suggestion",
+ { engine: searchEngineId },
+ ],
+ ],
+ { category: "navigation", method: "search" }
+ );
+
+ TelemetryTestUtils.assertHistogram(
+ resultMethodHist,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.click,
+ 1
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ });
+});
+
+// Selects and presses the Return (Enter) key on the first suggestion offered by
+// the test search engine in search mode. This only tests the
+// FX_URLBAR_SELECTED_RESULT_METHOD histogram since
+// test_searchmode_suggestion_click covers everything else.
+add_task(async function test_searchmode_suggestion_arrowEnterSelection() {
+ Services.telemetry.clearScalars();
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ );
+
+ await withNewSearchEngine(async function (engine) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Type a query. Suggestions should be generated by the test engine.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("query");
+ await UrlbarTestUtils.enterSearchMode(window, {
+ engineName: engine.name,
+ });
+ info("Select the second result and press Return.");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ TelemetryTestUtils.assertHistogram(
+ resultMethodHist,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection,
+ 1
+ );
+ BrowserTestUtils.removeTab(tab);
+ });
+});
+
+// Selects through tab and presses the Return (Enter) key on the first
+// suggestion offered by the test search engine in search mode.
+add_task(async function test_suggestion_tabEnterSelection() {
+ Services.telemetry.clearScalars();
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ );
+
+ await withNewSearchEngine(async function (engine) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Type a query. Suggestions should be generated by the test engine.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("query");
+ await UrlbarTestUtils.enterSearchMode(window, {
+ engineName: engine.name,
+ });
+ info("Select the second result and press Return.");
+ EventUtils.synthesizeKey("KEY_Tab");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ TelemetryTestUtils.assertHistogram(
+ resultMethodHist,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.tabEnterSelection,
+ 1
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ });
+});
+
+// Selects through code and presses the Return (Enter) key on the first
+// suggestion offered by the test search engine in search mode.
+add_task(async function test_suggestion_enterSelection() {
+ Services.telemetry.clearScalars();
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ );
+
+ await withNewSearchEngine(async function (engine) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Type a query. Suggestions should be generated by the test engine.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("query");
+ await UrlbarTestUtils.enterSearchMode(window, {
+ engineName: engine.name,
+ });
+ info("Select the second result and press Return.");
+ UrlbarTestUtils.setSelectedRowIndex(window, 1);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ TelemetryTestUtils.assertHistogram(
+ resultMethodHist,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.enterSelection,
+ 1
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ });
+});
+
+// Clicks a form history result.
+add_task(async function test_formHistory_click() {
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+ await UrlbarTestUtils.formHistory.clear();
+ await UrlbarTestUtils.formHistory.add(["foobar"]);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.maxHistoricalSearchSuggestions", 1]],
+ });
+
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ );
+ let search_hist =
+ TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS");
+
+ await withNewSearchEngine(async engine => {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Type a query. There should be form history.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("foo");
+ info("Clicking the form history.");
+ await clickURLBarSuggestion("foobar");
+ await p;
+
+ // Check if the scalars contain the expected values.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ SCALAR_URLBAR,
+ "search_formhistory",
+ 1
+ );
+ Assert.equal(
+ Object.keys(scalars[SCALAR_URLBAR]).length,
+ 1,
+ "This search must only increment one entry in the scalar."
+ );
+
+ // SEARCH_COUNTS should be incremented.
+ let searchEngineId = "other-" + engine.name;
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ searchEngineId + ".urlbar",
+ 1
+ );
+
+ // Also check events.
+ TelemetryTestUtils.assertEvents(
+ [
+ [
+ "navigation",
+ "search",
+ "urlbar",
+ "formhistory",
+ { engine: searchEngineId },
+ ],
+ ],
+ { category: "navigation", method: "search" }
+ );
+
+ TelemetryTestUtils.assertHistogram(
+ resultMethodHist,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.click,
+ 1
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ await UrlbarTestUtils.formHistory.clear();
+ await SpecialPowers.popPrefEnv();
+ });
+});
+
+// Selects and presses the Return (Enter) key on a form history result. This
+// only tests the FX_URLBAR_SELECTED_RESULT_METHOD histogram since
+// test_formHistory_click covers everything else.
+add_task(async function test_formHistory_arrowEnterSelection() {
+ Services.telemetry.clearScalars();
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ );
+
+ await UrlbarTestUtils.formHistory.clear();
+ await UrlbarTestUtils.formHistory.add(["foobar"]);
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.maxHistoricalSearchSuggestions", 1]],
+ });
+
+ await withNewSearchEngine(async function () {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Type a query. There should be form history.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("foo");
+ info("Select the form history result and press Return.");
+ while (gURLBar.untrimmedValue != "foobar") {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ TelemetryTestUtils.assertHistogram(
+ resultMethodHist,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection,
+ 1
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ await UrlbarTestUtils.formHistory.clear();
+ await SpecialPowers.popPrefEnv();
+ });
+});
+
+// Selects through tab and presses the Return (Enter) key on a form history
+// result.
+add_task(async function test_formHistory_tabEnterSelection() {
+ Services.telemetry.clearScalars();
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ );
+
+ await UrlbarTestUtils.formHistory.clear();
+ await UrlbarTestUtils.formHistory.add(["foobar"]);
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.maxHistoricalSearchSuggestions", 1]],
+ });
+
+ await withNewSearchEngine(async function () {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Type a query. There should be form history.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("foo");
+ info("Select the form history result and press Return.");
+ while (gURLBar.untrimmedValue != "foobar") {
+ EventUtils.synthesizeKey("KEY_Tab");
+ }
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ TelemetryTestUtils.assertHistogram(
+ resultMethodHist,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.tabEnterSelection,
+ 1
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ await UrlbarTestUtils.formHistory.clear();
+ await SpecialPowers.popPrefEnv();
+ });
+});
+
+// Selects through code and presses the Return (Enter) key on a form history
+// result.
+add_task(async function test_formHistory_enterSelection() {
+ Services.telemetry.clearScalars();
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ );
+
+ await UrlbarTestUtils.formHistory.clear();
+ await UrlbarTestUtils.formHistory.add(["foobar"]);
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.maxHistoricalSearchSuggestions", 1]],
+ });
+
+ await withNewSearchEngine(async function () {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Type a query. There should be form history.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("foo");
+ info("Select the second result and press Return.");
+ let index = 1;
+ while (gURLBar.untrimmedValue != "foobar") {
+ UrlbarTestUtils.setSelectedRowIndex(window, index++);
+ }
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ TelemetryTestUtils.assertHistogram(
+ resultMethodHist,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.enterSelection,
+ 1
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ await UrlbarTestUtils.formHistory.clear();
+ await SpecialPowers.popPrefEnv();
+ });
+});
+
+add_task(async function test_privateWindow() {
+ // This test assumes the showSearchTerms feature is not enabled,
+ // as multiple searches are made one after another, relying on
+ // urlbar as the keyed scalar SAP, not urlbar_persisted.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.showSearchTerms.featureGate", false]],
+ });
+
+ // Override the search telemetry search provider info to
+ // count in-content SEARCH_COUNTs telemetry for our test engine.
+ SearchSERPTelemetry.overrideSearchTelemetryForTests([
+ {
+ telemetryId: "example",
+ searchPageRegexp: "^https://example\\.com/",
+ queryParamName: "q",
+ },
+ ]);
+
+ let search_hist =
+ TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS");
+
+ // First, do a bunch of searches in a private window.
+ let win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+
+ info("Search in a private window and the pref does not exist");
+ let p = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await searchInAwesomebar("query", win);
+ EventUtils.synthesizeKey("KEY_Enter", undefined, win);
+ await p;
+
+ // SEARCH_COUNTS should be incremented.
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.urlbar",
+ 1
+ );
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", true);
+ console.log(scalars);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "browser.search.content.urlbar",
+ "example:organic:none",
+ 1
+ );
+
+ info("Search again in a private window after setting the pref to true");
+ Services.prefs.setBoolPref("browser.engagement.search_counts.pbm", true);
+ p = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await searchInAwesomebar("another query", win);
+ EventUtils.synthesizeKey("KEY_Enter", undefined, win);
+ await p;
+
+ // SEARCH_COUNTS should *not* be incremented.
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.urlbar",
+ 1
+ );
+ scalars = TelemetryTestUtils.getProcessScalars("parent", true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "browser.search.content.urlbar",
+ "example:organic:none",
+ 1
+ );
+
+ info("Search again in a private window after setting the pref to false");
+ Services.prefs.setBoolPref("browser.engagement.search_counts.pbm", false);
+ p = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await searchInAwesomebar("another query", win);
+ EventUtils.synthesizeKey("KEY_Enter", undefined, win);
+ await p;
+
+ // SEARCH_COUNTS should be incremented.
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.urlbar",
+ 2
+ );
+ scalars = TelemetryTestUtils.getProcessScalars("parent", true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "browser.search.content.urlbar",
+ "example:organic:none",
+ 2
+ );
+
+ info("Search again in a private window after clearing the pref");
+ Services.prefs.clearUserPref("browser.engagement.search_counts.pbm");
+ p = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await searchInAwesomebar("another query", win);
+ EventUtils.synthesizeKey("KEY_Enter", undefined, win);
+ await p;
+
+ // SEARCH_COUNTS should be incremented.
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.urlbar",
+ 3
+ );
+ scalars = TelemetryTestUtils.getProcessScalars("parent", true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "browser.search.content.urlbar",
+ "example:organic:none",
+ 3
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+
+ // Now, do a bunch of searches in a non-private window. Telemetry should
+ // always be recorded regardless of the pref's value.
+ win = await BrowserTestUtils.openNewBrowserWindow();
+
+ info("Search in a non-private window and the pref does not exist");
+ p = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await searchInAwesomebar("query", win);
+ EventUtils.synthesizeKey("KEY_Enter", undefined, win);
+ await p;
+
+ // SEARCH_COUNTS should be incremented.
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.urlbar",
+ 4
+ );
+ scalars = TelemetryTestUtils.getProcessScalars("parent", true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "browser.search.content.urlbar",
+ "example:organic:none",
+ 4
+ );
+
+ info("Search again in a non-private window after setting the pref to true");
+ Services.prefs.setBoolPref("browser.engagement.search_counts.pbm", true);
+ p = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await searchInAwesomebar("another query", win);
+ EventUtils.synthesizeKey("KEY_Enter", undefined, win);
+ await p;
+
+ // SEARCH_COUNTS should be incremented.
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.urlbar",
+ 5
+ );
+ scalars = TelemetryTestUtils.getProcessScalars("parent", true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "browser.search.content.urlbar",
+ "example:organic:none",
+ 5
+ );
+
+ info("Search again in a non-private window after setting the pref to false");
+ Services.prefs.setBoolPref("browser.engagement.search_counts.pbm", false);
+ p = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await searchInAwesomebar("another query", win);
+ EventUtils.synthesizeKey("KEY_Enter", undefined, win);
+ await p;
+
+ // SEARCH_COUNTS should be incremented.
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.urlbar",
+ 6
+ );
+ scalars = TelemetryTestUtils.getProcessScalars("parent", true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "browser.search.content.urlbar",
+ "example:organic:none",
+ 6
+ );
+
+ info("Search again in a non-private window after clearing the pref");
+ Services.prefs.clearUserPref("browser.engagement.search_counts.pbm");
+ p = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ await searchInAwesomebar("another query", win);
+ EventUtils.synthesizeKey("KEY_Enter", undefined, win);
+ await p;
+
+ // SEARCH_COUNTS should be incremented.
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.urlbar",
+ 7
+ );
+ scalars = TelemetryTestUtils.getProcessScalars("parent", true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "browser.search.content.urlbar",
+ "example:organic:none",
+ 7
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+
+ // Reset the search provider info.
+ SearchSERPTelemetry.overrideSearchTelemetryForTests();
+ await UrlbarTestUtils.formHistory.clear();
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_autofill.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_autofill.js
new file mode 100644
index 0000000000..8336bde462
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_autofill.js
@@ -0,0 +1,733 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests urlbar autofill telemetry.
+ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarProviderPreloadedSites:
+ "resource:///modules/UrlbarProviderPreloadedSites.sys.mjs",
+});
+
+const SCALAR_URLBAR = "browser.engagement.navigation.urlbar";
+
+function assertSearchTelemetryEmpty(search_hist) {
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false);
+ Assert.ok(
+ !(SCALAR_URLBAR in scalars),
+ `Should not have recorded ${SCALAR_URLBAR}`
+ );
+
+ // SEARCH_COUNTS should not contain any engine counts at all. The keys in this
+ // histogram are search engine telemetry identifiers.
+ Assert.deepEqual(
+ Object.keys(search_hist.snapshot()),
+ [],
+ "SEARCH_COUNTS is empty"
+ );
+
+ // Also check events.
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ );
+ events = (events.parent || []).filter(
+ e => e[1] == "navigation" && e[2] == "search"
+ );
+ Assert.deepEqual(
+ events,
+ [],
+ "Should not have recorded any navigation search events"
+ );
+}
+
+function snapshotHistograms() {
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+ return {
+ resultMethodHist: TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ ),
+ search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"),
+ };
+}
+
+function assertTelemetryResults(histograms, type, index, method) {
+ TelemetryTestUtils.assertHistogram(histograms.resultMethodHist, method, 1);
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ `urlbar.picked.${type}`,
+ index,
+ 1
+ );
+}
+
+/**
+ * Performs a search and picks the first result.
+ *
+ * @param {string} searchString
+ * The search string. Assumed to trigger an autofill result
+ * @param {string} autofilledValue
+ * The input's expected value after autofill occurs.
+ * @param {string} unpickResult
+ * Optional: If true, do not pick any result. Default value is false.
+ * @param {string} urlToSelect
+ * Optional: If want to select result except autofill, pass the URL.
+ */
+async function triggerAutofillAndPickResult(
+ searchString,
+ autofilledValue,
+ unpickResult = false,
+ urlToSelect = null
+) {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: searchString,
+ fireInputEvent: true,
+ });
+
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill, "Result is autofill");
+ Assert.equal(gURLBar.value, autofilledValue, "gURLBar.value");
+ Assert.equal(gURLBar.selectionStart, searchString.length, "selectionStart");
+ Assert.equal(gURLBar.selectionEnd, autofilledValue.length, "selectionEnd");
+
+ if (urlToSelect) {
+ for (let row = 0; row < UrlbarTestUtils.getResultCount(window); row++) {
+ const result = await UrlbarTestUtils.getDetailsOfResultAt(window, row);
+ if (result.url === urlToSelect) {
+ UrlbarTestUtils.setSelectedRowIndex(window, row);
+ break;
+ }
+ }
+ }
+
+ if (unpickResult) {
+ // Close popup without any action.
+ await UrlbarTestUtils.promisePopupClose(window);
+ return;
+ }
+
+ let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+
+ let url;
+ if (urlToSelect) {
+ url = urlToSelect;
+ } else {
+ url = autofilledValue.includes(":")
+ ? autofilledValue
+ : "http://" + autofilledValue;
+ }
+ Assert.equal(gBrowser.currentURI.spec, url, "Loaded URL is correct");
+ });
+}
+
+function createOtherAutofillProvider(searchString, autofilledValue) {
+ return new UrlbarTestUtils.TestProvider({
+ priority: Infinity,
+ type: UrlbarUtils.PROVIDER_TYPE.HEURISTIC,
+ results: [
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ title: "Test",
+ url: "http://example.com/",
+ }
+ ),
+ {
+ heuristic: true,
+ autofill: {
+ value: autofilledValue,
+ selectionStart: searchString.length,
+ selectionEnd: autofilledValue.length,
+ // Leave out `type` to trigger "other"
+ },
+ }
+ ),
+ ],
+ });
+}
+
+// Allow more time for Mac machines so they don't time out in verify mode.
+if (AppConstants.platform == "macosx") {
+ requestLongerTimeout(3);
+}
+
+add_setup(async function () {
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesTestUtils.clearInputHistory();
+
+ // Enable local telemetry recording for the duration of the tests.
+ const originalCanRecord = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+
+ registerCleanupFunction(async () => {
+ Services.telemetry.canRecordExtended = originalCanRecord;
+ await PlacesTestUtils.clearInputHistory();
+ await PlacesUtils.history.clear();
+ });
+});
+
+// Checks adaptive history, origin, and URL autofill.
+add_task(async function history() {
+ const testData = [
+ {
+ useAdaptiveHistory: true,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ userInput: "ex",
+ autofilled: "example.com/",
+ expected: "autofill_origin",
+ },
+ {
+ useAdaptiveHistory: true,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ userInput: "exa",
+ autofilled: "example.com/test",
+ expected: "autofill_adaptive",
+ },
+ {
+ useAdaptiveHistory: true,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ userInput: "exam",
+ autofilled: "example.com/test",
+ expected: "autofill_adaptive",
+ },
+ {
+ useAdaptiveHistory: true,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ userInput: "example.com",
+ autofilled: "example.com/test",
+ expected: "autofill_adaptive",
+ },
+ {
+ useAdaptiveHistory: true,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ userInput: "example.com/",
+ autofilled: "example.com/test",
+ expected: "autofill_adaptive",
+ },
+ {
+ useAdaptiveHistory: true,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ userInput: "example.com/test",
+ autofilled: "example.com/test",
+ expected: "autofill_adaptive",
+ },
+ {
+ useAdaptiveHistory: true,
+ visitHistory: ["http://example.com/test", "http://example.org/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ userInput: "example.org",
+ autofilled: "example.org/",
+ expected: "autofill_origin",
+ },
+ {
+ useAdaptiveHistory: true,
+ visitHistory: ["http://example.com/test", "http://example.com/test/url"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ userInput: "example.com/test/",
+ autofilled: "example.com/test/",
+ expected: "autofill_url",
+ },
+ {
+ useAdaptiveHistory: true,
+ visitHistory: [{ uri: "http://example.com/test" }],
+ inputHistory: [
+ { uri: "http://example.com/test", input: "http://example.com/test" },
+ ],
+ userInput: "http://example.com/test",
+ autofilled: "http://example.com/test",
+ expected: "autofill_adaptive",
+ },
+ {
+ useAdaptiveHistory: false,
+ visitHistory: [{ uri: "http://example.com/test" }],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ userInput: "example",
+ autofilled: "example.com/",
+ expected: "autofill_origin",
+ },
+ {
+ useAdaptiveHistory: false,
+ visitHistory: [{ uri: "http://example.com/test" }],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ userInput: "example.com/te",
+ autofilled: "example.com/test",
+ expected: "autofill_url",
+ },
+ ];
+
+ for (const {
+ useAdaptiveHistory,
+ visitHistory,
+ inputHistory,
+ userInput,
+ autofilled,
+ expected,
+ } of testData) {
+ const histograms = snapshotHistograms();
+
+ await PlacesTestUtils.addVisits(visitHistory);
+ for (const { uri, input } of inputHistory) {
+ await UrlbarUtils.addToInputHistory(uri, input);
+ }
+
+ UrlbarPrefs.set("autoFill.adaptiveHistory.enabled", useAdaptiveHistory);
+
+ await triggerAutofillAndPickResult(userInput, autofilled);
+
+ assertSearchTelemetryEmpty(histograms.search_hist);
+ assertTelemetryResults(
+ histograms,
+ expected,
+ 0,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.enter
+ );
+
+ UrlbarPrefs.clear("autoFill.adaptiveHistory.enabled");
+ await PlacesTestUtils.clearInputHistory();
+ await PlacesUtils.history.clear();
+ }
+});
+
+// Checks about-page autofill (e.g., "about:about").
+add_task(async function about() {
+ let histograms = snapshotHistograms();
+ await triggerAutofillAndPickResult("about:abou", "about:about");
+
+ assertSearchTelemetryEmpty(histograms.search_hist);
+ assertTelemetryResults(
+ histograms,
+ "autofill_about",
+ 0,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.enter
+ );
+
+ await PlacesUtils.history.clear();
+});
+
+// Checks preloaded sites autofill.
+add_task(async function preloaded() {
+ UrlbarPrefs.set("usepreloadedtopurls.enabled", true);
+ UrlbarPrefs.set("usepreloadedtopurls.expire_days", 100);
+ UrlbarProviderPreloadedSites.populatePreloadedSiteStorage([
+ ["http://example.com/", "Example"],
+ ]);
+
+ let histograms = snapshotHistograms();
+ await triggerAutofillAndPickResult("example", "example.com/");
+
+ assertSearchTelemetryEmpty(histograms.search_hist);
+ assertTelemetryResults(
+ histograms,
+ "autofill_preloaded",
+ 0,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.enter
+ );
+
+ await PlacesUtils.history.clear();
+ UrlbarPrefs.clear("usepreloadedtopurls.enabled");
+ UrlbarPrefs.clear("usepreloadedtopurls.expire_days");
+});
+
+// Checks the "other" fallback, which shouldn't normally happen.
+add_task(async function other() {
+ let searchString = "exam";
+ let autofilledValue = "example.com/";
+ let provider = createOtherAutofillProvider(searchString, autofilledValue);
+ UrlbarProvidersManager.registerProvider(provider);
+
+ let histograms = snapshotHistograms();
+ await triggerAutofillAndPickResult(searchString, autofilledValue);
+
+ assertSearchTelemetryEmpty(histograms.search_hist);
+ assertTelemetryResults(
+ histograms,
+ "autofill_other",
+ 0,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.enter
+ );
+
+ await PlacesUtils.history.clear();
+ UrlbarProvidersManager.unregisterProvider(provider);
+});
+
+// Checks impression telemetry.
+add_task(async function impression() {
+ const testData = [
+ {
+ description: "Adaptive history autofill and pick it",
+ useAdaptiveHistory: true,
+ visitHistory: ["http://example.com/first", "http://example.com/second"],
+ inputHistory: [{ uri: "http://example.com/first", input: "exa" }],
+ userInput: "exa",
+ autofilled: "example.com/first",
+ expected: "autofill_adaptive",
+ },
+ {
+ description: "Adaptive history autofill but pick another result",
+ useAdaptiveHistory: true,
+ visitHistory: ["http://example.com/first", "http://example.com/second"],
+ inputHistory: [{ uri: "http://example.com/first", input: "exa" }],
+ userInput: "exa",
+ urlToSelect: "http://example.com/second",
+ autofilled: "example.com/first",
+ expected: "autofill_adaptive",
+ },
+ {
+ description: "Adaptive history autofill but not pick any result",
+ unpickResult: true,
+ useAdaptiveHistory: true,
+ visitHistory: ["http://example.com/first", "http://example.com/second"],
+ inputHistory: [{ uri: "http://example.com/first", input: "exa" }],
+ userInput: "exa",
+ autofilled: "example.com/first",
+ },
+ {
+ description: "Origin autofill and pick it",
+ visitHistory: ["http://example.com/first", "http://example.com/second"],
+ userInput: "exa",
+ autofilled: "example.com/",
+ expected: "autofill_origin",
+ },
+ {
+ description: "Origin autofill but pick another result",
+ visitHistory: ["http://example.com/first", "http://example.com/second"],
+ userInput: "exa",
+ urlToSelect: "http://example.com/second",
+ autofilled: "example.com/",
+ expected: "autofill_origin",
+ },
+ {
+ description: "Origin autofill but not pick any result",
+ unpickResult: true,
+ visitHistory: ["http://example.com/first", "http://example.com/second"],
+ userInput: "exa",
+ autofilled: "example.com/",
+ },
+ {
+ description: "URL autofill and pick it",
+ visitHistory: ["http://example.com/first", "http://example.com/second"],
+ userInput: "example.com/",
+ autofilled: "example.com/",
+ expected: "autofill_url",
+ },
+ {
+ description: "URL autofill but pick another result",
+ visitHistory: ["http://example.com/first", "http://example.com/second"],
+ userInput: "example.com/",
+ urlToSelect: "http://example.com/second",
+ autofilled: "example.com/",
+ expected: "autofill_url",
+ },
+ {
+ description: "URL autofill but not pick any result",
+ unpickResult: true,
+ visitHistory: ["http://example.com/first", "http://example.com/second"],
+ userInput: "example.com/",
+ autofilled: "example.com/",
+ },
+ {
+ description: "about page autofill and pick it",
+ userInput: "about:a",
+ autofilled: "about:about",
+ expected: "autofill_about",
+ },
+ {
+ description: "about page autofill but pick another result",
+ userInput: "about:a",
+ urlToSelect: "about:addons",
+ autofilled: "about:about",
+ expected: "autofill_about",
+ },
+ {
+ description: "about page autofill but not pick any result",
+ unpickResult: true,
+ userInput: "about:a",
+ autofilled: "about:about",
+ },
+ {
+ description: "Preloaded site autofill and pick it",
+ usePreloadedSite: true,
+ preloadedSites: [["http://example.com/", "Example"]],
+ userInput: "exa",
+ autofilled: "example.com/",
+ expected: "autofill_preloaded",
+ },
+ {
+ description: "Preloaded site autofill but not pick any result",
+ unpickResult: true,
+ usePreloadedSite: true,
+ preloadedSites: [["http://example.com/", "Example"]],
+ userInput: "exa",
+ autofilled: "example.com/",
+ },
+ {
+ description: "Other provider's autofill and pick it",
+ useOtherProvider: true,
+ userInput: "example",
+ autofilled: "example.com/",
+ expected: "autofill_other",
+ },
+ {
+ description: "Other provider's autofill but not pick any result",
+ unpickResult: true,
+ useOtherProvider: true,
+ userInput: "example",
+ autofilled: "example.com/",
+ },
+ ];
+
+ for (const {
+ description,
+ useAdaptiveHistory = false,
+ usePreloadedSite = false,
+ useOtherProvider = false,
+ unpickResult = false,
+ visitHistory,
+ inputHistory,
+ preloadedSites,
+ userInput,
+ select,
+ autofilled,
+ expected,
+ } of testData) {
+ info(description);
+
+ UrlbarPrefs.set("autoFill.adaptiveHistory.enabled", useAdaptiveHistory);
+ if (usePreloadedSite) {
+ UrlbarPrefs.set("usepreloadedtopurls.enabled", true);
+ UrlbarPrefs.set("usepreloadedtopurls.expire_days", 100);
+ }
+ let otherProvider;
+ if (useOtherProvider) {
+ otherProvider = createOtherAutofillProvider(userInput, autofilled);
+ UrlbarProvidersManager.registerProvider(otherProvider);
+ }
+
+ if (visitHistory) {
+ await PlacesTestUtils.addVisits(visitHistory);
+ }
+ if (inputHistory) {
+ for (const { uri, input } of inputHistory) {
+ await UrlbarUtils.addToInputHistory(uri, input);
+ }
+ }
+ if (preloadedSites) {
+ UrlbarProviderPreloadedSites.populatePreloadedSiteStorage(preloadedSites);
+ }
+
+ await triggerAutofillAndPickResult(
+ userInput,
+ autofilled,
+ unpickResult,
+ select
+ );
+
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ if (unpickResult) {
+ TelemetryTestUtils.assertScalarUnset(
+ scalars,
+ "urlbar.impression.autofill_adaptive"
+ );
+ TelemetryTestUtils.assertScalarUnset(
+ scalars,
+ "urlbar.impression.autofill_origin"
+ );
+ TelemetryTestUtils.assertScalarUnset(
+ scalars,
+ "urlbar.impression.autofill_url"
+ );
+ TelemetryTestUtils.assertScalarUnset(
+ scalars,
+ "urlbar.impression.autofill_about"
+ );
+ } else {
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ `urlbar.impression.${expected}`,
+ 1
+ );
+ }
+
+ UrlbarPrefs.clear("autoFill.adaptiveHistory.enabled");
+ UrlbarPrefs.clear("usepreloadedtopurls.enabled");
+ UrlbarPrefs.clear("usepreloadedtopurls.expire_days");
+
+ if (otherProvider) {
+ UrlbarProvidersManager.unregisterProvider(otherProvider);
+ }
+
+ await PlacesTestUtils.clearInputHistory();
+ await PlacesUtils.history.clear();
+ }
+});
+
+// Checks autofill deletion telemetry.
+add_task(async function deletion() {
+ await PlacesTestUtils.addVisits(["http://example.com/"]);
+
+ info("Delete autofilled value by DELETE key");
+ await doDeletionTest({
+ firstSearchString: "exa",
+ firstAutofilledValue: "example.com/",
+ trigger: () => {
+ EventUtils.synthesizeKey("KEY_Delete");
+ Assert.equal(gURLBar.value, "exa");
+ },
+ expectedScalar: 1,
+ });
+
+ info("Delete autofilled value by BACKSPACE key");
+ await doDeletionTest({
+ firstSearchString: "exa",
+ firstAutofilledValue: "example.com/",
+ trigger: () => {
+ EventUtils.synthesizeKey("KEY_Backspace");
+ Assert.equal(gURLBar.value, "exa");
+ },
+ expectedScalar: 1,
+ });
+
+ info("Delete autofilled value twice");
+ await doDeletionTest({
+ firstSearchString: "exa",
+ firstAutofilledValue: "example.com/",
+ trigger: () => {
+ // Delete autofilled string.
+ EventUtils.synthesizeKey("KEY_Delete");
+ Assert.equal(gURLBar.value, "exa");
+
+ // Re-autofilling.
+ EventUtils.synthesizeKey("m");
+ Assert.equal(gURLBar.value, "example.com/");
+ Assert.equal(gURLBar.selectionStart, "exam".length);
+ Assert.equal(gURLBar.selectionEnd, "example.com/".length);
+
+ // Delete autofilled string again.
+ EventUtils.synthesizeKey("KEY_Backspace");
+ Assert.equal(gURLBar.value, "exam");
+ },
+ expectedScalar: 2,
+ });
+
+ info("Delete one char after unselecting autofilled string");
+ await doDeletionTest({
+ firstSearchString: "exa",
+ firstAutofilledValue: "example.com/",
+ trigger: () => {
+ // Cancel selection.
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ Assert.equal(gURLBar.selectionStart, "example.com/".length);
+ Assert.equal(gURLBar.selectionEnd, "example.com/".length);
+
+ EventUtils.synthesizeKey("KEY_Backspace");
+ Assert.equal(gURLBar.value, "example.com");
+ },
+ expectedScalar: 0,
+ });
+
+ info("Delete autofilled value after unselecting autofilled string");
+ await doDeletionTest({
+ firstSearchString: "exa",
+ firstAutofilledValue: "example.com/",
+ trigger: () => {
+ // Cancel selection.
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ Assert.equal(gURLBar.selectionStart, "example.com/".length);
+ Assert.equal(gURLBar.selectionEnd, "example.com/".length);
+
+ // Delete autofilled string one by one.
+ for (let i = 0; i < "mple.com/".length; i++) {
+ EventUtils.synthesizeKey("KEY_Backspace");
+ }
+ Assert.equal(gURLBar.value, "exa");
+ },
+ expectedScalar: 0,
+ });
+
+ info(
+ "Delete autofilled value after unselecting autofilled string then selecting them manually again"
+ );
+ await doDeletionTest({
+ firstSearchString: "exa",
+ firstAutofilledValue: "example.com/",
+ trigger: () => {
+ // Cancel selection.
+ const previousSelectionStart = gURLBar.selectionStart;
+ const previousSelectionEnd = gURLBar.selectionEnd;
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ Assert.equal(gURLBar.selectionStart, "example.com/".length);
+ Assert.equal(gURLBar.selectionEnd, "example.com/".length);
+
+ // Select same range again.
+ gURLBar.selectionStart = previousSelectionStart;
+ gURLBar.selectionEnd = previousSelectionEnd;
+
+ EventUtils.synthesizeKey("KEY_Backspace");
+ Assert.equal(gURLBar.value, "exa");
+ },
+ expectedScalar: 1,
+ });
+
+ await PlacesUtils.history.clear();
+});
+
+async function doDeletionTest({
+ firstSearchString,
+ firstAutofilledValue,
+ trigger,
+ expectedScalar,
+}) {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: firstSearchString,
+ fireInputEvent: true,
+ });
+ const details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.ok(details.autofill, "Result is autofill");
+ Assert.equal(gURLBar.value, firstAutofilledValue, "gURLBar.value");
+ Assert.equal(
+ gURLBar.selectionStart,
+ firstSearchString.length,
+ "selectionStart"
+ );
+ Assert.equal(
+ gURLBar.selectionEnd,
+ firstAutofilledValue.length,
+ "selectionEnd"
+ );
+
+ await trigger();
+
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ if (expectedScalar) {
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "urlbar.autofill_deletion",
+ expectedScalar
+ );
+ } else {
+ TelemetryTestUtils.assertScalarUnset(scalars, "urlbar.autofill_deletion");
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ });
+}
diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_dynamic.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_dynamic.js
new file mode 100644
index 0000000000..d4f4e77d57
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_dynamic.js
@@ -0,0 +1,136 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This file tests urlbar telemetry for dynamic results.
+ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs",
+ UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs",
+ UrlbarView: "resource:///modules/UrlbarView.sys.mjs",
+});
+
+const DYNAMIC_TYPE_NAME = "test";
+
+/**
+ * A test URLBar provider.
+ */
+class TestProvider extends UrlbarTestUtils.TestProvider {
+ constructor() {
+ super({
+ priority: Infinity,
+ results: [
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ dynamicType: DYNAMIC_TYPE_NAME,
+ }
+ ),
+ { heuristic: true }
+ ),
+ ],
+ });
+ }
+
+ getViewUpdate(result, idsByName) {
+ return {
+ title: {
+ textContent: "This is a dynamic result.",
+ },
+ button: {
+ textContent: "Click Me",
+ },
+ };
+ }
+}
+
+add_task(async function test() {
+ // Add a dynamic result type.
+ UrlbarResult.addDynamicResultType(DYNAMIC_TYPE_NAME);
+ UrlbarView.addDynamicViewTemplate(DYNAMIC_TYPE_NAME, {
+ stylesheet:
+ getRootDirectory(gTestPath) + "urlbarTelemetryUrlbarDynamic.css",
+ children: [
+ {
+ name: "title",
+ tag: "span",
+ },
+ {
+ name: "buttonSpacer",
+ tag: "span",
+ },
+ {
+ name: "button",
+ tag: "span",
+ attributes: {
+ role: "button",
+ },
+ },
+ ],
+ });
+ registerCleanupFunction(() => {
+ UrlbarView.removeDynamicViewTemplate(DYNAMIC_TYPE_NAME);
+ UrlbarResult.removeDynamicResultType(DYNAMIC_TYPE_NAME);
+ });
+
+ // Register a provider that returns the dynamic result type.
+ let provider = new TestProvider();
+ UrlbarProvidersManager.registerProvider(provider);
+ registerCleanupFunction(() => {
+ UrlbarProvidersManager.unregisterProvider(provider);
+ });
+
+ const histograms = snapshotHistograms();
+
+ // Do a search to show the dynamic result.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value: "test",
+ fireInputEvent: true,
+ });
+
+ // Press enter on the result's button. It will be preselected since the
+ // result is the heuristic.
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Enter")
+ );
+
+ assertTelemetryResults(
+ histograms,
+ "dynamic",
+ 0,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.enter
+ );
+
+ // Clean up for subsequent tests.
+ gURLBar.handleRevert();
+});
+
+function snapshotHistograms() {
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+ return {
+ resultMethodHist: TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ ),
+ };
+}
+
+function assertTelemetryResults(histograms, type, index, method) {
+ TelemetryTestUtils.assertHistogram(histograms.resultMethodHist, method, 1);
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ `urlbar.picked.${type}`,
+ index,
+ 1
+ );
+}
diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_extension.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_extension.js
new file mode 100644
index 0000000000..28eae06a6f
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_extension.js
@@ -0,0 +1,155 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This file tests urlbar telemetry with extension actions.
+ */
+
+"use strict";
+
+const SCALAR_URLBAR = "browser.engagement.navigation.urlbar";
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs",
+});
+
+function assertSearchTelemetryEmpty(search_hist) {
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false);
+ Assert.ok(
+ !(SCALAR_URLBAR in scalars),
+ `Should not have recorded ${SCALAR_URLBAR}`
+ );
+
+ // Make sure SEARCH_COUNTS contains identical values.
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.urlbar",
+ undefined
+ );
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.alias",
+ undefined
+ );
+
+ // Also check events.
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ );
+ events = (events.parent || []).filter(
+ e => e[1] == "navigation" && e[2] == "search"
+ );
+ Assert.deepEqual(
+ events,
+ [],
+ "Should not have recorded any navigation search events"
+ );
+}
+
+function snapshotHistograms() {
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+ return {
+ resultMethodHist: TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ ),
+ search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"),
+ };
+}
+
+function assertTelemetryResults(histograms, type, index, method) {
+ TelemetryTestUtils.assertHistogram(histograms.resultMethodHist, method, 1);
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ `urlbar.picked.${type}`,
+ index,
+ 1
+ );
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Disable search suggestions in the urlbar.
+ ["browser.urlbar.suggest.searches", false],
+ // Clear historical search suggestions to avoid interference from previous
+ // tests.
+ ["browser.urlbar.maxHistoricalSearchSuggestions", 0],
+ // Turn autofill off.
+ ["browser.urlbar.autoFill", false],
+ ],
+ });
+
+ // Enable local telemetry recording for the duration of the tests.
+ let oldCanRecord = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+
+ // Enable event recording for the events tested here.
+ Services.telemetry.setEventRecordingEnabled("navigation", true);
+
+ // Clear history so that history added by previous tests doesn't mess up this
+ // test when it selects results in the urlbar.
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ // Make sure to restore the engine once we're done.
+ registerCleanupFunction(async function () {
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ Services.telemetry.setEventRecordingEnabled("navigation", false);
+ });
+});
+
+add_task(async function test_extension() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ omnibox: {
+ keyword: "omniboxtest",
+ },
+
+ background() {
+ /* global browser */
+ browser.omnibox.setDefaultSuggestion({
+ description: "doit",
+ });
+ // Just do nothing for this test.
+ browser.omnibox.onInputEntered.addListener(() => {});
+ browser.omnibox.onInputChanged.addListener((text, suggest) => {
+ suggest([]);
+ });
+ },
+ },
+ });
+
+ await extension.startup();
+
+ const histograms = snapshotHistograms();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value: "omniboxtest ",
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("KEY_Enter");
+
+ assertSearchTelemetryEmpty(histograms.search_hist);
+ assertTelemetryResults(
+ histograms,
+ "extension",
+ 0,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.enter
+ );
+
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_handoff.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_handoff.js
new file mode 100644
index 0000000000..c2e1413a27
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_handoff.js
@@ -0,0 +1,182 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { SearchSERPTelemetry } = ChromeUtils.importESModule(
+ "resource:///modules/SearchSERPTelemetry.sys.mjs"
+);
+
+const TEST_PROVIDER_INFO = [
+ {
+ telemetryId: "example",
+ searchPageRegexp:
+ /^https:\/\/example.com\/browser\/browser\/components\/search\/test\/browser\/searchTelemetry(?:Ad)?.html/,
+ queryParamName: "s",
+ codeParamName: "abc",
+ taggedCodes: ["ff"],
+ followOnParamNames: ["a"],
+ extraAdServersRegexps: [/^https:\/\/example\.com\/ad2?/],
+ },
+];
+
+function getPageUrl(useAdPage = false) {
+ let page = useAdPage ? "searchTelemetryAd.html" : "searchTelemetry.html";
+ return `https://example.com/browser/browser/components/search/test/browser/${page}`;
+}
+
+// sharedData messages are only passed to the child on idle. Therefore
+// we wait for a few idles to try and ensure the messages have been able
+// to be passed across and handled.
+async function waitForIdle() {
+ for (let i = 0; i < 10; i++) {
+ await new Promise(resolve => Services.tm.idleDispatchToMainThread(resolve));
+ }
+}
+
+add_setup(async function () {
+ SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO);
+ await waitForIdle();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar",
+ true,
+ ],
+ ],
+ });
+
+ await SearchTestUtils.installSearchExtension(
+ {
+ search_url: getPageUrl(true),
+ search_url_get_params: "s={searchTerms}&abc=ff",
+ suggest_url:
+ "https://example.com/browser/browser/components/search/test/browser/searchSuggestionEngine.sjs",
+ suggest_url_get_params: "query={searchTerms}",
+ },
+ { setAsDefault: true }
+ );
+
+ const oldCanRecord = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+ Services.telemetry.setEventRecordingEnabled("navigation", true);
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.history.clear();
+
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ Services.telemetry.setEventRecordingEnabled("navigation", false);
+
+ SearchSERPTelemetry.overrideSearchTelemetryForTests();
+
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+ });
+});
+
+add_task(async function test_search() {
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+
+ const histogram =
+ TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS");
+
+ info("Load about:newtab in new window");
+ const newtab = "about:newtab";
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, newtab);
+ await BrowserTestUtils.browserStopped(tab.linkedBrowser, newtab);
+
+ info("Focus on search input in newtab content");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const searchInput = content.document.querySelector(".fake-editable");
+ searchInput.click();
+ });
+
+ info("Search and wait the result");
+ const onLoaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ EventUtils.synthesizeKey("q");
+ EventUtils.synthesizeKey("VK_RETURN");
+ await onLoaded;
+
+ info("Check the telemetries");
+ await assertHandoffResult(histogram);
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_search_private_mode() {
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+
+ const histogram =
+ TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS");
+
+ info("Open private window");
+ let privateWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ let tab = privateWindow.gBrowser.selectedTab;
+
+ info("Focus on search input in newtab content");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const searchInput = content.document.querySelector(".fake-editable");
+ searchInput.click();
+ });
+
+ info("Search and wait the result");
+ const onLoaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ EventUtils.synthesizeKey("q", {}, privateWindow);
+ EventUtils.synthesizeKey("VK_RETURN", {}, privateWindow);
+ await onLoaded;
+
+ info("Check the telemetries");
+ await assertHandoffResult(histogram);
+
+ await BrowserTestUtils.closeWindow(privateWindow);
+});
+
+async function assertHandoffResult(histogram) {
+ await assertScalars([
+ ["browser.engagement.navigation.urlbar_handoff", "search_enter", 1],
+ ["browser.search.content.urlbar_handoff", "example:tagged:ff", 1],
+ ]);
+ await assertHistogram(histogram, [["other-Example.urlbar-handoff", 1]]);
+ TelemetryTestUtils.assertEvents(
+ [
+ [
+ "navigation",
+ "search",
+ "urlbar_handoff",
+ "enter",
+ { engine: "other-Example" },
+ ],
+ ],
+ { category: "navigation", method: "search" }
+ );
+}
+
+async function assertHistogram(histogram, expectedResults) {
+ await TestUtils.waitForCondition(() => {
+ const snapshot = histogram.snapshot();
+ return expectedResults.every(([key]) => key in snapshot);
+ }, "Wait until the histogram has expected keys");
+
+ for (const [key, value] of expectedResults) {
+ TelemetryTestUtils.assertKeyedHistogramSum(histogram, key, value);
+ }
+}
+
+async function assertScalars(expectedResults) {
+ await TestUtils.waitForCondition(() => {
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true);
+ return expectedResults.every(([scalarName]) => scalarName in scalars);
+ }, "Wait until the scalars have expected keyed scalars");
+
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true);
+
+ for (const [scalarName, key, value] of expectedResults) {
+ TelemetryTestUtils.assertKeyedScalar(scalars, scalarName, key, value);
+ }
+}
diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_persisted.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_persisted.js
new file mode 100644
index 0000000000..904e774a2c
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_persisted.js
@@ -0,0 +1,270 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This file tests browser.engagement.navigation.urlbar_persisted and the
+ * event navigation.search.urlbar_persisted
+ */
+
+"use strict";
+
+const { SearchSERPTelemetry } = ChromeUtils.importESModule(
+ "resource:///modules/SearchSERPTelemetry.sys.mjs"
+);
+
+const SCALAR_URLBAR_PERSISTED =
+ "browser.engagement.navigation.urlbar_persisted";
+
+const SEARCH_STRING = "chocolate";
+
+let testEngine;
+add_setup(async () => {
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.showSearchTerms.featureGate", true]],
+ });
+
+ await SearchTestUtils.installSearchExtension(
+ {
+ name: "MozSearch",
+ search_url: "https://www.example.com/",
+ search_url_get_params: "q={searchTerms}&pc=fake_code",
+ },
+ { setAsDefault: true }
+ );
+
+ testEngine = Services.search.getEngineByName("MozSearch");
+
+ // Enable event recording for the events.
+ Services.telemetry.setEventRecordingEnabled("navigation", true);
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.history.clear();
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+ Services.telemetry.setEventRecordingEnabled("navigation", false);
+ });
+});
+
+async function searchForString(searchString, tab) {
+ info(`Search for string: ${searchString}.`);
+ let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl(
+ testEngine,
+ searchString
+ );
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ expectedSearchUrl
+ );
+ gURLBar.focus();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value: searchString,
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("KEY_Enter");
+ await browserLoadedPromise;
+ info("Finished loading search.");
+ return expectedSearchUrl;
+}
+
+async function gotoUrl(url, tab) {
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ url
+ );
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, url);
+ await browserLoadedPromise;
+ info(`Loaded page: ${url}`);
+}
+
+async function goBack(browser) {
+ let pageShowPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "pageshow"
+ );
+ browser.goBack();
+ await pageShowPromise;
+ info("Go back a page.");
+}
+
+async function goForward(browser) {
+ let pageShowPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "pageshow"
+ );
+ browser.goForward();
+ await pageShowPromise;
+ info("Go forward a page.");
+}
+
+function assertScalarSearchEnter(number) {
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ SCALAR_URLBAR_PERSISTED,
+ "search_enter",
+ number
+ );
+}
+
+function assertScalarDoesNotExist(scalar) {
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", true, false);
+ Assert.ok(!(scalar in scalars), scalar + " must not be recorded.");
+}
+
+function assertTelemetryEvents() {
+ TelemetryTestUtils.assertEvents(
+ [
+ [
+ "navigation",
+ "search",
+ "urlbar",
+ "enter",
+ { engine: "other-MozSearch" },
+ ],
+ [
+ "navigation",
+ "search",
+ "urlbar_persisted",
+ "enter",
+ { engine: "other-MozSearch" },
+ ],
+ ],
+ {
+ category: "navigation",
+ method: "search",
+ }
+ );
+}
+
+// A user making a search after making a search should result
+// in the telemetry being recorded.
+add_task(async function search_after_search() {
+ let search_hist =
+ TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS");
+
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ await searchForString(SEARCH_STRING, tab);
+
+ // Scalar should not exist from a blank page, only when a search
+ // is conducted from a default SERP.
+ await assertScalarDoesNotExist(SCALAR_URLBAR_PERSISTED);
+
+ // After the first search, we should expect the SAP to change
+ // because the search term should show up on the SERP.
+ await searchForString(SEARCH_STRING, tab);
+ assertScalarSearchEnter(1);
+
+ // Check search counts.
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.urlbar-persisted",
+ 1
+ );
+
+ // Check events.
+ assertTelemetryEvents();
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// A user going to a tab that contains a SERP should
+// trigger the telemetry when conducting a search.
+add_task(async function switch_to_tab_and_search() {
+ let search_hist =
+ TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS");
+
+ const tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ await searchForString(SEARCH_STRING, tab1);
+
+ const tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ await gotoUrl("https://www.example.com/some-place", tab2);
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ await searchForString(SEARCH_STRING, tab1);
+ assertScalarSearchEnter(1);
+
+ // Check search count.
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.urlbar-persisted",
+ 1
+ );
+
+ // Check events.
+ assertTelemetryEvents();
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
+
+// When a user reverts the Urlbar after the search terms persist,
+// conducting another search should still be registered as a
+// urlbar-persisted SAP.
+add_task(async function handle_revert() {
+ let search_hist =
+ TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS");
+
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ await searchForString(SEARCH_STRING, tab);
+
+ gURLBar.handleRevert();
+ await searchForString(SEARCH_STRING, tab);
+
+ assertScalarSearchEnter(1);
+
+ // Check search count.
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.urlbar-persisted",
+ 1
+ );
+
+ // Check events.
+ assertTelemetryEvents();
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// A user going back and forth in history should trigger
+// urlbar-persisted telemetry when returning to a SERP
+// and conducting a search.
+add_task(async function back_and_forth() {
+ let search_hist =
+ TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS");
+
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ // Create three pages in history: a page, a SERP, and a page.
+ await gotoUrl("https://www.example.com/some-place", tab);
+ await searchForString(SEARCH_STRING, tab);
+ await gotoUrl("https://www.example.com/another-page", tab);
+
+ // Go back to the SERP by using both back and forward.
+ await goBack(tab.linkedBrowser);
+ await goBack(tab.linkedBrowser);
+ await goForward(tab.linkedBrowser);
+ await assertScalarDoesNotExist(SCALAR_URLBAR_PERSISTED);
+
+ // Then do a search.
+ await searchForString(SEARCH_STRING, tab);
+ assertScalarSearchEnter(1);
+
+ // Check search count.
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.urlbar-persisted",
+ 1
+ );
+
+ // Check events.
+ assertTelemetryEvents();
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_places.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_places.js
new file mode 100644
index 0000000000..26500033eb
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_places.js
@@ -0,0 +1,270 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This file tests urlbar telemetry with places related actions (e.g. history/
+ * bookmark selection).
+ */
+
+"use strict";
+
+const SCALAR_URLBAR = "browser.engagement.navigation.urlbar";
+
+const TEST_URL = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://mochi.test:8888"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs",
+});
+
+function searchInAwesomebar(value, win = window) {
+ return UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ waitForFocus,
+ value,
+ fireInputEvent: true,
+ });
+}
+
+function assertSearchTelemetryEmpty(search_hist) {
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false);
+ Assert.ok(
+ !(SCALAR_URLBAR in scalars),
+ `Should not have recorded ${SCALAR_URLBAR}`
+ );
+
+ // Make sure SEARCH_COUNTS contains identical values.
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.urlbar",
+ undefined
+ );
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.alias",
+ undefined
+ );
+
+ // Also check events.
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ );
+ events = (events.parent || []).filter(
+ e => e[1] == "navigation" && e[2] == "search"
+ );
+ Assert.deepEqual(
+ events,
+ [],
+ "Should not have recorded any navigation search events"
+ );
+}
+
+function snapshotHistograms() {
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+ return {
+ resultMethodHist: TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ ),
+ search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"),
+ };
+}
+
+function assertTelemetryResults(histograms, type, index, method) {
+ TelemetryTestUtils.assertHistogram(histograms.resultMethodHist, method, 1);
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ `urlbar.picked.${type}`,
+ index,
+ 1
+ );
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Disable search suggestions in the urlbar.
+ ["browser.urlbar.suggest.searches", false],
+ // Clear historical search suggestions to avoid interference from previous
+ // tests.
+ ["browser.urlbar.maxHistoricalSearchSuggestions", 0],
+ // Turn autofill off.
+ ["browser.urlbar.autoFill", false],
+ ],
+ });
+
+ // Enable local telemetry recording for the duration of the tests.
+ let oldCanRecord = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+
+ // Enable event recording for the events tested here.
+ Services.telemetry.setEventRecordingEnabled("navigation", true);
+
+ // Clear history so that history added by previous tests doesn't mess up this
+ // test when it selects results in the urlbar.
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ await PlacesUtils.keywords.insert({
+ keyword: "get",
+ url: TEST_URL + "?q=%s",
+ });
+
+ // Make sure to restore the engine once we're done.
+ registerCleanupFunction(async function () {
+ await PlacesUtils.keywords.remove("get");
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ Services.telemetry.setEventRecordingEnabled("navigation", false);
+ });
+});
+
+add_task(async function test_history() {
+ const histograms = snapshotHistograms();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com",
+ title: "example",
+ transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ },
+ ]);
+
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("example");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ assertSearchTelemetryEmpty(histograms.search_hist);
+ assertTelemetryResults(
+ histograms,
+ "history",
+ 1,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_bookmark() {
+ const histograms = snapshotHistograms();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ let bm = await PlacesUtils.bookmarks.insert({
+ url: "http://example.com",
+ title: "example",
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ });
+
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("example");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ assertSearchTelemetryEmpty(histograms.search_hist);
+ assertTelemetryResults(
+ histograms,
+ "bookmark",
+ 1,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection
+ );
+
+ await PlacesUtils.bookmarks.remove(bm);
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_keyword() {
+ const histograms = snapshotHistograms();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("get example");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ assertSearchTelemetryEmpty(histograms.search_hist);
+ assertTelemetryResults(
+ histograms,
+ "keyword",
+ 0,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.enter
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_switchtab() {
+ const histograms = snapshotHistograms();
+
+ let homeTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:buildconfig"
+ );
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:mozilla"
+ );
+
+ let p = BrowserTestUtils.waitForEvent(gBrowser, "TabSwitchDone");
+ await searchInAwesomebar("about:buildconfig");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ assertSearchTelemetryEmpty(histograms.search_hist);
+ assertTelemetryResults(
+ histograms,
+ "switchtab",
+ 1,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(homeTab);
+});
+
+add_task(async function test_visitURL() {
+ const histograms = snapshotHistograms();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInAwesomebar("http://example.com/a/");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ assertSearchTelemetryEmpty(histograms.search_hist);
+ assertTelemetryResults(
+ histograms,
+ "visiturl",
+ 0,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.enter
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_quickactions.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_quickactions.js
new file mode 100644
index 0000000000..b29807900b
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_quickactions.js
@@ -0,0 +1,133 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This file tests urlbar telemetry for quickactions.
+ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarProviderQuickActions:
+ "resource:///modules/UrlbarProviderQuickActions.sys.mjs",
+});
+
+let testActionCalled = 0;
+
+add_setup(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.quickactions", true],
+ ["browser.urlbar.quickactions.enabled", true],
+ ],
+ });
+
+ UrlbarProviderQuickActions.addAction("testaction", {
+ commands: ["testaction"],
+ label: "quickactions-downloads2",
+ onPick: () => testActionCalled++,
+ });
+
+ registerCleanupFunction(() => {
+ UrlbarProviderQuickActions.removeAction("testaction");
+ });
+});
+
+add_task(async function test() {
+ const histograms = snapshotHistograms();
+
+ // Do a search to show the quickaction.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "testaction",
+ waitForFocus,
+ fireInputEvent: true,
+ });
+
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_Enter");
+ });
+
+ Assert.equal(testActionCalled, 1, "Test action was called");
+
+ TelemetryTestUtils.assertHistogram(
+ histograms.resultMethodHist,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection,
+ 1
+ );
+
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ `urlbar.picked.quickaction`,
+ 1,
+ 1
+ );
+
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "quickaction.picked",
+ "testaction-10",
+ 1
+ );
+
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "quickaction.impression",
+ "testaction-10",
+ 1
+ );
+
+ // Clean up for subsequent tests.
+ gURLBar.handleRevert();
+});
+
+add_task(async function test_impressions() {
+ UrlbarProviderQuickActions.addAction("testaction2", {
+ commands: ["testaction2"],
+ label: "quickactions-downloads2",
+ onPick: () => {},
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "testaction",
+ waitForFocus,
+ fireInputEvent: true,
+ });
+
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_Enter");
+ });
+
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "quickaction.impression",
+ `testaction-10`,
+ 1
+ );
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "quickaction.impression",
+ `testaction2-10`,
+ 1
+ );
+
+ UrlbarProviderQuickActions.removeAction("testaction2");
+ gURLBar.handleRevert();
+});
+
+function snapshotHistograms() {
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+ return {
+ resultMethodHist: TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ ),
+ };
+}
diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_remotetab.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_remotetab.js
new file mode 100644
index 0000000000..ffa3158f2b
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_remotetab.js
@@ -0,0 +1,185 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This file tests urlbar telemetry with remote tab action.
+ */
+
+"use strict";
+
+const SCALAR_URLBAR = "browser.engagement.navigation.urlbar";
+
+ChromeUtils.defineESModuleGetters(this, {
+ SyncedTabs: "resource://services-sync/SyncedTabs.sys.mjs",
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs",
+});
+
+function assertSearchTelemetryEmpty(search_hist) {
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false);
+ Assert.ok(
+ !(SCALAR_URLBAR in scalars),
+ `Should not have recorded ${SCALAR_URLBAR}`
+ );
+
+ // Make sure SEARCH_COUNTS contains identical values.
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.urlbar",
+ undefined
+ );
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.alias",
+ undefined
+ );
+
+ // Also check events.
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ );
+ events = (events.parent || []).filter(
+ e => e[1] == "navigation" && e[2] == "search"
+ );
+ Assert.deepEqual(
+ events,
+ [],
+ "Should not have recorded any navigation search events"
+ );
+}
+
+function snapshotHistograms() {
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+ return {
+ resultMethodHist: TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ ),
+ search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"),
+ };
+}
+
+function assertTelemetryResults(histograms, type, index, method) {
+ TelemetryTestUtils.assertHistogram(histograms.resultMethodHist, method, 1);
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ `urlbar.picked.${type}`,
+ index,
+ 1
+ );
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Disable search suggestions in the urlbar.
+ ["browser.urlbar.suggest.searches", false],
+ // Clear historical search suggestions to avoid interference from previous
+ // tests.
+ ["browser.urlbar.maxHistoricalSearchSuggestions", 0],
+ // Turn autofill off.
+ ["browser.urlbar.autoFill", false],
+ // Special prefs for remote tabs.
+ ["services.sync.username", "fake"],
+ ["services.sync.syncedTabs.showRemoteTabs", true],
+ ],
+ });
+
+ // Enable local telemetry recording for the duration of the tests.
+ let oldCanRecord = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+
+ // Enable event recording for the events tested here.
+ Services.telemetry.setEventRecordingEnabled("navigation", true);
+
+ // Clear history so that history added by previous tests doesn't mess up this
+ // test when it selects results in the urlbar.
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ const REMOTE_TAB = {
+ id: "7cqCr77ptzX3",
+ type: "client",
+ lastModified: 1492201200,
+ name: "zcarter's Nightly on MacBook-Pro-25",
+ clientType: "desktop",
+ tabs: [
+ {
+ type: "tab",
+ title: "Test Remote",
+ url: "http://example.com",
+ icon: UrlbarUtils.ICON.DEFAULT,
+ client: "7cqCr77ptzX3",
+ lastUsed: Math.floor(Date.now() / 1000),
+ },
+ ],
+ };
+
+ const sandbox = sinon.createSandbox();
+
+ let originalSyncedTabsInternal = SyncedTabs._internal;
+ SyncedTabs._internal = {
+ isConfiguredToSyncTabs: true,
+ hasSyncedThisSession: true,
+ getTabClients() {
+ return Promise.resolve([]);
+ },
+ syncTabs() {
+ return Promise.resolve();
+ },
+ };
+
+ // Tell the Sync XPCOM service it is initialized.
+ let weaveXPCService = Cc["@mozilla.org/weave/service;1"].getService(
+ Ci.nsISupports
+ ).wrappedJSObject;
+ let oldWeaveServiceReady = weaveXPCService.ready;
+ weaveXPCService.ready = true;
+
+ sandbox
+ .stub(SyncedTabs._internal, "getTabClients")
+ .callsFake(() => Promise.resolve(Cu.cloneInto([REMOTE_TAB], {})));
+
+ // Make sure to restore the engine once we're done.
+ registerCleanupFunction(async function () {
+ sandbox.restore();
+ weaveXPCService.ready = oldWeaveServiceReady;
+ SyncedTabs._internal = originalSyncedTabsInternal;
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ Services.telemetry.setEventRecordingEnabled("navigation", false);
+ });
+});
+
+add_task(async function test_remotetab() {
+ const histograms = snapshotHistograms();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value: "example",
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ assertSearchTelemetryEmpty(histograms.search_hist);
+ assertTelemetryResults(
+ histograms,
+ "remotetab",
+ 1,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_searchmode.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_searchmode.js
new file mode 100644
index 0000000000..7830102cf6
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_searchmode.js
@@ -0,0 +1,592 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This file tests the urlbar.searchmode.* scalars telemetry with search mode
+ * related actions.
+ */
+
+"use strict";
+
+const ENTRY_SCALAR_PREFIX = "urlbar.searchmode.";
+const PICKED_SCALAR_PREFIX = "urlbar.picked.searchmode.";
+const ENGINE_ALIAS = "alias";
+const TEST_QUERY = "test";
+let engineName;
+let engineDomain;
+
+// The preference to enable suggestions.
+const SUGGEST_PREF = "browser.search.suggest.enabled";
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarProviderTabToSearch:
+ "resource:///modules/UrlbarProviderTabToSearch.sys.mjs",
+});
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "TouchBarHelper",
+ "@mozilla.org/widget/touchbarhelper;1",
+ "nsITouchBarHelper"
+);
+
+/**
+ * Asserts that search mode telemetry was recorded correctly. Checks both the
+ * urlbar.searchmode.* and urlbar.searchmode_picked.* probes.
+ *
+ * @param {string} entry
+ * A search mode entry point.
+ * @param {string} engineOrSource
+ * An engine name or a search mode source.
+ * @param {number} [resultIndex]
+ * The index of the result picked while in search mode. Only pass this
+ * parameter if a result is picked.
+ */
+function assertSearchModeScalars(entry, engineOrSource, resultIndex = -1) {
+ // Check if the urlbar.searchmode.entry scalar contains the expected value.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ ENTRY_SCALAR_PREFIX + entry,
+ engineOrSource,
+ 1
+ );
+
+ for (let e of UrlbarUtils.SEARCH_MODE_ENTRY) {
+ if (e == entry) {
+ Assert.equal(
+ Object.keys(scalars[ENTRY_SCALAR_PREFIX + entry]).length,
+ 1,
+ `This search must only increment one entry in the correct scalar: ${e}`
+ );
+ } else {
+ Assert.ok(
+ !scalars[ENTRY_SCALAR_PREFIX + e],
+ `No other urlbar.searchmode scalars should be recorded. Checking ${e}`
+ );
+ }
+ }
+
+ if (resultIndex >= 0) {
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ PICKED_SCALAR_PREFIX + entry,
+ resultIndex,
+ 1
+ );
+ }
+
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Disable tab-to-search onboarding results for general tests. They are
+ // enabled in tests that specifically address onboarding.
+ ["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0],
+ ],
+ });
+
+ // Create an engine to generate search suggestions and add it as default
+ // for this test.
+ let suggestionEngine = await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + "urlbarTelemetrySearchSuggestions.xml",
+ setAsDefault: true,
+ });
+ suggestionEngine.alias = ENGINE_ALIAS;
+ engineDomain = suggestionEngine.searchUrlDomain;
+ engineName = suggestionEngine.name;
+
+ // And the first one-off engine.
+ await Services.search.moveEngine(suggestionEngine, 0);
+
+ // Enable local telemetry recording for the duration of the tests.
+ let oldCanRecord = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+
+ // Clear history so that history added by previous tests doesn't mess up this
+ // test when it selects results in the urlbar.
+ await PlacesUtils.history.clear();
+
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+
+ // Clear historical search suggestions to avoid interference from previous
+ // tests.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.maxHistoricalSearchSuggestions", 0]],
+ });
+
+ // Make sure to restore the engine once we're done.
+ registerCleanupFunction(async function () {
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ await PlacesUtils.history.clear();
+ Services.telemetry.setEventRecordingEnabled("navigation", false);
+ });
+});
+
+// Clicks the first one off.
+add_task(async function test_oneoff_remote() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_QUERY,
+ });
+ // Enters search mode by clicking a one-off.
+ await UrlbarTestUtils.enterSearchMode(window);
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+ assertSearchModeScalars("oneoff", "other", 0);
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Clicks the history one off.
+add_task(async function test_oneoff_local() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_QUERY,
+ });
+ // Enters search mode by clicking a one-off.
+ await UrlbarTestUtils.enterSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ });
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+ assertSearchModeScalars("oneoff", "history", 0);
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Checks that the Amazon search mode name is collapsed to "Amazon".
+add_task(async function test_oneoff_amazon() {
+ // Disable suggestions to avoid hitting Amazon servers.
+ await SpecialPowers.pushPrefEnv({
+ set: [[SUGGEST_PREF, false]],
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_QUERY,
+ });
+ // Enters search mode by clicking a one-off.
+ await UrlbarTestUtils.enterSearchMode(window, {
+ engineName: "Amazon.com",
+ });
+ assertSearchModeScalars("oneoff", "Amazon");
+ await UrlbarTestUtils.exitSearchMode(window);
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+// Checks that the Wikipedia search mode name is collapsed to "Wikipedia".
+add_task(async function test_oneoff_wikipedia() {
+ // Disable suggestions to avoid hitting Wikipedia servers.
+ await SpecialPowers.pushPrefEnv({
+ set: [[SUGGEST_PREF, false]],
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_QUERY,
+ });
+ // Enters search mode by clicking a one-off.
+ await UrlbarTestUtils.enterSearchMode(window, {
+ engineName: "Wikipedia (en)",
+ });
+ assertSearchModeScalars("oneoff", "Wikipedia");
+ await UrlbarTestUtils.exitSearchMode(window);
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+// Enters search mode by pressing the keyboard shortcut.
+add_task(async function test_shortcut() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_QUERY,
+ });
+ // Enter search mode by pressing the keyboard shortcut.
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("k", { accelKey: true });
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ entry: "shortcut",
+ });
+ assertSearchModeScalars("shortcut", "other");
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Enters search mode by selecting a Top Site from the Urlbar.
+add_task(async function test_topsites_urlbar() {
+ // Disable suggestions to avoid hitting Amazon servers.
+ await SpecialPowers.pushPrefEnv({
+ set: [[SUGGEST_PREF, false]],
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ // Enter search mode by selecting a Top Site from the Urlbar.
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ if (gURLBar.getAttribute("pageproxystate") == "invalid") {
+ gURLBar.handleRevert();
+ }
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ let amazonSearch = await UrlbarTestUtils.waitForAutocompleteResultAt(
+ window,
+ 0
+ );
+ Assert.equal(
+ amazonSearch.result.payload.keyword,
+ "@amazon",
+ "First result should have the Amazon keyword."
+ );
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeMouseAtCenter(amazonSearch, {});
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: amazonSearch.result.payload.engine,
+ entry: "topsites_urlbar",
+ });
+ assertSearchModeScalars("topsites_urlbar", "Amazon");
+ await UrlbarTestUtils.exitSearchMode(window);
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+// Enters search mode by selecting a keyword offer result.
+add_task(async function test_keywordoffer() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ // Do a search for "@" + our test alias. It should autofill with a trailing
+ // space, and the heuristic result should be an autofill result with a keyword
+ // offer.
+ let alias = "@" + ENGINE_ALIAS;
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: alias,
+ });
+ let keywordOfferResult = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ 0
+ );
+ Assert.equal(
+ keywordOfferResult.searchParams.keyword,
+ alias,
+ "The first result should be a keyword search result with the correct alias."
+ );
+
+ // Pick the keyword offer result.
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName,
+ entry: "keywordoffer",
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ });
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+ assertSearchModeScalars("keywordoffer", "other", 0);
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Enters search mode by typing an alias.
+add_task(async function test_typed() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ // Enter search mode by selecting a keywordoffer result.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: `${ENGINE_ALIAS} `,
+ });
+
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey(" ");
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName,
+ entry: "typed",
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ });
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+ assertSearchModeScalars("typed", "other", 0);
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Enters search mode by calling the same function called by the Search
+// Bookmarks menu item in Library > Bookmarks.
+add_task(async function test_bookmarkmenu() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ PlacesCommandHook.searchBookmarks();
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ entry: "bookmarkmenu",
+ });
+ assertSearchModeScalars("bookmarkmenu", "bookmarks");
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Enters search mode by calling the same function called from a History
+// menu.
+add_task(async function test_historymenu() {
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ PlacesCommandHook.searchHistory();
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ entry: "historymenu",
+ });
+ assertSearchModeScalars("historymenu", "history");
+});
+
+// Enters search mode by calling the same function called by the Search Tabs
+// menu item in the tab overflow menu.
+add_task(async function test_tabmenu() {
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ gTabsPanel.searchTabs();
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.TABS,
+ entry: "tabmenu",
+ });
+ assertSearchModeScalars("tabmenu", "tabs");
+});
+
+// Enters search mode by performing a search handoff on about:privatebrowsing.
+// Note that handoff-to-search-mode only occurs when suggestions are disabled
+// in the Urlbar.
+// NOTE: We don't test handoff on about:home. Running mochitests on about:home
+// is quite difficult. This subtest verifies that `handoff` is a valid scalar
+// suffix and that a call to UrlbarInput.handoff(value, searchEngine) records
+// values in the urlbar.searchmode.handoff scalar. PlacesFeed.test.js verfies that
+// about:home handoff makes that exact call.
+add_task(async function test_handoff_pbm() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.searches", false]],
+ });
+ let win = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ waitForTabURL: "about:privatebrowsing",
+ });
+ let tab = win.gBrowser.selectedBrowser;
+
+ await SpecialPowers.spawn(tab, [], async function () {
+ let btn = content.document.getElementById("search-handoff-button");
+ btn.click();
+ });
+
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(win);
+ await new Promise(r => EventUtils.synthesizeKey("f", {}, win, r));
+ await searchPromise;
+ await UrlbarTestUtils.assertSearchMode(win, {
+ engineName,
+ entry: "handoff",
+ });
+ assertSearchModeScalars("handoff", "other");
+
+ await UrlbarTestUtils.exitSearchMode(win);
+ await UrlbarTestUtils.promisePopupClose(win);
+ await BrowserTestUtils.closeWindow(win);
+ await SpecialPowers.popPrefEnv();
+});
+
+// Enters search mode by tapping a search shortcut on the Touch Bar.
+add_task(async function test_touchbar() {
+ if (AppConstants.platform != "macosx") {
+ return;
+ }
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: TEST_QUERY,
+ });
+ // We have to fake the tap on the Touch Bar since mochitests have no way of
+ // interacting with the Touch Bar.
+ TouchBarHelper.insertRestrictionInUrlbar(UrlbarTokenizer.RESTRICT.HISTORY);
+ await UrlbarTestUtils.assertSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ entry: "touchbar",
+ });
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+ assertSearchModeScalars("touchbar", "history", 0);
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Enters search mode by selecting a tab-to-search result.
+// Tests that tab-to-search results preview search mode when highlighted. These
+// results are worth testing separately since they do not set the
+// payload.keyword parameter.
+add_task(async function test_tabtosearch() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Do not show the onboarding result for this subtest.
+ ["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0],
+ ],
+ });
+ await PlacesTestUtils.addVisits([`http://${engineDomain}/`]);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: engineDomain.slice(0, 4),
+ });
+ let tabToSearchResult = (
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)
+ ).result;
+ Assert.equal(
+ tabToSearchResult.providerName,
+ "TabToSearch",
+ "The second result is a tab-to-search result."
+ );
+ Assert.equal(
+ tabToSearchResult.payload.engine,
+ engineName,
+ "The tab-to-search result is for the correct engine."
+ );
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 1,
+ "Sanity check: The second result is selected."
+ );
+ // Pick the tab-to-search result.
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName,
+ entry: "tabtosearch",
+ });
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ });
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+ assertSearchModeScalars("tabtosearch", "other", 0);
+
+ BrowserTestUtils.removeTab(tab);
+
+ await PlacesUtils.history.clear();
+ await SpecialPowers.popPrefEnv();
+});
+
+// Enters search mode by selecting a tab-to-search onboarding result.
+add_task(async function test_tabtosearch_onboard() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 3]],
+ });
+ await PlacesTestUtils.addVisits([`http://${engineDomain}/`]);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: engineDomain.slice(0, 4),
+ fireInputEvent: true,
+ });
+ let tabToSearchResult = (
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)
+ ).result;
+ Assert.equal(
+ tabToSearchResult.providerName,
+ "TabToSearch",
+ "The second result is a tab-to-search result."
+ );
+ Assert.equal(
+ tabToSearchResult.payload.engine,
+ engineName,
+ "The tab-to-search result is for the correct engine."
+ );
+ Assert.equal(
+ tabToSearchResult.payload.dynamicType,
+ "onboardTabToSearch",
+ "The tab-to-search result is an onboarding result."
+ );
+ await UrlbarTestUtils.assertSearchMode(window, null);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 1,
+ "Sanity check: The second result is selected."
+ );
+ // Pick the tab-to-search onboarding result.
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName,
+ entry: "tabtosearch_onboard",
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ });
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+ assertSearchModeScalars("tabtosearch_onboard", "other", 0);
+
+ UrlbarPrefs.set("tabToSearch.onboard.interactionsLeft", 3);
+ delete UrlbarProviderTabToSearch.onboardingInteractionAtTime;
+
+ BrowserTestUtils.removeTab(tab);
+ await PlacesUtils.history.clear();
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_sponsored_topsites.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_sponsored_topsites.js
new file mode 100644
index 0000000000..74d1fdb0db
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_sponsored_topsites.js
@@ -0,0 +1,181 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ CONTEXTUAL_SERVICES_PING_TYPES:
+ "resource:///modules/PartnerLinkAttribution.sys.mjs",
+ NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs",
+ PartnerLinkAttribution: "resource:///modules/PartnerLinkAttribution.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ HttpServer: "resource://testing-common/httpd.js",
+});
+
+const EN_US_TOPSITES =
+ "https://www.youtube.com/,https://www.facebook.com/,https://www.amazon.com/,https://www.reddit.com/,https://www.wikipedia.org/,https://twitter.com/";
+
+// This is used for "sendAttributionRequest"
+var gHttpServer = null;
+var gRequests = [];
+
+function submitHandler(request, response) {
+ gRequests.push(request);
+ response.setStatusLine(request.httpVersion, 200, "Ok");
+}
+
+// Spy for telemetry sender
+let spy;
+
+add_setup(async function () {
+ sandbox = sinon.createSandbox();
+ spy = sandbox.spy(
+ PartnerLinkAttribution._pingCentre,
+ "sendStructuredIngestionPing"
+ );
+
+ let topsitesAttribution = Services.prefs.getStringPref(
+ "browser.partnerlink.campaign.topsites"
+ );
+ gHttpServer = new HttpServer();
+ gHttpServer.registerPathHandler(`/cid/${topsitesAttribution}`, submitHandler);
+ gHttpServer.start(-1);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.sponsoredTopSites", true],
+ ["browser.urlbar.suggest.topsites", true],
+ ["browser.newtabpage.activity-stream.default.sites", EN_US_TOPSITES],
+ [
+ "browser.partnerlink.attributionURL",
+ `http://localhost:${gHttpServer.identity.primaryPort}/cid/`,
+ ],
+ ],
+ });
+
+ await updateTopSites(
+ sites => sites && sites.length == EN_US_TOPSITES.split(",").length
+ );
+
+ registerCleanupFunction(async () => {
+ sandbox.restore();
+ await gHttpServer.stop();
+ gHttpServer = null;
+ });
+});
+
+add_task(async function send_impression_and_click() {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ let link = {
+ label: "test_label",
+ url: "http://example.com/",
+ sponsored_position: 1,
+ sendAttributionRequest: true,
+ sponsored_tile_id: 42,
+ sponsored_impression_url: "http://impression.test.com/",
+ sponsored_click_url: "http://click.test.com/",
+ };
+ // Pin a sponsored TopSite to set up the test fixture
+ NewTabUtils.pinnedLinks.pin(link, 0);
+
+ await updateTopSites(sites => sites && sites[0] && sites[0].isPinned);
+
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ // Select the first result and confirm it.
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+
+ let loadPromise = BrowserTestUtils.waitForDocLoadAndStopIt(
+ result.url,
+ gBrowser.selectedBrowser
+ );
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+
+ Assert.ok(
+ spy.calledTwice,
+ "Should send an impression ping and a click ping"
+ );
+
+ // Validate the impression ping
+ let [payload, endpoint] = spy.firstCall.args;
+ Assert.ok(
+ endpoint.includes(CONTEXTUAL_SERVICES_PING_TYPES.TOPSITES_IMPRESSION),
+ "Should set the endpoint for TopSites impression"
+ );
+ Assert.ok(!!payload.context_id, "Should set the context_id");
+ Assert.equal(payload.advertiser, "test_label", "Should set the advertiser");
+ Assert.equal(
+ payload.reporting_url,
+ "http://impression.test.com/",
+ "Should set the impression reporting URL"
+ );
+ Assert.equal(payload.tile_id, 42, "Should set the tile_id");
+ Assert.equal(payload.position, 1, "Should set the position");
+
+ // Validate the click ping
+ [payload, endpoint] = spy.secondCall.args;
+ Assert.ok(
+ endpoint.includes(CONTEXTUAL_SERVICES_PING_TYPES.TOPSITES_SELECTION),
+ "Should set the endpoint for TopSites click"
+ );
+ Assert.ok(!!payload.context_id, "Should set the context_id");
+ Assert.equal(
+ payload.reporting_url,
+ "http://click.test.com/",
+ "Should set the click reporting URL"
+ );
+ Assert.equal(payload.tile_id, 42, "Should set the tile_id");
+ Assert.equal(payload.position, 1, "Should set the position");
+
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ gURLBar.blur();
+ });
+
+ NewTabUtils.pinnedLinks.unpin(link);
+ });
+});
+
+add_task(async function zero_ping() {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ spy.resetHistory();
+
+ // Reload the TopSites
+ await updateTopSites(
+ sites => sites && sites.length == EN_US_TOPSITES.split(",").length
+ );
+
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ // Select the first result and confirm it.
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+
+ let loadPromise = BrowserTestUtils.waitForDocLoadAndStopIt(
+ result.url,
+ gBrowser.selectedBrowser
+ );
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+
+ Assert.ok(
+ spy.notCalled,
+ "Should not send any ping if there is no sponsored Top Site"
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ gURLBar.blur();
+ });
+ });
+});
diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tabtosearch.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tabtosearch.js
new file mode 100644
index 0000000000..35607d2f94
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tabtosearch.js
@@ -0,0 +1,416 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This file tests telemetry for tabtosearch results.
+ * NB: This file does not test the search mode `entry` field for tab-to-search
+ * results. That is tested in browser_UsageTelemetry_urlbar_searchmode.js.
+ */
+
+"use strict";
+
+const ENGINE_NAME = "MozSearch";
+const ENGINE_DOMAIN = "example.com";
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarProviderTabToSearch:
+ "resource:///modules/UrlbarProviderTabToSearch.sys.mjs",
+});
+
+function snapshotHistograms() {
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+ return {
+ resultMethodHist: TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ ),
+ search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"),
+ };
+}
+
+function assertTelemetryResults(histograms, type, index, method) {
+ TelemetryTestUtils.assertHistogram(histograms.resultMethodHist, method, 1);
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ `urlbar.picked.${type}`,
+ index,
+ 1
+ );
+}
+
+/**
+ * Checks to see if the second result in the Urlbar is a tab-to-search result
+ * with the correct engine.
+ *
+ * @param {string} engineName
+ * The expected engine name.
+ * @param {boolean} [isOnboarding]
+ * If true, expects the tab-to-search result to be an onbarding result.
+ */
+async function checkForTabToSearchResult(engineName, isOnboarding) {
+ Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Popup should be open.");
+ let tabToSearchResult = (
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)
+ ).result;
+ Assert.equal(
+ tabToSearchResult.providerName,
+ "TabToSearch",
+ "The second result is a tab-to-search result."
+ );
+ Assert.equal(
+ tabToSearchResult.payload.engine,
+ engineName,
+ "The tab-to-search result is for the first engine."
+ );
+ if (isOnboarding) {
+ Assert.equal(
+ tabToSearchResult.payload.dynamicType,
+ "onboardTabToSearch",
+ "The tab-to-search result is an onboarding result."
+ );
+ } else {
+ Assert.ok(
+ !tabToSearchResult.payload.dynamicType,
+ "The tab-to-search result should not be an onboarding result."
+ );
+ }
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0]],
+ });
+
+ await SearchTestUtils.installSearchExtension({
+ name: ENGINE_NAME,
+ search_url: `https://${ENGINE_DOMAIN}/`,
+ });
+
+ // Reset the enginesShown sets in case a previous test showed a tab-to-search
+ // result but did not end its engagement.
+ UrlbarProviderTabToSearch.enginesShown.regular.clear();
+ UrlbarProviderTabToSearch.enginesShown.onboarding.clear();
+
+ // Enable local telemetry recording for the duration of the tests.
+ let oldCanRecord = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+
+ registerCleanupFunction(async () => {
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ });
+});
+
+add_task(async function test() {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ const histograms = snapshotHistograms();
+
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits([`https://${ENGINE_DOMAIN}/`]);
+ }
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: ENGINE_DOMAIN.slice(0, 4),
+ fireInputEvent: true,
+ });
+
+ let tabToSearchResult = (
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)
+ ).result;
+ Assert.equal(
+ tabToSearchResult.providerName,
+ "TabToSearch",
+ "The second result is a tab-to-search result."
+ );
+ Assert.equal(
+ tabToSearchResult.payload.engine,
+ ENGINE_NAME,
+ "The tab-to-search result is for the correct engine."
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 1,
+ "Sanity check: The second result is selected."
+ );
+
+ // Select the tab-to-search result.
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await searchPromise;
+
+ await UrlbarTestUtils.assertSearchMode(window, {
+ engineName: ENGINE_NAME,
+ entry: "tabtosearch",
+ });
+
+ assertTelemetryResults(
+ histograms,
+ "tabtosearch",
+ 1,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection
+ );
+
+ await UrlbarTestUtils.exitSearchMode(window);
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ gURLBar.blur();
+ });
+ await PlacesUtils.history.clear();
+ });
+
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+});
+
+add_task(async function impressions() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0]],
+ });
+ await impressions_test(false);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function onboarding_impressions() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 3]],
+ });
+ await impressions_test(true);
+ await SpecialPowers.popPrefEnv();
+ delete UrlbarProviderTabToSearch.onboardingInteractionAtTime;
+});
+
+async function impressions_test(isOnboarding) {
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ const firstEngineHost = "example";
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: `${ENGINE_NAME}2`,
+ search_url: `https://${firstEngineHost}-2.com/`,
+ },
+ { skipUnload: true }
+ );
+
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits([`https://${firstEngineHost}-2.com`]);
+ await PlacesTestUtils.addVisits([`https://${ENGINE_DOMAIN}/`]);
+ }
+
+ // First do multiple searches for substrings of firstEngineHost. The view
+ // should show the same tab-to-search onboarding result the entire time, so
+ // we should not continue to increment urlbar.tips.
+ for (let i = 1; i < firstEngineHost.length; i++) {
+ info(
+ `Search for "${firstEngineHost.slice(
+ 0,
+ i
+ )}". Only record one impression for this sequence.`
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: firstEngineHost.slice(0, i),
+ fireInputEvent: true,
+ });
+ await checkForTabToSearchResult(ENGINE_NAME, isOnboarding);
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ isOnboarding ? "tabtosearch_onboard-shown" : "tabtosearch-shown",
+ 1
+ );
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ isOnboarding
+ ? "urlbar.tabtosearch.impressions_onboarding"
+ : "urlbar.tabtosearch.impressions",
+ // "other" is recorded as the engine name because we're not using a built-in engine.
+ "other",
+ 1
+ );
+
+ info("Type through autofill to second engine hostname. Record impression.");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: firstEngineHost,
+ fireInputEvent: true,
+ });
+ await checkForTabToSearchResult(ENGINE_NAME, isOnboarding);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: `${firstEngineHost}-`,
+ fireInputEvent: true,
+ });
+ await checkForTabToSearchResult(`${ENGINE_NAME}2`, isOnboarding);
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+ // Since the user typed past the autofill for the first engine, we showed a
+ // different onboarding result and now we increment
+ // tabtosearch_onboard-shown.
+ scalars = TelemetryTestUtils.getProcessScalars("parent", true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ isOnboarding ? "tabtosearch_onboard-shown" : "tabtosearch-shown",
+ 3
+ );
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ isOnboarding
+ ? "urlbar.tabtosearch.impressions_onboarding"
+ : "urlbar.tabtosearch.impressions",
+ "other",
+ 3
+ );
+
+ info("Make a typo and return to autofill. Do not record impression.");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: `${firstEngineHost}-`,
+ fireInputEvent: true,
+ });
+ await checkForTabToSearchResult(`${ENGINE_NAME}2`, isOnboarding);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: `${firstEngineHost}-3`,
+ fireInputEvent: true,
+ });
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 1,
+ "We are not showing a tab-to-search result."
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: `${firstEngineHost}-2`,
+ fireInputEvent: true,
+ });
+ await checkForTabToSearchResult(`${ENGINE_NAME}2`, isOnboarding);
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+ scalars = TelemetryTestUtils.getProcessScalars("parent", true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ isOnboarding ? "tabtosearch_onboard-shown" : "tabtosearch-shown",
+ 4
+ );
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ isOnboarding
+ ? "urlbar.tabtosearch.impressions_onboarding"
+ : "urlbar.tabtosearch.impressions",
+ "other",
+ 4
+ );
+
+ info(
+ "Cancel then restart autofill. Continue to show the tab-to-search result."
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: `${firstEngineHost}-2`,
+ fireInputEvent: true,
+ });
+ await checkForTabToSearchResult(`${ENGINE_NAME}2`, isOnboarding);
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_Backspace");
+ await searchPromise;
+ await checkForTabToSearchResult(`${ENGINE_NAME}2`, isOnboarding);
+ searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ // Type the "." from `example-2.com`.
+ EventUtils.synthesizeKey(".");
+ await searchPromise;
+ await checkForTabToSearchResult(`${ENGINE_NAME}2`, isOnboarding);
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+ scalars = TelemetryTestUtils.getProcessScalars("parent", true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ isOnboarding ? "tabtosearch_onboard-shown" : "tabtosearch-shown",
+ 5
+ );
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ isOnboarding
+ ? "urlbar.tabtosearch.impressions_onboarding"
+ : "urlbar.tabtosearch.impressions",
+ // "other" is recorded as the engine name because we're not using a built-in engine.
+ "other",
+ 5
+ );
+
+ // See javadoc for UrlbarProviderTabToSearch.onEngagement for discussion
+ // about retained results.
+ info("Reopen the result set with retained results. Record impression.");
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ await checkForTabToSearchResult(`${ENGINE_NAME}2`, isOnboarding);
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+ scalars = TelemetryTestUtils.getProcessScalars("parent", true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ isOnboarding ? "tabtosearch_onboard-shown" : "tabtosearch-shown",
+ 6
+ );
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ isOnboarding
+ ? "urlbar.tabtosearch.impressions_onboarding"
+ : "urlbar.tabtosearch.impressions",
+ "other",
+ 6
+ );
+
+ info(
+ "Open a result page and then autofill engine host. Record impression."
+ );
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: firstEngineHost,
+ fireInputEvent: true,
+ });
+ await checkForTabToSearchResult(ENGINE_NAME, isOnboarding);
+ // Press enter on the heuristic result so we visit example.com without
+ // doing an additional search.
+ let loadPromise = BrowserTestUtils.browserLoaded(browser);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+ // Click the Urlbar and type to simulate what a user would actually do. If
+ // we use promiseAutocompleteResultPopup, no query would be made between
+ // this one and the previous tab-to-search query. Thus
+ // `onboardingEnginesShown` would not be cleared. This would not happen
+ // in real-world usage.
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+ searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey(firstEngineHost.slice(0, 4));
+ await searchPromise;
+ await checkForTabToSearchResult(ENGINE_NAME, isOnboarding);
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+ // We clear the scalar this time.
+ scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "urlbar.tips",
+ isOnboarding ? "tabtosearch_onboard-shown" : "tabtosearch-shown",
+ 8
+ );
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ isOnboarding
+ ? "urlbar.tabtosearch.impressions_onboarding"
+ : "urlbar.tabtosearch.impressions",
+ "other",
+ 8
+ );
+
+ await PlacesUtils.history.clear();
+ await extension.unload();
+ });
+}
diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tip.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tip.js
new file mode 100644
index 0000000000..c234bc3ed8
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tip.js
@@ -0,0 +1,130 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This file tests urlbar telemetry for tip results.
+ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarProvider: "resource:///modules/UrlbarUtils.sys.mjs",
+ UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs",
+ UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs",
+});
+
+function snapshotHistograms() {
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+ return {
+ resultMethodHist: TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ ),
+ search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"),
+ };
+}
+
+function assertTelemetryResults(histograms, type, index, method) {
+ TelemetryTestUtils.assertHistogram(histograms.resultMethodHist, method, 1);
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ `urlbar.picked.${type}`,
+ index,
+ 1
+ );
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Disable search suggestions in the urlbar.
+ ["browser.urlbar.suggest.searches", false],
+ // Turn autofill off.
+ ["browser.urlbar.autoFill", false],
+ ],
+ });
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test() {
+ // Add a restricting provider that returns a preselected heuristic tip result.
+ let provider = new TipProvider([
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TIP,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ helpUrl: "https://example.com/",
+ type: "test",
+ titleL10n: { id: "urlbar-search-tips-confirm" },
+ buttons: [
+ {
+ url: "https://example.com/",
+ l10n: { id: "urlbar-search-tips-confirm" },
+ },
+ ],
+ }
+ ),
+ { heuristic: true }
+ ),
+ ]);
+ UrlbarProvidersManager.registerProvider(provider);
+
+ const histograms = snapshotHistograms();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ // Show the view and press enter to select the tip.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value: "test",
+ fireInputEvent: true,
+ });
+ EventUtils.synthesizeKey("KEY_Enter");
+
+ assertTelemetryResults(
+ histograms,
+ "tip",
+ 0,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.enter
+ );
+
+ UrlbarProvidersManager.unregisterProvider(provider);
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * A test URLBar provider.
+ */
+class TipProvider extends UrlbarProvider {
+ constructor(results) {
+ super();
+ this._results = results;
+ }
+ get name() {
+ return "TestProviderTip";
+ }
+ get type() {
+ return UrlbarUtils.PROVIDER_TYPE.PROFILE;
+ }
+ isActive(context) {
+ return true;
+ }
+ getPriority(context) {
+ return 1;
+ }
+ async startQuery(context, addCallback) {
+ context.preselected = true;
+ for (const result of this._results) {
+ addCallback(this, result);
+ }
+ }
+}
diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_topsite.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_topsite.js
new file mode 100644
index 0000000000..12adb27caf
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_topsite.js
@@ -0,0 +1,136 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This file tests urlbar telemetry for topsite results.
+ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AboutNewTab: "resource:///modules/AboutNewTab.jsm",
+});
+
+const EN_US_TOPSITES =
+ "https://www.youtube.com/,https://www.facebook.com/,https://www.amazon.com/,https://www.reddit.com/,https://www.wikipedia.org/,https://twitter.com/";
+
+function snapshotHistograms() {
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+ return {
+ resultMethodHist: TelemetryTestUtils.getAndClearHistogram(
+ "FX_URLBAR_SELECTED_RESULT_METHOD"
+ ),
+ search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"),
+ };
+}
+
+function assertTelemetryResults(histograms, type, index, method) {
+ TelemetryTestUtils.assertHistogram(histograms.resultMethodHist, method, 1);
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ `urlbar.picked.${type}`,
+ index,
+ 1
+ );
+}
+
+/**
+ * Updates the Top Sites feed.
+ *
+ * @param {Function} condition
+ * A callback that returns true after Top Sites are successfully updated.
+ * @param {boolean} searchShortcuts
+ * True if Top Sites search shortcuts should be enabled.
+ */
+async function updateTopSites(condition, searchShortcuts = false) {
+ // Toggle the pref to clear the feed cache and force an update.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "browser.newtabpage.activity-stream.discoverystream.endpointSpocsClear",
+ "",
+ ],
+ ["browser.newtabpage.activity-stream.feeds.system.topsites", false],
+ ["browser.newtabpage.activity-stream.feeds.system.topsites", true],
+ [
+ "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts",
+ searchShortcuts,
+ ],
+ ],
+ });
+
+ // Wait for the feed to be updated.
+ await TestUtils.waitForCondition(() => {
+ let sites = AboutNewTab.getTopSites();
+ return condition(sites);
+ }, "Waiting for top sites to be updated");
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.topsites", true],
+ ["browser.newtabpage.activity-stream.default.sites", EN_US_TOPSITES],
+ ["browser.urlbar.suggest.quickactions", false],
+ ],
+ });
+ await updateTopSites(
+ sites => sites && sites.length == EN_US_TOPSITES.split(",").length
+ );
+});
+
+add_task(async function test() {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ let sites = AboutNewTab.getTopSites();
+ Assert.equal(
+ sites.length,
+ 6,
+ "The test suite browser should have 6 Top Sites."
+ );
+
+ const histograms = snapshotHistograms();
+
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ });
+
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ sites.length,
+ "The number of results should be the same as the number of Top Sites (6)."
+ );
+ // Select the first resultm and confirm it.
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.equal(
+ UrlbarTestUtils.getSelectedRowIndex(window),
+ 0,
+ "The first result should be selected"
+ );
+
+ let loadPromise = BrowserTestUtils.waitForDocLoadAndStopIt(
+ result.url,
+ gBrowser.selectedBrowser
+ );
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+
+ assertTelemetryResults(
+ histograms,
+ "topsite",
+ 0,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection
+ );
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ gURLBar.blur();
+ });
+ });
+});
diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_zeroPrefix.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_zeroPrefix.js
new file mode 100644
index 0000000000..b331921553
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_zeroPrefix.js
@@ -0,0 +1,266 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This file tests urlbar telemetry related to the zero-prefix view, i.e., when
+ * the search string is empty.
+ */
+
+"use strict";
+
+const HISTOGRAM_DWELL_TIME = "FX_URLBAR_ZERO_PREFIX_DWELL_TIME_MS";
+const SCALARS = {
+ ABANDONMENT: "urlbar.zeroprefix.abandonment",
+ ENGAGEMENT: "urlbar.zeroprefix.engagement",
+ EXPOSURE: "urlbar.zeroprefix.exposure",
+};
+
+add_setup(async function () {
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ Services.telemetry.clearScalars();
+
+ await SearchTestUtils.installSearchExtension({}, { setAsDefault: true });
+ await updateTopSitesAndAwaitChanged();
+});
+
+// zero prefix engagement
+add_task(async function engagement() {
+ let dwellHistogram =
+ TelemetryTestUtils.getAndClearHistogram(HISTOGRAM_DWELL_TIME);
+
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await showZeroPrefix();
+ checkScalars({
+ [SCALARS.EXPOSURE]: 1,
+ });
+ checkAndClearHistogram(dwellHistogram, false);
+
+ info("Finding row with result type URL");
+ let foundURLRow = false;
+ let count = UrlbarTestUtils.getResultCount(window);
+ for (let i = 0; i < count && !foundURLRow; i++) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ let index = UrlbarTestUtils.getSelectedRowIndex(window);
+ Assert.equal(index, i, "The expected row index should be selected");
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ info(`Checked row at index ${i}, result type is: ${details.type}`);
+ if (details.type == UrlbarUtils.RESULT_TYPE.URL) {
+ foundURLRow = true;
+ }
+ }
+ Assert.ok(foundURLRow, "Should have found a row with result type URL");
+
+ let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+ });
+
+ checkScalars({
+ [SCALARS.ENGAGEMENT]: 1,
+ });
+ checkAndClearHistogram(dwellHistogram, true);
+});
+
+// zero prefix abandonment
+add_task(async function abandonment() {
+ let dwellHistogram =
+ TelemetryTestUtils.getAndClearHistogram(HISTOGRAM_DWELL_TIME);
+
+ // Open and close the view twice. The second time the view will used a cached
+ // query context and that shouldn't interfere with telemetry.
+ for (let i = 0; i < 2; i++) {
+ await showZeroPrefix();
+ checkScalars({
+ [SCALARS.EXPOSURE]: 1,
+ });
+ checkAndClearHistogram(dwellHistogram, false);
+
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+ checkScalars({
+ [SCALARS.ABANDONMENT]: 1,
+ });
+ dwellHistogram = checkAndClearHistogram(dwellHistogram, true);
+ }
+});
+
+// Shows the zero-prefix view, does some searches, then shows it again by doing
+// a search for an empty string.
+add_task(async function searches() {
+ let dwellHistogram =
+ TelemetryTestUtils.getAndClearHistogram(HISTOGRAM_DWELL_TIME);
+
+ info("Show zero prefix");
+ await showZeroPrefix();
+ checkScalars({
+ [SCALARS.EXPOSURE]: 1,
+ });
+ checkAndClearHistogram(dwellHistogram, false);
+
+ info("Search for 't'");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "t",
+ });
+ checkScalars({});
+ dwellHistogram = checkAndClearHistogram(dwellHistogram, true);
+
+ info("Search for 'te'");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "te",
+ });
+ checkScalars({});
+ checkAndClearHistogram(dwellHistogram, false);
+
+ info("Search for 't'");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "t",
+ });
+ checkScalars({});
+ checkAndClearHistogram(dwellHistogram, false);
+
+ info("Search for ''");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "",
+ });
+ checkScalars({
+ [SCALARS.EXPOSURE]: 1,
+ });
+ checkAndClearHistogram(dwellHistogram, false);
+
+ info("Blur urlbar and close view");
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+ checkScalars({
+ [SCALARS.ABANDONMENT]: 1,
+ });
+ checkAndClearHistogram(dwellHistogram, true);
+});
+
+// A zero prefix engagement should not be recorded when the view isn't showing
+// zero prefix.
+add_task(async function notZeroPrefix_engagement() {
+ let dwellHistogram =
+ TelemetryTestUtils.getAndClearHistogram(HISTOGRAM_DWELL_TIME);
+
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+ let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+ });
+
+ checkScalars({});
+ checkAndClearHistogram(dwellHistogram, false);
+});
+
+// A zero prefix abandonment should not be recorded when the view isn't showing
+// zero prefix.
+add_task(async function notZeroPrefix_abandonment() {
+ let dwellHistogram =
+ TelemetryTestUtils.getAndClearHistogram(HISTOGRAM_DWELL_TIME);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+
+ checkScalars({});
+ checkAndClearHistogram(dwellHistogram, false);
+});
+
+function checkScalars(expected) {
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ for (let scalar of Object.values(SCALARS)) {
+ if (expected.hasOwnProperty(scalar)) {
+ TelemetryTestUtils.assertScalar(scalars, scalar, expected[scalar]);
+ } else {
+ Assert.ok(
+ !scalars.hasOwnProperty(scalar),
+ "Scalar should not be recorded: " + scalar
+ );
+ }
+ }
+}
+
+function checkAndClearHistogram(histogram, expected) {
+ if (expected) {
+ Assert.deepEqual(
+ Object.values(histogram.snapshot().values).filter(v => v > 0),
+ [1],
+ "Dwell histogram should be updated"
+ );
+ } else {
+ Assert.strictEqual(
+ histogram.snapshot().sum,
+ 0,
+ "Dwell histogram should not be updated"
+ );
+ }
+
+ return TelemetryTestUtils.getAndClearHistogram(histogram.name());
+}
+
+async function showZeroPrefix() {
+ let { promise, cleanup } = waitForQueryFinished();
+ await SimpleTest.promiseFocus(window);
+ await UrlbarTestUtils.promisePopupOpen(window, () =>
+ document.getElementById("Browser:OpenLocation").doCommand()
+ );
+ await promise;
+ cleanup();
+
+ Assert.greater(
+ UrlbarTestUtils.getResultCount(window),
+ 0,
+ "There should be at least one row in the zero prefix view"
+ );
+}
+
+/**
+ * Returns a promise that's resolved on the next `onQueryFinished()`. It's
+ * important to wait for `onQueryFinished()` because that's when the view checks
+ * whether it's showing zero prefix.
+ *
+ * @returns {object}
+ * An object with the following properties:
+ * {Promise} promise
+ * Resolved when `onQueryFinished()` is called.
+ * {Function} cleanup
+ * This should be called to remove the listener.
+ */
+function waitForQueryFinished() {
+ let deferred = PromiseUtils.defer();
+ let listener = {
+ onQueryFinished: () => deferred.resolve(),
+ };
+ gURLBar.controller.addQueryListener(listener);
+
+ return {
+ promise: deferred.promise,
+ cleanup() {
+ gURLBar.controller.removeQueryListener(listener);
+ },
+ };
+}
+
+async function updateTopSitesAndAwaitChanged() {
+ let url = "http://mochi.test:8888/topsite";
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits(url);
+ }
+
+ info("Updating top sites and awaiting newtab-top-sites-changed");
+ let changedPromise = TestUtils.topicObserved("newtab-top-sites-changed").then(
+ () => info("Observed newtab-top-sites-changed")
+ );
+ await updateTopSites(sites => sites?.length);
+ await changedPromise;
+}
diff --git a/browser/components/urlbar/tests/browser/browser_userTypedValue.js b/browser/components/urlbar/tests/browser/browser_userTypedValue.js
new file mode 100644
index 0000000000..8319b37962
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_userTypedValue.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+ const URI = TEST_BASE_URL + "file_userTypedValue.html";
+ window.browserDOMWindow.openURI(
+ makeURI(URI),
+ null,
+ Ci.nsIBrowserDOMWindow.OPEN_NEWTAB,
+ Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL,
+ Services.scriptSecurityManager.getSystemPrincipal()
+ );
+
+ is(gBrowser.userTypedValue, URI, "userTypedValue matches test URI");
+ is(gURLBar.value, URI, "location bar value matches test URI");
+
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.removeCurrentTab({ skipPermitUnload: true });
+ is(
+ gBrowser.userTypedValue,
+ URI,
+ "userTypedValue matches test URI after switching tabs"
+ );
+ is(
+ gURLBar.value,
+ URI,
+ "location bar value matches test URI after switching tabs"
+ );
+
+ waitForExplicitFinish();
+ BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(() => {
+ is(
+ gBrowser.userTypedValue,
+ null,
+ "userTypedValue is null as the page has loaded"
+ );
+ is(
+ gURLBar.value,
+ URI,
+ "location bar value matches test URI as the page has loaded"
+ );
+
+ gBrowser.removeCurrentTab({ skipPermitUnload: true });
+ finish();
+ });
+}
diff --git a/browser/components/urlbar/tests/browser/browser_valueOnTabSwitch.js b/browser/components/urlbar/tests/browser/browser_valueOnTabSwitch.js
new file mode 100644
index 0000000000..9d3e922692
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_valueOnTabSwitch.js
@@ -0,0 +1,166 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This tests for the correct URL being displayed in the URL bar after switching
+ * tabs which are in different states (e.g. deleted, partially deleted).
+ */
+
+"use strict";
+
+const TEST_URL = `${TEST_BASE_URL}dummy_page.html`;
+
+add_task(async function () {
+ // autofill may conflict with the test scope, by filling missing parts of
+ // the url due to autoOpen.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.autoFill", false]],
+ });
+
+ let charsToDelete,
+ deletedURLTab,
+ fullURLTab,
+ partialURLTab,
+ testPartialURL,
+ testURL;
+
+ charsToDelete = 5;
+ deletedURLTab = BrowserTestUtils.addTab(gBrowser);
+ fullURLTab = BrowserTestUtils.addTab(gBrowser);
+ partialURLTab = BrowserTestUtils.addTab(gBrowser);
+ testURL = TEST_URL;
+
+ let loaded1 = BrowserTestUtils.browserLoaded(
+ deletedURLTab.linkedBrowser,
+ false,
+ testURL
+ );
+ let loaded2 = BrowserTestUtils.browserLoaded(
+ fullURLTab.linkedBrowser,
+ false,
+ testURL
+ );
+ let loaded3 = BrowserTestUtils.browserLoaded(
+ partialURLTab.linkedBrowser,
+ false,
+ testURL
+ );
+ BrowserTestUtils.loadURIString(deletedURLTab.linkedBrowser, testURL);
+ BrowserTestUtils.loadURIString(fullURLTab.linkedBrowser, testURL);
+ BrowserTestUtils.loadURIString(partialURLTab.linkedBrowser, testURL);
+ await Promise.all([loaded1, loaded2, loaded3]);
+
+ testURL = BrowserUIUtils.trimURL(testURL);
+ testPartialURL = testURL.substr(0, testURL.length - charsToDelete);
+
+ function cleanUp() {
+ gBrowser.removeTab(fullURLTab);
+ gBrowser.removeTab(partialURLTab);
+ gBrowser.removeTab(deletedURLTab);
+ }
+
+ async function cycleTabs() {
+ await BrowserTestUtils.switchTab(gBrowser, fullURLTab);
+ is(
+ gURLBar.value,
+ testURL,
+ "gURLBar.value should be testURL after switching back to fullURLTab"
+ );
+
+ await BrowserTestUtils.switchTab(gBrowser, partialURLTab);
+ is(
+ gURLBar.value,
+ testPartialURL,
+ "gURLBar.value should be testPartialURL after switching back to partialURLTab"
+ );
+ await BrowserTestUtils.switchTab(gBrowser, deletedURLTab);
+ is(
+ gURLBar.value,
+ testURL,
+ "gURLBar.value should be testURL after switching back to deletedURLTab"
+ );
+
+ await BrowserTestUtils.switchTab(gBrowser, fullURLTab);
+ is(
+ gURLBar.value,
+ testURL,
+ "gURLBar.value should be testURL after switching back to fullURLTab"
+ );
+ }
+
+ function urlbarBackspace(removeAll) {
+ return new Promise((resolve, reject) => {
+ gBrowser.selectedBrowser.focus();
+ gURLBar.addEventListener(
+ "input",
+ function () {
+ resolve();
+ },
+ { once: true }
+ );
+ gURLBar.focus();
+ if (removeAll) {
+ gURLBar.select();
+ } else {
+ gURLBar.selectionStart = gURLBar.selectionEnd = gURLBar.value.length;
+ }
+ EventUtils.synthesizeKey("KEY_Backspace");
+ });
+ }
+
+ async function prepareDeletedURLTab() {
+ await BrowserTestUtils.switchTab(gBrowser, deletedURLTab);
+ is(
+ gURLBar.value,
+ testURL,
+ "gURLBar.value should be testURL after initial switch to deletedURLTab"
+ );
+
+ // simulate the user removing the whole url from the location bar
+ await urlbarBackspace(true);
+ is(gURLBar.value, "", 'gURLBar.value should be "" (just set)');
+ }
+
+ async function prepareFullURLTab() {
+ await BrowserTestUtils.switchTab(gBrowser, fullURLTab);
+ is(
+ gURLBar.value,
+ testURL,
+ "gURLBar.value should be testURL after initial switch to fullURLTab"
+ );
+ }
+
+ async function preparePartialURLTab() {
+ await BrowserTestUtils.switchTab(gBrowser, partialURLTab);
+ is(
+ gURLBar.value,
+ testURL,
+ "gURLBar.value should be testURL after initial switch to partialURLTab"
+ );
+
+ // simulate the user removing part of the url from the location bar
+ let deleted = 0;
+ while (deleted < charsToDelete) {
+ await urlbarBackspace(false);
+ deleted++;
+ }
+
+ is(
+ gURLBar.value,
+ testPartialURL,
+ "gURLBar.value should be testPartialURL (just set)"
+ );
+ }
+
+ // prepare the three tabs required by this test
+
+ // First tab
+ await prepareFullURLTab();
+ await preparePartialURLTab();
+ await prepareDeletedURLTab();
+
+ // now cycle the tabs and make sure everything looks good
+ await cycleTabs();
+ cleanUp();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_view_emptyResultSet.js b/browser/components/urlbar/tests/browser/browser_view_emptyResultSet.js
new file mode 100644
index 0000000000..f7a2721093
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_view_emptyResultSet.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the view results are cleared and the view is closed, when an empty
+// result set arrives after a non-empty one.
+
+add_task(async function () {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ });
+ Assert.ok(
+ UrlbarTestUtils.getResultCount(window) > 0,
+ `There should be some results in the view.`
+ );
+ Assert.ok(gURLBar.view.isOpen, `The view should be open.`);
+
+ // Register an high priority empty result provider.
+ let provider = new UrlbarTestUtils.TestProvider({
+ results: [],
+ priority: 999,
+ });
+ UrlbarProvidersManager.registerProvider(provider);
+ registerCleanupFunction(async function () {
+ UrlbarProvidersManager.unregisterProvider(provider);
+ await PlacesUtils.history.clear();
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ });
+ Assert.ok(
+ UrlbarTestUtils.getResultCount(window) == 0,
+ `There should be no results in the view.`
+ );
+ Assert.ok(!gURLBar.view.isOpen, `The view should have been closed.`);
+});
diff --git a/browser/components/urlbar/tests/browser/browser_view_resultDisplay.js b/browser/components/urlbar/tests/browser/browser_view_resultDisplay.js
new file mode 100644
index 0000000000..c4053eaed7
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_view_resultDisplay.js
@@ -0,0 +1,354 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that a result has the various elements displayed in the URL bar as
+ * we expect them to be.
+ */
+
+add_setup(async function () {
+ await PlacesUtils.history.clear();
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.history.clear();
+ Services.prefs.clearUserPref("browser.urlbar.trimURLs");
+ });
+});
+
+async function testResult(input, expected, index = 1) {
+ const ESCAPED_URL = encodeURI(input.url);
+
+ await PlacesUtils.history.clear();
+ if (index > 0) {
+ await PlacesTestUtils.addVisits({
+ uri: input.url,
+ title: input.title,
+ });
+ }
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: input.query,
+ });
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index);
+ Assert.equal(result.url, ESCAPED_URL, "Should have the correct url to load");
+ Assert.equal(
+ result.displayed.url,
+ expected.displayedUrl,
+ "Should have the correct displayed url"
+ );
+ Assert.equal(
+ result.displayed.title,
+ input.title,
+ "Should have the expected title"
+ );
+ Assert.equal(
+ result.displayed.typeIcon,
+ "none",
+ "Should not have a type icon"
+ );
+ if (index > 0) {
+ Assert.equal(
+ result.image,
+ `page-icon:${ESCAPED_URL}`,
+ "Should have the correct favicon"
+ );
+ }
+
+ assertDisplayedHighlights(
+ "title",
+ result.element.title,
+ expected.highlightedTitle
+ );
+
+ assertDisplayedHighlights("url", result.element.url, expected.highlightedUrl);
+}
+
+function assertDisplayedHighlights(elementName, element, expectedResults) {
+ Assert.equal(
+ element.childNodes.length,
+ expectedResults.length,
+ `Should have the correct number of child nodes for ${elementName}`
+ );
+
+ for (let i = 0; i < element.childNodes.length; i++) {
+ let child = element.childNodes[i];
+ Assert.equal(
+ child.textContent,
+ expectedResults[i][0],
+ `Should have the correct text for the ${i} part of the ${elementName}`
+ );
+ Assert.equal(
+ child.nodeName,
+ expectedResults[i][1] ? "strong" : "#text",
+ `Should have the correct text/strong status for the ${i} part of the ${elementName}`
+ );
+ }
+}
+
+add_task(async function test_url_result() {
+ await testResult(
+ {
+ query: "\u6e2C\u8a66",
+ title: "The \u6e2C\u8a66 URL",
+ url: "https://example.com/\u6e2C\u8a66test",
+ },
+ {
+ displayedUrl: "example.com/\u6e2C\u8a66test",
+ highlightedTitle: [
+ ["The ", false],
+ ["\u6e2C\u8a66", true],
+ [" URL", false],
+ ],
+ highlightedUrl: [
+ ["example.com/", false],
+ ["\u6e2C\u8a66", true],
+ ["test", false],
+ ],
+ }
+ );
+});
+
+add_task(async function test_url_result_no_path() {
+ await testResult(
+ {
+ query: "ample",
+ title: "The Title",
+ url: "https://example.com/",
+ },
+ {
+ displayedUrl: "example.com",
+ highlightedTitle: [["The Title", false]],
+ highlightedUrl: [
+ ["ex", false],
+ ["ample", true],
+ [".com", false],
+ ],
+ }
+ );
+});
+
+add_task(async function test_url_result_www() {
+ await testResult(
+ {
+ query: "ample",
+ title: "The Title",
+ url: "https://www.example.com/",
+ },
+ {
+ displayedUrl: "example.com",
+ highlightedTitle: [["The Title", false]],
+ highlightedUrl: [
+ ["ex", false],
+ ["ample", true],
+ [".com", false],
+ ],
+ }
+ );
+});
+
+add_task(async function test_url_result_no_trimming() {
+ Services.prefs.setBoolPref("browser.urlbar.trimURLs", false);
+
+ await testResult(
+ {
+ query: "\u6e2C\u8a66",
+ title: "The \u6e2C\u8a66 URL",
+ url: "http://example.com/\u6e2C\u8a66test",
+ },
+ {
+ displayedUrl: "http://example.com/\u6e2C\u8a66test",
+ highlightedTitle: [
+ ["The ", false],
+ ["\u6e2C\u8a66", true],
+ [" URL", false],
+ ],
+ highlightedUrl: [
+ ["http://example.com/", false],
+ ["\u6e2C\u8a66", true],
+ ["test", false],
+ ],
+ }
+ );
+
+ Services.prefs.clearUserPref("browser.urlbar.trimURLs");
+});
+
+add_task(async function test_case_insensitive_highlights_1() {
+ await testResult(
+ {
+ query: "exam",
+ title: "The examPLE URL EXAMple",
+ url: "https://example.com/ExAm",
+ },
+ {
+ displayedUrl: "example.com/ExAm",
+ highlightedTitle: [
+ ["The ", false],
+ ["exam", true],
+ ["PLE URL ", false],
+ ["EXAM", true],
+ ["ple", false],
+ ],
+ highlightedUrl: [
+ ["exam", true],
+ ["ple.com/", false],
+ ["ExAm", true],
+ ],
+ }
+ );
+});
+
+add_task(async function test_case_insensitive_highlights_2() {
+ await testResult(
+ {
+ query: "EXAM",
+ title: "The examPLE URL EXAMple",
+ url: "https://example.com/ExAm",
+ },
+ {
+ displayedUrl: "example.com/ExAm",
+ highlightedTitle: [
+ ["The ", false],
+ ["exam", true],
+ ["PLE URL ", false],
+ ["EXAM", true],
+ ["ple", false],
+ ],
+ highlightedUrl: [
+ ["exam", true],
+ ["ple.com/", false],
+ ["ExAm", true],
+ ],
+ }
+ );
+});
+
+add_task(async function test_case_insensitive_highlights_3() {
+ await testResult(
+ {
+ query: "eXaM",
+ title: "The examPLE URL EXAMple",
+ url: "https://example.com/ExAm",
+ },
+ {
+ displayedUrl: "example.com/ExAm",
+ highlightedTitle: [
+ ["The ", false],
+ ["exam", true],
+ ["PLE URL ", false],
+ ["EXAM", true],
+ ["ple", false],
+ ],
+ highlightedUrl: [
+ ["exam", true],
+ ["ple.com/", false],
+ ["ExAm", true],
+ ],
+ }
+ );
+});
+
+add_task(async function test_case_insensitive_highlights_4() {
+ await testResult(
+ {
+ query: "ExAm",
+ title: "The examPLE URL EXAMple",
+ url: "https://example.com/ExAm",
+ },
+ {
+ displayedUrl: "example.com/ExAm",
+ highlightedTitle: [
+ ["The ", false],
+ ["exam", true],
+ ["PLE URL ", false],
+ ["EXAM", true],
+ ["ple", false],
+ ],
+ highlightedUrl: [
+ ["exam", true],
+ ["ple.com/", false],
+ ["ExAm", true],
+ ],
+ }
+ );
+});
+
+add_task(async function test_case_insensitive_highlights_5() {
+ await testResult(
+ {
+ query: "exam foo",
+ title: "The examPLE URL foo EXAMple FOO",
+ url: "https://example.com/ExAm/fOo",
+ },
+ {
+ displayedUrl: "example.com/ExAm/fOo",
+ highlightedTitle: [
+ ["The ", false],
+ ["exam", true],
+ ["PLE URL ", false],
+ ["foo", true],
+ [" ", false],
+ ["EXAM", true],
+ ["ple ", false],
+ ["FOO", true],
+ ],
+ highlightedUrl: [
+ ["exam", true],
+ ["ple.com/", false],
+ ["ExAm", true],
+ ["/", false],
+ ["fOo", true],
+ ],
+ }
+ );
+});
+
+add_task(async function test_case_insensitive_highlights_6() {
+ await testResult(
+ {
+ query: "EXAM FOO",
+ title: "The examPLE URL foo EXAMple FOO",
+ url: "https://example.com/ExAm/fOo",
+ },
+ {
+ displayedUrl: "example.com/ExAm/fOo",
+ highlightedTitle: [
+ ["The ", false],
+ ["exam", true],
+ ["PLE URL ", false],
+ ["foo", true],
+ [" ", false],
+ ["EXAM", true],
+ ["ple ", false],
+ ["FOO", true],
+ ],
+ highlightedUrl: [
+ ["exam", true],
+ ["ple.com/", false],
+ ["ExAm", true],
+ ["/", false],
+ ["fOo", true],
+ ],
+ }
+ );
+});
+
+add_task(async function test_no_highlight_fallback_heuristic_url() {
+ info("Test unvisited heuristic (fallback provider)");
+ await testResult(
+ {
+ query: "nonexisting.com",
+ title: "http://nonexisting.com/",
+ url: "http://nonexisting.com/",
+ },
+ {
+ displayedUrl: "", // URL heuristic only has title.
+ highlightedTitle: [["http://nonexisting.com/", false]],
+ highlightedUrl: [],
+ },
+ 0 // Test the heuristic result.
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_view_resultTypes_display.js b/browser/components/urlbar/tests/browser/browser_view_resultTypes_display.js
new file mode 100644
index 0000000000..0cf56f107d
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_view_resultTypes_display.js
@@ -0,0 +1,317 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { SyncedTabs } = ChromeUtils.importESModule(
+ "resource://services-sync/SyncedTabs.sys.mjs"
+);
+
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+
+function assertElementsDisplayed(details, expected) {
+ Assert.equal(
+ details.type,
+ expected.type,
+ "Should be displaying a row of the correct type"
+ );
+ Assert.equal(
+ details.title,
+ expected.title,
+ "Should be displaying the correct title"
+ );
+ let separatorVisible =
+ window.getComputedStyle(details.element.separator).display != "none" &&
+ window.getComputedStyle(details.element.separator).visibility != "collapse";
+ Assert.equal(
+ expected.separator,
+ separatorVisible,
+ `Should${expected.separator ? " " : " not "}be displaying a separator`
+ );
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.searches", false],
+ // Disable search suggestions in the urlbar.
+ ["browser.urlbar.suggest.searches", false],
+ // Clear historical search suggestions to avoid interference from previous
+ // tests.
+ ["browser.urlbar.maxHistoricalSearchSuggestions", 0],
+ // Turn autofill off.
+ ["browser.urlbar.autoFill", false],
+ ],
+ });
+
+ await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME,
+ setAsDefault: true,
+ });
+
+ // Move the mouse away from the results panel, because hovering a result may
+ // change its aspect (e.g. by showing a " - search with Engine" suffix).
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mousemove",
+ target: gURLBar.inputField,
+ offsetX: 0,
+ offsetY: 0,
+ });
+});
+
+add_task(async function test_tab_switch_result() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:mozilla"
+ );
+
+ await BrowserTestUtils.withNewTab({ gBrowser }, async () => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "about:mozilla",
+ fireInputEvent: true,
+ });
+
+ const details = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+
+ assertElementsDisplayed(details, {
+ separator: true,
+ title: "about:mozilla",
+ type: UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ });
+ });
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_search_result() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", true);
+
+ await BrowserTestUtils.withNewTab({ gBrowser }, async () => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "foo",
+ fireInputEvent: true,
+ });
+
+ let index = await UrlbarTestUtils.promiseSuggestionsPresent(window);
+ const details = await UrlbarTestUtils.getDetailsOfResultAt(window, index);
+
+ // We'll initially display no separator.
+ assertElementsDisplayed(details, {
+ separator: false,
+ title: "foofoo",
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ });
+
+ // Down to select the first search suggestion.
+ for (let i = index; i > 0; --i) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+
+ // We should now be displaying one.
+ assertElementsDisplayed(details, {
+ separator: true,
+ title: "foofoo",
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ });
+ });
+
+ await PlacesUtils.history.clear();
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+});
+
+add_task(async function test_url_result() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com",
+ title: "example",
+ transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ },
+ ]);
+
+ await BrowserTestUtils.withNewTab({ gBrowser }, async () => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "example",
+ fireInputEvent: true,
+ });
+
+ const details = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+
+ assertElementsDisplayed(details, {
+ separator: true,
+ title: "example",
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ });
+ });
+
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function test_keyword_result() {
+ const TEST_URL = `${TEST_BASE_URL}print_postdata.sjs`;
+
+ await PlacesUtils.keywords.insert({
+ keyword: "get",
+ url: TEST_URL + "?q=%s",
+ });
+
+ await BrowserTestUtils.withNewTab({ gBrowser }, async () => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "get ",
+ fireInputEvent: true,
+ });
+
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+
+ // Because only the keyword is typed, we show the bookmark url.
+ assertElementsDisplayed(details, {
+ separator: true,
+ title: TEST_URL + "?q=",
+ type: UrlbarUtils.RESULT_TYPE.KEYWORD,
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "get test",
+ fireInputEvent: true,
+ });
+
+ details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+
+ assertElementsDisplayed(details, {
+ separator: false,
+ title: "example.com: test",
+ type: UrlbarUtils.RESULT_TYPE.KEYWORD,
+ });
+ });
+});
+
+add_task(async function test_omnibox_result() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ omnibox: {
+ keyword: "omniboxtest",
+ },
+
+ background() {
+ /* global browser */
+ browser.omnibox.setDefaultSuggestion({
+ description: "doit",
+ });
+ // Just do nothing for this test.
+ browser.omnibox.onInputEntered.addListener(() => {});
+ browser.omnibox.onInputChanged.addListener((text, suggest) => {
+ suggest([]);
+ });
+ },
+ },
+ });
+
+ await extension.startup();
+
+ await BrowserTestUtils.withNewTab({ gBrowser }, async () => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "omniboxtest ",
+ fireInputEvent: true,
+ });
+
+ const details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+
+ assertElementsDisplayed(details, {
+ separator: true,
+ title: "Generated extension",
+ type: UrlbarUtils.RESULT_TYPE.OMNIBOX,
+ });
+ });
+
+ await extension.unload();
+});
+
+add_task(async function test_remote_tab_result() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["services.sync.username", "fake"],
+ ["services.sync.syncedTabs.showRemoteTabs", true],
+ ],
+ });
+ // Clear history so that history added by previous tests doesn't mess up this
+ // test when it selects results in the urlbar.
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ const REMOTE_TAB = {
+ id: "7cqCr77ptzX3",
+ type: "client",
+ lastModified: 1492201200,
+ name: "zcarter's Nightly on MacBook-Pro-25",
+ clientType: "desktop",
+ tabs: [
+ {
+ type: "tab",
+ title: "Test Remote",
+ url: "http://example.com",
+ icon: UrlbarUtils.ICON.DEFAULT,
+ client: "7cqCr77ptzX3",
+ lastUsed: Math.floor(Date.now() / 1000),
+ },
+ ],
+ };
+
+ const sandbox = sinon.createSandbox();
+
+ let originalSyncedTabsInternal = SyncedTabs._internal;
+ SyncedTabs._internal = {
+ isConfiguredToSyncTabs: true,
+ hasSyncedThisSession: true,
+ getTabClients() {
+ return Promise.resolve([]);
+ },
+ syncTabs() {
+ return Promise.resolve();
+ },
+ };
+
+ // Tell the Sync XPCOM service it is initialized.
+ let weaveXPCService = Cc["@mozilla.org/weave/service;1"].getService(
+ Ci.nsISupports
+ ).wrappedJSObject;
+ let oldWeaveServiceReady = weaveXPCService.ready;
+ weaveXPCService.ready = true;
+
+ sandbox
+ .stub(SyncedTabs._internal, "getTabClients")
+ .callsFake(() => Promise.resolve(Cu.cloneInto([REMOTE_TAB], {})));
+
+ // Reset internal cache in UrlbarProviderRemoteTabs.
+ Services.obs.notifyObservers(null, "weave:engine:sync:finish", "tabs");
+
+ registerCleanupFunction(async function () {
+ sandbox.restore();
+ weaveXPCService.ready = oldWeaveServiceReady;
+ SyncedTabs._internal = originalSyncedTabsInternal;
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ Services.telemetry.setEventRecordingEnabled("navigation", false);
+ });
+
+ await BrowserTestUtils.withNewTab({ gBrowser }, async () => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "example",
+ fireInputEvent: true,
+ });
+
+ const details = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+
+ assertElementsDisplayed(details, {
+ separator: true,
+ title: "Test Remote",
+ type: UrlbarUtils.RESULT_TYPE.REMOTE_TAB,
+ });
+ });
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/urlbar/tests/browser/browser_view_selectionByMouse.js b/browser/components/urlbar/tests/browser/browser_view_selectionByMouse.js
new file mode 100644
index 0000000000..2de8439b58
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_view_selectionByMouse.js
@@ -0,0 +1,607 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test selection on result view by mouse.
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarProviderQuickActions:
+ "resource:///modules/UrlbarProviderQuickActions.sys.mjs",
+});
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.quickactions.enabled", true],
+ ["browser.urlbar.suggest.quickactions", true],
+ ["browser.urlbar.shortcuts.quickactions", true],
+ ],
+ });
+
+ UrlbarTestUtils.disableResultMenuAutohide(window);
+
+ await SearchTestUtils.installSearchExtension({}, { setAsDefault: true });
+
+ UrlbarProviderQuickActions.addAction("test-addons", {
+ commands: ["test-addons"],
+ label: "quickactions-addons",
+ onPick: () =>
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, "about:about"),
+ });
+ UrlbarProviderQuickActions.addAction("test-downloads", {
+ commands: ["test-downloads"],
+ label: "quickactions-downloads2",
+ onPick: () =>
+ BrowserTestUtils.loadURIString(
+ gBrowser.selectedBrowser,
+ "about:downloads"
+ ),
+ });
+
+ registerCleanupFunction(function () {
+ UrlbarProviderQuickActions.removeAction("test-addons");
+ UrlbarProviderQuickActions.removeAction("test-downloads");
+ });
+});
+
+add_task(async function basic() {
+ const testData = [
+ {
+ description: "Normal result to quick action button",
+ mousedown: ".urlbarView-row:nth-child(1) > .urlbarView-row-inner",
+ mouseup: ".urlbarView-quickaction-row[data-key=test-downloads]",
+ expected: "about:downloads",
+ },
+ {
+ description: "Normal result to out of result",
+ mousedown: ".urlbarView-row:nth-child(1) > .urlbarView-row-inner",
+ mouseup: "body",
+ expected: false,
+ },
+ {
+ description: "Quick action button to normal result",
+ mousedown: ".urlbarView-quickaction-row[data-key=test-addons]",
+ mouseup: ".urlbarView-row:nth-child(1)",
+ expected: "https://example.com/?q=test",
+ },
+ {
+ description: "Quick action button to quick action button",
+ mousedown: ".urlbarView-quickaction-row[data-key=test-addons]",
+ mouseup: ".urlbarView-quickaction-row[data-key=test-downloads]",
+ expected: "about:downloads",
+ },
+ {
+ description: "Quick action button to out of result",
+ mousedown: ".urlbarView-quickaction-row[data-key=test-downloads]",
+ mouseup: "body",
+ expected: false,
+ },
+ ];
+
+ for (const { description, mousedown, mouseup, expected } of testData) {
+ info(description);
+
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ value: "test",
+ window,
+ });
+ let [downElement, upElement] = await waitForElements([
+ mousedown,
+ mouseup,
+ ]);
+
+ EventUtils.synthesizeMouseAtCenter(downElement, {
+ type: "mousedown",
+ });
+ Assert.ok(
+ downElement.hasAttribute("selected"),
+ "Mousedown element should be selected after mousedown"
+ );
+
+ EventUtils.synthesizeMouseAtCenter(upElement, { type: "mouseup" });
+ Assert.ok(
+ !downElement.hasAttribute("selected"),
+ "Mousedown element should not be selected after mouseup"
+ );
+ Assert.ok(
+ !upElement.hasAttribute("selected"),
+ "Mouseup element should not be selected after mouseup"
+ );
+
+ if (expected) {
+ await BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ expected
+ );
+ Assert.ok(true, "Expected page is opened");
+ }
+ });
+ }
+});
+
+add_task(async function outOfBrowser() {
+ const testData = [
+ {
+ description: "Normal result to out of browser",
+ mousedown: ".urlbarView-row:nth-child(1) > .urlbarView-row-inner",
+ },
+ {
+ description: "Normal result to out of result",
+ mousedown: ".urlbarView-row:nth-child(1) > .urlbarView-row-inner",
+ expected: false,
+ },
+ {
+ description: "Quick action button to out of browser",
+ mousedown: ".urlbarView-quickaction-row[data-key=test-addons]",
+ },
+ ];
+
+ for (const { description, mousedown } of testData) {
+ info(description);
+
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ value: "test",
+ window,
+ });
+ let [downElement] = await waitForElements([mousedown]);
+
+ EventUtils.synthesizeMouseAtCenter(downElement, {
+ type: "mousedown",
+ });
+ Assert.ok(
+ downElement.hasAttribute("selected"),
+ "Mousedown element should be selected after mousedown"
+ );
+
+ // Mouseup at out of browser.
+ EventUtils.synthesizeMouse(document.documentElement, -1, -1, {
+ type: "mouseup",
+ });
+
+ Assert.ok(
+ !downElement.hasAttribute("selected"),
+ "Mousedown element should not be selected after mouseup"
+ );
+ });
+ }
+});
+
+add_task(async function withSelectionByKeyboard() {
+ const testData = [
+ {
+ description: "Select normal result, then click on out of result",
+ mousedown: "body",
+ mouseup: "body",
+ expected: {
+ selectedElementByKey:
+ "#urlbar-results .urlbarView-row > .urlbarView-row-inner[selected]",
+ selectedElementAfterMouseDown:
+ "#urlbar-results .urlbarView-row > .urlbarView-row-inner[selected]",
+ actionedPage: false,
+ },
+ },
+ {
+ description: "Select quick action button, then click on out of result",
+ arrowDown: 1,
+ mousedown: "body",
+ mouseup: "body",
+ expected: {
+ selectedElementByKey:
+ "#urlbar-results .urlbarView-quickaction-row[selected]",
+ selectedElementAfterMouseDown:
+ "#urlbar-results .urlbarView-quickaction-row[selected]",
+ actionedPage: false,
+ },
+ },
+ {
+ description: "Select normal result, then click on about:downloads",
+ mousedown: ".urlbarView-quickaction-row[data-key=test-downloads]",
+ mouseup: ".urlbarView-quickaction-row[data-key=test-downloads]",
+ expected: {
+ selectedElementByKey:
+ "#urlbar-results .urlbarView-row > .urlbarView-row-inner[selected]",
+ selectedElementAfterMouseDown:
+ ".urlbarView-quickaction-row[data-key=test-downloads]",
+ actionedPage: "about:downloads",
+ },
+ },
+ ];
+
+ for (const {
+ description,
+ arrowDown,
+ mousedown,
+ mouseup,
+ expected,
+ } of testData) {
+ info(description);
+
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ value: "test",
+ window,
+ });
+ let [downElement, upElement] = await waitForElements([
+ mousedown,
+ mouseup,
+ ]);
+
+ if (arrowDown) {
+ EventUtils.synthesizeKey(
+ "KEY_ArrowDown",
+ { repeat: arrowDown },
+ window
+ );
+ }
+
+ let [selectedElementByKey] = await waitForElements([
+ expected.selectedElementByKey,
+ ]);
+ Assert.ok(
+ selectedElementByKey.hasAttribute("selected"),
+ "selectedElementByKey should be selected after arrow down"
+ );
+
+ EventUtils.synthesizeMouseAtCenter(downElement, {
+ type: "mousedown",
+ });
+
+ if (
+ expected.selectedElementByKey !== expected.selectedElementAfterMouseDown
+ ) {
+ let [selectedElementAfterMouseDown] = await waitForElements([
+ expected.selectedElementAfterMouseDown,
+ ]);
+ Assert.ok(
+ selectedElementAfterMouseDown.hasAttribute("selected"),
+ "selectedElementAfterMouseDown should be selected after mousedown"
+ );
+ Assert.ok(
+ !selectedElementByKey.hasAttribute("selected"),
+ "selectedElementByKey should not be selected after mousedown"
+ );
+ }
+
+ EventUtils.synthesizeMouseAtCenter(upElement, {
+ type: "mouseup",
+ });
+
+ if (expected.actionedPage) {
+ Assert.ok(
+ !selectedElementByKey.hasAttribute("selected"),
+ "selectedElementByKey should not be selected after page starts load"
+ );
+ await BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ expected.actionedPage
+ );
+ Assert.ok(true, "Expected page is opened");
+ } else {
+ Assert.ok(
+ selectedElementByKey.hasAttribute("selected"),
+ "selectedElementByKey should remain selected"
+ );
+ }
+ });
+ }
+});
+
+add_task(async function withDnsFirstForSingleWordsPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.fixup.dns_first_for_single_words", true]],
+ });
+
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "https://example.org/",
+ title: "example",
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ value: "ex",
+ window,
+ });
+
+ const details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ const target = details.element.action;
+ EventUtils.synthesizeMouseAtCenter(target, { type: "mousedown" });
+ const onLoaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ "https://example.org/"
+ );
+ EventUtils.synthesizeMouseAtCenter(target, { type: "mouseup" });
+ await onLoaded;
+ Assert.ok(true, "Expected page is opened");
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function buttons() {
+ let initialTabUrl = "https://example.com/initial";
+ let mainResultUrl = "https://example.com/main";
+ let mainResultHelpUrl = "https://example.com/help";
+ let otherResultUrl = "https://example.com/other";
+
+ let searchString = "test";
+
+ // Add a provider with two results: The first has buttons and the second has a
+ // URL that should or shouldn't become the input's value when the block button
+ // in the first result is clicked, depending on the test.
+ let provider = new UrlbarTestUtils.TestProvider({
+ priority: Infinity,
+ results: [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ url: mainResultUrl,
+ helpUrl: mainResultHelpUrl,
+ helpL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-learn-more-about-firefox-suggest"
+ : "firefox-suggest-urlbar-learn-more",
+ },
+ isBlockable: true,
+ blockL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-dismiss-firefox-suggest"
+ : "firefox-suggest-urlbar-block",
+ },
+ }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ url: otherResultUrl,
+ }
+ ),
+ ],
+ });
+
+ // Implement the provider's `onEngagement()` so it removes the result.
+ let onEngagementCallCount = 0;
+ provider.onEngagement = (isPrivate, state, queryContext, details) => {
+ onEngagementCallCount++;
+ queryContext.view.controller.removeResult(details.result);
+ };
+
+ UrlbarProvidersManager.registerProvider(provider);
+
+ let assertBlockResultCalled = () => {
+ Assert.equal(
+ onEngagementCallCount,
+ 1,
+ "blockResult() should have been called once"
+ );
+ onEngagementCallCount = 0;
+
+ let rowUrls = [];
+ let rows = UrlbarTestUtils.getResultsContainer(window).children;
+ for (let row of rows) {
+ rowUrls.push(row.result.payload.url);
+ }
+ Assert.ok(
+ !rowUrls.includes(mainResultUrl),
+ "The main result should not be in the view after blocking it: " +
+ JSON.stringify(rowUrls)
+ );
+ };
+ let assertResultMenuOpen = () => {
+ Assert.equal(
+ gURLBar.view.resultMenu.state,
+ "showing",
+ "Result menu is showing"
+ );
+ EventUtils.synthesizeKey("KEY_Escape");
+ };
+
+ let testData = [
+ {
+ description: UrlbarPrefs.get("resultMenu")
+ ? "Menu button to menu button"
+ : "Block button to block button",
+ mousedown: UrlbarPrefs.get("resultMenu")
+ ? ".urlbarView-row:nth-child(1) .urlbarView-button-menu"
+ : ".urlbarView-row:nth-child(1) .urlbarView-button-block",
+ afterMouseupCallback: UrlbarPrefs.get("resultMenu")
+ ? assertResultMenuOpen
+ : assertBlockResultCalled,
+ expected: {
+ mousedownSelected: false,
+ topSites: {
+ pageProxyState: "valid",
+ value: initialTabUrl,
+ },
+ searchString: {
+ pageProxyState: "invalid",
+ value: searchString,
+ },
+ },
+ },
+ {
+ skip: UrlbarPrefs.get("resultMenu"),
+ description: "Help button to help button",
+ mousedown: ".urlbarView-row:nth-child(1) .urlbarView-button-help",
+ expected: {
+ mousedownSelected: false,
+ url: mainResultHelpUrl,
+ newTab: true,
+ },
+ },
+ {
+ description: UrlbarPrefs.get("resultMenu")
+ ? "Row-inner to menu button"
+ : "Row-inner to block button",
+ mousedown: ".urlbarView-row:nth-child(1) > .urlbarView-row-inner",
+ mouseup: UrlbarPrefs.get("resultMenu")
+ ? ".urlbarView-row:nth-child(1) .urlbarView-button-menu"
+ : ".urlbarView-row:nth-child(1) .urlbarView-button-block",
+ afterMouseupCallback: UrlbarPrefs.get("resultMenu")
+ ? assertResultMenuOpen
+ : assertBlockResultCalled,
+ expected: {
+ mousedownSelected: true,
+ topSites: {
+ pageProxyState: "invalid",
+ value: UrlbarPrefs.get("resultMenu") ? initialTabUrl : otherResultUrl,
+ },
+ searchString: {
+ pageProxyState: "invalid",
+ value: UrlbarPrefs.get("resultMenu") ? searchString : otherResultUrl,
+ },
+ },
+ },
+ {
+ description: UrlbarPrefs.get("resultMenu")
+ ? "Menu button to row-inner"
+ : "Block button to row-inner",
+ mousedown: UrlbarPrefs.get("resultMenu")
+ ? ".urlbarView-row:nth-child(1) .urlbarView-button-menu"
+ : ".urlbarView-row:nth-child(1) .urlbarView-button-block",
+ mouseup: ".urlbarView-row:nth-child(1) > .urlbarView-row-inner",
+ expected: {
+ mousedownSelected: false,
+ url: mainResultUrl,
+ newTab: false,
+ },
+ },
+ ];
+
+ for (let showTopSites of [true, false]) {
+ for (let {
+ description,
+ mousedown,
+ mouseup,
+ expected,
+ afterMouseupCallback = null,
+ skip = false,
+ } of testData) {
+ if (skip) {
+ info(
+ `Skipping test with showTopSites = ${showTopSites}: ${description}`
+ );
+ continue;
+ }
+ info(`Running test with showTopSites = ${showTopSites}: ${description}`);
+ mouseup ||= mousedown;
+
+ await BrowserTestUtils.withNewTab(initialTabUrl, async () => {
+ Assert.equal(
+ gURLBar.getAttribute("pageproxystate"),
+ "valid",
+ "Sanity check: pageproxystate should be valid initially"
+ );
+ Assert.equal(
+ gURLBar.value,
+ initialTabUrl,
+ "Sanity check: input.value should be the initial URL initially"
+ );
+
+ if (showTopSites) {
+ // Open the view and show top sites by performing the accel+L command.
+ await SimpleTest.promiseFocus(window);
+ let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
+ document.getElementById("Browser:OpenLocation").doCommand();
+ await searchPromise;
+ } else {
+ // Do a search.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: searchString,
+ });
+ }
+
+ let [downElement, upElement] = await waitForElements([
+ mousedown,
+ mouseup,
+ ]);
+
+ // Mousedown and check the selection.
+ EventUtils.synthesizeMouseAtCenter(downElement, {
+ type: "mousedown",
+ });
+ if (expected.mousedownSelected) {
+ Assert.ok(
+ downElement.hasAttribute("selected"),
+ "Mousedown element should be selected after mousedown"
+ );
+ } else {
+ Assert.ok(
+ !downElement.hasAttribute("selected"),
+ "Mousedown element should not be selected after mousedown"
+ );
+ }
+
+ let loadPromise;
+ if (expected.url) {
+ loadPromise = expected.newTab
+ ? BrowserTestUtils.waitForNewTab(gBrowser, expected.url)
+ : BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ null,
+ expected.url
+ );
+ }
+
+ // Mouseup and check the selection.
+ EventUtils.synthesizeMouseAtCenter(upElement, { type: "mouseup" });
+ Assert.ok(
+ !downElement.hasAttribute("selected"),
+ "Mousedown element should not be selected after mouseup"
+ );
+ Assert.ok(
+ !upElement.hasAttribute("selected"),
+ "Mouseup element should not be selected after mouseup"
+ );
+
+ // If we expect a URL to load, we're done since the view will close and
+ // the input value will be set to the URL.
+ if (loadPromise) {
+ info("Waiting for URL to load: " + expected.url);
+ let tab = await loadPromise;
+ Assert.ok(true, "Expected URL loaded");
+ if (expected.newTab) {
+ BrowserTestUtils.removeTab(tab);
+ }
+ return;
+ }
+
+ if (afterMouseupCallback) {
+ await afterMouseupCallback();
+ }
+
+ let state = showTopSites ? expected.topSites : expected.searchString;
+ Assert.equal(
+ gURLBar.getAttribute("pageproxystate"),
+ state.pageProxyState,
+ "pageproxystate should be as expected"
+ );
+ Assert.equal(
+ gURLBar.value,
+ state.value,
+ "input.value should be as expected"
+ );
+ });
+ }
+ }
+
+ UrlbarProvidersManager.unregisterProvider(provider);
+});
+
+async function waitForElements(selectors) {
+ let elements;
+ await BrowserTestUtils.waitForCondition(() => {
+ elements = selectors.map(s => document.querySelector(s));
+ return elements.every(e => e && BrowserTestUtils.is_visible(e));
+ }, "Waiting for elements to become visible: " + JSON.stringify(selectors));
+ return elements;
+}
diff --git a/browser/components/urlbar/tests/browser/browser_waitForLoadOrTimeout.js b/browser/components/urlbar/tests/browser/browser_waitForLoadOrTimeout.js
new file mode 100644
index 0000000000..352e37b9d0
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_waitForLoadOrTimeout.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the waitForLoadOrTimeout test helper function in head.js.
+ */
+
+"use strict";
+
+add_task(async function load() {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ let url = "http://example.com/";
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: url,
+ });
+
+ let loadPromise = waitForLoadOrTimeout();
+ EventUtils.synthesizeKey("KEY_Enter");
+ let loadEvent = await loadPromise;
+
+ Assert.ok(loadEvent, "Page should have loaded before timeout");
+ Assert.equal(
+ loadEvent.target.currentURI.spec,
+ url,
+ "example.com should have loaded"
+ );
+ });
+});
+
+add_task(async function timeout() {
+ let loadEvent = await waitForLoadOrTimeout();
+ Assert.ok(
+ !loadEvent,
+ "No page should have loaded, and timeout should have fired"
+ );
+});
diff --git a/browser/components/urlbar/tests/browser/browser_whereToOpen.js b/browser/components/urlbar/tests/browser/browser_whereToOpen.js
new file mode 100644
index 0000000000..339a20d90e
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_whereToOpen.js
@@ -0,0 +1,192 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const NON_EMPTY_TAB = "example.com/non-empty";
+const EMPTY_TAB = "about:blank";
+const META_KEY = AppConstants.platform == "macosx" ? "metaKey" : "ctrlKey";
+const ENTER = new KeyboardEvent("keydown", {});
+const ALT_ENTER = new KeyboardEvent("keydown", { altKey: true });
+const ALTGR_ENTER = new KeyboardEvent("keydown", { modifierAltGraph: true });
+const CLICK = new MouseEvent("click", { button: 0 });
+const META_CLICK = new MouseEvent("click", { button: 0, [META_KEY]: true });
+const MIDDLE_CLICK = new MouseEvent("click", { button: 1 });
+
+let old_openintab = Preferences.get("browser.urlbar.openintab");
+registerCleanupFunction(async function () {
+ Preferences.set("browser.urlbar.openintab", old_openintab);
+});
+
+add_task(async function openInTab() {
+ // Open a non-empty tab.
+ let tab = (gBrowser.selectedTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ NON_EMPTY_TAB
+ ));
+
+ for (let test of [
+ {
+ pref: false,
+ event: ALT_ENTER,
+ desc: "Alt+Enter, non-empty tab, default prefs",
+ },
+ {
+ pref: false,
+ event: ALTGR_ENTER,
+ desc: "AltGr+Enter, non-empty tab, default prefs",
+ },
+ {
+ pref: false,
+ event: META_CLICK,
+ desc: "Meta+click, non-empty tab, default prefs",
+ },
+ {
+ pref: false,
+ event: MIDDLE_CLICK,
+ desc: "Middle click, non-empty tab, default prefs",
+ },
+ { pref: true, event: ENTER, desc: "Enter, non-empty tab, openInTab" },
+ {
+ pref: true,
+ event: CLICK,
+ desc: "Normal click, non-empty tab, openInTab",
+ },
+ ]) {
+ info(test.desc);
+
+ Preferences.set("browser.urlbar.openintab", test.pref);
+ let where = gURLBar._whereToOpen(test.event);
+ is(where, "tab", "URL would be loaded in a new tab");
+ }
+
+ // Clean up.
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function keepEmptyTab() {
+ // Open an empty tab.
+ let tab = (gBrowser.selectedTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ EMPTY_TAB
+ ));
+
+ for (let test of [
+ {
+ pref: false,
+ event: META_CLICK,
+ desc: "Meta+click, empty tab, default prefs",
+ },
+ {
+ pref: false,
+ event: MIDDLE_CLICK,
+ desc: "Middle click, empty tab, default prefs",
+ },
+ ]) {
+ info(test.desc);
+
+ Preferences.set("browser.urlbar.openintab", test.pref);
+ let where = gURLBar._whereToOpen(test.event);
+ is(where, "tab", "URL would be loaded in a new tab");
+ }
+
+ // Clean up.
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function reuseEmptyTab() {
+ // Open an empty tab.
+ let tab = (gBrowser.selectedTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ EMPTY_TAB
+ ));
+
+ for (let test of [
+ {
+ pref: false,
+ event: ALT_ENTER,
+ desc: "Alt+Enter, empty tab, default prefs",
+ },
+ {
+ pref: false,
+ event: ALTGR_ENTER,
+ desc: "AltGr+Enter, empty tab, default prefs",
+ },
+ { pref: true, event: ENTER, desc: "Enter, empty tab, openInTab" },
+ { pref: true, event: CLICK, desc: "Normal click, empty tab, openInTab" },
+ ]) {
+ info(test.desc);
+ Preferences.set("browser.urlbar.openintab", test.pref);
+ let where = gURLBar._whereToOpen(test.event);
+ is(where, "current", "New URL would reuse the current empty tab");
+ }
+
+ // Clean up.
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function openInCurrentTab() {
+ for (let test of [
+ {
+ pref: false,
+ url: NON_EMPTY_TAB,
+ event: ENTER,
+ desc: "Enter, non-empty tab, default prefs",
+ },
+ {
+ pref: false,
+ url: NON_EMPTY_TAB,
+ event: CLICK,
+ desc: "Normal click, non-empty tab, default prefs",
+ },
+ {
+ pref: false,
+ url: EMPTY_TAB,
+ event: ENTER,
+ desc: "Enter, empty tab, default prefs",
+ },
+ {
+ pref: false,
+ url: EMPTY_TAB,
+ event: CLICK,
+ desc: "Normal click, empty tab, default prefs",
+ },
+ {
+ pref: true,
+ url: NON_EMPTY_TAB,
+ event: ALT_ENTER,
+ desc: "Alt+Enter, non-empty tab, openInTab",
+ },
+ {
+ pref: true,
+ url: NON_EMPTY_TAB,
+ event: ALTGR_ENTER,
+ desc: "AltGr+Enter, non-empty tab, openInTab",
+ },
+ {
+ pref: true,
+ url: NON_EMPTY_TAB,
+ event: META_CLICK,
+ desc: "Meta+click, non-empty tab, openInTab",
+ },
+ {
+ pref: true,
+ url: NON_EMPTY_TAB,
+ event: MIDDLE_CLICK,
+ desc: "Middle click, non-empty tab, openInTab",
+ },
+ ]) {
+ info(test.desc);
+
+ // Open a new tab.
+ let tab = (gBrowser.selectedTab =
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, test.url));
+
+ Preferences.set("browser.urlbar.openintab", test.pref);
+ let where = gURLBar._whereToOpen(test.event);
+ is(where, "current", "URL would open in the current tab");
+
+ // Clean up.
+ BrowserTestUtils.removeTab(tab);
+ }
+});
diff --git a/browser/components/urlbar/tests/browser/dummy_page.html b/browser/components/urlbar/tests/browser/dummy_page.html
new file mode 100644
index 0000000000..1a87e28408
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/dummy_page.html
@@ -0,0 +1,9 @@
+<html>
+<head>
+<title>Dummy test page</title>
+<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta>
+</head>
+<body>
+<p>Dummy test page</p>
+</body>
+</html>
diff --git a/browser/components/urlbar/tests/browser/dynamicResult0.css b/browser/components/urlbar/tests/browser/dynamicResult0.css
new file mode 100644
index 0000000000..328127b594
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/dynamicResult0.css
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+#urlbar {
+ --testDynamicResult0: ok0;
+}
+
+.urlbarView-row[dynamicType=test] > .urlbarView-row-inner {
+ display: flex;
+ align-items: center;
+ min-height: 32px;
+ width: 100%;
+}
+
+.urlbarView-dynamic-test-buttonBox {
+ display: flex;
+ align-items: center;
+ min-height: 32px;
+}
+
+.urlbarView-dynamic-test-text {
+ flex-grow: 1;
+ flex-shrink: 1;
+ padding: 10px;
+}
+
+.urlbarView-dynamic-test-selectable,
+.urlbarView-dynamic-test-button1,
+.urlbarView-dynamic-test-button2 {
+ min-height: 16px;
+ padding: 8px;
+ border: none;
+ border-radius: 2px;
+ font-size: 0.93em;
+ color: inherit;
+ background-color: var(--urlbarView-button-background);
+ min-width: 8.75em;
+ text-align: center;
+ flex-basis: initial;
+ flex-shrink: 0;
+ margin-inline-end: 10px;
+}
+
+.urlbarView-dynamic-test-selectable[selected],
+.urlbarView-dynamic-test-button1[selected],
+.urlbarView-dynamic-test-button2[selected] {
+ color: white;
+ background-color: var(--urlbarView-primary-button-background);
+ box-shadow: 0 0 0 1px #0a84ff inset, 0 0 0 1px #0a84ff, 0 0 0 4px rgba(10, 132, 255, 0.3);
+}
diff --git a/browser/components/urlbar/tests/browser/dynamicResult1.css b/browser/components/urlbar/tests/browser/dynamicResult1.css
new file mode 100644
index 0000000000..ae43fd3f9a
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/dynamicResult1.css
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+#urlbar {
+ --testDynamicResult1: ok1;
+}
+
+.urlbarView-row[dynamicType=test] > .urlbarView-row-inner {
+ display: flex;
+ align-items: center;
+ min-height: 32px;
+ width: 100%;
+}
+
+.urlbarView-dynamic-test-buttonBox {
+ display: flex;
+ align-items: center;
+ min-height: 32px;
+}
+
+.urlbarView-dynamic-test-text {
+ flex-grow: 1;
+ flex-shrink: 1;
+ padding: 10px;
+}
+
+.urlbarView-dynamic-test-selectable,
+.urlbarView-dynamic-test-button1,
+.urlbarView-dynamic-test-button2 {
+ min-height: 16px;
+ padding: 8px;
+ border: none;
+ border-radius: 2px;
+ font-size: 0.93em;
+ color: inherit;
+ background-color: var(--urlbarView-button-background);
+ min-width: 8.75em;
+ text-align: center;
+ flex-basis: initial;
+ flex-shrink: 0;
+ margin-inline-end: 10px;
+}
+
+.urlbarView-dynamic-test-selectable[selected],
+.urlbarView-dynamic-test-button1[selected],
+.urlbarView-dynamic-test-button2[selected] {
+ color: white;
+ background-color: var(--urlbarView-primary-button-background);
+ box-shadow: 0 0 0 1px #0a84ff inset, 0 0 0 1px #0a84ff, 0 0 0 4px rgba(10, 132, 255, 0.3);
+}
diff --git a/browser/components/urlbar/tests/browser/file_blank_but_not_blank.html b/browser/components/urlbar/tests/browser/file_blank_but_not_blank.html
new file mode 100644
index 0000000000..1f5fea8dcf
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/file_blank_but_not_blank.html
@@ -0,0 +1,2 @@
+<script>var q = "1";</script>
+<a href="javascript:q">Click me</a>
diff --git a/browser/components/urlbar/tests/browser/file_urlbar_edit_dos.html b/browser/components/urlbar/tests/browser/file_urlbar_edit_dos.html
new file mode 100644
index 0000000000..e02242f6a1
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/file_urlbar_edit_dos.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Try editing the URL bar</title>
+<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta>
+</head>
+<body>
+<script>
+function dos_hash() {
+ location.hash = "#";
+}
+
+function dos_pushState() {
+ history.pushState({}, "Some title", "");
+}
+</script>
+</body>
+</html>
diff --git a/browser/components/urlbar/tests/browser/file_userTypedValue.html b/browser/components/urlbar/tests/browser/file_userTypedValue.html
new file mode 100644
index 0000000000..a787b70898
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/file_userTypedValue.html
@@ -0,0 +1 @@
+<html><body>bug562649</body></html>
diff --git a/browser/components/urlbar/tests/browser/head-common.js b/browser/components/urlbar/tests/browser/head-common.js
new file mode 100644
index 0000000000..4c41483944
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/head-common.js
@@ -0,0 +1,156 @@
+ChromeUtils.defineESModuleGetters(this, {
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+ Preferences: "resource://gre/modules/Preferences.sys.mjs",
+ UrlbarProvider: "resource:///modules/UrlbarUtils.sys.mjs",
+ UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs",
+ UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
+ UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.sys.mjs",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ HttpServer: "resource://testing-common/httpd.js",
+});
+
+XPCOMUtils.defineLazyGetter(this, "TEST_BASE_URL", () =>
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ )
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "clipboardHelper",
+ "@mozilla.org/widget/clipboardhelper;1",
+ "nsIClipboardHelper"
+);
+
+XPCOMUtils.defineLazyGetter(this, "UrlbarTestUtils", () => {
+ const { UrlbarTestUtils: module } = ChromeUtils.importESModule(
+ "resource://testing-common/UrlbarTestUtils.sys.mjs"
+ );
+ module.init(this);
+ return module;
+});
+
+XPCOMUtils.defineLazyGetter(this, "SearchTestUtils", () => {
+ const { SearchTestUtils: module } = ChromeUtils.importESModule(
+ "resource://testing-common/SearchTestUtils.sys.mjs"
+ );
+ module.init(this);
+ return module;
+});
+
+/**
+ * Initializes an HTTP Server, and runs a task with it.
+ *
+ * @param {object} details {scheme, host, port}
+ * @param {Function} taskFn The task to run, gets the server as argument.
+ */
+async function withHttpServer(
+ details = { scheme: "http", host: "localhost", port: -1 },
+ taskFn
+) {
+ let server = new HttpServer();
+ let url = `${details.scheme}://${details.host}:${details.port}`;
+ try {
+ info(`starting HTTP Server for ${url}`);
+ try {
+ server.start(details.port);
+ details.port = server.identity.primaryPort;
+ server.identity.setPrimary(details.scheme, details.host, details.port);
+ } catch (ex) {
+ throw new Error("We can't launch our http server successfully. " + ex);
+ }
+ Assert.ok(
+ server.identity.has(details.scheme, details.host, details.port),
+ `${url} is listening.`
+ );
+ try {
+ await taskFn(server);
+ } catch (ex) {
+ throw new Error("Exception in the task function " + ex);
+ }
+ } finally {
+ server.identity.remove(details.scheme, details.host, details.port);
+ try {
+ await new Promise(resolve => server.stop(resolve));
+ } catch (ex) {}
+ server = null;
+ }
+}
+
+/**
+ * Updates the Top Sites feed.
+ *
+ * @param {Function} condition
+ * A callback that returns true after Top Sites are successfully updated.
+ * @param {boolean} searchShortcuts
+ * True if Top Sites search shortcuts should be enabled.
+ */
+async function updateTopSites(condition, searchShortcuts = false) {
+ // Toggle the pref to clear the feed cache and force an update.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "browser.newtabpage.activity-stream.discoverystream.endpointSpocsClear",
+ "",
+ ],
+ ["browser.newtabpage.activity-stream.feeds.system.topsites", false],
+ ["browser.newtabpage.activity-stream.feeds.system.topsites", true],
+ [
+ "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts",
+ searchShortcuts,
+ ],
+ ],
+ });
+
+ // Wait for the feed to be updated.
+ await TestUtils.waitForCondition(() => {
+ let sites = AboutNewTab.getTopSites();
+ return condition(sites);
+ }, "Waiting for top sites to be updated");
+}
+
+/**
+ * Asserts a search term is in the url bar and state values are
+ * what they should be.
+ *
+ * @param {string} searchString
+ * String that should be matched in the url bar.
+ * @param {object | null} options
+ * Options for the assertions.
+ * @param {Window | null} options.window
+ * Window to use for tests.
+ * @param {string | null} options.pageProxyState
+ * The pageproxystate that should be expected. Defaults to "valid".
+ * @param {string | null} options.userTypedValue
+ * The userTypedValue that should be expected. Defaults to null.
+ */
+function assertSearchStringIsInUrlbar(
+ searchString,
+ { win = window, pageProxyState = "valid", userTypedValue = null } = {}
+) {
+ Assert.equal(
+ win.gURLBar.value,
+ searchString,
+ `Search string should be the urlbar value.`
+ );
+ Assert.equal(
+ win.gBrowser.selectedBrowser.searchTerms,
+ searchString,
+ `Search terms should match.`
+ );
+ Assert.equal(
+ win.gBrowser.userTypedValue,
+ userTypedValue,
+ "userTypedValue should match."
+ );
+ Assert.equal(
+ win.gURLBar.getAttribute("pageproxystate"),
+ pageProxyState,
+ "Pageproxystate should match."
+ );
+}
diff --git a/browser/components/urlbar/tests/browser/head.js b/browser/components/urlbar/tests/browser/head.js
new file mode 100644
index 0000000000..4d381320c9
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/head.js
@@ -0,0 +1,125 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * These tests unit test the result/url loading functionality of UrlbarController.
+ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs",
+ ExperimentFakes: "resource://testing-common/NimbusTestUtils.sys.mjs",
+ PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs",
+ PromptTestUtils: "resource://testing-common/PromptTestUtils.sys.mjs",
+ ResetProfile: "resource://gre/modules/ResetProfile.sys.mjs",
+ SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs",
+ TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
+ UrlbarController: "resource:///modules/UrlbarController.sys.mjs",
+ UrlbarQueryContext: "resource:///modules/UrlbarUtils.sys.mjs",
+ UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
+ UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.sys.mjs",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
+ UrlbarView: "resource:///modules/UrlbarView.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AboutNewTab: "resource:///modules/AboutNewTab.jsm",
+ ObjectUtils: "resource://gre/modules/ObjectUtils.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(this, "PlacesFrecencyRecalculator", () => {
+ return Cc["@mozilla.org/places/frecency-recalculator;1"].getService(
+ Ci.nsIObserver
+ ).wrappedJSObject;
+});
+
+let sandbox;
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/urlbar/tests/browser/head-common.js",
+ this
+);
+
+registerCleanupFunction(async () => {
+ // Ensure the Urlbar popup is always closed at the end of a test, to save having
+ // to do it within each test.
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+async function selectAndPaste(str, win = window) {
+ await SimpleTest.promiseClipboardChange(str, () => {
+ clipboardHelper.copyString(str);
+ });
+ win.gURLBar.select();
+ win.document.commandDispatcher
+ .getControllerForCommand("cmd_paste")
+ .doCommand("cmd_paste");
+}
+
+/**
+ * Waits for a load in any browser or a timeout, whichever comes first.
+ *
+ * @param {window} win
+ * The top-level browser window to listen in.
+ * @param {number} timeoutMs
+ * The timeout in ms.
+ * @returns {event|null}
+ * If a load event was detected before the timeout fired, then the event is
+ * returned. event.target will be the browser in which the load occurred. If
+ * the timeout fired before a load was detected, null is returned.
+ */
+async function waitForLoadOrTimeout(win = window, timeoutMs = 1000) {
+ let event;
+ let listener;
+ let timeout;
+ let eventName = "BrowserTestUtils:ContentEvent:load";
+ try {
+ event = await Promise.race([
+ new Promise(resolve => {
+ listener = resolve;
+ win.addEventListener(eventName, listener, true);
+ }),
+ new Promise(resolve => {
+ timeout = win.setTimeout(resolve, timeoutMs);
+ }),
+ ]);
+ } finally {
+ win.removeEventListener(eventName, listener, true);
+ win.clearTimeout(timeout);
+ }
+ return event || null;
+}
+
+/**
+ * Opens the url bar context menu by synthesizing a click.
+ * Returns a menu item that is specified by an id.
+ *
+ * @param {string} anonid - Identifier of a menu item of the url bar context menu.
+ * @returns {string} - The element that has the corresponding identifier.
+ */
+async function promiseContextualMenuitem(anonid) {
+ let textBox = gURLBar.querySelector("moz-input-box");
+ let cxmenu = textBox.menupopup;
+ let cxmenuPromise = BrowserTestUtils.waitForEvent(cxmenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {
+ type: "contextmenu",
+ button: 2,
+ });
+ await cxmenuPromise;
+ return textBox.getMenuItem(anonid);
+}
+
+/**
+ * Puts all CustomizableUI widgetry back to their default locations, and
+ * then fires the `aftercustomization` toolbox event so that UrlbarInput
+ * knows to reinitialize itself.
+ *
+ * @param {window} [win=window]
+ * The top-level browser window to fire the `aftercustomization` event in.
+ */
+function resetCUIAndReinitUrlbarInput(win = window) {
+ CustomizableUI.reset();
+ CustomizableUI.dispatchToolboxEvent("aftercustomization", {}, win);
+}
diff --git a/browser/components/urlbar/tests/browser/moz.png b/browser/components/urlbar/tests/browser/moz.png
new file mode 100644
index 0000000000..769c636340
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/moz.png
Binary files differ
diff --git a/browser/components/urlbar/tests/browser/print_postdata.sjs b/browser/components/urlbar/tests/browser/print_postdata.sjs
new file mode 100644
index 0000000000..5884a1d598
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/print_postdata.sjs
@@ -0,0 +1,25 @@
+const CC = Components.Constructor;
+const BinaryInputStream = CC(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+
+function handleRequest(request, response) {
+ response.setHeader("Content-Type", "text/plain", false);
+ if (request.method == "GET") {
+ response.write(request.queryString);
+ } else {
+ let body = new BinaryInputStream(request.bodyInputStream);
+
+ let avail;
+ let bytes = [];
+
+ while ((avail = body.available()) > 0) {
+ Array.prototype.push.apply(bytes, body.readByteArray(avail));
+ }
+
+ let data = String.fromCharCode.apply(null, bytes);
+ response.bodyOutputStream.write(data, data.length);
+ }
+}
diff --git a/browser/components/urlbar/tests/browser/redirect_error.sjs b/browser/components/urlbar/tests/browser/redirect_error.sjs
new file mode 100644
index 0000000000..a3937b0e7a
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/redirect_error.sjs
@@ -0,0 +1,16 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const REDIRECT_TO = "https://www.bank1.com/"; // Bad-cert host.
+
+function handleRequest(aRequest, aResponse) {
+ // Set HTTP Status
+ aResponse.setStatusLine(aRequest.httpVersion, 301, "Moved Permanently");
+
+ // Set redirect URI, mirroring the hash value.
+ let hash = /\#.+/.test(aRequest.path)
+ ? "#" + aRequest.path.split("#")[1]
+ : "";
+ aResponse.setHeader("Location", REDIRECT_TO + hash);
+}
diff --git a/browser/components/urlbar/tests/browser/redirect_to.sjs b/browser/components/urlbar/tests/browser/redirect_to.sjs
new file mode 100644
index 0000000000..b52ebdc63e
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/redirect_to.sjs
@@ -0,0 +1,9 @@
+"use strict";
+
+function handleRequest(request, response) {
+ // redirect_to.sjs?ctxmenu-image.png
+ // redirects to : ctxmenu-image.png
+ const redirectUrl = request.queryString;
+ response.setStatusLine(request.httpVersion, "302", "Found");
+ response.setHeader("Location", redirectUrl, false);
+}
diff --git a/browser/components/urlbar/tests/browser/searchSuggestionEngine.sjs b/browser/components/urlbar/tests/browser/searchSuggestionEngine.sjs
new file mode 100644
index 0000000000..145392fcf2
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/searchSuggestionEngine.sjs
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let gTimer;
+
+function handleRequest(req, resp) {
+ // Parse the query params. If the params aren't in the form "foo=bar", then
+ // treat the entire query string as a search string.
+ let params = req.queryString.split("&").reduce((memo, pair) => {
+ let [key, val] = pair.split("=");
+ if (!val) {
+ // This part isn't in the form "foo=bar". Treat it as the search string
+ // (the "query").
+ val = key;
+ key = "query";
+ }
+ memo[decode(key)] = decode(val);
+ return memo;
+ }, {});
+
+ let timeout = parseInt(params.timeout);
+ if (timeout) {
+ // Write the response after a timeout.
+ resp.processAsync();
+ gTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ gTimer.init(
+ () => {
+ writeResponse(params, resp);
+ resp.finish();
+ },
+ timeout,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+ return;
+ }
+
+ writeResponse(params, resp);
+}
+
+function writeResponse(params, resp) {
+ // Echo back the search string with "foo" and "bar" appended.
+ let suffixes = ["foo", "bar"];
+ if (params.count) {
+ // Add more suffixes.
+ let serial = 0;
+ while (suffixes.length < params.count) {
+ suffixes.push(++serial);
+ }
+ }
+ let data = [params.query, suffixes.map(s => params.query + s)];
+ resp.setHeader("Content-Type", "application/json", false);
+ resp.write(JSON.stringify(data));
+}
+
+function decode(str) {
+ return decodeURIComponent(str.replace(/\+/g, encodeURIComponent(" ")));
+}
diff --git a/browser/components/urlbar/tests/browser/searchSuggestionEngine.xml b/browser/components/urlbar/tests/browser/searchSuggestionEngine.xml
new file mode 100644
index 0000000000..142c91849c
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/searchSuggestionEngine.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>browser_searchSuggestionEngine searchSuggestionEngine.xml</ShortName>
+<Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/searchSuggestionEngine.sjs?{searchTerms}"/>
+<Url type="text/html" method="GET" template="http://mochi.test:8888/" rel="searchform">
+ <Param name="terms" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/browser/components/urlbar/tests/browser/searchSuggestionEngine2.xml b/browser/components/urlbar/tests/browser/searchSuggestionEngine2.xml
new file mode 100644
index 0000000000..565aaf2bc0
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/searchSuggestionEngine2.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>browser_searchSuggestionEngine2 searchSuggestionEngine2.xml</ShortName>
+<!-- Redirect the actual search request to the test-server because of proxy restriction -->
+<Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/searchSuggestionEngine.sjs?{searchTerms}"/>
+<!-- Redirect speculative connect to a local http server we run for this test -->
+<Url type="text/html" method="GET" template="http://localhost:20709/" rel="searchform">
+ <Param name="terms" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/browser/components/urlbar/tests/browser/searchSuggestionEngineMany.xml b/browser/components/urlbar/tests/browser/searchSuggestionEngineMany.xml
new file mode 100644
index 0000000000..7e77e32029
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/searchSuggestionEngineMany.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>browser_searchSuggestionEngineMany searchSuggestionEngineMany.xml</ShortName>
+<Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/searchSuggestionEngine.sjs?{searchTerms}&amp;count=10"/>
+<Url type="text/html" method="GET" template="http://mochi.test:8888/" rel="searchform">
+ <Param name="terms" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/browser/components/urlbar/tests/browser/searchSuggestionEngineSlow.xml b/browser/components/urlbar/tests/browser/searchSuggestionEngineSlow.xml
new file mode 100644
index 0000000000..e7214e65cc
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/searchSuggestionEngineSlow.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>searchSuggestionEngineSlow.xml</ShortName>
+<Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/searchSuggestionEngine.sjs?query={searchTerms}&amp;timeout=3000"/>
+<Url type="text/html" method="GET" template="http://mochi.test:8888/" rel="searchform">
+ <Param name="terms" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/browser/components/urlbar/tests/browser/slow-page.sjs b/browser/components/urlbar/tests/browser/slow-page.sjs
new file mode 100644
index 0000000000..ce9a759744
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/slow-page.sjs
@@ -0,0 +1,23 @@
+"use strict";
+
+let timer;
+
+const DELAY_MS = 5000;
+function handleRequest(request, response) {
+ if (request.queryString.endsWith("faster")) {
+ response.setHeader("Content-Type", "text/html", false);
+ response.write("<body>Not so slow!</body>");
+ return;
+ }
+ response.processAsync();
+ timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.init(
+ () => {
+ response.setHeader("Content-Type", "text/html", false);
+ response.write("<body>This is a slow loading page.</body>");
+ response.finish();
+ },
+ DELAY_MS,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+}
diff --git a/browser/components/urlbar/tests/browser/urlbarTelemetrySearchSuggestions.sjs b/browser/components/urlbar/tests/browser/urlbarTelemetrySearchSuggestions.sjs
new file mode 100644
index 0000000000..1978b4f665
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/urlbarTelemetrySearchSuggestions.sjs
@@ -0,0 +1,9 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(req, resp) {
+ let suffixes = ["foo", "bar"];
+ let data = [req.queryString, suffixes.map(s => req.queryString + s)];
+ resp.setHeader("Content-Type", "application/json", false);
+ resp.write(JSON.stringify(data));
+}
diff --git a/browser/components/urlbar/tests/browser/urlbarTelemetrySearchSuggestions.xml b/browser/components/urlbar/tests/browser/urlbarTelemetrySearchSuggestions.xml
new file mode 100644
index 0000000000..8ed4fef6f1
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/urlbarTelemetrySearchSuggestions.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>browser_urlbar_telemetry urlbarTelemetrySearchSuggestions.xml</ShortName>
+<Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/urlbarTelemetrySearchSuggestions.sjs?{searchTerms}"/>
+<Url type="text/html" method="GET" template="http://example.com" rel="searchform"/>
+</SearchPlugin>
diff --git a/browser/components/urlbar/tests/browser/urlbarTelemetryUrlbarDynamic.css b/browser/components/urlbar/tests/browser/urlbarTelemetryUrlbarDynamic.css
new file mode 100644
index 0000000000..e81052522f
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/urlbarTelemetryUrlbarDynamic.css
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+.urlbarView-row[dynamicType=test] > .urlbarView-row-inner {
+ display: flex;
+ align-items: center;
+ min-height: 32px;
+ width: 100%;
+}
+
+.urlbarView-dynamic-test-button {
+ min-height: 16px;
+ padding: 8px;
+ border: none;
+ border-radius: 2px;
+ font-size: 0.93em;
+ color: inherit;
+ background-color: var(--urlbarView-button-background);
+ min-width: 8.75em;
+ text-align: center;
+ flex-basis: initial;
+ flex-shrink: 0;
+}
+
+.urlbarView-dynamic-test-button[selected] {
+ color: white;
+ background-color: var(--urlbarView-primary-button-background);
+ box-shadow: 0 0 0 1px #0a84ff inset, 0 0 0 1px #0a84ff, 0 0 0 4px rgba(10, 132, 255, 0.3);
+}
+
+.urlbarView-dynamic-test-button:hover {
+ color: white;
+ background-color: var(--urlbarView-primary-button-background-hover);
+}
+
+.urlbarView-dynamic-test-button:active {
+ color: white;
+ background-color: var(--urlbarView-primary-button-background-active);
+}
+
+.urlbarView-dynamic-test-buttonSpacer {
+ flex-basis: 48px;
+ flex-grow: 1;
+ flex-shrink: 1;
+}
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser.ini b/browser/components/urlbar/tests/engagementTelemetry/browser/browser.ini
new file mode 100644
index 0000000000..762173562d
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser.ini
@@ -0,0 +1,52 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+[DEFAULT]
+support-files =
+ head.js
+ head-exposure.js
+ head-groups.js
+ head-interaction.js
+ head-n_chars_n_words.js
+ head-sap.js
+ head-search_mode.js
+ ../../browser-tips/head.js
+ ../../ext/browser/head.js
+ ../../ext/api.js
+ ../../ext/schema.json
+skip-if =
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+
+[browser_glean_telemetry_abandonment_groups.js]
+[browser_glean_telemetry_abandonment_interaction.js]
+[browser_glean_telemetry_abandonment_interaction_persisted_search_terms_disabled.js]
+[browser_glean_telemetry_abandonment_interaction_persisted_search_terms_enabled.js]
+[browser_glean_telemetry_abandonment_n_chars_n_words.js]
+[browser_glean_telemetry_abandonment_sap.js]
+[browser_glean_telemetry_abandonment_search_mode.js]
+[browser_glean_telemetry_abandonment_tips.js]
+[browser_glean_telemetry_engagement_edge_cases.js]
+[browser_glean_telemetry_engagement_groups.js]
+[browser_glean_telemetry_engagement_interaction.js]
+[browser_glean_telemetry_engagement_interaction_persisted_search_terms_disabled.js]
+[browser_glean_telemetry_engagement_interaction_persisted_search_terms_enabled.js]
+[browser_glean_telemetry_engagement_n_chars_n_words.js]
+[browser_glean_telemetry_engagement_sap.js]
+[browser_glean_telemetry_engagement_search_mode.js]
+[browser_glean_telemetry_engagement_selected_result.js]
+support-files =
+ ../../../../search/test/browser/trendingSuggestionEngine.sjs
+[browser_glean_telemetry_engagement_tips.js]
+[browser_glean_telemetry_engagement_type.js]
+[browser_glean_telemetry_exposure.js]
+[browser_glean_telemetry_impression_groups.js]
+[browser_glean_telemetry_impression_interaction.js]
+[browser_glean_telemetry_impression_interaction_persisted_search_terms_disabled.js]
+[browser_glean_telemetry_impression_interaction_persisted_search_terms_enabled.js]
+[browser_glean_telemetry_impression_n_chars_n_words.js]
+[browser_glean_telemetry_impression_preferences.js]
+[browser_glean_telemetry_impression_sap.js]
+[browser_glean_telemetry_impression_search_mode.js]
+[browser_glean_telemetry_impression_timing.js]
+[browser_glean_telemetry_record_preferences.js]
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_groups.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_groups.js
new file mode 100644
index 0000000000..6341a21a1a
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_groups.js
@@ -0,0 +1,202 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for the following data of abandonment telemetry.
+// - groups
+// - results
+// - n_results
+
+add_setup(async function () {
+ await initGroupTest();
+});
+
+add_task(async function heuristics() {
+ await doHeuristicsTest({
+ trigger: () => doBlur(),
+ assert: () =>
+ assertAbandonmentTelemetry([
+ { groups: "heuristic", results: "search_engine" },
+ ]),
+ });
+});
+
+add_task(async function adaptive_history() {
+ await doAdaptiveHistoryTest({
+ trigger: () => doBlur(),
+ assert: () =>
+ assertAbandonmentTelemetry([
+ {
+ groups: "heuristic,adaptive_history",
+ results: "search_engine,history",
+ n_results: 2,
+ },
+ ]),
+ });
+});
+
+add_task(async function search_history() {
+ await doSearchHistoryTest({
+ trigger: () => doBlur(),
+ assert: () =>
+ assertAbandonmentTelemetry([
+ {
+ groups: "heuristic,search_history,search_history",
+ results: "search_engine,search_history,search_history",
+ n_results: 3,
+ },
+ ]),
+ });
+});
+
+add_task(async function search_suggest() {
+ await doSearchSuggestTest({
+ trigger: () => doBlur(),
+ assert: () =>
+ assertAbandonmentTelemetry([
+ {
+ groups: "heuristic,search_suggest,search_suggest",
+ results: "search_engine,search_suggest,search_suggest",
+ n_results: 3,
+ },
+ ]),
+ });
+
+ await doTailSearchSuggestTest({
+ trigger: () => doBlur(),
+ assert: () =>
+ assertAbandonmentTelemetry([
+ {
+ groups: "heuristic,search_suggest",
+ results: "search_engine,search_suggest",
+ n_results: 2,
+ },
+ ]),
+ });
+});
+
+add_task(async function top_pick() {
+ await doTopPickTest({
+ trigger: () => doBlur(),
+ assert: () =>
+ assertAbandonmentTelemetry([
+ {
+ groups: "heuristic,top_pick,search_suggest,search_suggest",
+ results:
+ "search_engine,rs_adm_sponsored,search_suggest,search_suggest",
+ n_results: 4,
+ },
+ ]),
+ });
+});
+
+add_task(async function top_site() {
+ await doTopSiteTest({
+ trigger: () => doBlur(),
+ assert: () =>
+ assertAbandonmentTelemetry([
+ {
+ groups: "top_site,suggested_index",
+ results: "top_site,action",
+ n_results: 2,
+ },
+ ]),
+ });
+});
+
+add_task(async function remote_tab() {
+ await doRemoteTabTest({
+ trigger: () => doBlur(),
+ assert: () =>
+ assertAbandonmentTelemetry([
+ {
+ groups: "heuristic,remote_tab",
+ results: "search_engine,remote_tab",
+ n_results: 2,
+ },
+ ]),
+ });
+});
+
+add_task(async function addon() {
+ await doAddonTest({
+ trigger: () => doBlur(),
+ assert: () =>
+ assertAbandonmentTelemetry([
+ {
+ groups: "addon",
+ results: "addon",
+ n_results: 1,
+ },
+ ]),
+ });
+});
+
+add_task(async function general() {
+ await doGeneralBookmarkTest({
+ trigger: () => doBlur(),
+ assert: () =>
+ assertAbandonmentTelemetry([
+ {
+ groups: "heuristic,suggested_index,general",
+ results: "search_engine,action,bookmark",
+ n_results: 3,
+ },
+ ]),
+ });
+
+ await doGeneralHistoryTest({
+ trigger: () => doBlur(),
+ assert: () =>
+ assertAbandonmentTelemetry([
+ {
+ groups: "heuristic,general",
+ results: "search_engine,history",
+ n_results: 2,
+ },
+ ]),
+ });
+});
+
+add_task(async function suggest() {
+ await doSuggestTest({
+ trigger: () => doBlur(),
+ assert: () =>
+ assertAbandonmentTelemetry([
+ {
+ groups: "heuristic,suggest",
+ results: "search_engine,rs_adm_nonsponsored",
+ n_results: 2,
+ },
+ ]),
+ });
+});
+
+add_task(async function about_page() {
+ await doAboutPageTest({
+ trigger: () => doBlur(),
+ assert: () =>
+ assertAbandonmentTelemetry([
+ {
+ groups: "heuristic,about_page,about_page",
+ results: "search_engine,history,history",
+ n_results: 3,
+ },
+ ]),
+ });
+});
+
+add_task(async function suggested_index() {
+ await doSuggestedIndexTest({
+ trigger: () => doBlur(),
+ assert: () =>
+ assertAbandonmentTelemetry([
+ {
+ groups: "heuristic,suggested_index",
+ results: "search_engine,unit",
+ n_results: 2,
+ },
+ ]),
+ });
+});
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_interaction.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_interaction.js
new file mode 100644
index 0000000000..0462833008
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_interaction.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for the following data of abandonment telemetry.
+// - interaction
+
+add_setup(async function () {
+ await initInteractionTest();
+});
+
+add_task(async function topsites() {
+ await doTopsitesTest({
+ trigger: () => doBlur(),
+ assert: () => assertAbandonmentTelemetry([{ interaction: "topsites" }]),
+ });
+});
+
+add_task(async function typed() {
+ await doTypedTest({
+ trigger: () => doBlur(),
+ assert: () => assertAbandonmentTelemetry([{ interaction: "typed" }]),
+ });
+
+ await doTypedWithResultsPopupTest({
+ trigger: () => doBlur(),
+ assert: () => assertAbandonmentTelemetry([{ interaction: "typed" }]),
+ });
+});
+
+add_task(async function pasted() {
+ await doPastedTest({
+ trigger: () => doBlur(),
+ assert: () => assertAbandonmentTelemetry([{ interaction: "pasted" }]),
+ });
+
+ await doPastedWithResultsPopupTest({
+ trigger: () => doBlur(),
+ assert: () => assertAbandonmentTelemetry([{ interaction: "pasted" }]),
+ });
+});
+
+add_task(async function topsite_search() {
+ // TODO: https://bugzilla.mozilla.org/show_bug.cgi?id=1804010
+ // assertAbandonmentTelemetry([{ interaction: "topsite_search" }]);
+});
+
+add_task(async function returned_restarted_refined() {
+ await doReturnedRestartedRefinedTest({
+ trigger: () => doBlur(),
+ assert: expected =>
+ assertAbandonmentTelemetry([
+ { interaction: "typed" },
+ { interaction: expected },
+ ]),
+ });
+});
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_interaction_persisted_search_terms_disabled.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_interaction_persisted_search_terms_disabled.js
new file mode 100644
index 0000000000..68799544b0
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_interaction_persisted_search_terms_disabled.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test abandonment telemetry with persisted search terms disabled.
+
+// Allow more time for Mac machines so they don't time out in verify mode.
+if (AppConstants.platform == "macosx") {
+ requestLongerTimeout(3);
+}
+
+add_setup(async function () {
+ await initInteractionTest();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.showSearchTerms.featureGate", false]],
+ });
+});
+
+add_task(async function persisted_search_terms() {
+ await doPersistedSearchTermsTest({
+ trigger: () => doBlur(),
+ assert: () => assertAbandonmentTelemetry([{ interaction: "typed" }]),
+ });
+});
+
+add_task(async function persisted_search_terms_restarted_refined() {
+ await doPersistedSearchTermsRestartedRefinedTest({
+ enabled: false,
+ trigger: () => doBlur(),
+ assert: expected => assertAbandonmentTelemetry([{ interaction: expected }]),
+ });
+});
+
+add_task(
+ async function persisted_search_terms_restarted_refined_via_abandonment() {
+ await doPersistedSearchTermsRestartedRefinedViaAbandonmentTest({
+ enabled: false,
+ trigger: () => doBlur(),
+ assert: expected =>
+ assertAbandonmentTelemetry([
+ { interaction: "typed" },
+ { interaction: expected },
+ ]),
+ });
+ }
+);
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_interaction_persisted_search_terms_enabled.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_interaction_persisted_search_terms_enabled.js
new file mode 100644
index 0000000000..f0a217805f
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_interaction_persisted_search_terms_enabled.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test abandonment telemetry with persisted search terms enabled.
+
+// Allow more time for Mac machines so they don't time out in verify mode.
+if (AppConstants.platform == "macosx") {
+ requestLongerTimeout(3);
+}
+
+add_setup(async function () {
+ await initInteractionTest();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.showSearchTerms.featureGate", true],
+ ["browser.urlbar.showSearchTerms.enabled", true],
+ ["browser.search.widget.inNavBar", false],
+ ],
+ });
+});
+
+add_task(async function persisted_search_terms() {
+ await doPersistedSearchTermsTest({
+ trigger: () => doBlur(),
+ assert: () =>
+ assertAbandonmentTelemetry([{ interaction: "persisted_search_terms" }]),
+ });
+});
+
+add_task(async function persisted_search_terms_restarted_refined() {
+ await doPersistedSearchTermsRestartedRefinedTest({
+ enabled: true,
+ trigger: () => doBlur(),
+ assert: expected => assertAbandonmentTelemetry([{ interaction: expected }]),
+ });
+});
+
+add_task(
+ async function persisted_search_terms_restarted_refined_via_abandonment() {
+ await doPersistedSearchTermsRestartedRefinedViaAbandonmentTest({
+ enabled: true,
+ trigger: () => doBlur(),
+ assert: expected =>
+ assertAbandonmentTelemetry([
+ { interaction: "persisted_search_terms_restarted" },
+ { interaction: expected },
+ ]),
+ });
+ }
+);
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_n_chars_n_words.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_n_chars_n_words.js
new file mode 100644
index 0000000000..7427db8cbf
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_n_chars_n_words.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for the following data of abandonment telemetry.
+// - n_chars
+// - n_words
+
+add_setup(async function () {
+ await initNCharsAndNWordsTest();
+});
+
+add_task(async function n_chars() {
+ await doNCharsTest({
+ trigger: () => doBlur(),
+ assert: nChars => assertAbandonmentTelemetry([{ n_chars: nChars }]),
+ });
+
+ await doNCharsWithOverMaxTextLengthCharsTest({
+ trigger: () => doBlur(),
+ assert: nChars => assertAbandonmentTelemetry([{ n_chars: nChars }]),
+ });
+});
+
+add_task(async function n_words() {
+ await doNWordsTest({
+ trigger: () => doBlur(),
+ assert: nWords => assertAbandonmentTelemetry([{ n_words: nWords }]),
+ });
+
+ await doNWordsWithOverMaxTextLengthCharsTest({
+ trigger: () => doBlur(),
+ assert: nWords => assertAbandonmentTelemetry([{ n_words: nWords }]),
+ });
+});
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_sap.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_sap.js
new file mode 100644
index 0000000000..3d0af65379
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_sap.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for the following data of abandonment telemetry.
+// - sap
+
+add_setup(async function () {
+ await initSapTest();
+});
+
+add_task(async function urlbar_newtab() {
+ await doUrlbarNewTabTest({
+ trigger: () => doBlur(),
+ assert: () => assertAbandonmentTelemetry([{ sap: "urlbar_newtab" }]),
+ });
+});
+
+add_task(async function urlbar() {
+ await doUrlbarTest({
+ trigger: () => doBlur(),
+ assert: () => assertAbandonmentTelemetry([{ sap: "urlbar" }]),
+ });
+});
+
+add_task(async function handoff() {
+ await doHandoffTest({
+ trigger: () => doBlur(),
+ assert: () => assertAbandonmentTelemetry([{ sap: "handoff" }]),
+ });
+});
+
+add_task(async function urlbar_addonpage() {
+ await doUrlbarAddonpageTest({
+ trigger: () => doBlur(),
+ assert: () => assertAbandonmentTelemetry([{ sap: "urlbar_addonpage" }]),
+ });
+});
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_search_mode.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_search_mode.js
new file mode 100644
index 0000000000..7edcc47a30
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_search_mode.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for the following data of abandonment telemetry.
+// - search_mode
+
+add_setup(async function () {
+ await initSearchModeTest();
+});
+
+add_task(async function not_search_mode() {
+ await doNotSearchModeTest({
+ trigger: () => doBlur(),
+ assert: () => assertAbandonmentTelemetry([{ search_mode: "" }]),
+ });
+});
+
+add_task(async function search_engine() {
+ await doSearchEngineTest({
+ trigger: () => doBlur(),
+ assert: () =>
+ assertAbandonmentTelemetry([{ search_mode: "search_engine" }]),
+ });
+});
+
+add_task(async function bookmarks() {
+ await doBookmarksTest({
+ trigger: () => doBlur(),
+ assert: () => assertAbandonmentTelemetry([{ search_mode: "bookmarks" }]),
+ });
+});
+
+add_task(async function history() {
+ await doHistoryTest({
+ trigger: () => doBlur(),
+ assert: () => assertAbandonmentTelemetry([{ search_mode: "history" }]),
+ });
+});
+
+add_task(async function tabs() {
+ await doTabTest({
+ trigger: () => doBlur(),
+ assert: () => assertAbandonmentTelemetry([{ search_mode: "tabs" }]),
+ });
+});
+
+add_task(async function actions() {
+ await doActionsTest({
+ trigger: () => doBlur(),
+ assert: () => assertAbandonmentTelemetry([{ search_mode: "actions" }]),
+ });
+});
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_tips.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_tips.js
new file mode 100644
index 0000000000..f773b0fb28
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_tips.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for abandonment telemetry for tips using Glean.
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/urlbar/tests/browser-tips/head.js",
+ this
+);
+
+add_setup(async function () {
+ Services.fog.setMetricsFeatureConfig(
+ JSON.stringify({ "urlbar.abandonment": false })
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.searchTips.test.ignoreShowLimits", true],
+ ["browser.urlbar.showSearchTerms.featureGate", true],
+ ],
+ });
+ const engine = await SearchTestUtils.promiseNewSearchEngine({
+ url: "chrome://mochitests/content/browser/browser/components/urlbar/tests/browser/searchSuggestionEngine.xml",
+ });
+ const originalDefaultEngine = await Services.search.getDefault();
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ await Services.search.moveEngine(engine, 0);
+
+ registerCleanupFunction(async function () {
+ Services.fog.setMetricsFeatureConfig("{}");
+ await SpecialPowers.popPrefEnv();
+ await Services.search.setDefault(
+ originalDefaultEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ resetSearchTipsProvider();
+ });
+});
+
+add_task(async function tip_persist() {
+ await doTest(async browser => {
+ await showPersistSearchTip("test");
+ gURLBar.focus();
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ gURLBar.blur();
+ });
+
+ assertAbandonmentTelemetry([{ results: "tip_persist" }]);
+ });
+});
+
+add_task(async function mouse_down_with_tip() {
+ await doTest(async browser => {
+ await showPersistSearchTip("test");
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeMouseAtCenter(browser, {});
+ });
+
+ assertAbandonmentTelemetry([{ results: "tip_persist" }]);
+ });
+});
+
+add_task(async function mouse_down_without_tip() {
+ await doTest(async browser => {
+ EventUtils.synthesizeMouseAtCenter(browser, {});
+
+ assertAbandonmentTelemetry([]);
+ });
+});
+
+async function showPersistSearchTip(word) {
+ await openPopup(word);
+ await doEnter();
+ await BrowserTestUtils.waitForCondition(async () => {
+ for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) {
+ const detail = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ if (detail.result.payload?.type === "searchTip_persist") {
+ return true;
+ }
+ }
+ return false;
+ });
+}
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_edge_cases.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_edge_cases.js
new file mode 100644
index 0000000000..d7b2e775b8
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_edge_cases.js
@@ -0,0 +1,218 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test edge cases for engagement.
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/urlbar/tests/ext/browser/head.js",
+ this
+);
+
+add_setup(async function () {
+ await setup();
+});
+
+/**
+ * UrlbarProvider that does not add any result.
+ */
+class NoResponseTestProvider extends UrlbarTestUtils.TestProvider {
+ constructor() {
+ super({ name: "TestProviderNoResponse ", results: [] });
+ this.#deferred = PromiseUtils.defer();
+ }
+
+ get type() {
+ return UrlbarUtils.PROVIDER_TYPE.HEURISTIC;
+ }
+
+ async startQuery(context, addCallback) {
+ await this.#deferred.promise;
+ }
+
+ done() {
+ this.#deferred.resolve();
+ }
+
+ #deferred = null;
+}
+const noResponseProvider = new NoResponseTestProvider();
+
+/**
+ * UrlbarProvider that adds a heuristic result immediately as usual.
+ */
+class AnotherHeuristicProvider extends UrlbarTestUtils.TestProvider {
+ constructor({ results }) {
+ super({ name: "TestProviderAnotherHeuristic ", results });
+ this.#deferred = PromiseUtils.defer();
+ }
+
+ get type() {
+ return UrlbarUtils.PROVIDER_TYPE.HEURISTIC;
+ }
+
+ async startQuery(context, addCallback) {
+ for (const result of this._results) {
+ addCallback(this, result);
+ }
+
+ this.#deferred.resolve(context);
+ }
+
+ onQueryStarted() {
+ return this.#deferred.promise;
+ }
+
+ #deferred = null;
+}
+const anotherHeuristicProvider = new AnotherHeuristicProvider({
+ results: [
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ { url: "https://example.com/immediate" }
+ ),
+ { heuristic: true }
+ ),
+ ],
+});
+
+add_task(async function engagement_before_showing_results() {
+ await SpecialPowers.pushPrefEnv({
+ // Avoid showing search tip.
+ set: [["browser.urlbar.tipShownCount.searchTip_onboard", 999]],
+ });
+
+ // Update chunkResultsDelayMs to delay the call to notifyResults.
+ const originalChuldResultDelayMs =
+ UrlbarProvidersManager._chunkResultsDelayMs;
+ UrlbarProvidersManager._chunkResultsDelayMs = 1000000;
+
+ // Add a provider that waits forever in startQuery() to avoid fireing
+ // heuristicProviderTimer.
+ UrlbarProvidersManager.registerProvider(noResponseProvider);
+
+ // Add a provider that add a result immediately as usual.
+ UrlbarProvidersManager.registerProvider(anotherHeuristicProvider);
+
+ const cleanup = () => {
+ UrlbarProvidersManager.unregisterProvider(noResponseProvider);
+ UrlbarProvidersManager.unregisterProvider(anotherHeuristicProvider);
+ UrlbarProvidersManager._chunkResultsDelayMs = originalChuldResultDelayMs;
+ };
+ registerCleanupFunction(cleanup);
+
+ await doTest(async browser => {
+ // Try to show the results.
+ await UrlbarTestUtils.inputIntoURLBar(window, "exam");
+
+ // Wait until starting the query and filling expected results.
+ const context = await anotherHeuristicProvider.onQueryStarted();
+ const query = UrlbarProvidersManager.queries.get(context);
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ query.unsortedResults.some(
+ r => r.providerName === "HeuristicFallback"
+ ) &&
+ query.unsortedResults.some(
+ r => r.providerName === anotherHeuristicProvider.name
+ )
+ );
+
+ // Type Enter key before showing any results.
+ await doEnter();
+
+ assertEngagementTelemetry([
+ {
+ selected_result: "input_field",
+ selected_result_subtype: "",
+ provider: undefined,
+ results: "",
+ groups: "",
+ },
+ ]);
+
+ // Clear the pending query.
+ noResponseProvider.done();
+ });
+
+ cleanup();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function engagement_after_closing_results() {
+ const TRIGGERS = [
+ () => EventUtils.synthesizeKey("KEY_Escape"),
+ () =>
+ EventUtils.synthesizeMouseAtCenter(
+ document.getElementById("customizableui-special-spring2"),
+ {}
+ ),
+ ];
+
+ for (const trigger of TRIGGERS) {
+ await doTest(async browser => {
+ await openPopup("test");
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ trigger();
+ });
+ Assert.equal(
+ gURLBar.value,
+ "test",
+ "The inputted text remains even if closing the results"
+ );
+ // The tested trigger should not record abandonment event.
+ assertAbandonmentTelemetry([]);
+
+ // Endgagement.
+ await doEnter();
+
+ assertEngagementTelemetry([
+ {
+ selected_result: "input_field",
+ selected_result_subtype: "",
+ provider: undefined,
+ results: "",
+ groups: "",
+ },
+ ]);
+ });
+ }
+});
+
+add_task(async function enter_to_reload_current_url() {
+ await doTest(async browser => {
+ // Open a URL once.
+ await openPopup("https://example.com");
+ await doEnter();
+
+ // Focus the urlbar.
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ await BrowserTestUtils.waitForCondition(
+ () => window.document.activeElement === gURLBar.inputField
+ );
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ // Press Enter key to reload the page without selecting any suggestions.
+ await doEnter();
+
+ assertEngagementTelemetry([
+ {
+ selected_result: "url",
+ selected_result_subtype: "",
+ provider: "HeuristicFallback",
+ results: "url",
+ groups: "heuristic",
+ },
+ {
+ selected_result: "input_field",
+ selected_result_subtype: "",
+ provider: undefined,
+ results: "action",
+ groups: "suggested_index",
+ },
+ ]);
+ });
+});
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_groups.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_groups.js
new file mode 100644
index 0000000000..9060835562
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_groups.js
@@ -0,0 +1,259 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for the following data of engagement telemetry.
+// - groups
+// - results
+// - n_results
+
+add_setup(async function () {
+ await initGroupTest();
+});
+
+add_task(async function heuristics() {
+ await doHeuristicsTest({
+ trigger: () => doEnter(),
+ assert: () =>
+ assertEngagementTelemetry([
+ { groups: "heuristic", results: "search_engine" },
+ ]),
+ });
+});
+
+add_task(async function adaptive_history() {
+ await doAdaptiveHistoryTest({
+ trigger: () => doEnter(),
+ assert: () =>
+ assertEngagementTelemetry([
+ {
+ groups: "heuristic,adaptive_history",
+ results: "search_engine,history",
+ n_results: 2,
+ },
+ ]),
+ });
+});
+
+add_task(async function search_history() {
+ await doSearchHistoryTest({
+ trigger: () => doEnter(),
+ assert: () =>
+ assertEngagementTelemetry([
+ {
+ groups: "heuristic,search_history,search_history",
+ results: "search_engine,search_history,search_history",
+ n_results: 3,
+ },
+ ]),
+ });
+});
+
+add_task(async function search_suggest() {
+ await doSearchSuggestTest({
+ trigger: () => doEnter(),
+ assert: () =>
+ assertEngagementTelemetry([
+ {
+ groups: "heuristic,search_suggest,search_suggest",
+ results: "search_engine,search_suggest,search_suggest",
+ n_results: 3,
+ },
+ ]),
+ });
+
+ await doTailSearchSuggestTest({
+ trigger: () => doEnter(),
+ assert: () =>
+ assertEngagementTelemetry([
+ {
+ groups: "heuristic,search_suggest",
+ results: "search_engine,search_suggest",
+ n_results: 2,
+ },
+ ]),
+ });
+});
+
+add_task(async function top_pick() {
+ await doTopPickTest({
+ trigger: () => doEnter(),
+ assert: () =>
+ assertEngagementTelemetry([
+ {
+ groups: "heuristic,top_pick,search_suggest,search_suggest",
+ results:
+ "search_engine,rs_adm_sponsored,search_suggest,search_suggest",
+ n_results: 4,
+ },
+ ]),
+ });
+});
+
+add_task(async function top_site() {
+ await doTopSiteTest({
+ trigger: () => doEnter(),
+ assert: () =>
+ assertEngagementTelemetry([
+ {
+ groups: "top_site,suggested_index",
+ results: "top_site,action",
+ n_results: 2,
+ },
+ ]),
+ });
+});
+
+add_task(async function remote_tab() {
+ await doRemoteTabTest({
+ trigger: () => doEnter(),
+ assert: () =>
+ assertEngagementTelemetry([
+ {
+ groups: "heuristic,remote_tab",
+ results: "search_engine,remote_tab",
+ n_results: 2,
+ },
+ ]),
+ });
+});
+
+add_task(async function addon() {
+ await doAddonTest({
+ trigger: () => doEnter(),
+ assert: () =>
+ assertEngagementTelemetry([
+ {
+ groups: "addon",
+ results: "addon",
+ n_results: 1,
+ },
+ ]),
+ });
+});
+
+add_task(async function general() {
+ await doGeneralBookmarkTest({
+ trigger: () => doEnter(),
+ assert: () =>
+ assertEngagementTelemetry([
+ {
+ groups: "heuristic,suggested_index,general",
+ results: "search_engine,action,bookmark",
+ n_results: 3,
+ },
+ ]),
+ });
+
+ await doGeneralHistoryTest({
+ trigger: () => doEnter(),
+ assert: () =>
+ assertEngagementTelemetry([
+ {
+ groups: "heuristic,general",
+ results: "search_engine,history",
+ n_results: 2,
+ },
+ ]),
+ });
+});
+
+add_task(async function suggest() {
+ await doSuggestTest({
+ trigger: () => doEnter(),
+ assert: () =>
+ assertEngagementTelemetry([
+ {
+ groups: "heuristic,suggest",
+ results: "search_engine,rs_adm_nonsponsored",
+ n_results: 2,
+ },
+ ]),
+ });
+});
+
+add_task(async function about_page() {
+ await doAboutPageTest({
+ trigger: () => doEnter(),
+ assert: () =>
+ assertEngagementTelemetry([
+ {
+ groups: "heuristic,about_page,about_page",
+ results: "search_engine,history,history",
+ n_results: 3,
+ },
+ ]),
+ });
+});
+
+add_task(async function suggested_index() {
+ await doSuggestedIndexTest({
+ trigger: () =>
+ SimpleTest.promiseClipboardChange("100 cm", () => {
+ EventUtils.synthesizeKey("KEY_Enter");
+ }),
+ assert: () =>
+ assertEngagementTelemetry([
+ {
+ groups: "heuristic,suggested_index",
+ results: "search_engine,unit",
+ n_results: 2,
+ },
+ ]),
+ });
+});
+
+add_task(async function always_empty_if_drop_go() {
+ const expected = [
+ {
+ engagement_type: "drop_go",
+ groups: "",
+ results: "",
+ n_results: 0,
+ },
+ ];
+
+ await doTest(async browser => {
+ await doDropAndGo("example.com");
+
+ assertEngagementTelemetry(expected);
+ });
+
+ await doTest(async browser => {
+ // Open the results view once.
+ await showResultByArrowDown();
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ await doDropAndGo("example.com");
+
+ assertEngagementTelemetry(expected);
+ });
+});
+
+add_task(async function always_empty_if_paste_go() {
+ const expected = [
+ {
+ engagement_type: "paste_go",
+ groups: "",
+ results: "",
+ n_results: 0,
+ },
+ ];
+
+ await doTest(async browser => {
+ await doPasteAndGo("example.com");
+
+ assertEngagementTelemetry(expected);
+ });
+
+ await doTest(async browser => {
+ // Open the results view once.
+ await showResultByArrowDown();
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ await doPasteAndGo("example.com");
+
+ assertEngagementTelemetry(expected);
+ });
+});
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_interaction.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_interaction.js
new file mode 100644
index 0000000000..9de0de8953
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_interaction.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for the following data of engagement telemetry.
+// - interaction
+
+// Allow more time for Mac machines so they don't time out in verify mode.
+if (AppConstants.platform == "macosx") {
+ requestLongerTimeout(3);
+}
+
+add_setup(async function () {
+ await initInteractionTest();
+});
+
+add_task(async function topsites() {
+ await doTopsitesTest({
+ trigger: () => doEnter(),
+ assert: () => assertEngagementTelemetry([{ interaction: "topsites" }]),
+ });
+});
+
+add_task(async function typed() {
+ await doTypedTest({
+ trigger: () => doEnter(),
+ assert: () => assertEngagementTelemetry([{ interaction: "typed" }]),
+ });
+
+ await doTypedWithResultsPopupTest({
+ trigger: () => doEnter(),
+ assert: () => assertEngagementTelemetry([{ interaction: "typed" }]),
+ });
+});
+
+add_task(async function dropped() {
+ await doTest(async browser => {
+ await doDropAndGo("example.com");
+
+ assertEngagementTelemetry([{ interaction: "dropped" }]);
+ });
+
+ await doTest(async browser => {
+ await showResultByArrowDown();
+ await doDropAndGo("example.com");
+
+ assertEngagementTelemetry([{ interaction: "dropped" }]);
+ });
+});
+
+add_task(async function pasted() {
+ await doPastedTest({
+ trigger: () => doEnter(),
+ assert: () => assertEngagementTelemetry([{ interaction: "pasted" }]),
+ });
+
+ await doPastedWithResultsPopupTest({
+ trigger: () => doEnter(),
+ assert: () => assertEngagementTelemetry([{ interaction: "pasted" }]),
+ });
+
+ await doTest(async browser => {
+ await doPasteAndGo("www.example.com");
+
+ assertEngagementTelemetry([{ interaction: "pasted" }]);
+ });
+
+ await doTest(async browser => {
+ await showResultByArrowDown();
+ await doPasteAndGo("www.example.com");
+
+ assertEngagementTelemetry([{ interaction: "pasted" }]);
+ });
+});
+
+add_task(async function topsite_search() {
+ // TODO: https://bugzilla.mozilla.org/show_bug.cgi?id=1804010
+ // assertEngagementTelemetry([{ interaction: "topsite_search" }]);
+});
+
+add_task(async function returned_restarted_refined() {
+ await doReturnedRestartedRefinedTest({
+ trigger: () => doEnter(),
+ assert: expected => assertEngagementTelemetry([{ interaction: expected }]),
+ });
+});
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_interaction_persisted_search_terms_disabled.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_interaction_persisted_search_terms_disabled.js
new file mode 100644
index 0000000000..1bdb4f0b61
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_interaction_persisted_search_terms_disabled.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test engagement telemetry with persisted search terms disabled.
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/urlbar/tests/engagementTelemetry/browser/head-interaction.js",
+ this
+);
+
+// Allow more time for Mac machines so they don't time out in verify mode.
+if (AppConstants.platform == "macosx") {
+ requestLongerTimeout(3);
+}
+
+add_setup(async function () {
+ await initInteractionTest();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.showSearchTerms.featureGate", false]],
+ });
+});
+
+add_task(async function persisted_search_terms() {
+ await doPersistedSearchTermsTest({
+ trigger: () => doEnter(),
+ assert: () =>
+ assertEngagementTelemetry([
+ { interaction: "typed" },
+ { interaction: "typed" },
+ ]),
+ });
+});
+
+add_task(async function persisted_search_terms_restarted_refined() {
+ await doPersistedSearchTermsRestartedRefinedTest({
+ enabled: false,
+ trigger: () => doEnter(),
+ assert: expected =>
+ assertEngagementTelemetry([
+ { interaction: "typed" },
+ { interaction: expected },
+ ]),
+ });
+});
+
+add_task(
+ async function persisted_search_terms_restarted_refined_via_abandonment() {
+ await doPersistedSearchTermsRestartedRefinedViaAbandonmentTest({
+ enabled: false,
+ trigger: () => doEnter(),
+ assert: expected =>
+ assertEngagementTelemetry([
+ { interaction: "typed" },
+ { interaction: expected },
+ ]),
+ });
+ }
+);
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_interaction_persisted_search_terms_enabled.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_interaction_persisted_search_terms_enabled.js
new file mode 100644
index 0000000000..33a01fdd22
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_interaction_persisted_search_terms_enabled.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test engagement telemetry with persisted search terms enabled.
+
+// Allow more time for Mac machines so they don't time out in verify mode.
+if (AppConstants.platform == "macosx") {
+ requestLongerTimeout(3);
+}
+
+add_setup(async function () {
+ await initInteractionTest();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.showSearchTerms.featureGate", true],
+ ["browser.urlbar.showSearchTerms.enabled", true],
+ ["browser.search.widget.inNavBar", false],
+ ],
+ });
+});
+
+add_task(async function persisted_search_terms() {
+ await doPersistedSearchTermsTest({
+ trigger: () => doEnter(),
+ assert: () =>
+ assertEngagementTelemetry([
+ { interaction: "typed" },
+ { interaction: "persisted_search_terms" },
+ ]),
+ });
+});
+
+add_task(async function persisted_search_terms_restarted_refined() {
+ await doPersistedSearchTermsRestartedRefinedTest({
+ enabled: true,
+ trigger: () => doEnter(),
+ assert: expected =>
+ assertEngagementTelemetry([
+ { interaction: "typed" },
+ { interaction: expected },
+ ]),
+ });
+});
+
+add_task(
+ async function persisted_search_terms_restarted_refined_via_abandonment() {
+ await doPersistedSearchTermsRestartedRefinedViaAbandonmentTest({
+ enabled: true,
+ trigger: () => doEnter(),
+ assert: expected =>
+ assertEngagementTelemetry([
+ { interaction: "typed" },
+ { interaction: expected },
+ ]),
+ });
+ }
+);
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_n_chars_n_words.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_n_chars_n_words.js
new file mode 100644
index 0000000000..498ffd9532
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_n_chars_n_words.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for the following data of engagement telemetry.
+// - n_chars
+// - n_words
+
+add_setup(async function () {
+ await initNCharsAndNWordsTest();
+});
+
+add_task(async function n_chars() {
+ await doNCharsTest({
+ trigger: () => doEnter(),
+ assert: nChars => assertEngagementTelemetry([{ n_chars: nChars }]),
+ });
+
+ await doNCharsWithOverMaxTextLengthCharsTest({
+ trigger: () => doEnter(),
+ assert: nChars => assertEngagementTelemetry([{ n_chars: nChars }]),
+ });
+});
+
+add_task(async function n_words() {
+ await doNWordsTest({
+ trigger: () => doEnter(),
+ assert: nWords => assertEngagementTelemetry([{ n_words: nWords }]),
+ });
+
+ await doNWordsWithOverMaxTextLengthCharsTest({
+ trigger: () => doEnter(),
+ assert: nWords => assertEngagementTelemetry([{ n_words: nWords }]),
+ });
+});
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_sap.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_sap.js
new file mode 100644
index 0000000000..d361d70229
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_sap.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for the following data of engagement telemetry.
+// - sap
+
+add_setup(async function () {
+ await initSapTest();
+});
+
+add_task(async function urlbar() {
+ await doUrlbarTest({
+ trigger: () => doEnter(),
+ assert: () =>
+ assertEngagementTelemetry([{ sap: "urlbar_newtab" }, { sap: "urlbar" }]),
+ });
+});
+
+add_task(async function handoff() {
+ await doHandoffTest({
+ trigger: () => doEnter(),
+ assert: () => assertEngagementTelemetry([{ sap: "handoff" }]),
+ });
+});
+
+add_task(async function urlbar_addonpage() {
+ await doUrlbarAddonpageTest({
+ trigger: () => doEnter(),
+ assert: () => assertEngagementTelemetry([{ sap: "urlbar_addonpage" }]),
+ });
+});
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_search_mode.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_search_mode.js
new file mode 100644
index 0000000000..62f87f0664
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_search_mode.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for the following data of engagement telemetry.
+// - search_mode
+
+add_setup(async function () {
+ await initSearchModeTest();
+});
+
+add_task(async function not_search_mode() {
+ await doNotSearchModeTest({
+ trigger: () => doEnter(),
+ assert: () => assertEngagementTelemetry([{ search_mode: "" }]),
+ });
+});
+
+add_task(async function search_engine() {
+ await doSearchEngineTest({
+ trigger: () => doEnter(),
+ assert: () => assertEngagementTelemetry([{ search_mode: "search_engine" }]),
+ });
+});
+
+add_task(async function bookmarks() {
+ await doBookmarksTest({
+ trigger: () => doEnter(),
+ assert: () => assertEngagementTelemetry([{ search_mode: "bookmarks" }]),
+ });
+});
+
+add_task(async function history() {
+ await doHistoryTest({
+ trigger: () => doEnter(),
+ assert: () => assertEngagementTelemetry([{ search_mode: "history" }]),
+ });
+});
+
+add_task(async function tabs() {
+ await doTabTest({
+ trigger: async () => {
+ const currentTab = gBrowser.selectedTab;
+ EventUtils.synthesizeKey("KEY_Enter");
+ await BrowserTestUtils.waitForCondition(
+ () => gBrowser.selectedTab !== currentTab
+ );
+ },
+ assert: () => assertEngagementTelemetry([{ search_mode: "tabs" }]),
+ });
+});
+
+add_task(async function actions() {
+ await doActionsTest({
+ trigger: async () => {
+ const onLoad = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ doClickSubButton(".urlbarView-quickaction-row[data-key=addons]");
+ await onLoad;
+ },
+ assert: () => assertEngagementTelemetry([{ search_mode: "actions" }]),
+ });
+});
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_selected_result.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_selected_result.js
new file mode 100644
index 0000000000..c19c511ccc
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_selected_result.js
@@ -0,0 +1,920 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for the following data of engagement telemetry.
+// - selected_result
+// - selected_result_subtype
+// - provider
+// - results
+
+// This test has many subtests and can time out in verify mode.
+requestLongerTimeout(5);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/urlbar/tests/ext/browser/head.js",
+ this
+);
+
+add_setup(async function () {
+ await setup();
+});
+
+add_task(async function selected_result_autofill_about() {
+ await doTest(async browser => {
+ await openPopup("about:about");
+ await doEnter();
+
+ assertEngagementTelemetry([
+ {
+ selected_result: "autofill_about",
+ selected_result_subtype: "",
+ provider: "Autofill",
+ results: "autofill_about",
+ },
+ ]);
+ });
+});
+
+add_task(async function selected_result_autofill_adaptive() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.autoFill.adaptiveHistory.enabled", true]],
+ });
+
+ await doTest(async browser => {
+ await PlacesTestUtils.addVisits("https://example.com/test");
+ await UrlbarUtils.addToInputHistory("https://example.com/test", "exa");
+ await openPopup("exa");
+ await doEnter();
+
+ assertEngagementTelemetry([
+ {
+ selected_result: "autofill_adaptive",
+ selected_result_subtype: "",
+ provider: "Autofill",
+ results: "autofill_adaptive",
+ },
+ ]);
+ });
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function selected_result_autofill_origin() {
+ await doTest(async browser => {
+ await PlacesTestUtils.addVisits("https://example.com/test");
+ await openPopup("exa");
+ await doEnter();
+
+ assertEngagementTelemetry([
+ {
+ selected_result: "autofill_origin",
+ selected_result_subtype: "",
+ provider: "Autofill",
+ results: "autofill_origin,history",
+ },
+ ]);
+ });
+});
+
+add_task(async function selected_result_autofill_url() {
+ await doTest(async browser => {
+ await PlacesTestUtils.addVisits("https://example.com/test");
+ await openPopup("https://example.com/test");
+ await doEnter();
+
+ assertEngagementTelemetry([
+ {
+ selected_result: "autofill_url",
+ selected_result_subtype: "",
+ provider: "Autofill",
+ results: "autofill_url",
+ },
+ ]);
+ });
+});
+
+add_task(async function selected_result_bookmark() {
+ await doTest(async browser => {
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "https://example.com/bookmark",
+ title: "bookmark",
+ });
+
+ await openPopup("bookmark");
+ await selectRowByURL("https://example.com/bookmark");
+ await doEnter();
+
+ assertEngagementTelemetry([
+ {
+ selected_result: "bookmark",
+ selected_result_subtype: "",
+ provider: "Places",
+ results: "search_engine,action,bookmark",
+ },
+ ]);
+ });
+});
+
+add_task(async function selected_result_history() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.autoFill", false]],
+ });
+
+ await doTest(async browser => {
+ await PlacesTestUtils.addVisits("https://example.com/test");
+
+ await openPopup("example");
+ await selectRowByURL("https://example.com/test");
+ await doEnter();
+
+ assertEngagementTelemetry([
+ {
+ selected_result: "history",
+ selected_result_subtype: "",
+ provider: "Places",
+ results: "search_engine,history",
+ },
+ ]);
+ });
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function selected_result_keyword() {
+ await doTest(async browser => {
+ await PlacesUtils.keywords.insert({
+ keyword: "keyword",
+ url: "https://example.com/?q=%s",
+ });
+
+ await openPopup("keyword test");
+ await doEnter();
+
+ assertEngagementTelemetry([
+ {
+ selected_result: "keyword",
+ selected_result_subtype: "",
+ provider: "BookmarkKeywords",
+ results: "keyword",
+ },
+ ]);
+
+ await PlacesUtils.keywords.remove("keyword");
+ });
+});
+
+add_task(async function selected_result_search_engine() {
+ await doTest(async browser => {
+ await openPopup("x");
+ await doEnter();
+
+ assertEngagementTelemetry([
+ {
+ selected_result: "search_engine",
+ selected_result_subtype: "",
+ provider: "HeuristicFallback",
+ results: "search_engine",
+ },
+ ]);
+ });
+});
+
+add_task(async function selected_result_search_suggest() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.searches", true],
+ ["browser.urlbar.maxHistoricalSearchSuggestions", 2],
+ ],
+ });
+
+ await doTest(async browser => {
+ await openPopup("foo");
+ await selectRowByURL("http://mochi.test:8888/?terms=foofoo");
+ await doEnter();
+
+ assertEngagementTelemetry([
+ {
+ selected_result: "search_suggest",
+ selected_result_subtype: "",
+ provider: "SearchSuggestions",
+ results: "search_engine,search_suggest,search_suggest",
+ },
+ ]);
+ });
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function selected_result_search_history() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.searches", true],
+ ["browser.urlbar.maxHistoricalSearchSuggestions", 2],
+ ],
+ });
+
+ await doTest(async browser => {
+ await UrlbarTestUtils.formHistory.add(["foofoo", "foobar"]);
+
+ await openPopup("foo");
+ await selectRowByURL("http://mochi.test:8888/?terms=foofoo");
+ await doEnter();
+
+ assertEngagementTelemetry([
+ {
+ selected_result: "search_history",
+ selected_result_subtype: "",
+ provider: "SearchSuggestions",
+ results: "search_engine,search_history,search_history",
+ },
+ ]);
+ });
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function selected_result_url() {
+ await doTest(async browser => {
+ await openPopup("https://example.com/");
+ await doEnter();
+
+ assertEngagementTelemetry([
+ {
+ selected_result: "url",
+ selected_result_subtype: "",
+ provider: "HeuristicFallback",
+ results: "url",
+ },
+ ]);
+ });
+});
+
+add_task(async function selected_result_action() {
+ await doTest(async browser => {
+ await showResultByArrowDown();
+ await selectRowByProvider("quickactions");
+ const onLoad = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ doClickSubButton(".urlbarView-quickaction-row[data-key=addons]");
+ await onLoad;
+
+ assertEngagementTelemetry([
+ {
+ selected_result: "action",
+ selected_result_subtype: "addons",
+ provider: "quickactions",
+ results: "action",
+ },
+ ]);
+ });
+});
+
+add_task(async function selected_result_tab() {
+ const tab = BrowserTestUtils.addTab(gBrowser, "https://example.com/");
+
+ await doTest(async browser => {
+ await openPopup("example");
+ await selectRowByProvider("Places");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await BrowserTestUtils.waitForCondition(() => gBrowser.selectedTab === tab);
+
+ assertEngagementTelemetry([
+ {
+ selected_result: "tab",
+ selected_result_subtype: "",
+ provider: "Places",
+ results: "search_engine,search_suggest,search_suggest,tab",
+ },
+ ]);
+ });
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function selected_result_remote_tab() {
+ const remoteTab = await loadRemoteTab("https://example.com");
+
+ await doTest(async browser => {
+ await openPopup("example");
+ await selectRowByProvider("RemoteTabs");
+ await doEnter();
+
+ assertEngagementTelemetry([
+ {
+ selected_result: "remote_tab",
+ selected_result_subtype: "",
+ provider: "RemoteTabs",
+ results: "search_engine,remote_tab",
+ },
+ ]);
+ });
+
+ await remoteTab.unload();
+});
+
+add_task(async function selected_result_addon() {
+ const addon = loadOmniboxAddon({ keyword: "omni" });
+ await addon.startup();
+
+ await doTest(async browser => {
+ await openPopup("omni test");
+ await doEnter();
+
+ assertEngagementTelemetry([
+ {
+ selected_result: "addon",
+ selected_result_subtype: "",
+ provider: "Omnibox",
+ results: "addon",
+ },
+ ]);
+ });
+
+ await addon.unload();
+});
+
+add_task(async function selected_result_tab_to_search() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0]],
+ });
+
+ await SearchTestUtils.installSearchExtension({
+ name: "mozengine",
+ search_url: "https://mozengine/",
+ });
+
+ await doTest(async browser => {
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits(["https://mozengine/"]);
+ }
+
+ await openPopup("moze");
+ await selectRowByProvider("TabToSearch");
+ const onComplete = UrlbarTestUtils.promiseSearchComplete(window);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await onComplete;
+
+ assertEngagementTelemetry([
+ {
+ selected_result: "tab_to_search",
+ selected_result_subtype: "",
+ provider: "TabToSearch",
+ results: "search_engine,tab_to_search,history",
+ },
+ ]);
+ });
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function selected_result_top_site() {
+ await doTest(async browser => {
+ await addTopSites("https://example.com/");
+ await showResultByArrowDown();
+ await selectRowByURL("https://example.com/");
+ await doEnter();
+
+ assertEngagementTelemetry([
+ {
+ selected_result: "top_site",
+ selected_result_subtype: "",
+ provider: "UrlbarProviderTopSites",
+ results: "top_site,action",
+ },
+ ]);
+ });
+});
+
+add_task(async function selected_result_calc() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.calculator", true]],
+ });
+
+ await doTest(async browser => {
+ await openPopup("8*8");
+ await selectRowByProvider("calculator");
+ await SimpleTest.promiseClipboardChange("64", () => {
+ EventUtils.synthesizeKey("KEY_Enter");
+ });
+
+ assertEngagementTelemetry([
+ {
+ selected_result: "calc",
+ selected_result_subtype: "",
+ provider: "calculator",
+ results: "search_engine,calc",
+ },
+ ]);
+ });
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function selected_result_unit() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.unitConversion.enabled", true]],
+ });
+
+ await doTest(async browser => {
+ await openPopup("1m to cm");
+ await selectRowByProvider("UnitConversion");
+ await SimpleTest.promiseClipboardChange("100 cm", () => {
+ EventUtils.synthesizeKey("KEY_Enter");
+ });
+
+ assertEngagementTelemetry([
+ {
+ selected_result: "unit",
+ selected_result_subtype: "",
+ provider: "UnitConversion",
+ results: "search_engine,unit",
+ },
+ ]);
+ });
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function selected_result_site_specific_contextual_search() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.contextualSearch.enabled", true]],
+ });
+
+ await doTest(async browser => {
+ const extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: "Contextual",
+ search_url: "https://example.com/browser",
+ },
+ { skipUnload: true }
+ );
+ const onLoaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ "https://example.com/"
+ );
+ BrowserTestUtils.loadURIString(
+ gBrowser.selectedBrowser,
+ "https://example.com/"
+ );
+ await onLoaded;
+
+ await openPopup("search");
+ await selectRowByProvider("UrlbarProviderContextualSearch");
+ await doEnter();
+
+ assertEngagementTelemetry([
+ {
+ selected_result: "site_specific_contextual_search",
+ selected_result_subtype: "",
+ provider: "UrlbarProviderContextualSearch",
+ results: "search_engine,site_specific_contextual_search",
+ },
+ ]);
+
+ await extension.unload();
+ });
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function selected_result_experimental_addon() {
+ const extension = await loadExtension({
+ background: async () => {
+ browser.experiments.urlbar.addDynamicResultType("testDynamicType");
+ browser.experiments.urlbar.addDynamicViewTemplate("testDynamicType", {
+ children: [
+ {
+ name: "text",
+ tag: "span",
+ attributes: {
+ role: "button",
+ },
+ },
+ ],
+ });
+ browser.urlbar.onBehaviorRequested.addListener(query => {
+ return "active";
+ }, "testProvider");
+ browser.urlbar.onResultsRequested.addListener(query => {
+ return [
+ {
+ type: "dynamic",
+ source: "local",
+ payload: {
+ dynamicType: "testDynamicType",
+ },
+ },
+ ];
+ }, "testProvider");
+ browser.experiments.urlbar.onViewUpdateRequested.addListener(payload => {
+ return {
+ text: {
+ textContent: "This is a dynamic result.",
+ },
+ };
+ }, "testProvider");
+ },
+ });
+
+ await TestUtils.waitForCondition(
+ () =>
+ UrlbarProvidersManager.getProvider("testProvider") &&
+ UrlbarResult.getDynamicResultType("testDynamicType"),
+ "Waiting for provider and dynamic type to be registered"
+ );
+
+ await doTest(async browser => {
+ await openPopup("test");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Enter")
+ );
+
+ assertEngagementTelemetry([
+ {
+ selected_result: "experimental_addon",
+ selected_result_subtype: "",
+ provider: "testProvider",
+ results: "search_engine,experimental_addon",
+ },
+ ]);
+ });
+
+ await extension.unload();
+});
+
+add_task(async function selected_result_adm_sponsored() {
+ const cleanupQuickSuggest = await ensureQuickSuggestInit();
+
+ await doTest(async browser => {
+ await openPopup("sponsored");
+ await selectRowByURL("https://example.com/sponsored");
+ await doEnter();
+
+ assertEngagementTelemetry([
+ {
+ selected_result: "rs_adm_sponsored",
+ selected_result_subtype: "",
+ provider: "UrlbarProviderQuickSuggest",
+ results: "search_engine,rs_adm_sponsored",
+ },
+ ]);
+ });
+
+ cleanupQuickSuggest();
+});
+
+add_task(async function selected_result_adm_nonsponsored() {
+ const cleanupQuickSuggest = await ensureQuickSuggestInit();
+
+ await doTest(async browser => {
+ await openPopup("nonsponsored");
+ await selectRowByURL("https://example.com/nonsponsored");
+ await doEnter();
+
+ assertEngagementTelemetry([
+ {
+ selected_result: "rs_adm_nonsponsored",
+ selected_result_subtype: "",
+ provider: "UrlbarProviderQuickSuggest",
+ results: "search_engine,rs_adm_nonsponsored",
+ },
+ ]);
+ });
+
+ cleanupQuickSuggest();
+});
+
+add_task(async function selected_result_input_field() {
+ const expected = [
+ {
+ selected_result: "input_field",
+ selected_result_subtype: "",
+ provider: null,
+ results: "",
+ },
+ ];
+
+ await doTest(async browser => {
+ await doDropAndGo("example.com");
+
+ assertEngagementTelemetry(expected);
+ });
+
+ await doTest(async browser => {
+ await doPasteAndGo("example.com");
+
+ assertEngagementTelemetry(expected);
+ });
+});
+
+add_task(async function selected_result_weather() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.quickactions.enabled", false]],
+ });
+
+ const cleanupQuickSuggest = await ensureQuickSuggestInit();
+ await MerinoTestUtils.initWeather();
+
+ await doTest(async browser => {
+ await openPopup(MerinoTestUtils.WEATHER_KEYWORD);
+ await selectRowByProvider("Weather");
+ await doEnter();
+
+ assertEngagementTelemetry([
+ {
+ selected_result: "weather",
+ selected_result_subtype: "",
+ provider: "Weather",
+ results: "search_engine,weather",
+ },
+ ]);
+ });
+
+ cleanupQuickSuggest();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function selected_result_navigational() {
+ const cleanupQuickSuggest = await ensureQuickSuggestInit({
+ merinoSuggestions: [
+ {
+ title: "Navigational suggestion",
+ url: "https://example.com/navigational-suggestion",
+ provider: "top_picks",
+ is_sponsored: false,
+ score: 0.25,
+ block_id: 0,
+ is_top_pick: true,
+ },
+ ],
+ });
+
+ await doTest(async browser => {
+ await openPopup("only match the Merino suggestion");
+ await selectRowByProvider("UrlbarProviderQuickSuggest");
+ await doEnter();
+
+ assertEngagementTelemetry([
+ {
+ selected_result: "merino_top_picks",
+ selected_result_subtype: "",
+ provider: "UrlbarProviderQuickSuggest",
+ results: "search_engine,merino_top_picks",
+ },
+ ]);
+ });
+
+ cleanupQuickSuggest();
+});
+
+add_task(async function selected_result_dynamic_wikipedia() {
+ const cleanupQuickSuggest = await ensureQuickSuggestInit({
+ merinoSuggestions: [
+ {
+ block_id: 1,
+ url: "https://example.com/dynamic-wikipedia",
+ title: "Dynamic Wikipedia suggestion",
+ click_url: "https://example.com/click",
+ impression_url: "https://example.com/impression",
+ advertiser: "dynamic-wikipedia",
+ provider: "wikipedia",
+ iab_category: "5 - Education",
+ },
+ ],
+ });
+
+ await doTest(async browser => {
+ await openPopup("only match the Merino suggestion");
+ await selectRowByProvider("UrlbarProviderQuickSuggest");
+ await doEnter();
+
+ assertEngagementTelemetry([
+ {
+ selected_result: "merino_wikipedia",
+ selected_result_subtype: "",
+ provider: "UrlbarProviderQuickSuggest",
+ results: "search_engine,merino_wikipedia",
+ },
+ ]);
+ });
+
+ cleanupQuickSuggest();
+});
+
+add_task(async function selected_result_search_shortcut_button() {
+ await doTest(async browser => {
+ const oneOffSearchButtons = UrlbarTestUtils.getOneOffSearchButtons(window);
+ await openPopup("x");
+ Assert.ok(!oneOffSearchButtons.selectedButton);
+
+ // Select oneoff button added for test in setup().
+ for (;;) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ if (!oneOffSearchButtons.selectedButton) {
+ continue;
+ }
+
+ if (
+ oneOffSearchButtons.selectedButton.engine.name.includes(
+ "searchSuggestionEngine.xml"
+ )
+ ) {
+ break;
+ }
+ }
+
+ // Search immediately.
+ await doEnter({ shiftKey: true });
+
+ assertEngagementTelemetry([
+ {
+ selected_result: "search_shortcut_button",
+ selected_result_subtype: "",
+ provider: null,
+ results: "search_engine",
+ },
+ ]);
+ });
+});
+
+add_task(async function selected_result_trending() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.searches", true],
+ ["browser.urlbar.trending.featureGate", true],
+ ["browser.urlbar.trending.requireSearchMode", false],
+ ["browser.urlbar.trending.maxResultsNoSearchMode", 1],
+ ["browser.urlbar.weather.featureGate", false],
+ ],
+ });
+
+ let defaultEngine = await Services.search.getDefault();
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: "mozengine",
+ search_url: "https://example.org/",
+ },
+ { setAsDefault: true, skipUnload: true }
+ );
+
+ SearchTestUtils.useMockIdleService();
+ await SearchTestUtils.updateRemoteSettingsConfig([
+ {
+ webExtension: { id: "mozengine@tests.mozilla.org" },
+ urls: {
+ trending: {
+ fullPath:
+ "https://example.com/browser/browser/components/search/test/browser/trendingSuggestionEngine.sjs",
+ query: "",
+ },
+ },
+ appliesTo: [{ included: { everywhere: true } }],
+ default: "yes",
+ },
+ ]);
+
+ await doTest(async browser => {
+ await openPopup("");
+ await selectRowByProvider("SearchSuggestions");
+ await doEnter();
+
+ assertEngagementTelemetry([
+ {
+ selected_result: "trending_search",
+ selected_result_subtype: "",
+ provider: "SearchSuggestions",
+ results: "trending_search",
+ },
+ ]);
+ });
+
+ await extension.unload();
+ await Services.search.setDefault(
+ defaultEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ let settingsWritten = SearchTestUtils.promiseSearchNotification(
+ "write-settings-to-disk-complete"
+ );
+ await SearchTestUtils.updateRemoteSettingsConfig();
+ await settingsWritten;
+ await PlacesUtils.history.clear();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function selected_result_trending_rich() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.richSuggestions.featureGate", true],
+ ["browser.urlbar.suggest.searches", true],
+ ["browser.urlbar.trending.featureGate", true],
+ ["browser.urlbar.trending.requireSearchMode", false],
+ ["browser.urlbar.trending.maxResultsNoSearchMode", 1],
+ ["browser.urlbar.weather.featureGate", false],
+ ],
+ });
+
+ let defaultEngine = await Services.search.getDefault();
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: "mozengine",
+ search_url: "https://example.org/",
+ },
+ { setAsDefault: true, skipUnload: true }
+ );
+
+ SearchTestUtils.useMockIdleService();
+ await SearchTestUtils.updateRemoteSettingsConfig([
+ {
+ webExtension: { id: "mozengine@tests.mozilla.org" },
+ urls: {
+ trending: {
+ fullPath:
+ "https://example.com/browser/browser/components/search/test/browser/trendingSuggestionEngine.sjs?richsuggestions=true",
+ query: "",
+ },
+ },
+ appliesTo: [{ included: { everywhere: true } }],
+ default: "yes",
+ },
+ ]);
+
+ await doTest(async browser => {
+ await openPopup("");
+ await selectRowByProvider("SearchSuggestions");
+ await doEnter();
+
+ assertEngagementTelemetry([
+ {
+ selected_result: "trending_search_rich",
+ selected_result_subtype: "",
+ provider: "SearchSuggestions",
+ results: "trending_search_rich",
+ },
+ ]);
+ });
+
+ await extension.unload();
+ await Services.search.setDefault(
+ defaultEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ let settingsWritten = SearchTestUtils.promiseSearchNotification(
+ "write-settings-to-disk-complete"
+ );
+ await SearchTestUtils.updateRemoteSettingsConfig();
+ await settingsWritten;
+ await PlacesUtils.history.clear();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function selected_result_addons() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.addons.featureGate", true],
+ ["browser.urlbar.suggest.searches", false],
+ ],
+ });
+
+ const cleanupQuickSuggest = await ensureQuickSuggestInit({
+ merinoSuggestions: [
+ {
+ provider: "amo",
+ icon: "https://example.com/good-addon.svg",
+ url: "https://example.com/good-addon",
+ title: "Good Addon",
+ description: "This is a good addon",
+ custom_details: {
+ amo: {
+ rating: "4.8",
+ number_of_ratings: "1234567",
+ guid: "good@addon",
+ },
+ },
+ is_top_pick: true,
+ },
+ ],
+ });
+
+ await doTest(async browser => {
+ await openPopup("only match the Merino suggestion");
+ await selectRowByProvider("UrlbarProviderQuickSuggest");
+ await doEnter();
+
+ assertEngagementTelemetry([
+ {
+ selected_result: "merino_amo",
+ selected_result_subtype: "",
+ provider: "UrlbarProviderQuickSuggest",
+ results: "search_engine,merino_amo",
+ },
+ ]);
+ });
+
+ cleanupQuickSuggest();
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_tips.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_tips.js
new file mode 100644
index 0000000000..104e292788
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_tips.js
@@ -0,0 +1,175 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for engagement telemetry for tips using Glean.
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/urlbar/tests/browser-tips/head.js",
+ this
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs",
+});
+
+add_setup(async function () {
+ makeProfileResettable();
+
+ Services.fog.setMetricsFeatureConfig(
+ JSON.stringify({ "urlbar.engagement": false })
+ );
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.quickactions.enabled", false]],
+ });
+
+ registerCleanupFunction(async function () {
+ Services.fog.setMetricsFeatureConfig("{}");
+ await SpecialPowers.popPrefEnv();
+ });
+});
+
+add_task(async function selected_result_tip() {
+ const testData = [
+ {
+ type: "searchTip_onboard",
+ expected: "tip_onboard",
+ },
+ {
+ type: "searchTip_persist",
+ expected: "tip_persist",
+ },
+ {
+ type: "searchTip_redirect",
+ expected: "tip_redirect",
+ },
+ {
+ type: "test",
+ expected: "tip_unknown",
+ },
+ ];
+
+ for (const { type, expected } of testData) {
+ const deferred = PromiseUtils.defer();
+ const provider = new UrlbarTestUtils.TestProvider({
+ results: [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TIP,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ type,
+ helpUrl: "https://example.com/",
+ titleL10n: { id: "urlbar-search-tips-confirm" },
+ buttons: [
+ {
+ url: "https://example.com/",
+ l10n: { id: "urlbar-search-tips-confirm" },
+ },
+ ],
+ }
+ ),
+ ],
+ priority: 1,
+ onEngagement: () => {
+ deferred.resolve();
+ },
+ });
+ UrlbarProvidersManager.registerProvider(provider);
+
+ await doTest(async browser => {
+ await openPopup("example");
+ await selectRowByType(type);
+ EventUtils.synthesizeKey("VK_RETURN");
+ await deferred.promise;
+
+ assertEngagementTelemetry([
+ {
+ selected_result: expected,
+ results: expected,
+ },
+ ]);
+ });
+
+ UrlbarProvidersManager.unregisterProvider(provider);
+ }
+});
+
+add_task(async function selected_result_intervention_clear() {
+ await doInterventionTest(
+ SEARCH_STRINGS.CLEAR,
+ "intervention_clear",
+ "chrome://browser/content/sanitize.xhtml",
+ [
+ {
+ selected_result: "intervention_clear",
+ results: "search_engine,intervention_clear",
+ },
+ ]
+ );
+});
+
+add_task(async function selected_result_intervention_refresh() {
+ await doInterventionTest(
+ SEARCH_STRINGS.REFRESH,
+ "intervention_refresh",
+ "chrome://global/content/resetProfile.xhtml",
+ [
+ {
+ selected_result: "intervention_refresh",
+ results: "search_engine,intervention_refresh",
+ },
+ ]
+ );
+});
+
+add_task(async function selected_result_intervention_update() {
+ // Updates are disabled for MSIX packages, this test is irrelevant for them.
+ if (
+ AppConstants.platform === "win" &&
+ Services.sysinfo.getProperty("hasWinPackageId")
+ ) {
+ return;
+ }
+ await UpdateUtils.setAppUpdateAutoEnabled(false);
+ await initUpdate({ queryString: "&noUpdates=1" });
+ UrlbarProviderInterventions.checkForBrowserUpdate(true);
+ await processUpdateSteps([
+ {
+ panelId: "checkingForUpdates",
+ checkActiveUpdate: null,
+ continueFile: CONTINUE_CHECK,
+ },
+ {
+ panelId: "noUpdatesFound",
+ checkActiveUpdate: null,
+ continueFile: null,
+ },
+ ]);
+
+ await doInterventionTest(
+ SEARCH_STRINGS.UPDATE,
+ "intervention_update_refresh",
+ "chrome://global/content/resetProfile.xhtml",
+ [
+ {
+ selected_result: "intervention_update",
+ results: "search_engine,intervention_update",
+ },
+ ]
+ );
+});
+
+async function doInterventionTest(keyword, type, dialog, expectedTelemetry) {
+ await doTest(async browser => {
+ await openPopup(keyword);
+ await selectRowByType(type);
+ const onDialog = BrowserTestUtils.promiseAlertDialog("cancel", dialog, {
+ isSubDialog: true,
+ });
+ EventUtils.synthesizeKey("VK_RETURN");
+ await onDialog;
+
+ assertEngagementTelemetry(expectedTelemetry);
+ });
+}
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_type.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_type.js
new file mode 100644
index 0000000000..b2b2233b52
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_type.js
@@ -0,0 +1,151 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for the following data of engagement telemetry.
+// - engagement_type
+
+add_setup(async function () {
+ await setup();
+});
+
+add_task(async function engagement_type_click() {
+ await doTest(async browser => {
+ await openPopup("x");
+ await doClick();
+
+ assertEngagementTelemetry([{ engagement_type: "click" }]);
+ });
+});
+
+add_task(async function engagement_type_enter() {
+ await doTest(async browser => {
+ await openPopup("x");
+ await doEnter();
+
+ assertEngagementTelemetry([{ engagement_type: "enter" }]);
+ });
+});
+
+add_task(async function engagement_type_go_button() {
+ await doTest(async browser => {
+ await openPopup("x");
+ EventUtils.synthesizeMouseAtCenter(gURLBar.goButton, {});
+
+ assertEngagementTelemetry([{ engagement_type: "go_button" }]);
+ });
+});
+
+add_task(async function engagement_type_drop_go() {
+ await doTest(async browser => {
+ await doDropAndGo("example.com");
+
+ assertEngagementTelemetry([{ engagement_type: "drop_go" }]);
+ });
+});
+
+add_task(async function engagement_type_paste_go() {
+ await doTest(async browser => {
+ await doPasteAndGo("www.example.com");
+
+ assertEngagementTelemetry([{ engagement_type: "paste_go" }]);
+ });
+});
+
+add_task(async function engagement_type_dismiss() {
+ const cleanupQuickSuggest = await ensureQuickSuggestInit({
+ // eslint-disable-next-line mozilla/valid-lazy
+ config: lazy.QuickSuggestTestUtils.BEST_MATCH_CONFIG,
+ });
+
+ for (const isBestMatchTest of [true, false]) {
+ const prefs = isBestMatchTest
+ ? [
+ ["browser.urlbar.bestMatch.enabled", true],
+ ["browser.urlbar.bestMatch.blockingEnabled", true],
+ ["browser.urlbar.quicksuggest.blockingEnabled", false],
+ ]
+ : [
+ ["browser.urlbar.bestMatch.enabled", false],
+ ["browser.urlbar.bestMatch.blockingEnabled", false],
+ ["browser.urlbar.quicksuggest.blockingEnabled", true],
+ ];
+ await SpecialPowers.pushPrefEnv({ set: prefs });
+
+ await doTest(async browser => {
+ await openPopup("sponsored");
+
+ const originalResultCount = UrlbarTestUtils.getResultCount(window);
+ await selectRowByURL("https://example.com/sponsored");
+ if (UrlbarPrefs.get("resultMenu")) {
+ UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "D");
+ } else {
+ doClickSubButton(".urlbarView-button-block");
+ }
+ await BrowserTestUtils.waitForCondition(
+ () => originalResultCount != UrlbarTestUtils.getResultCount(window)
+ );
+
+ assertEngagementTelemetry([{ engagement_type: "dismiss" }]);
+
+ // The view should stay open after dismissing the result. Now pick the
+ // heuristic result. Another "click" engagement event should be recorded.
+ Assert.ok(
+ gURLBar.view.isOpen,
+ "View should remain open after dismissing result"
+ );
+ await doClick();
+ assertEngagementTelemetry([
+ { engagement_type: "dismiss" },
+ { engagement_type: "click", interaction: "typed" },
+ ]);
+ });
+
+ await doTest(async browser => {
+ await openPopup("sponsored");
+
+ const originalResultCount = UrlbarTestUtils.getResultCount(window);
+ await selectRowByURL("https://example.com/sponsored");
+ EventUtils.synthesizeKey("KEY_Delete", { shiftKey: true });
+ await BrowserTestUtils.waitForCondition(
+ () => originalResultCount != UrlbarTestUtils.getResultCount(window)
+ );
+
+ assertEngagementTelemetry([{ engagement_type: "dismiss" }]);
+ });
+
+ await SpecialPowers.popPrefEnv();
+ }
+
+ cleanupQuickSuggest();
+});
+
+add_task(async function engagement_type_help() {
+ const cleanupQuickSuggest = await ensureQuickSuggestInit();
+
+ for (const isBestMatchTest of [true, false]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.bestMatch.enabled", isBestMatchTest]],
+ });
+
+ await doTest(async browser => {
+ await openPopup("sponsored");
+ await selectRowByURL("https://example.com/sponsored");
+ const onTabOpened = BrowserTestUtils.waitForNewTab(gBrowser);
+ if (UrlbarPrefs.get("resultMenu")) {
+ UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "L");
+ } else {
+ doClickSubButton(".urlbarView-button-help");
+ }
+ const tab = await onTabOpened;
+ BrowserTestUtils.removeTab(tab);
+
+ assertEngagementTelemetry([{ engagement_type: "help" }]);
+ });
+
+ await SpecialPowers.popPrefEnv();
+ }
+
+ cleanupQuickSuggest();
+});
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_exposure.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_exposure.js
new file mode 100644
index 0000000000..7529f29455
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_exposure.js
@@ -0,0 +1,107 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const SPONSORED_QUERY = "sponsored";
+const NONSPONSORED_QUERY = "nonsponsored";
+
+// test for exposure events
+add_setup(async function () {
+ await initExposureTest();
+});
+
+add_task(async function exposureSponsoredOnEngagement() {
+ await doExposureTest({
+ prefs: [
+ ["browser.urlbar.exposureResults", "rs_adm_sponsored"],
+ ["browser.urlbar.showExposureResults", true],
+ ],
+ query: SPONSORED_QUERY,
+ trigger: () => doClick(),
+ assert: () => assertExposureTelemetry([{ results: "rs_adm_sponsored" }]),
+ });
+});
+
+add_task(async function exposureSponsoredOnAbandonment() {
+ await doExposureTest({
+ prefs: [
+ ["browser.urlbar.exposureResults", "rs_adm_sponsored"],
+ ["browser.urlbar.showExposureResults", true],
+ ],
+ query: SPONSORED_QUERY,
+ trigger: () => doBlur(),
+ assert: () => assertExposureTelemetry([{ results: "rs_adm_sponsored" }]),
+ });
+});
+
+add_task(async function exposureFilter() {
+ await doExposureTest({
+ prefs: [
+ ["browser.urlbar.exposureResults", "rs_adm_sponsored"],
+ ["browser.urlbar.showExposureResults", false],
+ ],
+ query: SPONSORED_QUERY,
+ select: async () => {
+ // assert that the urlbar has no results
+ Assert.equal(await getResultByType("rs_adm_sponsored"), null);
+ },
+ trigger: () => doBlur(),
+ assert: () => assertExposureTelemetry([{ results: "rs_adm_sponsored" }]),
+ });
+});
+
+add_task(async function innerQueryExposure() {
+ await doExposureTest({
+ prefs: [
+ ["browser.urlbar.exposureResults", "rs_adm_sponsored"],
+ ["browser.urlbar.showExposureResults", true],
+ ],
+ query: NONSPONSORED_QUERY,
+ select: () => {},
+ trigger: async () => {
+ // delete the old query
+ gURLBar.select();
+ EventUtils.synthesizeKey("KEY_Backspace");
+ await openPopup(SPONSORED_QUERY);
+ await defaultSelect(SPONSORED_QUERY);
+ await doClick();
+ },
+ assert: () => assertExposureTelemetry([{ results: "rs_adm_sponsored" }]),
+ });
+});
+
+add_task(async function innerQueryInvertedExposure() {
+ await doExposureTest({
+ prefs: [
+ ["browser.urlbar.exposureResults", "rs_adm_sponsored"],
+ ["browser.urlbar.showExposureResults", true],
+ ],
+ query: SPONSORED_QUERY,
+ select: () => {},
+ trigger: async () => {
+ // delete the old query
+ gURLBar.select();
+ EventUtils.synthesizeKey("KEY_Backspace");
+ await openPopup(NONSPONSORED_QUERY);
+ await defaultSelect(SPONSORED_QUERY);
+ await doClick();
+ },
+ assert: () => assertExposureTelemetry([{ results: "rs_adm_sponsored" }]),
+ });
+});
+
+add_task(async function multipleProviders() {
+ await doExposureTest({
+ prefs: [
+ [
+ "browser.urlbar.exposureResults",
+ "rs_adm_sponsored,rs_adm_nonsponsored",
+ ],
+ ["browser.urlbar.showExposureResults", true],
+ ],
+ query: NONSPONSORED_QUERY,
+ trigger: () => doClick(),
+ assert: () => assertExposureTelemetry([{ results: "rs_adm_nonsponsored" }]),
+ });
+});
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_groups.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_groups.js
new file mode 100644
index 0000000000..199460e312
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_groups.js
@@ -0,0 +1,223 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for the following data of impression telemetry.
+// - groups
+// - results
+// - n_results
+
+add_setup(async function () {
+ await initGroupTest();
+ // Increase the pausing time to ensure to ready for all suggestions.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "browser.urlbar.searchEngagementTelemetry.pauseImpressionIntervalMs",
+ 500,
+ ],
+ ],
+ });
+});
+
+add_task(async function heuristics() {
+ await doHeuristicsTest({
+ trigger: () => waitForPauseImpression(),
+ assert: () =>
+ assertImpressionTelemetry([
+ { reason: "pause", groups: "heuristic", results: "search_engine" },
+ ]),
+ });
+});
+
+add_task(async function adaptive_history() {
+ await doAdaptiveHistoryTest({
+ trigger: () => waitForPauseImpression(),
+ assert: () =>
+ assertImpressionTelemetry([
+ {
+ reason: "pause",
+ groups: "heuristic,adaptive_history",
+ results: "search_engine,history",
+ n_results: 2,
+ },
+ ]),
+ });
+});
+
+add_task(async function search_history() {
+ await doSearchHistoryTest({
+ trigger: () => waitForPauseImpression(),
+ assert: () =>
+ assertImpressionTelemetry([
+ {
+ reason: "pause",
+ groups: "heuristic,search_history,search_history",
+ results: "search_engine,search_history,search_history",
+ n_results: 3,
+ },
+ ]),
+ });
+});
+
+add_task(async function search_suggest() {
+ await doSearchSuggestTest({
+ trigger: () => waitForPauseImpression(),
+ assert: () =>
+ assertImpressionTelemetry([
+ {
+ reason: "pause",
+ groups: "heuristic,search_suggest,search_suggest",
+ results: "search_engine,search_suggest,search_suggest",
+ n_results: 3,
+ },
+ ]),
+ });
+
+ await doTailSearchSuggestTest({
+ trigger: () => waitForPauseImpression(),
+ assert: () =>
+ assertImpressionTelemetry([
+ {
+ reason: "pause",
+ groups: "heuristic,search_suggest",
+ results: "search_engine,search_suggest",
+ n_results: 2,
+ },
+ ]),
+ });
+});
+
+add_task(async function top_pick() {
+ await doTopPickTest({
+ trigger: () => waitForPauseImpression(),
+ assert: () =>
+ assertImpressionTelemetry([
+ {
+ reason: "pause",
+ groups: "heuristic,top_pick,search_suggest,search_suggest",
+ results:
+ "search_engine,rs_adm_sponsored,search_suggest,search_suggest",
+ n_results: 4,
+ },
+ ]),
+ });
+});
+
+add_task(async function top_site() {
+ await doTopSiteTest({
+ trigger: () => waitForPauseImpression(),
+ assert: () =>
+ assertImpressionTelemetry([
+ {
+ reason: "pause",
+ groups: "top_site,suggested_index",
+ results: "top_site,action",
+ n_results: 2,
+ },
+ ]),
+ });
+});
+
+add_task(async function remote_tab() {
+ await doRemoteTabTest({
+ trigger: () => waitForPauseImpression(),
+ assert: () =>
+ assertImpressionTelemetry([
+ {
+ reason: "pause",
+ groups: "heuristic,remote_tab",
+ results: "search_engine,remote_tab",
+ n_results: 2,
+ },
+ ]),
+ });
+});
+
+add_task(async function addon() {
+ await doAddonTest({
+ trigger: () => waitForPauseImpression(),
+ assert: () =>
+ assertImpressionTelemetry([
+ {
+ reason: "pause",
+ groups: "addon",
+ results: "addon",
+ n_results: 1,
+ },
+ ]),
+ });
+});
+
+add_task(async function general() {
+ await doGeneralBookmarkTest({
+ trigger: () => waitForPauseImpression(),
+ assert: () =>
+ assertImpressionTelemetry([
+ {
+ reason: "pause",
+ groups: "heuristic,suggested_index,general",
+ results: "search_engine,action,bookmark",
+ n_results: 3,
+ },
+ ]),
+ });
+
+ await doGeneralHistoryTest({
+ trigger: () => waitForPauseImpression(),
+ assert: () =>
+ assertImpressionTelemetry([
+ {
+ reason: "pause",
+ groups: "heuristic,general",
+ results: "search_engine,history",
+ n_results: 2,
+ },
+ ]),
+ });
+});
+
+add_task(async function suggest() {
+ await doSuggestTest({
+ trigger: () => waitForPauseImpression(),
+ assert: () =>
+ assertImpressionTelemetry([
+ {
+ groups: "heuristic,suggest",
+ results: "search_engine,rs_adm_nonsponsored",
+ n_results: 2,
+ },
+ ]),
+ });
+});
+
+add_task(async function about_page() {
+ await doAboutPageTest({
+ trigger: () => waitForPauseImpression(),
+ assert: () =>
+ assertImpressionTelemetry([
+ {
+ reason: "pause",
+ groups: "heuristic,about_page,about_page",
+ results: "search_engine,history,history",
+ n_results: 3,
+ },
+ ]),
+ });
+});
+
+add_task(async function suggested_index() {
+ await doSuggestedIndexTest({
+ trigger: () => waitForPauseImpression(),
+ assert: () =>
+ assertImpressionTelemetry([
+ {
+ reason: "pause",
+ groups: "heuristic,suggested_index",
+ results: "search_engine,unit",
+ n_results: 2,
+ },
+ ]),
+ });
+});
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_interaction.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_interaction.js
new file mode 100644
index 0000000000..f783bf766c
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_interaction.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for the following data of impression telemetry.
+// - interaction
+
+add_setup(async function () {
+ await initInteractionTest();
+});
+
+add_task(async function topsites() {
+ await doTopsitesTest({
+ trigger: () => waitForPauseImpression(),
+ assert: () =>
+ assertImpressionTelemetry([{ reason: "pause", interaction: "topsites" }]),
+ });
+});
+
+add_task(async function typed() {
+ await doTypedTest({
+ trigger: () => waitForPauseImpression(),
+ assert: () =>
+ assertImpressionTelemetry([{ reason: "pause", interaction: "typed" }]),
+ });
+
+ await doTypedWithResultsPopupTest({
+ trigger: () => waitForPauseImpression(),
+ assert: () =>
+ assertImpressionTelemetry([{ reason: "pause", interaction: "typed" }]),
+ });
+});
+
+add_task(async function pasted() {
+ await doPastedTest({
+ trigger: () => waitForPauseImpression(),
+ assert: () =>
+ assertImpressionTelemetry([{ reason: "pause", interaction: "pasted" }]),
+ });
+
+ await doPastedWithResultsPopupTest({
+ trigger: () => waitForPauseImpression(),
+ assert: () =>
+ assertImpressionTelemetry([{ reason: "pause", interaction: "pasted" }]),
+ });
+});
+
+add_task(async function topsite_search() {
+ // TODO: https://bugzilla.mozilla.org/show_bug.cgi?id=1804010
+ // assertImpressionTelemetry([{ interaction: "topsite_search" }]);
+});
+
+add_task(async function returned_restarted_refined() {
+ await doReturnedRestartedRefinedTest({
+ trigger: () => waitForPauseImpression(),
+ assert: expected =>
+ assertImpressionTelemetry([
+ { reason: "pause" },
+ { reason: "pause", interaction: expected },
+ ]),
+ });
+});
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_interaction_persisted_search_terms_disabled.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_interaction_persisted_search_terms_disabled.js
new file mode 100644
index 0000000000..af7134b3a0
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_interaction_persisted_search_terms_disabled.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test impression telemetry with persisted search terms disabled.
+
+// Allow more time for Mac machines so they don't time out in verify mode.
+if (AppConstants.platform == "macosx") {
+ requestLongerTimeout(3);
+}
+
+add_setup(async function () {
+ await initInteractionTest();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.showSearchTerms.featureGate", false]],
+ });
+});
+
+add_task(async function persisted_search_terms() {
+ await doPersistedSearchTermsTest({
+ trigger: () => waitForPauseImpression(),
+ assert: () =>
+ assertImpressionTelemetry([
+ { reason: "pause" },
+ { reason: "pause", interaction: "typed" },
+ ]),
+ });
+});
+
+add_task(async function persisted_search_terms_restarted_refined() {
+ await doPersistedSearchTermsRestartedRefinedTest({
+ enabled: false,
+ trigger: () => waitForPauseImpression(),
+ assert: expected =>
+ assertImpressionTelemetry([
+ { reason: "pause" },
+ { reason: "pause", interaction: expected },
+ ]),
+ });
+});
+
+add_task(
+ async function persisted_search_terms_restarted_refined_via_abandonment() {
+ await doPersistedSearchTermsRestartedRefinedViaAbandonmentTest({
+ enabled: false,
+ trigger: () => waitForPauseImpression(),
+ assert: expected =>
+ assertImpressionTelemetry([
+ { reason: "pause" },
+ { reason: "pause" },
+ { reason: "pause", interaction: expected },
+ ]),
+ });
+ }
+);
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_interaction_persisted_search_terms_enabled.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_interaction_persisted_search_terms_enabled.js
new file mode 100644
index 0000000000..a29ff98b78
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_interaction_persisted_search_terms_enabled.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test impression telemetry with persisted search terms enabled.
+
+// Allow more time for Mac machines so they don't time out in verify mode.
+if (AppConstants.platform == "macosx") {
+ requestLongerTimeout(3);
+}
+
+add_setup(async function () {
+ await initInteractionTest();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.showSearchTerms.featureGate", true],
+ ["browser.urlbar.showSearchTerms.enabled", true],
+ ["browser.search.widget.inNavBar", false],
+ ],
+ });
+});
+
+add_task(async function interaction_persisted_search_terms() {
+ await doPersistedSearchTermsTest({
+ trigger: () => waitForPauseImpression(),
+ assert: () =>
+ assertImpressionTelemetry([
+ { reason: "pause" },
+ { reason: "pause", interaction: "persisted_search_terms" },
+ ]),
+ });
+});
+
+add_task(async function interaction_persisted_search_terms_restarted_refined() {
+ await doPersistedSearchTermsRestartedRefinedTest({
+ enabled: true,
+ trigger: () => waitForPauseImpression(),
+ assert: expected =>
+ assertImpressionTelemetry([
+ { reason: "pause" },
+ { reason: "pause", interaction: expected },
+ ]),
+ });
+});
+
+add_task(
+ async function interaction_persisted_search_terms_restarted_refined_via_abandonment() {
+ await doPersistedSearchTermsRestartedRefinedViaAbandonmentTest({
+ enabled: true,
+ trigger: () => waitForPauseImpression(),
+ assert: expected =>
+ assertImpressionTelemetry([
+ { reason: "pause" },
+ { reason: "pause" },
+ { reason: "pause", interaction: expected },
+ ]),
+ });
+ }
+);
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_n_chars_n_words.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_n_chars_n_words.js
new file mode 100644
index 0000000000..528cc318e0
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_n_chars_n_words.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for the following data of impression telemetry.
+// - n_chars
+// - n_words
+
+add_setup(async function () {
+ await initNCharsAndNWordsTest();
+});
+
+add_task(async function n_chars() {
+ await doNCharsTest({
+ trigger: () => waitForPauseImpression(),
+ assert: nChars =>
+ assertImpressionTelemetry([{ reason: "pause", n_chars: nChars }]),
+ });
+
+ await doNCharsWithOverMaxTextLengthCharsTest({
+ trigger: () => waitForPauseImpression(),
+ assert: nChars =>
+ assertImpressionTelemetry([{ reason: "pause", n_chars: nChars }]),
+ });
+});
+
+add_task(async function n_words() {
+ await doNWordsTest({
+ trigger: () => waitForPauseImpression(),
+ assert: nWords =>
+ assertImpressionTelemetry([{ reason: "pause", n_words: nWords }]),
+ });
+
+ await doNWordsWithOverMaxTextLengthCharsTest({
+ trigger: () => waitForPauseImpression(),
+ assert: nWords =>
+ assertImpressionTelemetry([{ reason: "pause", n_words: nWords }]),
+ });
+});
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_preferences.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_preferences.js
new file mode 100644
index 0000000000..344e238e24
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_preferences.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the impression telemetry behavior with its preferences.
+
+add_setup(async function () {
+ await setup();
+});
+
+add_task(async function pauseImpressionIntervalMs() {
+ const additionalInterval = 1000;
+ const originalInterval = UrlbarPrefs.get(
+ "searchEngagementTelemetry.pauseImpressionIntervalMs"
+ );
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "browser.urlbar.searchEngagementTelemetry.pauseImpressionIntervalMs",
+ originalInterval + additionalInterval,
+ ],
+ ],
+ });
+
+ await doTest(async browser => {
+ await openPopup("https://example.com");
+
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, originalInterval));
+ await Services.fog.testFlushAllChildren();
+ assertImpressionTelemetry([]);
+
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, additionalInterval));
+ await Services.fog.testFlushAllChildren();
+ assertImpressionTelemetry([{ sap: "urlbar_newtab" }]);
+ });
+
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_sap.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_sap.js
new file mode 100644
index 0000000000..482b906024
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_sap.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for the following data of impression telemetry.
+// - sap
+
+add_setup(async function () {
+ await initSapTest();
+});
+
+add_task(async function urlbar() {
+ await doUrlbarTest({
+ trigger: () => waitForPauseImpression(),
+ assert: () =>
+ assertImpressionTelemetry([
+ { reason: "pause", sap: "urlbar_newtab" },
+ { reason: "pause", sap: "urlbar" },
+ ]),
+ });
+});
+
+add_task(async function handoff() {
+ await doHandoffTest({
+ trigger: () => waitForPauseImpression(),
+ assert: () =>
+ assertImpressionTelemetry([{ reason: "pause", sap: "handoff" }]),
+ });
+});
+
+add_task(async function urlbar_addonpage() {
+ await doUrlbarAddonpageTest({
+ trigger: () => waitForPauseImpression(),
+ assert: () =>
+ assertImpressionTelemetry([{ reason: "pause", sap: "urlbar_addonpage" }]),
+ });
+});
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_search_mode.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_search_mode.js
new file mode 100644
index 0000000000..727afa3cef
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_search_mode.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for the following data of impression telemetry.
+// - search_mode
+
+add_setup(async function () {
+ await initSearchModeTest();
+ // Increase the pausing time to ensure entering search mode.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "browser.urlbar.searchEngagementTelemetry.pauseImpressionIntervalMs",
+ 1000,
+ ],
+ ],
+ });
+});
+
+add_task(async function not_search_mode() {
+ await doNotSearchModeTest({
+ trigger: () => waitForPauseImpression(),
+ assert: () =>
+ assertImpressionTelemetry([{ reason: "pause", search_mode: "" }]),
+ });
+});
+
+add_task(async function search_engine() {
+ await doSearchEngineTest({
+ trigger: () => waitForPauseImpression(),
+ assert: () =>
+ assertImpressionTelemetry([
+ { reason: "pause", search_mode: "search_engine" },
+ ]),
+ });
+});
+
+add_task(async function bookmarks() {
+ await doBookmarksTest({
+ trigger: () => waitForPauseImpression(),
+ assert: () =>
+ assertImpressionTelemetry([
+ { reason: "pause", search_mode: "bookmarks" },
+ ]),
+ });
+});
+
+add_task(async function history() {
+ await doHistoryTest({
+ trigger: () => waitForPauseImpression(),
+ assert: () =>
+ assertImpressionTelemetry([{ reason: "pause", search_mode: "history" }]),
+ });
+});
+
+add_task(async function tabs() {
+ await doTabTest({
+ trigger: () => waitForPauseImpression(),
+ assert: () =>
+ assertImpressionTelemetry([{ reason: "pause", search_mode: "tabs" }]),
+ });
+});
+
+add_task(async function actions() {
+ await doActionsTest({
+ trigger: () => waitForPauseImpression(),
+ assert: () =>
+ assertImpressionTelemetry([{ reason: "pause", search_mode: "actions" }]),
+ });
+});
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_timing.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_timing.js
new file mode 100644
index 0000000000..31f64996f3
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_impression_timing.js
@@ -0,0 +1,91 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for the taking timing for the impression telemetry.
+
+add_setup(async function () {
+ await setup();
+});
+
+add_task(async function cancelImpressionTimerByEngagementEvent() {
+ const additionalInterval = 1000;
+ const originalInterval = UrlbarPrefs.get(
+ "searchEngagementTelemetry.pauseImpressionIntervalMs"
+ );
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "browser.urlbar.searchEngagementTelemetry.pauseImpressionIntervalMs",
+ originalInterval + additionalInterval,
+ ],
+ ],
+ });
+
+ for (const trigger of [doEnter, doBlur]) {
+ await doTest(async browser => {
+ await openPopup("https://example.com");
+ await trigger();
+
+ // Check whether the impression timer was canceled.
+ await new Promise(r =>
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(r, originalInterval + additionalInterval)
+ );
+ assertImpressionTelemetry([]);
+ });
+ }
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function cancelInpressionTimerByType() {
+ const originalInterval = UrlbarPrefs.get(
+ "searchEngagementTelemetry.pauseImpressionIntervalMs"
+ );
+
+ await doTest(async browser => {
+ await openPopup("x");
+ await new Promise(r =>
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(r, originalInterval / 10)
+ );
+ assertImpressionTelemetry([]);
+
+ EventUtils.synthesizeKey(" ");
+ EventUtils.synthesizeKey("z");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ assertImpressionTelemetry([]);
+ await waitForPauseImpression();
+
+ assertImpressionTelemetry([{ n_chars: 3 }]);
+ });
+});
+
+add_task(async function oneImpressionInOneSession() {
+ await doTest(async browser => {
+ await openPopup("x");
+ await waitForPauseImpression();
+
+ // Sanity check.
+ assertImpressionTelemetry([{ n_chars: 1 }]);
+
+ // Add a keyword to start new query.
+ EventUtils.synthesizeKey(" ");
+ EventUtils.synthesizeKey("z");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ await waitForPauseImpression();
+
+ // No more taking impression telemetry.
+ assertImpressionTelemetry([{ n_chars: 1 }]);
+
+ // Finish the current session.
+ await doEnter();
+
+ // Should take pause impression since new session started.
+ await openPopup("x z y");
+ await waitForPauseImpression();
+ assertImpressionTelemetry([{ n_chars: 1 }, { n_chars: 5 }]);
+ });
+});
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_record_preferences.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_record_preferences.js
new file mode 100644
index 0000000000..6760385841
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_record_preferences.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for preference telemetry.
+
+add_setup(async function () {
+ await Services.fog.testFlushAllChildren();
+ Services.fog.testResetFOG();
+
+ // Create a new window in order to initialize TelemetryEvent of
+ // UrlbarController.
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+ registerCleanupFunction(async function () {
+ await BrowserTestUtils.closeWindow(win);
+ });
+});
+
+add_task(async function prefMaxRichResults() {
+ Assert.equal(
+ Glean.urlbar.prefMaxResults.testGetValue(),
+ UrlbarPrefs.get("maxRichResults"),
+ "Record prefMaxResults when UrlbarController is initialized"
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.maxRichResults", 0]],
+ });
+ Assert.equal(
+ Glean.urlbar.prefMaxResults.testGetValue(),
+ UrlbarPrefs.get("maxRichResults"),
+ "Record prefMaxResults when the maxRichResults pref is updated"
+ );
+});
+
+add_task(async function prefSuggestTopsites() {
+ Assert.equal(
+ Glean.urlbar.prefSuggestTopsites.testGetValue(),
+ UrlbarPrefs.get("suggest.topsites"),
+ "Record prefSuggestTopsites when UrlbarController is initialized"
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.topsites", !UrlbarPrefs.get("suggest.topsites")],
+ ],
+ });
+ Assert.equal(
+ Glean.urlbar.prefSuggestTopsites.testGetValue(),
+ UrlbarPrefs.get("suggest.topsites"),
+ "Record prefSuggestTopsites when the suggest.topsites pref is updated"
+ );
+});
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/head-exposure.js b/browser/components/urlbar/tests/engagementTelemetry/browser/head-exposure.js
new file mode 100644
index 0000000000..4ce2c6f869
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/head-exposure.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from head.js */
+
+async function doExposureTest({
+ prefs,
+ query,
+ trigger,
+ assert,
+ select = defaultSelect,
+}) {
+ const cleanupQuickSuggest = await ensureQuickSuggestInit();
+ await SpecialPowers.pushPrefEnv({
+ set: prefs,
+ });
+
+ await doTest(async () => {
+ await openPopup(query);
+ await select(query);
+
+ await trigger();
+ await assert();
+ });
+
+ await SpecialPowers.popPrefEnv();
+ cleanupQuickSuggest();
+}
+
+async function defaultSelect(query) {
+ await selectRowByURL(`https://example.com/${query}`);
+}
+
+async function getResultByType(provider) {
+ for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) {
+ const detail = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ const telemetryType = UrlbarUtils.searchEngagementTelemetryType(
+ detail.result
+ );
+ if (telemetryType === provider) {
+ return detail.result;
+ }
+ }
+ return null;
+}
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/head-groups.js b/browser/components/urlbar/tests/engagementTelemetry/browser/head-groups.js
new file mode 100644
index 0000000000..b34221d749
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/head-groups.js
@@ -0,0 +1,294 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from head.js */
+
+async function doHeuristicsTest({ trigger, assert }) {
+ await doTest(async browser => {
+ await openPopup("x");
+
+ await trigger();
+ await assert();
+ });
+}
+
+async function doAdaptiveHistoryTest({ trigger, assert }) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.autoFill", false]],
+ });
+
+ await doTest(async browser => {
+ await PlacesTestUtils.addVisits(["https://example.com/test"]);
+ await UrlbarUtils.addToInputHistory("https://example.com/test", "examp");
+
+ await openPopup("exa");
+ await selectRowByURL("https://example.com/test");
+
+ await trigger();
+ await assert();
+ });
+
+ await SpecialPowers.popPrefEnv();
+}
+
+async function doSearchHistoryTest({ trigger, assert }) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.searches", true],
+ ["browser.urlbar.maxHistoricalSearchSuggestions", 2],
+ ],
+ });
+
+ await doTest(async browser => {
+ await UrlbarTestUtils.formHistory.add(["foofoo", "foobar"]);
+
+ await openPopup("foo");
+ await selectRowByURL("http://mochi.test:8888/?terms=foofoo");
+
+ await trigger();
+ await assert();
+ });
+
+ await SpecialPowers.popPrefEnv();
+}
+
+async function doSearchSuggestTest({ trigger, assert }) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.searches", true],
+ ["browser.urlbar.maxHistoricalSearchSuggestions", 2],
+ ],
+ });
+
+ await doTest(async browser => {
+ await openPopup("foo");
+ await selectRowByURL("http://mochi.test:8888/?terms=foofoo");
+
+ await trigger();
+ await assert();
+ });
+
+ await SpecialPowers.popPrefEnv();
+}
+
+async function doTailSearchSuggestTest({ trigger, assert }) {
+ const cleanup = await _useTailSuggestionsEngine();
+
+ await doTest(async browser => {
+ await openPopup("hello");
+ await selectRowByProvider("SearchSuggestions");
+
+ await trigger();
+ await assert();
+ });
+
+ await cleanup();
+}
+
+async function doTopPickTest({ trigger, assert }) {
+ const cleanupQuickSuggest = await ensureQuickSuggestInit({
+ // eslint-disable-next-line mozilla/valid-lazy
+ config: lazy.QuickSuggestTestUtils.BEST_MATCH_CONFIG,
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.bestMatch.enabled", true]],
+ });
+
+ await doTest(async browser => {
+ await openPopup("sponsored");
+ await selectRowByURL("https://example.com/sponsored");
+
+ await trigger();
+ await assert();
+ });
+
+ await SpecialPowers.popPrefEnv();
+ cleanupQuickSuggest();
+}
+
+async function doTopSiteTest({ trigger, assert }) {
+ await doTest(async browser => {
+ await addTopSites("https://example.com/");
+
+ await showResultByArrowDown();
+ await selectRowByURL("https://example.com/");
+
+ await trigger();
+ await assert();
+ });
+}
+
+async function doRemoteTabTest({ trigger, assert }) {
+ const remoteTab = await loadRemoteTab("https://example.com");
+
+ await doTest(async browser => {
+ await openPopup("example");
+ await selectRowByProvider("RemoteTabs");
+
+ await trigger();
+ await assert();
+ });
+
+ await remoteTab.unload();
+}
+
+async function doAddonTest({ trigger, assert }) {
+ const addon = loadOmniboxAddon({ keyword: "omni" });
+ await addon.startup();
+
+ await doTest(async browser => {
+ await openPopup("omni test");
+
+ await trigger();
+ await assert();
+ });
+
+ await addon.unload();
+}
+
+async function doGeneralBookmarkTest({ trigger, assert }) {
+ await doTest(async browser => {
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "https://example.com/bookmark",
+ title: "bookmark",
+ });
+
+ await openPopup("bookmark");
+ await selectRowByURL("https://example.com/bookmark");
+
+ await trigger();
+ await assert();
+ });
+}
+
+async function doGeneralHistoryTest({ trigger, assert }) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.autoFill", false]],
+ });
+
+ await doTest(async browser => {
+ await PlacesTestUtils.addVisits("https://example.com/test");
+
+ await openPopup("example");
+ await selectRowByURL("https://example.com/test");
+
+ await trigger();
+ await assert();
+ });
+
+ await SpecialPowers.popPrefEnv();
+}
+
+async function doSuggestTest({ trigger, assert }) {
+ const cleanupQuickSuggest = await ensureQuickSuggestInit();
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.bestMatch.enabled", false]],
+ });
+
+ await doTest(async browser => {
+ await openPopup("nonsponsored");
+ await selectRowByURL("https://example.com/nonsponsored");
+
+ await trigger();
+ await assert();
+ });
+
+ await SpecialPowers.popPrefEnv();
+ cleanupQuickSuggest();
+}
+
+async function doAboutPageTest({ trigger, assert }) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.maxRichResults", 3]],
+ });
+
+ await doTest(async browser => {
+ await openPopup("about:");
+ await selectRowByURL("about:robots");
+
+ await trigger();
+ await assert();
+ });
+
+ await SpecialPowers.popPrefEnv();
+}
+
+async function doSuggestedIndexTest({ trigger, assert }) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.unitConversion.enabled", true]],
+ });
+
+ await doTest(async browser => {
+ await openPopup("1m to cm");
+ await selectRowByProvider("UnitConversion");
+
+ await trigger();
+ await assert();
+ });
+
+ await SpecialPowers.popPrefEnv();
+}
+
+/**
+ * Creates a search engine that returns tail suggestions and sets it as the
+ * default engine.
+ *
+ * @returns {Function}
+ * A cleanup function that will revert the default search engine and stop http
+ * server.
+ */
+async function _useTailSuggestionsEngine() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.search.suggest.enabled", true],
+ ["browser.urlbar.suggest.searches", true],
+ ["browser.urlbar.richSuggestions.tail", true],
+ ],
+ });
+
+ const engineName = "TailSuggestions";
+ const httpServer = new HttpServer();
+ httpServer.start(-1);
+ httpServer.registerPathHandler("/suggest", (req, resp) => {
+ const params = new URLSearchParams(req.queryString);
+ const searchStr = params.get("q");
+ const suggestions = [
+ searchStr,
+ [searchStr + "-tail"],
+ [],
+ {
+ "google:suggestdetail": [{ t: "-tail", mp: "… " }],
+ },
+ ];
+ resp.setHeader("Content-Type", "application/json", false);
+ resp.write(JSON.stringify(suggestions));
+ });
+
+ await SearchTestUtils.installSearchExtension({
+ name: engineName,
+ search_url: `http://localhost:${httpServer.identity.primaryPort}/search`,
+ suggest_url: `http://localhost:${httpServer.identity.primaryPort}/suggest`,
+ suggest_url_get_params: "?q={searchTerms}",
+ search_form: `http://localhost:${httpServer.identity.primaryPort}/search?q={searchTerms}`,
+ });
+
+ const tailEngine = Services.search.getEngineByName(engineName);
+ const originalEngine = await Services.search.getDefault();
+ Services.search.setDefault(
+ tailEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+
+ return async () => {
+ Services.search.setDefault(
+ originalEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ httpServer.stop(() => {});
+ await SpecialPowers.popPrefEnv();
+ };
+}
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/head-interaction.js b/browser/components/urlbar/tests/engagementTelemetry/browser/head-interaction.js
new file mode 100644
index 0000000000..6bc0fd3b0e
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/head-interaction.js
@@ -0,0 +1,238 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from head.js */
+
+async function doTopsitesTest({ trigger, assert }) {
+ await doTest(async browser => {
+ await addTopSites("https://example.com/");
+
+ await showResultByArrowDown();
+ await selectRowByURL("https://example.com/");
+
+ await trigger();
+ await assert();
+ });
+}
+
+async function doTypedTest({ trigger, assert }) {
+ await doTest(async browser => {
+ await openPopup("x");
+
+ await trigger();
+ await assert();
+ });
+}
+
+async function doTypedWithResultsPopupTest({ trigger, assert }) {
+ await doTest(async browser => {
+ await showResultByArrowDown();
+ EventUtils.synthesizeKey("x");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ await trigger();
+ await assert();
+ });
+}
+
+async function doPastedTest({ trigger, assert }) {
+ await doTest(async browser => {
+ await doPaste("www.example.com");
+
+ await trigger();
+ await assert();
+ });
+}
+
+async function doPastedWithResultsPopupTest({ trigger, assert }) {
+ await doTest(async browser => {
+ await showResultByArrowDown();
+ await doPaste("x");
+
+ await trigger();
+ await assert();
+ });
+}
+
+async function doReturnedRestartedRefinedTest({ trigger, assert }) {
+ const testData = [
+ {
+ firstInput: "x",
+ // Just move the focus to the URL bar after blur.
+ secondInput: null,
+ expected: "returned",
+ },
+ {
+ firstInput: "x",
+ secondInput: "x",
+ expected: "returned",
+ },
+ {
+ firstInput: "x",
+ secondInput: "y",
+ expected: "restarted",
+ },
+ {
+ firstInput: "x",
+ secondInput: "x y",
+ expected: "refined",
+ },
+ {
+ firstInput: "x y",
+ secondInput: "x",
+ expected: "refined",
+ },
+ ];
+
+ for (const { firstInput, secondInput, expected } of testData) {
+ await doTest(async browser => {
+ await openPopup(firstInput);
+ await waitForPauseImpression();
+ await doBlur();
+
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ document.getElementById("Browser:OpenLocation").doCommand();
+ });
+ if (secondInput) {
+ for (let i = 0; i < secondInput.length; i++) {
+ EventUtils.synthesizeKey(secondInput.charAt(i));
+ }
+ }
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ await trigger();
+ await assert(expected);
+ });
+ }
+}
+
+async function doPersistedSearchTermsTest({ trigger, assert }) {
+ await doTest(async browser => {
+ await openPopup("x");
+ await waitForPauseImpression();
+ await doEnter();
+
+ await openPopup("x");
+
+ await trigger();
+ await assert();
+ });
+}
+
+async function doPersistedSearchTermsRestartedRefinedTest({
+ enabled,
+ trigger,
+ assert,
+}) {
+ const testData = [
+ {
+ firstInput: "x",
+ // Just move the focus to the URL bar after engagement.
+ secondInput: null,
+ expected: enabled ? "persisted_search_terms" : "topsites",
+ },
+ {
+ firstInput: "x",
+ secondInput: "x",
+ expected: enabled ? "persisted_search_terms" : "typed",
+ },
+ {
+ firstInput: "x",
+ secondInput: "y",
+ expected: enabled ? "persisted_search_terms_restarted" : "typed",
+ },
+ {
+ firstInput: "x",
+ secondInput: "x y",
+ expected: enabled ? "persisted_search_terms_refined" : "typed",
+ },
+ {
+ firstInput: "x y",
+ secondInput: "x",
+ expected: enabled ? "persisted_search_terms_refined" : "typed",
+ },
+ ];
+
+ for (const { firstInput, secondInput, expected } of testData) {
+ await doTest(async browser => {
+ await openPopup(firstInput);
+ await waitForPauseImpression();
+ await doEnter();
+
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeKey("l", { accelKey: true });
+ });
+ if (secondInput) {
+ for (let i = 0; i < secondInput.length; i++) {
+ EventUtils.synthesizeKey(secondInput.charAt(i));
+ }
+ }
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ await trigger();
+ await assert(expected);
+ });
+ }
+}
+
+async function doPersistedSearchTermsRestartedRefinedViaAbandonmentTest({
+ enabled,
+ trigger,
+ assert,
+}) {
+ const testData = [
+ {
+ firstInput: "x",
+ // Just move the focus to the URL bar after blur.
+ secondInput: null,
+ expected: enabled ? "persisted_search_terms" : "returned",
+ },
+ {
+ firstInput: "x",
+ secondInput: "x",
+ expected: enabled ? "persisted_search_terms" : "returned",
+ },
+ {
+ firstInput: "x",
+ secondInput: "y",
+ expected: enabled ? "persisted_search_terms_restarted" : "restarted",
+ },
+ {
+ firstInput: "x",
+ secondInput: "x y",
+ expected: enabled ? "persisted_search_terms_refined" : "refined",
+ },
+ {
+ firstInput: "x y",
+ secondInput: "x",
+ expected: enabled ? "persisted_search_terms_refined" : "refined",
+ },
+ ];
+
+ for (const { firstInput, secondInput, expected } of testData) {
+ await doTest(async browser => {
+ await openPopup("any search");
+ await waitForPauseImpression();
+ await doEnter();
+
+ await openPopup(firstInput);
+ await waitForPauseImpression();
+ await doBlur();
+
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeKey("l", { accelKey: true });
+ });
+ if (secondInput) {
+ for (let i = 0; i < secondInput.length; i++) {
+ EventUtils.synthesizeKey(secondInput.charAt(i));
+ }
+ }
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ await trigger();
+ await assert(expected);
+ });
+ }
+}
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/head-n_chars_n_words.js b/browser/components/urlbar/tests/engagementTelemetry/browser/head-n_chars_n_words.js
new file mode 100644
index 0000000000..6d4c61c7f0
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/head-n_chars_n_words.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from head.js */
+
+async function doNCharsTest({ trigger, assert }) {
+ for (const input of ["x", "xx", "xx x", "xx x "]) {
+ await doTest(async browser => {
+ await openPopup(input);
+
+ await trigger();
+ await assert(input.length);
+ });
+ }
+}
+
+async function doNCharsWithOverMaxTextLengthCharsTest({ trigger, assert }) {
+ await doTest(async browser => {
+ let input = "";
+ for (let i = 0; i < UrlbarUtils.MAX_TEXT_LENGTH * 2; i++) {
+ input += "x";
+ }
+ await openPopup(input);
+
+ await trigger();
+ await assert(UrlbarUtils.MAX_TEXT_LENGTH * 2);
+ });
+}
+
+async function doNWordsTest({ trigger, assert }) {
+ for (const input of ["x", "xx", "xx x", "xx x "]) {
+ await doTest(async browser => {
+ await openPopup(input);
+
+ await trigger();
+ const splits = input.trim().split(" ");
+ await assert(splits.length);
+ });
+ }
+}
+
+async function doNWordsWithOverMaxTextLengthCharsTest({ trigger, assert }) {
+ await doTest(async browser => {
+ const word = "1234 ";
+ let input = "";
+ while (input.length < UrlbarUtils.MAX_TEXT_LENGTH * 2) {
+ input += word;
+ }
+ await openPopup(input);
+
+ await trigger();
+ await assert(UrlbarUtils.MAX_TEXT_LENGTH / word.length);
+ });
+}
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/head-sap.js b/browser/components/urlbar/tests/engagementTelemetry/browser/head-sap.js
new file mode 100644
index 0000000000..be4e852b1c
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/head-sap.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from head.js */
+
+async function doUrlbarNewTabTest({ trigger, assert }) {
+ await doTest(async browser => {
+ await openPopup("x");
+
+ await trigger();
+ await assert();
+ });
+}
+
+async function doUrlbarTest({ trigger, assert }) {
+ await doTest(async browser => {
+ await openPopup("x");
+ await waitForPauseImpression();
+ await doEnter();
+ await openPopup("y");
+
+ await trigger();
+ await assert();
+ });
+}
+
+async function doHandoffTest({ trigger, assert }) {
+ await doTest(async browser => {
+ BrowserTestUtils.loadURIString(browser, "about:newtab");
+ await BrowserTestUtils.browserStopped(browser, "about:newtab");
+ await SpecialPowers.spawn(browser, [], function () {
+ const searchInput = content.document.querySelector(".fake-editable");
+ searchInput.click();
+ });
+ EventUtils.synthesizeKey("x");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ await trigger();
+ await assert();
+ });
+}
+
+async function doUrlbarAddonpageTest({ trigger, assert }) {
+ const extensionData = {
+ files: {
+ "page.html": "<!DOCTYPE html>hello",
+ },
+ };
+ const extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+ const extensionURL = `moz-extension://${extension.uuid}/page.html`;
+
+ await doTest(async browser => {
+ const onLoad = BrowserTestUtils.browserLoaded(browser);
+ BrowserTestUtils.loadURIString(browser, extensionURL);
+ await onLoad;
+ await openPopup("x");
+
+ await trigger();
+ await assert();
+ });
+
+ await extension.unload();
+}
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/head-search_mode.js b/browser/components/urlbar/tests/engagementTelemetry/browser/head-search_mode.js
new file mode 100644
index 0000000000..5c877da05f
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/head-search_mode.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from head.js */
+
+async function doNotSearchModeTest({ trigger, assert }) {
+ await doTest(async browser => {
+ await openPopup("x");
+
+ await trigger();
+ await assert();
+ });
+}
+
+async function doSearchEngineTest({ trigger, assert }) {
+ await doTest(async browser => {
+ await openPopup("x");
+ await UrlbarTestUtils.enterSearchMode(window);
+
+ await trigger();
+ await assert();
+ });
+}
+
+async function doBookmarksTest({ trigger, assert }) {
+ await doTest(async browser => {
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "https://example.com/bookmark",
+ title: "bookmark",
+ });
+ await openPopup("bookmark");
+ await UrlbarTestUtils.enterSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ });
+ await selectRowByURL("https://example.com/bookmark");
+
+ await trigger();
+ await assert();
+ });
+}
+
+async function doHistoryTest({ trigger, assert }) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.autoFill", false]],
+ });
+
+ await doTest(async browser => {
+ await PlacesTestUtils.addVisits("https://example.com/test");
+ await openPopup("example");
+ await UrlbarTestUtils.enterSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ });
+ await selectRowByURL("https://example.com/test");
+
+ await trigger();
+ await assert();
+ });
+
+ await SpecialPowers.popPrefEnv();
+}
+
+async function doTabTest({ trigger, assert }) {
+ const tab = BrowserTestUtils.addTab(gBrowser, "https://example.com/");
+
+ await doTest(async browser => {
+ await openPopup("example");
+ await UrlbarTestUtils.enterSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.TABS,
+ });
+ await selectRowByProvider("Places");
+
+ await trigger();
+ await assert();
+ });
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+async function doActionsTest({ trigger, assert }) {
+ await doTest(async browser => {
+ await openPopup("add");
+ await UrlbarTestUtils.enterSearchMode(window, {
+ source: UrlbarUtils.RESULT_SOURCE.ACTIONS,
+ });
+ await selectRowByProvider("quickactions");
+
+ await trigger();
+ await assert();
+ });
+}
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/head.js b/browser/components/urlbar/tests/engagementTelemetry/browser/head.js
new file mode 100644
index 0000000000..fa623deb5c
--- /dev/null
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/head.js
@@ -0,0 +1,458 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/urlbar/tests/browser/head-common.js",
+ this
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs",
+});
+
+const lazy = {};
+
+XPCOMUtils.defineLazyGetter(lazy, "QuickSuggestTestUtils", () => {
+ const { QuickSuggestTestUtils: module } = ChromeUtils.importESModule(
+ "resource://testing-common/QuickSuggestTestUtils.sys.mjs"
+ );
+ module.init(this);
+ return module;
+});
+
+XPCOMUtils.defineLazyGetter(this, "MerinoTestUtils", () => {
+ const { MerinoTestUtils: module } = ChromeUtils.importESModule(
+ "resource://testing-common/MerinoTestUtils.sys.mjs"
+ );
+ module.init(this);
+ return module;
+});
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+async function addTopSites(url) {
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits(url);
+ }
+ await updateTopSites(sites => {
+ return sites && sites[0] && sites[0].url == url;
+ });
+}
+
+function assertAbandonmentTelemetry(expectedExtraList) {
+ _assertGleanTelemetry("abandonment", expectedExtraList);
+}
+
+function assertEngagementTelemetry(expectedExtraList) {
+ _assertGleanTelemetry("engagement", expectedExtraList);
+}
+
+function assertImpressionTelemetry(expectedExtraList) {
+ _assertGleanTelemetry("impression", expectedExtraList);
+}
+
+function assertExposureTelemetry(expectedExtraList) {
+ _assertGleanTelemetry("exposure", expectedExtraList);
+}
+
+function _assertGleanTelemetry(telemetryName, expectedExtraList) {
+ const telemetries = Glean.urlbar[telemetryName].testGetValue() ?? [];
+ Assert.equal(
+ telemetries.length,
+ expectedExtraList.length,
+ "Telemetry event length matches expected event length."
+ );
+
+ for (let i = 0; i < telemetries.length; i++) {
+ const telemetry = telemetries[i];
+ Assert.equal(telemetry.category, "urlbar");
+ Assert.equal(telemetry.name, telemetryName);
+
+ const expectedExtra = expectedExtraList[i];
+ for (const key of Object.keys(expectedExtra)) {
+ Assert.equal(
+ telemetry.extra[key],
+ expectedExtra[key],
+ `${key} is correct`
+ );
+ }
+ }
+}
+
+async function ensureQuickSuggestInit({
+ merinoSuggestions = undefined,
+ config = undefined,
+} = {}) {
+ return lazy.QuickSuggestTestUtils.ensureQuickSuggestInit({
+ config,
+ merinoSuggestions,
+ remoteSettingsResults: [
+ {
+ type: "data",
+ attachment: [
+ {
+ id: 1,
+ url: "https://example.com/sponsored",
+ title: "Sponsored suggestion",
+ keywords: ["sponsored"],
+ click_url: "https://example.com/click",
+ impression_url: "https://example.com/impression",
+ advertiser: "TestAdvertiser",
+ iab_category: "22 - Shopping",
+ },
+ {
+ id: 2,
+ url: `https://example.com/nonsponsored`,
+ title: "Non-sponsored suggestion",
+ keywords: ["nonsponsored"],
+ click_url: "https://example.com/click",
+ impression_url: "https://example.com/impression",
+ advertiser: "TestAdvertiser",
+ iab_category: "5 - Education",
+ },
+ ],
+ },
+ {
+ type: "weather",
+ weather: MerinoTestUtils.WEATHER_RS_DATA,
+ },
+ ],
+ });
+}
+
+async function doBlur() {
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ gURLBar.blur();
+ });
+}
+
+async function doClick() {
+ const selected = UrlbarTestUtils.getSelectedRow(window);
+ const onLoad = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ EventUtils.synthesizeMouseAtCenter(selected, {});
+ await onLoad;
+}
+
+async function doClickSubButton(selector) {
+ const selected = UrlbarTestUtils.getSelectedElement(window);
+ const button = selected.closest(".urlbarView-row").querySelector(selector);
+ EventUtils.synthesizeMouseAtCenter(button, {});
+}
+
+async function doDropAndGo(data) {
+ const onLoad = BrowserTestUtils.browserLoaded(browser);
+ EventUtils.synthesizeDrop(
+ document.getElementById("back-button"),
+ gURLBar.inputField,
+ [[{ type: "text/plain", data }]],
+ "copy",
+ window
+ );
+ await onLoad;
+}
+
+async function doEnter(modifier = {}) {
+ const onLoad = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ EventUtils.synthesizeKey("KEY_Enter", modifier);
+ await onLoad;
+}
+
+async function doPaste(data) {
+ await SimpleTest.promiseClipboardChange(data, () => {
+ clipboardHelper.copyString(data);
+ });
+
+ gURLBar.focus();
+ gURLBar.select();
+ document.commandDispatcher
+ .getControllerForCommand("cmd_paste")
+ .doCommand("cmd_paste");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+}
+
+async function doPasteAndGo(data) {
+ await SimpleTest.promiseClipboardChange(data, () => {
+ clipboardHelper.copyString(data);
+ });
+ const inputBox = gURLBar.querySelector("moz-input-box");
+ const contextMenu = inputBox.menupopup;
+ const onPopup = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {
+ type: "contextmenu",
+ button: 2,
+ });
+ await onPopup;
+ const onLoad = BrowserTestUtils.browserLoaded(browser);
+ const menuitem = inputBox.getMenuItem("paste-and-go");
+ contextMenu.activateItem(menuitem);
+ await onLoad;
+}
+
+async function doTest(testFn) {
+ await Services.fog.testFlushAllChildren();
+ Services.fog.testResetFOG();
+ // Enable recording telemetry for abandonment, engagement and impression.
+ Services.fog.setMetricsFeatureConfig(
+ JSON.stringify({
+ "urlbar.abandonment": true,
+ "urlbar.engagement": true,
+ "urlbar.impression": true,
+ })
+ );
+
+ gURLBar.controller.engagementEvent.reset();
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesTestUtils.clearHistoryVisits();
+ await PlacesTestUtils.clearInputHistory();
+ await UrlbarTestUtils.formHistory.clear(window);
+ await QuickSuggest.blockedSuggestions.clear();
+ await QuickSuggest.blockedSuggestions._test_readyPromise;
+ await updateTopSites(() => true);
+
+ try {
+ await BrowserTestUtils.withNewTab(gBrowser, testFn);
+ } finally {
+ Services.fog.setMetricsFeatureConfig("{}");
+ }
+}
+
+async function initGroupTest() {
+ /* import-globals-from head-groups.js */
+ Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/urlbar/tests/engagementTelemetry/browser/head-groups.js",
+ this
+ );
+ await setup();
+}
+
+async function initInteractionTest() {
+ /* import-globals-from head-interaction.js */
+ Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/urlbar/tests/engagementTelemetry/browser/head-interaction.js",
+ this
+ );
+ await setup();
+}
+
+async function initNCharsAndNWordsTest() {
+ /* import-globals-from head-n_chars_n_words.js */
+ Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/urlbar/tests/engagementTelemetry/browser/head-n_chars_n_words.js",
+ this
+ );
+ await setup();
+}
+
+async function initSapTest() {
+ /* import-globals-from head-sap.js */
+ Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/urlbar/tests/engagementTelemetry/browser/head-sap.js",
+ this
+ );
+ await setup();
+}
+
+async function initSearchModeTest() {
+ /* import-globals-from head-search_mode.js */
+ Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/urlbar/tests/engagementTelemetry/browser/head-search_mode.js",
+ this
+ );
+ await setup();
+}
+
+async function initExposureTest() {
+ /* import-globals-from head-exposure.js */
+ Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/urlbar/tests/engagementTelemetry/browser/head-exposure.js",
+ this
+ );
+ await setup();
+}
+
+function loadOmniboxAddon({ keyword }) {
+ return ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ omnibox: {
+ keyword,
+ },
+ },
+ background() {
+ /* global browser */
+ browser.omnibox.setDefaultSuggestion({
+ description: "doit",
+ });
+ browser.omnibox.onInputEntered.addListener(() => {
+ browser.tabs.update({ url: "https://example.com/" });
+ });
+ browser.omnibox.onInputChanged.addListener((text, suggest) => {
+ suggest([]);
+ });
+ },
+ });
+}
+
+async function loadRemoteTab(url) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.searches", false],
+ ["browser.urlbar.maxHistoricalSearchSuggestions", 0],
+ ["browser.urlbar.autoFill", false],
+ ["services.sync.username", "fake"],
+ ["services.sync.syncedTabs.showRemoteTabs", true],
+ ],
+ });
+
+ const REMOTE_TAB = {
+ id: "test",
+ type: "client",
+ lastModified: 1492201200,
+ name: "test",
+ clientType: "desktop",
+ tabs: [
+ {
+ type: "tab",
+ title: "tesrt",
+ url,
+ icon: UrlbarUtils.ICON.DEFAULT,
+ client: "test",
+ lastUsed: Math.floor(Date.now() / 1000),
+ },
+ ],
+ };
+
+ const sandbox = lazy.sinon.createSandbox();
+ // eslint-disable-next-line no-undef
+ const syncedTabs = SyncedTabs;
+ const originalSyncedTabsInternal = syncedTabs._internal;
+ syncedTabs._internal = {
+ isConfiguredToSyncTabs: true,
+ hasSyncedThisSession: true,
+ getTabClients() {
+ return Promise.resolve([]);
+ },
+ syncTabs() {
+ return Promise.resolve();
+ },
+ };
+ const weaveXPCService = Cc["@mozilla.org/weave/service;1"].getService(
+ Ci.nsISupports
+ ).wrappedJSObject;
+ const oldWeaveServiceReady = weaveXPCService.ready;
+ weaveXPCService.ready = true;
+ sandbox
+ .stub(syncedTabs._internal, "getTabClients")
+ .callsFake(() => Promise.resolve(Cu.cloneInto([REMOTE_TAB], {})));
+
+ return {
+ async unload() {
+ sandbox.restore();
+ weaveXPCService.ready = oldWeaveServiceReady;
+ syncedTabs._internal = originalSyncedTabsInternal;
+ // Reset internal cache in UrlbarProviderRemoteTabs.
+ Services.obs.notifyObservers(null, "weave:engine:sync:finish", "tabs");
+ await SpecialPowers.popPrefEnv();
+ },
+ };
+}
+
+async function openPopup(input) {
+ await UrlbarTestUtils.promisePopupOpen(window, async () => {
+ await UrlbarTestUtils.inputIntoURLBar(window, input);
+ });
+ await UrlbarTestUtils.promiseSearchComplete(window);
+}
+
+async function selectRowByURL(url) {
+ for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) {
+ const detail = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ if (detail.url === url) {
+ UrlbarTestUtils.setSelectedRowIndex(window, i);
+ return;
+ }
+ }
+}
+
+async function selectRowByProvider(provider) {
+ for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) {
+ const detail = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ if (detail.result.providerName === provider) {
+ UrlbarTestUtils.setSelectedRowIndex(window, i);
+ break;
+ }
+ }
+}
+
+async function selectRowByType(type) {
+ for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) {
+ const detail = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ if (detail.result.payload.type === type) {
+ UrlbarTestUtils.setSelectedRowIndex(window, i);
+ return;
+ }
+ }
+}
+
+async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.searchEngagementTelemetry.enabled", true],
+ ["browser.urlbar.quickactions.enabled", true],
+ ["browser.urlbar.quickactions.minimumSearchString", 0],
+ ["browser.urlbar.suggest.quickactions", true],
+ ["browser.urlbar.shortcuts.quickactions", true],
+ [
+ "browser.urlbar.searchEngagementTelemetry.pauseImpressionIntervalMs",
+ 100,
+ ],
+ ],
+ });
+
+ const engine = await SearchTestUtils.promiseNewSearchEngine({
+ url: "chrome://mochitests/content/browser/browser/components/urlbar/tests/browser/searchSuggestionEngine.xml",
+ });
+ const originalDefaultEngine = await Services.search.getDefault();
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ await Services.search.moveEngine(engine, 0);
+
+ registerCleanupFunction(async function () {
+ await SpecialPowers.popPrefEnv();
+ await Services.search.setDefault(
+ originalDefaultEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ });
+}
+
+async function setupNimbus(variables) {
+ return lazy.UrlbarTestUtils.initNimbusFeature(variables);
+}
+
+async function showResultByArrowDown() {
+ gURLBar.value = "";
+ gURLBar.select();
+ await UrlbarTestUtils.promisePopupOpen(window, () => {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ });
+ await UrlbarTestUtils.promiseSearchComplete(window);
+}
+
+async function waitForPauseImpression() {
+ await new Promise(r =>
+ setTimeout(
+ r,
+ UrlbarPrefs.get("searchEngagementTelemetry.pauseImpressionIntervalMs")
+ )
+ );
+ await Services.fog.testFlushAllChildren();
+}
diff --git a/browser/components/urlbar/tests/ext/api.js b/browser/components/urlbar/tests/ext/api.js
new file mode 100644
index 0000000000..77da790190
--- /dev/null
+++ b/browser/components/urlbar/tests/ext/api.js
@@ -0,0 +1,260 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* global ExtensionAPI, ExtensionCommon */
+
+"use strict";
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ Preferences: "resource://gre/modules/Preferences.sys.mjs",
+ UrlbarProviderExtension:
+ "resource:///modules/UrlbarProviderExtension.sys.mjs",
+ UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
+ UrlbarView: "resource:///modules/UrlbarView.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(
+ this,
+ "defaultPreferences",
+ () => new Preferences({ defaultBranch: true })
+);
+
+let { EventManager } = ExtensionCommon;
+
+this.experiments_urlbar = class extends ExtensionAPI {
+ getAPI(context) {
+ return {
+ experiments: {
+ urlbar: {
+ addDynamicResultType: (name, type) => {
+ this._addDynamicResultType(name, type);
+ },
+
+ addDynamicViewTemplate: (name, viewTemplate) => {
+ this._addDynamicViewTemplate(name, viewTemplate);
+ },
+
+ attributionURL: this._getDefaultSettingsAPI(
+ "browser.partnerlink.attributionURL"
+ ),
+
+ clearInput() {
+ let window = BrowserWindowTracker.getTopWindow();
+ window.gURLBar.value = "";
+ window.gURLBar.setPageProxyState("invalid");
+ },
+
+ engagementTelemetry: this._getDefaultSettingsAPI(
+ "browser.urlbar.eventTelemetry.enabled"
+ ),
+
+ extensionTimeout: this._getDefaultSettingsAPI(
+ "browser.urlbar.extension.timeout"
+ ),
+
+ onViewUpdateRequested: new EventManager({
+ context,
+ name: "experiments.urlbar.onViewUpdateRequested",
+ register: (fire, providerName) => {
+ let provider = UrlbarProviderExtension.getOrCreate(providerName);
+ provider.setEventListener(
+ "getViewUpdate",
+ (result, idsByName) => {
+ return fire.async(result.payload, idsByName).catch(error => {
+ throw context.normalizeError(error);
+ });
+ }
+ );
+ return () => provider.setEventListener("getViewUpdate", null);
+ },
+ }).api(),
+ },
+ },
+ };
+ }
+
+ onShutdown() {
+ // Reset the default prefs. This is necessary because
+ // ExtensionPreferencesManager doesn't properly reset prefs set on the
+ // default branch. See bug 1586543, bug 1578513, bug 1578508.
+ if (this._initialDefaultPrefs) {
+ for (let [pref, value] of this._initialDefaultPrefs.entries()) {
+ defaultPreferences.set(pref, value);
+ }
+ }
+
+ this._removeDynamicViewTemplates();
+ this._removeDynamicResultTypes();
+ }
+
+ _getDefaultSettingsAPI(pref) {
+ return {
+ get: details => {
+ return {
+ value: Preferences.get(pref),
+
+ // Nothing actually uses this, but on debug builds there are extra
+ // checks enabled in Schema.sys.mjs that fail if it's not present. The
+ // value doesn't matter.
+ levelOfControl: "controllable_by_this_extension",
+ };
+ },
+ set: details => {
+ if (!this._initialDefaultPrefs) {
+ this._initialDefaultPrefs = new Map();
+ }
+ if (!this._initialDefaultPrefs.has(pref)) {
+ this._initialDefaultPrefs.set(pref, defaultPreferences.get(pref));
+ }
+ defaultPreferences.set(pref, details.value);
+ return true;
+ },
+ clear: details => {
+ if (this._initialDefaultPrefs && this._initialDefaultPrefs.has(pref)) {
+ defaultPreferences.set(pref, this._initialDefaultPrefs.get(pref));
+ return true;
+ }
+ return false;
+ },
+ };
+ }
+
+ // We use the following four properties as bookkeeping to keep track of
+ // dynamic result types and view templates registered by extensions so that
+ // they can be properly removed on extension shutdown.
+
+ // Names of dynamic result types added by this extension.
+ _dynamicResultTypeNames = new Set();
+
+ // Names of dynamic result type view templates added by this extension.
+ _dynamicViewTemplateNames = new Set();
+
+ // Maps dynamic result type names to Sets of IDs of extensions that have
+ // registered those types.
+ static extIDsByDynamicResultTypeName = new Map();
+
+ // Maps dynamic result type view template names to Sets of IDs of extensions
+ // that have registered those view templates.
+ static extIDsByDynamicViewTemplateName = new Map();
+
+ /**
+ * Adds a dynamic result type and includes it in our bookkeeping. See
+ * UrlbarResult.addDynamicResultType().
+ *
+ * @param {string} name
+ * The name of the dynamic result type.
+ * @param {object} type
+ * The type.
+ */
+ _addDynamicResultType(name, type) {
+ this._dynamicResultTypeNames.add(name);
+ this._addExtIDToDynamicResultTypeMap(
+ experiments_urlbar.extIDsByDynamicResultTypeName,
+ name
+ );
+ UrlbarResult.addDynamicResultType(name, type);
+ }
+
+ /**
+ * Removes all dynamic result types added by the extension.
+ */
+ _removeDynamicResultTypes() {
+ for (let name of this._dynamicResultTypeNames) {
+ let allRemoved = this._removeExtIDFromDynamicResultTypeMap(
+ experiments_urlbar.extIDsByDynamicResultTypeName,
+ name
+ );
+ if (allRemoved) {
+ UrlbarResult.removeDynamicResultType(name);
+ }
+ }
+ }
+
+ /**
+ * Adds a dynamic result type view template and includes it in our
+ * bookkeeping. See UrlbarView.addDynamicViewTemplate().
+ *
+ * @param {string} name
+ * The view template will be registered for the dynamic result type with
+ * this name.
+ * @param {object} viewTemplate
+ * The view template.
+ */
+ _addDynamicViewTemplate(name, viewTemplate) {
+ this._dynamicViewTemplateNames.add(name);
+ this._addExtIDToDynamicResultTypeMap(
+ experiments_urlbar.extIDsByDynamicViewTemplateName,
+ name
+ );
+ if (viewTemplate.stylesheet) {
+ viewTemplate.stylesheet = this.extension.baseURI.resolve(
+ viewTemplate.stylesheet
+ );
+ }
+ UrlbarView.addDynamicViewTemplate(name, viewTemplate);
+ }
+
+ /**
+ * Removes all dynamic result type view templates added by the extension.
+ */
+ _removeDynamicViewTemplates() {
+ for (let name of this._dynamicViewTemplateNames) {
+ let allRemoved = this._removeExtIDFromDynamicResultTypeMap(
+ experiments_urlbar.extIDsByDynamicViewTemplateName,
+ name
+ );
+ if (allRemoved) {
+ UrlbarView.removeDynamicViewTemplate(name);
+ }
+ }
+ }
+
+ /**
+ * Adds a dynamic result type name and this extension's ID to a bookkeeping
+ * map.
+ *
+ * @param {Map} map
+ * Either extIDsByDynamicResultTypeName or extIDsByDynamicViewTemplateName.
+ * @param {string} dynamicTypeName
+ * The dynamic result type name.
+ */
+ _addExtIDToDynamicResultTypeMap(map, dynamicTypeName) {
+ let extIDs = map.get(dynamicTypeName);
+ if (!extIDs) {
+ extIDs = new Set();
+ map.set(dynamicTypeName, extIDs);
+ }
+ extIDs.add(this.extension.id);
+ }
+
+ /**
+ * Removes a dynamic result type name and this extension's ID from a
+ * bookkeeping map.
+ *
+ * @param {Map} map
+ * Either extIDsByDynamicResultTypeName or extIDsByDynamicViewTemplateName.
+ * @param {string} dynamicTypeName
+ * The dynamic result type name.
+ * @returns {boolean}
+ * True if no other extension IDs are in the map under the same
+ * dynamicTypeName, and false otherwise.
+ */
+ _removeExtIDFromDynamicResultTypeMap(map, dynamicTypeName) {
+ let extIDs = map.get(dynamicTypeName);
+ extIDs.delete(this.extension.id);
+ if (!extIDs.size) {
+ map.delete(dynamicTypeName);
+ return true;
+ }
+ return false;
+ }
+};
diff --git a/browser/components/urlbar/tests/ext/browser/.eslintrc.js b/browser/components/urlbar/tests/ext/browser/.eslintrc.js
new file mode 100644
index 0000000000..e57058ecb1
--- /dev/null
+++ b/browser/components/urlbar/tests/ext/browser/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ env: {
+ webextensions: true,
+ },
+};
diff --git a/browser/components/urlbar/tests/ext/browser/browser.ini b/browser/components/urlbar/tests/ext/browser/browser.ini
new file mode 100644
index 0000000000..416fc52eb3
--- /dev/null
+++ b/browser/components/urlbar/tests/ext/browser/browser.ini
@@ -0,0 +1,18 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+[DEFAULT]
+support-files =
+ ../../browser/head-common.js
+ ../api.js
+ ../schema.json
+ head.js
+
+[browser_ext_urlbar_attributionURL.js]
+[browser_ext_urlbar_clearInput.js]
+[browser_ext_urlbar_dynamicResult.js]
+support-files =
+ dynamicResult.css
+[browser_ext_urlbar_engagementTelemetry.js]
+[browser_ext_urlbar_extensionTimeout.js]
diff --git a/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_attributionURL.js b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_attributionURL.js
new file mode 100644
index 0000000000..a5bccc8eba
--- /dev/null
+++ b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_attributionURL.js
@@ -0,0 +1,16 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* global browser */
+
+// This tests the browser.experiments.urlbar.engagementTelemetry WebExtension
+// Experiment API.
+
+"use strict";
+
+add_settings_tasks("browser.partnerlink.attributionURL", "string", () => {
+ browser.test.onMessage.addListener(async (method, arg) => {
+ let result = await browser.experiments.urlbar.attributionURL[method](arg);
+ browser.test.sendMessage("done", result);
+ });
+});
diff --git a/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_clearInput.js b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_clearInput.js
new file mode 100644
index 0000000000..afeff3b8a1
--- /dev/null
+++ b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_clearInput.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* global browser */
+
+// This tests the browser.experiments.urlbar.clearInput WebExtension Experiment
+// API.
+
+"use strict";
+
+add_task(async function test() {
+ // Load a page so that pageproxystate is valid. When the extension calls
+ // clearInput, the pageproxystate should become invalid.
+ await BrowserTestUtils.withNewTab("http://example.com/", async () => {
+ Assert.notEqual(gURLBar.value, "", "Input is not empty");
+ Assert.equal(gURLBar.getAttribute("pageproxystate"), "valid");
+
+ let ext = await loadExtension({
+ background: async () => {
+ await browser.experiments.urlbar.clearInput();
+ browser.test.sendMessage("done");
+ },
+ });
+ await ext.awaitMessage("done");
+
+ Assert.equal(gURLBar.value, "", "Input is empty");
+ Assert.equal(gURLBar.getAttribute("pageproxystate"), "invalid");
+
+ await ext.unload();
+ });
+});
diff --git a/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_dynamicResult.js b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_dynamicResult.js
new file mode 100644
index 0000000000..a710d8949d
--- /dev/null
+++ b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_dynamicResult.js
@@ -0,0 +1,137 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* global browser */
+
+// This tests dynamic results using the WebExtension Experiment API.
+
+"use strict";
+
+add_task(async function test() {
+ let ext = await loadExtension({
+ extraFiles: {
+ "dynamicResult.css": await (
+ await fetch("file://" + getTestFilePath("dynamicResult.css"))
+ ).text(),
+ },
+ background: async () => {
+ browser.experiments.urlbar.addDynamicResultType("testDynamicType");
+ browser.experiments.urlbar.addDynamicViewTemplate("testDynamicType", {
+ stylesheet: "dynamicResult.css",
+ children: [
+ {
+ name: "text",
+ tag: "span",
+ },
+ {
+ name: "button",
+ tag: "span",
+ attributes: {
+ role: "button",
+ },
+ },
+ ],
+ });
+ browser.urlbar.onBehaviorRequested.addListener(query => {
+ return "restricting";
+ }, "test");
+ browser.urlbar.onResultsRequested.addListener(query => {
+ return [
+ {
+ type: "dynamic",
+ source: "local",
+ heuristic: true,
+ payload: {
+ dynamicType: "testDynamicType",
+ },
+ },
+ ];
+ }, "test");
+ browser.experiments.urlbar.onViewUpdateRequested.addListener(payload => {
+ return {
+ text: {
+ textContent: "This is a dynamic result.",
+ },
+ button: {
+ textContent: "Click Me",
+ },
+ };
+ }, "test");
+ browser.urlbar.onResultPicked.addListener((payload, elementName) => {
+ browser.test.sendMessage("onResultPicked", [payload, elementName]);
+ }, "test");
+ },
+ });
+
+ // Wait for the provider and dynamic type to be registered before continuing.
+ await TestUtils.waitForCondition(
+ () =>
+ UrlbarProvidersManager.getProvider("test") &&
+ UrlbarResult.getDynamicResultType("testDynamicType"),
+ "Waiting for provider and dynamic type to be registered"
+ );
+ Assert.ok(
+ UrlbarProvidersManager.getProvider("test"),
+ "Provider should be registered"
+ );
+ Assert.ok(
+ UrlbarResult.getDynamicResultType("testDynamicType"),
+ "Dynamic type should be registered"
+ );
+
+ // Do a search.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ waitForFocus: SimpleTest.waitForFocus,
+ });
+
+ // Get the row.
+ let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
+ Assert.equal(
+ row.result.type,
+ UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ "row.result.type"
+ );
+ Assert.equal(
+ row.getAttribute("dynamicType"),
+ "testDynamicType",
+ "row[dynamicType]"
+ );
+
+ let text = row.querySelector(".urlbarView-dynamic-testDynamicType-text");
+
+ // The view's call to provider.getViewUpdate is async, so we need to make sure
+ // the update has been applied before continuing to avoid intermittent
+ // failures.
+ await TestUtils.waitForCondition(
+ () => text.textContent == "This is a dynamic result."
+ );
+
+ // Check the elements.
+ Assert.equal(
+ text.textContent,
+ "This is a dynamic result.",
+ "text.textContent"
+ );
+ let button = row.querySelector(".urlbarView-dynamic-testDynamicType-button");
+ Assert.equal(button.textContent, "Click Me", "button.textContent");
+
+ // The result's button should be selected since the result is the heuristic.
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElement(window),
+ button,
+ "Button should be selected"
+ );
+
+ // Pick the button.
+ let pickPromise = ext.awaitMessage("onResultPicked");
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Enter")
+ );
+ let [payload, elementName] = await pickPromise;
+ Assert.equal(payload.dynamicType, "testDynamicType", "Picked payload");
+ Assert.equal(elementName, "button", "Picked element name");
+
+ await ext.unload();
+});
diff --git a/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_engagementTelemetry.js b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_engagementTelemetry.js
new file mode 100644
index 0000000000..50ded14d4e
--- /dev/null
+++ b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_engagementTelemetry.js
@@ -0,0 +1,18 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* global browser */
+
+// This tests the browser.experiments.urlbar.engagementTelemetry WebExtension
+// Experiment API.
+
+"use strict";
+
+add_settings_tasks("browser.urlbar.eventTelemetry.enabled", "boolean", () => {
+ browser.test.onMessage.addListener(async (method, arg) => {
+ let result = await browser.experiments.urlbar.engagementTelemetry[method](
+ arg
+ );
+ browser.test.sendMessage("done", result);
+ });
+});
diff --git a/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_extensionTimeout.js b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_extensionTimeout.js
new file mode 100644
index 0000000000..de09ef263c
--- /dev/null
+++ b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_extensionTimeout.js
@@ -0,0 +1,16 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* global browser */
+
+// This tests the browser.experiments.urlbar.engagementTelemetry WebExtension
+// Experiment API.
+
+"use strict";
+
+add_settings_tasks("browser.urlbar.extension.timeout", "number", () => {
+ browser.test.onMessage.addListener(async (method, arg) => {
+ let result = await browser.experiments.urlbar.extensionTimeout[method](arg);
+ browser.test.sendMessage("done", result);
+ });
+});
diff --git a/browser/components/urlbar/tests/ext/browser/dynamicResult.css b/browser/components/urlbar/tests/ext/browser/dynamicResult.css
new file mode 100644
index 0000000000..efd0c8c950
--- /dev/null
+++ b/browser/components/urlbar/tests/ext/browser/dynamicResult.css
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+.urlbarView-row[dynamicType=testDynamicType] > .urlbarView-row-inner {
+ display: flex;
+ align-items: center;
+ min-height: 32px;
+ width: 100%;
+}
+
+.urlbarView-dynamic-testDynamicType-text {
+ flex-grow: 1;
+ flex-shrink: 1;
+ padding: 10px;
+}
+
+.urlbarView-dynamic-testDynamicType-button {
+ min-height: 16px;
+ padding: 8px;
+ border: none;
+ border-radius: 2px;
+ font-size: 0.93em;
+ color: inherit;
+ background-color: var(--urlbarView-button-background);
+ min-width: 8.75em;
+ text-align: center;
+ flex-basis: initial;
+ flex-shrink: 0;
+ margin-inline-end: 10px;
+}
+
+.urlbarView-dynamic-testDynamicType-button[selected] {
+ color: white;
+ background-color: var(--urlbarView-primary-button-background);
+ box-shadow: 0 0 0 1px #0a84ff inset, 0 0 0 1px #0a84ff, 0 0 0 4px rgba(10, 132, 255, 0.3);
+}
diff --git a/browser/components/urlbar/tests/ext/browser/head.js b/browser/components/urlbar/tests/ext/browser/head.js
new file mode 100644
index 0000000000..8d11a88066
--- /dev/null
+++ b/browser/components/urlbar/tests/ext/browser/head.js
@@ -0,0 +1,253 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * The files in this directory test the browser.urlbarExperiments WebExtension
+ * Experiment APIs, which are the WebExtension APIs we ship in our urlbar
+ * experiment extensions. Unlike the WebExtension APIs we ship in mozilla-
+ * central, which have continuous test coverage [1], our WebExtension Experiment
+ * APIs would not have continuous test coverage were it not for the fact that we
+ * copy and test them here. This is especially useful for APIs that are used in
+ * experiments that target multiple versions of Firefox, and for APIs that are
+ * reused in multiple experiments. See [2] and [3] for more info on
+ * experiments.
+ *
+ * [1] See browser/components/extensions/test
+ * [2] browser/components/urlbar/docs/experiments.rst
+ * [3] https://firefox-source-docs.mozilla.org/toolkit/components/extensions/webextensions/basics.html#webextensions-experiments
+ */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/urlbar/tests/browser/head-common.js",
+ this
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ Preferences: "resource://gre/modules/Preferences.sys.mjs",
+});
+
+const SCHEMA_BASENAME = "schema.json";
+const SCRIPT_BASENAME = "api.js";
+
+const SCHEMA_PATH = getTestFilePath(SCHEMA_BASENAME);
+const SCRIPT_PATH = getTestFilePath(SCRIPT_BASENAME);
+
+let schemaSource;
+let scriptSource;
+
+add_setup(async function loadSource() {
+ schemaSource = await (await fetch("file://" + SCHEMA_PATH)).text();
+ scriptSource = await (await fetch("file://" + SCRIPT_PATH)).text();
+});
+
+/**
+ * Loads a mock extension with our browser.experiments.urlbar API and a
+ * background script. Be sure to call `await ext.unload()` when you're done
+ * with it.
+ *
+ * @param {object} options
+ * Options object
+ * @param {Function} options.background
+ * This function is serialized and becomes the background script.
+ * @param {object} [options.extraFiles]
+ * Extra files to load in the extension.
+ * @returns {object}
+ * The extension.
+ */
+async function loadExtension({ background, extraFiles = {} }) {
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["urlbar"],
+ experiment_apis: {
+ experiments_urlbar: {
+ schema: SCHEMA_BASENAME,
+ parent: {
+ scopes: ["addon_parent"],
+ paths: [["experiments", "urlbar"]],
+ script: SCRIPT_BASENAME,
+ },
+ },
+ },
+ },
+ files: {
+ [SCHEMA_BASENAME]: schemaSource,
+ [SCRIPT_BASENAME]: scriptSource,
+ ...extraFiles,
+ },
+ isPrivileged: true,
+ background,
+ });
+ await ext.startup();
+ return ext;
+}
+
+/**
+ * Tests toggling a preference value via an experiments.urlbar API.
+ *
+ * @param {string} prefName
+ * The name of the pref to be tested.
+ * @param {string} type
+ * The type of the pref being set. One of "string", "boolean", or "number".
+ * @param {Function} background
+ * Boilerplate function that returns the value from calling the
+ * browser.experiments.urlbar.prefName[method] APIs.
+ */
+function add_settings_tasks(prefName, type, background) {
+ let defaultPreferences = new Preferences({ defaultBranch: true });
+
+ let originalValue = defaultPreferences.get(prefName);
+ registerCleanupFunction(() => {
+ defaultPreferences.set(prefName, originalValue);
+ });
+
+ let firstValue, secondValue;
+ switch (type) {
+ case "string":
+ firstValue = "test value 1";
+ secondValue = "test value 2";
+ break;
+ case "number":
+ firstValue = 10;
+ secondValue = 100;
+ break;
+ case "boolean":
+ firstValue = false;
+ secondValue = true;
+ break;
+ default:
+ Assert.ok(
+ false,
+ `"type" parameter must be one of "string", "number", or "boolean"`
+ );
+ }
+
+ add_task(async function get() {
+ let ext = await loadExtension({ background });
+
+ defaultPreferences.set(prefName, firstValue);
+ ext.sendMessage("get", {});
+ let result = await ext.awaitMessage("done");
+ Assert.strictEqual(result.value, firstValue);
+
+ defaultPreferences.set(prefName, secondValue);
+ ext.sendMessage("get", {});
+ result = await ext.awaitMessage("done");
+ Assert.strictEqual(result.value, secondValue);
+
+ await ext.unload();
+ });
+
+ add_task(async function set() {
+ let ext = await loadExtension({ background });
+
+ defaultPreferences.set(prefName, firstValue);
+ ext.sendMessage("set", { value: secondValue });
+ let result = await ext.awaitMessage("done");
+ Assert.strictEqual(result, true);
+ Assert.strictEqual(defaultPreferences.get(prefName), secondValue);
+
+ ext.sendMessage("set", { value: firstValue });
+ result = await ext.awaitMessage("done");
+ Assert.strictEqual(result, true);
+ Assert.strictEqual(defaultPreferences.get(prefName), firstValue);
+
+ await ext.unload();
+ });
+
+ add_task(async function clear() {
+ // no set()
+ defaultPreferences.set(prefName, firstValue);
+ let ext = await loadExtension({ background });
+ ext.sendMessage("clear", {});
+ let result = await ext.awaitMessage("done");
+ Assert.strictEqual(result, false);
+ Assert.strictEqual(defaultPreferences.get(prefName), firstValue);
+ await ext.unload();
+
+ // firstValue -> secondValue
+ defaultPreferences.set(prefName, firstValue);
+ ext = await loadExtension({ background });
+ ext.sendMessage("set", { value: secondValue });
+ await ext.awaitMessage("done");
+ ext.sendMessage("clear", {});
+ result = await ext.awaitMessage("done");
+ Assert.strictEqual(result, true);
+ Assert.strictEqual(defaultPreferences.get(prefName), firstValue);
+ await ext.unload();
+
+ // secondValue -> firstValue
+ defaultPreferences.set(prefName, secondValue);
+ ext = await loadExtension({ background });
+ ext.sendMessage("set", { value: firstValue });
+ await ext.awaitMessage("done");
+ ext.sendMessage("clear", {});
+ result = await ext.awaitMessage("done");
+ Assert.strictEqual(result, true);
+ Assert.strictEqual(defaultPreferences.get(prefName), secondValue);
+ await ext.unload();
+
+ // firstValue -> firstValue
+ defaultPreferences.set(prefName, firstValue);
+ ext = await loadExtension({ background });
+ ext.sendMessage("set", { value: firstValue });
+ await ext.awaitMessage("done");
+ ext.sendMessage("clear", {});
+ result = await ext.awaitMessage("done");
+ Assert.strictEqual(result, true);
+ Assert.strictEqual(defaultPreferences.get(prefName), firstValue);
+ await ext.unload();
+
+ // secondValue -> secondValue
+ defaultPreferences.set(prefName, secondValue);
+ ext = await loadExtension({ background });
+ ext.sendMessage("set", { value: secondValue });
+ await ext.awaitMessage("done");
+ ext.sendMessage("clear", {});
+ result = await ext.awaitMessage("done");
+ Assert.strictEqual(result, true);
+ Assert.strictEqual(defaultPreferences.get(prefName), secondValue);
+ await ext.unload();
+ });
+
+ add_task(async function shutdown() {
+ // no set()
+ defaultPreferences.set(prefName, firstValue);
+ let ext = await loadExtension({ background });
+ await ext.unload();
+ Assert.strictEqual(defaultPreferences.get(prefName), firstValue);
+
+ // firstValue -> secondValue
+ defaultPreferences.set(prefName, firstValue);
+ ext = await loadExtension({ background });
+ ext.sendMessage("set", { value: secondValue });
+ await ext.awaitMessage("done");
+ await ext.unload();
+ Assert.strictEqual(defaultPreferences.get(prefName), firstValue);
+
+ // secondValue -> firstValue
+ defaultPreferences.set(prefName, secondValue);
+ ext = await loadExtension({ background });
+ ext.sendMessage("set", { value: firstValue });
+ await ext.awaitMessage("done");
+ await ext.unload();
+ Assert.strictEqual(defaultPreferences.get(prefName), secondValue);
+
+ // firstValue -> firstValue
+ defaultPreferences.set(prefName, firstValue);
+ ext = await loadExtension({ background });
+ ext.sendMessage("set", { value: firstValue });
+ await ext.awaitMessage("done");
+ await ext.unload();
+ Assert.strictEqual(defaultPreferences.get(prefName), firstValue);
+
+ // secondValue -> secondValue
+ defaultPreferences.set(prefName, secondValue);
+ ext = await loadExtension({ background });
+ ext.sendMessage("set", { value: secondValue });
+ await ext.awaitMessage("done");
+ await ext.unload();
+ Assert.strictEqual(defaultPreferences.get(prefName), secondValue);
+ });
+}
diff --git a/browser/components/urlbar/tests/ext/schema.json b/browser/components/urlbar/tests/ext/schema.json
new file mode 100644
index 0000000000..ced5deddaa
--- /dev/null
+++ b/browser/components/urlbar/tests/ext/schema.json
@@ -0,0 +1,113 @@
+[
+ {
+ "namespace": "experiments.urlbar",
+ "description": "APIs supporting urlbar experiments",
+ "types": [
+ {
+ "id": "DynamicResultType",
+ "type": "object",
+ "description": "Describes a dynamic result type.",
+ "properties": {
+ "viewTemplate": {
+ "type": "object",
+ "description": "An object describing the type's view.",
+ "additionalProperties": true
+ }
+ }
+ }
+ ],
+ "properties": {
+ "attributionURL": {
+ "$ref": "types.Setting",
+ "description": "Gets or sets the attribution URL for the current browser session."
+ },
+ "engagementTelemetry": {
+ "$ref": "types.Setting",
+ "description": "Enables or disables the engagement telemetry for the current browser session."
+ },
+ "extensionTimeout": {
+ "$ref": "types.Setting",
+ "description": "Sets the amount of time in ms that extensions have to return results to the browser.urlbar API."
+ }
+ },
+ "events": [
+ {
+ "name": "onViewUpdateRequested",
+ "type": "function",
+ "description": "Fired when the urlbar view updates the view of one of the results of the provider.",
+ "parameters": [
+ {
+ "name": "payload",
+ "type": "object",
+ "description": "The result's payload."
+ },
+ {
+ "name": "idsByName",
+ "type": "object",
+ "description": "A Map from an element's name, as defined by the provider; to its ID in the DOM, as defined by the browser."
+ }
+ ],
+ "extraParameters": [
+ {
+ "name": "providerName",
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9_-]+$",
+ "description": "The name of the provider you want to provide updates for."
+ }
+ ],
+ "returns": {
+ "type": "object",
+ "description": "An object describing the view update."
+ }
+ }
+ ],
+ "functions": [
+ {
+ "name": "addDynamicResultType",
+ "type": "function",
+ "async": true,
+ "description": "Adds a dynamic result type. See UrlbarResult.addDynamicResultType().",
+ "parameters": [
+ {
+ "name": "name",
+ "type": "string",
+ "description": "The name of the result type."
+ },
+ {
+ "name": "type",
+ "type": "object",
+ "default": {},
+ "optional": true,
+ "description": "The result type. Currently this should be an empty object (which is the default value)."
+ }
+ ]
+ },
+ {
+ "name": "addDynamicViewTemplate",
+ "type": "function",
+ "async": true,
+ "description": "Adds a view template for a dynamic result type. See UrlbarView.addDynamicViewTemplate().",
+ "parameters": [
+ {
+ "name": "name",
+ "type": "string",
+ "description": "The view template will be registered for the dynamic result type with this name."
+ },
+ {
+ "name": "viewTemplate",
+ "type": "object",
+ "additionalProperties": true,
+ "description": "The view template."
+ }
+ ]
+ },
+ {
+ "name": "clearInput",
+ "type": "function",
+ "async": true,
+ "description": "Sets urlbar.value to the empty string and the pageproxystate to invalid.",
+ "parameters": []
+ }
+ ]
+ }
+]
diff --git a/browser/components/urlbar/tests/quicksuggest/MerinoTestUtils.sys.mjs b/browser/components/urlbar/tests/quicksuggest/MerinoTestUtils.sys.mjs
new file mode 100644
index 0000000000..7763881b4f
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/MerinoTestUtils.sys.mjs
@@ -0,0 +1,765 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs",
+ QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs",
+ TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
+});
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+// The following properties and methods are copied from the test scope to the
+// test utils object so they can be easily accessed. Be careful about assuming a
+// particular property will be defined because depending on the scope -- browser
+// test or xpcshell test -- some may not be.
+const TEST_SCOPE_PROPERTIES = [
+ "Assert",
+ "EventUtils",
+ "info",
+ "registerCleanupFunction",
+];
+
+const SEARCH_PARAMS = {
+ CLIENT_VARIANTS: "client_variants",
+ PROVIDERS: "providers",
+ QUERY: "q",
+ SEQUENCE_NUMBER: "seq",
+ SESSION_ID: "sid",
+};
+
+const REQUIRED_SEARCH_PARAMS = [
+ SEARCH_PARAMS.QUERY,
+ SEARCH_PARAMS.SEQUENCE_NUMBER,
+ SEARCH_PARAMS.SESSION_ID,
+];
+
+// We set the client timeout to a large value to avoid intermittent failures in
+// CI, especially TV tests, where the Merino fetch unexpectedly doesn't finish
+// before the default timeout.
+const CLIENT_TIMEOUT_MS = 2000;
+
+const HISTOGRAM_LATENCY = "FX_URLBAR_MERINO_LATENCY_MS";
+const HISTOGRAM_RESPONSE = "FX_URLBAR_MERINO_RESPONSE";
+
+// Maps from string labels of the `FX_URLBAR_MERINO_RESPONSE` histogram to their
+// numeric values.
+const RESPONSE_HISTOGRAM_VALUES = {
+ success: 0,
+ timeout: 1,
+ network_error: 2,
+ http_error: 3,
+ no_suggestion: 4,
+};
+
+const WEATHER_KEYWORD = "weather";
+
+const WEATHER_RS_DATA = {
+ keywords: [WEATHER_KEYWORD],
+};
+
+const WEATHER_SUGGESTION = {
+ title: "Weather for San Francisco",
+ url: "https://example.com/weather",
+ provider: "accuweather",
+ is_sponsored: false,
+ score: 0.2,
+ icon: null,
+ city_name: "San Francisco",
+ current_conditions: {
+ url: "https://example.com/weather-current-conditions",
+ summary: "Mostly cloudy",
+ icon_id: 6,
+ temperature: { c: 15.5, f: 60.0 },
+ },
+ forecast: {
+ url: "https://example.com/weather-forecast",
+ summary: "Pleasant Saturday",
+ high: { c: 21.1, f: 70.0 },
+ low: { c: 13.9, f: 57.0 },
+ },
+};
+
+// We set the weather suggestion fetch interval to an absurdly large value so it
+// absolutely will not fire during tests.
+const WEATHER_FETCH_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
+
+/**
+ * Test utils for Merino.
+ */
+class _MerinoTestUtils {
+ /**
+ * Initializes the utils.
+ *
+ * @param {object} scope
+ * The global JS scope where tests are being run. This allows the instance
+ * to access test helpers like `Assert` that are available in the scope.
+ */
+ init(scope) {
+ if (!scope) {
+ throw new Error("MerinoTestUtils.init() must be called with a scope");
+ }
+
+ this.#initDepth++;
+ scope.info?.("MerinoTestUtils init: Depth is now " + this.#initDepth);
+
+ for (let p of TEST_SCOPE_PROPERTIES) {
+ this[p] = scope[p];
+ }
+ // If you add other properties to `this`, null them in `uninit()`.
+
+ if (!this.#server) {
+ this.#server = new MockMerinoServer(scope);
+ }
+ lazy.UrlbarPrefs.set("merino.timeoutMs", CLIENT_TIMEOUT_MS);
+ scope.registerCleanupFunction?.(() => {
+ scope.info?.("MerinoTestUtils cleanup function");
+ this.uninit();
+ });
+ }
+
+ /**
+ * Uninitializes the utils. If they were created with a test scope that
+ * defines `registerCleanupFunction()`, you don't need to call this yourself
+ * because it will automatically be called as a cleanup function. Otherwise
+ * you'll need to call this.
+ */
+ uninit() {
+ this.#initDepth--;
+ this.info?.("MerinoTestUtils uninit: Depth is now " + this.#initDepth);
+
+ if (this.#initDepth) {
+ this.info?.("MerinoTestUtils uninit: Bailing because depth > 0");
+ return;
+ }
+ this.info?.("MerinoTestUtils uninit: Now uninitializing");
+
+ for (let p of TEST_SCOPE_PROPERTIES) {
+ this[p] = null;
+ }
+ this.#server.uninit();
+ this.#server = null;
+ lazy.UrlbarPrefs.clear("merino.timeoutMs");
+ }
+
+ /**
+ * @returns {object}
+ * The names of URL search params.
+ */
+ get SEARCH_PARAMS() {
+ return SEARCH_PARAMS;
+ }
+
+ /**
+ * @returns {string}
+ * The weather keyword in `WEATHER_RS_DATA`. Can be used as a search string
+ * to match the weather suggestion.
+ */
+ get WEATHER_KEYWORD() {
+ return WEATHER_KEYWORD;
+ }
+
+ /**
+ * @returns {object}
+ * Default remote settings data that sets up `WEATHER_KEYWORD` as the
+ * keyword for the weather suggestion.
+ */
+ get WEATHER_RS_DATA() {
+ return { ...WEATHER_RS_DATA };
+ }
+
+ /**
+ * @returns {object}
+ * A mock weather suggestion.
+ */
+ get WEATHER_SUGGESTION() {
+ return WEATHER_SUGGESTION;
+ }
+
+ /**
+ * @returns {MockMerinoServer}
+ * The mock Merino server. The server isn't started until its `start()`
+ * method is called.
+ */
+ get server() {
+ return this.#server;
+ }
+
+ /**
+ * Clears the Merino-related histograms and returns them.
+ *
+ * @param {object} options
+ * Options
+ * @param {string} options.extraLatency
+ * The name of another latency histogram you expect to be updated.
+ * @param {string} options.extraResponse
+ * The name of another response histogram you expect to be updated.
+ * @returns {object}
+ * An object of histograms: `{ latency, response }`
+ * `latency` and `response` are both arrays of Histogram objects.
+ */
+ getAndClearHistograms({
+ extraLatency = undefined,
+ extraResponse = undefined,
+ } = {}) {
+ let histograms = {
+ latency: [
+ lazy.TelemetryTestUtils.getAndClearHistogram(HISTOGRAM_LATENCY),
+ ],
+ response: [
+ lazy.TelemetryTestUtils.getAndClearHistogram(HISTOGRAM_RESPONSE),
+ ],
+ };
+ if (extraLatency) {
+ histograms.latency.push(
+ lazy.TelemetryTestUtils.getAndClearHistogram(extraLatency)
+ );
+ }
+ if (extraResponse) {
+ histograms.response.push(
+ lazy.TelemetryTestUtils.getAndClearHistogram(extraResponse)
+ );
+ }
+ return histograms;
+ }
+
+ /**
+ * Asserts the Merino-related histograms are updated as expected. Clears the
+ * histograms before returning.
+ *
+ * @param {object} options
+ * Options object
+ * @param {MerinoClient} options.client
+ * The relevant `MerinoClient` instance. This is used to check the latency
+ * stopwatch.
+ * @param {object} options.histograms
+ * The histograms object returned from `getAndClearHistograms()`.
+ * @param {string} options.response
+ * The expected string label for the `response` histogram. If the histogram
+ * should not be recorded, pass null.
+ * @param {boolean} options.latencyRecorded
+ * Whether the latency histogram is expected to contain a value.
+ * @param {boolean} options.latencyStopwatchRunning
+ * Whether the latency stopwatch is expected to be running.
+ */
+ checkAndClearHistograms({
+ client,
+ histograms,
+ response,
+ latencyRecorded,
+ latencyStopwatchRunning = false,
+ }) {
+ // Check the response histograms.
+ if (response) {
+ this.Assert.ok(
+ RESPONSE_HISTOGRAM_VALUES.hasOwnProperty(response),
+ "Sanity check: Expected response is valid: " + response
+ );
+ for (let histogram of histograms.response) {
+ lazy.TelemetryTestUtils.assertHistogram(
+ histogram,
+ RESPONSE_HISTOGRAM_VALUES[response],
+ 1
+ );
+ }
+ } else {
+ for (let histogram of histograms.response) {
+ this.Assert.strictEqual(
+ histogram.snapshot().sum,
+ 0,
+ "Response histogram not updated: " + histogram.name()
+ );
+ }
+ }
+
+ // Check the latency histograms.
+ if (latencyRecorded) {
+ // There should be a single value across all buckets.
+ for (let histogram of histograms.latency) {
+ this.Assert.deepEqual(
+ Object.values(histogram.snapshot().values).filter(v => v > 0),
+ [1],
+ "Latency histogram updated: " + histogram.name()
+ );
+ }
+ } else {
+ for (let histogram of histograms.latency) {
+ this.Assert.strictEqual(
+ histogram.snapshot().sum,
+ 0,
+ "Latency histogram not updated: " + histogram.name()
+ );
+ }
+ }
+
+ // Check the latency stopwatch.
+ this.Assert.equal(
+ TelemetryStopwatch.running(
+ HISTOGRAM_LATENCY,
+ client._test_latencyStopwatchInstance
+ ),
+ latencyStopwatchRunning,
+ "Latency stopwatch running as expected"
+ );
+
+ // Clear histograms.
+ for (let histogramArray of Object.values(histograms)) {
+ for (let histogram of histogramArray) {
+ histogram.clear();
+ }
+ }
+ }
+
+ /**
+ * Initializes the quick suggest weather feature and mock Merino server.
+ */
+ async initWeather() {
+ await this.server.start();
+ this.server.response.body.suggestions = [WEATHER_SUGGESTION];
+
+ lazy.QuickSuggest.weather._test_fetchIntervalMs = WEATHER_FETCH_INTERVAL_MS;
+
+ // Enabling weather will trigger a fetch. Wait for it to finish so the
+ // suggestion is ready when this function returns.
+ let fetchPromise = lazy.QuickSuggest.weather.waitForFetches();
+ lazy.UrlbarPrefs.set("weather.featureGate", true);
+ lazy.UrlbarPrefs.set("suggest.weather", true);
+ await fetchPromise;
+
+ this.Assert.equal(
+ lazy.QuickSuggest.weather._test_pendingFetchCount,
+ 0,
+ "No pending fetches after awaiting initial fetch"
+ );
+
+ this.registerCleanupFunction?.(async () => {
+ lazy.UrlbarPrefs.clear("weather.featureGate");
+ lazy.UrlbarPrefs.clear("suggest.weather");
+ lazy.QuickSuggest.weather._test_fetchIntervalMs = -1;
+ });
+ }
+
+ #initDepth = 0;
+ #server = null;
+}
+
+/**
+ * A mock Merino server with useful helper methods.
+ */
+class MockMerinoServer {
+ /**
+ * Until `start()` is called the server isn't started and `this.url` is null.
+ *
+ * @param {object} scope
+ * The global JS scope where tests are being run. This allows the instance
+ * to access test helpers like `Assert` that are available in the scope.
+ */
+ constructor(scope) {
+ scope.info?.("MockMerinoServer constructor");
+
+ for (let p of TEST_SCOPE_PROPERTIES) {
+ this[p] = scope[p];
+ }
+
+ let path = "/merino";
+ this.#httpServer = new HttpServer();
+ this.#httpServer.registerPathHandler(path, (req, resp) =>
+ this.#handleRequest(req, resp)
+ );
+ this.#baseURL = new URL("http://localhost/");
+ this.#baseURL.pathname = path;
+
+ this.reset();
+ }
+
+ /**
+ * Uninitializes the server.
+ */
+ uninit() {
+ this.info?.("MockMerinoServer uninit");
+ for (let p of TEST_SCOPE_PROPERTIES) {
+ this[p] = null;
+ }
+ }
+
+ /**
+ * @returns {nsIHttpServer}
+ * The underlying HTTP server.
+ */
+ get httpServer() {
+ return this.#httpServer;
+ }
+
+ /**
+ * @returns {URL}
+ * The server's endpoint URL or null if the server isn't running.
+ */
+ get url() {
+ return this.#url;
+ }
+
+ /**
+ * @returns {Array}
+ * Array of received nsIHttpRequest objects. Requests are continually
+ * collected, and the list can be cleared with `reset()`.
+ */
+ get requests() {
+ return this.#requests;
+ }
+
+ /**
+ * @returns {object}
+ * An object that describes the response that the server will return. Can be
+ * modified or set to a different object to change the response. Can be
+ * reset to the default reponse by calling `reset()`. For details see
+ * `makeDefaultResponse()` and `#handleRequest()`. In summary:
+ *
+ * {
+ * status,
+ * contentType,
+ * delay,
+ * body: {
+ * request_id,
+ * suggestions,
+ * },
+ * }
+ */
+ get response() {
+ return this.#response;
+ }
+ set response(value) {
+ this.#response = value;
+ }
+
+ /**
+ * Starts the server and sets `this.url`. If the server was created with a
+ * test scope that defines `registerCleanupFunction()`, you don't need to call
+ * `stop()` yourself because it will automatically be called as a cleanup
+ * function. Otherwise you'll need to call `stop()`.
+ */
+ async start() {
+ if (this.#url) {
+ return;
+ }
+
+ this.info("MockMerinoServer starting");
+
+ this.#httpServer.start(-1);
+ this.#url = new URL(this.#baseURL);
+ this.#url.port = this.#httpServer.identity.primaryPort;
+
+ this._originalEndpointURL = lazy.UrlbarPrefs.get("merino.endpointURL");
+ lazy.UrlbarPrefs.set("merino.endpointURL", this.#url.toString());
+
+ this.registerCleanupFunction?.(() => this.stop());
+
+ // Wait for the server to actually start serving. In TV tests, where the
+ // server is created over and over again, sometimes it doesn't seem to be
+ // ready after being recreated even after `#httpServer.start()` is called.
+ this.info("MockMerinoServer waiting to start serving...");
+ this.reset();
+ let suggestion;
+ while (!suggestion) {
+ let response = await fetch(this.#url);
+ let body = await response?.json();
+ suggestion = body?.suggestions?.[0];
+ }
+ this.reset();
+ this.info("MockMerinoServer is now serving");
+ }
+
+ /**
+ * Stops the server and cleans up other state.
+ */
+ async stop() {
+ if (!this.#url) {
+ return;
+ }
+
+ // `uninit()` may have already been called by this point and removed
+ // `this.info()`, so don't assume it's defined.
+ this.info?.("MockMerinoServer stopping");
+
+ // Cancel delayed-response timers and resolve their promises. Otherwise, if
+ // a test awaits this method before finishing, it will hang until the timers
+ // fire and allow the server to send the responses.
+ this.#cancelDelayedResponses();
+
+ await this.#httpServer.stop();
+ this.#url = null;
+ lazy.UrlbarPrefs.set("merino.endpointURL", this._originalEndpointURL);
+
+ this.info?.("MockMerinoServer is now stopped");
+ }
+
+ /**
+ * Returns a new object that describes the default response the server will
+ * return.
+ *
+ * @returns {object}
+ */
+ makeDefaultResponse() {
+ return {
+ status: 200,
+ contentType: "application/json",
+ body: {
+ request_id: "request_id",
+ suggestions: [
+ {
+ provider: "adm",
+ full_keyword: "full_keyword",
+ title: "title",
+ url: "url",
+ icon: null,
+ impression_url: "impression_url",
+ click_url: "click_url",
+ block_id: 1,
+ advertiser: "advertiser",
+ is_sponsored: true,
+ score: 1,
+ },
+ ],
+ },
+ };
+ }
+
+ /**
+ * Clears the received requests and sets the response to the default.
+ */
+ reset() {
+ this.#requests = [];
+ this.response = this.makeDefaultResponse();
+ this.#cancelDelayedResponses();
+ }
+
+ /**
+ * Asserts a given list of requests has been received. Clears the list of
+ * received requests before returning.
+ *
+ * @param {Array} expected
+ * The expected requests. Each item should be an object: `{ params }`
+ */
+ checkAndClearRequests(expected) {
+ let actual = this.requests.map(req => {
+ let params = new URLSearchParams(req.queryString);
+ return { params: Object.fromEntries(params) };
+ });
+
+ this.info("Checking requests");
+ this.info("actual: " + JSON.stringify(actual));
+ this.info("expect: " + JSON.stringify(expected));
+
+ // Check the request count.
+ this.Assert.equal(actual.length, expected.length, "Expected request count");
+ if (actual.length != expected.length) {
+ return;
+ }
+
+ // Check each request.
+ for (let i = 0; i < actual.length; i++) {
+ let a = actual[i];
+ let e = expected[i];
+ this.info("Checking requests at index " + i);
+ this.info("actual: " + JSON.stringify(a));
+ this.info("expect: " + JSON.stringify(e));
+
+ // Check required search params.
+ for (let p of REQUIRED_SEARCH_PARAMS) {
+ this.Assert.ok(
+ a.params.hasOwnProperty(p),
+ "Required param is present in actual request: " + p
+ );
+ if (p != SEARCH_PARAMS.SESSION_ID) {
+ this.Assert.ok(
+ e.params.hasOwnProperty(p),
+ "Required param is present in expected request: " + p
+ );
+ }
+ }
+
+ // If the expected request doesn't include a session ID, then:
+ if (!e.params.hasOwnProperty(SEARCH_PARAMS.SESSION_ID)) {
+ if (e.params[SEARCH_PARAMS.SEQUENCE_NUMBER] == 0 || i == 0) {
+ // If its sequence number is zero, then copy the actual request's
+ // sequence number to the expected request. As a convenience, do the
+ // same if this is the first request.
+ e.params[SEARCH_PARAMS.SESSION_ID] =
+ a.params[SEARCH_PARAMS.SESSION_ID];
+ } else {
+ // Otherwise this is not the first request in the session and
+ // therefore the session ID should be the same as the ID in the
+ // previous expected request.
+ e.params[SEARCH_PARAMS.SESSION_ID] =
+ expected[i - 1].params[SEARCH_PARAMS.SESSION_ID];
+ }
+ }
+
+ this.Assert.deepEqual(a, e, "Expected request at index " + i);
+
+ let actualSessionID = a.params[SEARCH_PARAMS.SESSION_ID];
+ this.Assert.ok(actualSessionID, "Session ID exists");
+ this.Assert.ok(
+ /^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/i.test(actualSessionID),
+ "Session ID is a UUID"
+ );
+ }
+
+ this.#requests = [];
+ }
+
+ /**
+ * Temporarily creates the conditions for a network error. Any Merino fetches
+ * that occur during the callback will fail with a network error.
+ *
+ * @param {Function} callback
+ * Callback function.
+ */
+ async withNetworkError(callback) {
+ // Set the endpoint to a valid, unreachable URL.
+ let originalURL = lazy.UrlbarPrefs.get("merino.endpointURL");
+ lazy.UrlbarPrefs.set(
+ "merino.endpointURL",
+ "http://localhost/valid-but-unreachable-url"
+ );
+
+ // Set the timeout high enough that the network error exception will happen
+ // first. On Mac and Linux the fetch naturally times out fairly quickly but
+ // on Windows it seems to take 5s, so set our artificial timeout to 10s.
+ let originalTimeout = lazy.UrlbarPrefs.get("merino.timeoutMs");
+ lazy.UrlbarPrefs.set("merino.timeoutMs", 10000);
+
+ await callback();
+
+ lazy.UrlbarPrefs.set("merino.endpointURL", originalURL);
+ lazy.UrlbarPrefs.set("merino.timeoutMs", originalTimeout);
+ }
+
+ /**
+ * Returns a promise that will resolve when the next request is received.
+ *
+ * @returns {Promise}
+ */
+ waitForNextRequest() {
+ if (!this.#nextRequestDeferred) {
+ this.#nextRequestDeferred = lazy.PromiseUtils.defer();
+ }
+ return this.#nextRequestDeferred.promise;
+ }
+
+ /**
+ * nsIHttpServer request handler.
+ *
+ * @param {nsIHttpRequest} httpRequest
+ * Request.
+ * @param {nsIHttpResponse} httpResponse
+ * Response.
+ */
+ #handleRequest(httpRequest, httpResponse) {
+ this.info(
+ "MockMerinoServer received request with query string: " +
+ JSON.stringify(httpRequest.queryString)
+ );
+ this.info(
+ "MockMerinoServer replying with response: " +
+ JSON.stringify(this.response)
+ );
+
+ // Add the request to the list of received requests.
+ this.#requests.push(httpRequest);
+
+ // Resolve promises waiting on the next request.
+ this.#nextRequestDeferred?.resolve();
+ this.#nextRequestDeferred = null;
+
+ // Now set up and finish the response.
+ httpResponse.processAsync();
+
+ let { response } = this;
+
+ let finishResponse = () => {
+ let status = response.status || 200;
+ httpResponse.setStatusLine("", status, status);
+
+ let contentType = response.contentType || "application/json";
+ httpResponse.setHeader("Content-Type", contentType, false);
+
+ if (typeof response.body == "string") {
+ httpResponse.write(response.body);
+ } else if (response.body) {
+ httpResponse.write(JSON.stringify(response.body));
+ }
+
+ httpResponse.finish();
+ };
+
+ if (typeof response.delay != "number") {
+ finishResponse();
+ return;
+ }
+
+ // Set up a timer to wait until the delay elapses. Since we called
+ // `httpResponse.processAsync()`, we need to be careful to always finish the
+ // response, even if the timer is canceled. Otherwise the server will hang
+ // when we try to stop it at the end of the test. When an `nsITimer` is
+ // canceled, its callback is *not* called. Therefore we set up a race
+ // between the timer's callback and a deferred promise. If the timer is
+ // canceled, resolving the deferred promise will resolve the race, and the
+ // response can then be finished.
+
+ let delayedResponseID = this.#nextDelayedResponseID++;
+ this.info(
+ "MockMerinoServer delaying response: " +
+ JSON.stringify({ delayedResponseID, delay: response.delay })
+ );
+
+ let deferred = lazy.PromiseUtils.defer();
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ let record = { timer, resolve: deferred.resolve };
+ this.#delayedResponseRecords.add(record);
+
+ // Don't await this promise.
+ Promise.race([
+ deferred.promise,
+ new Promise(resolve => {
+ timer.initWithCallback(
+ resolve,
+ response.delay,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+ }),
+ ]).then(() => {
+ this.info(
+ "MockMerinoServer done delaying response: " +
+ JSON.stringify({ delayedResponseID })
+ );
+ deferred.resolve();
+ this.#delayedResponseRecords.delete(record);
+ finishResponse();
+ });
+ }
+
+ /**
+ * Cancels the timers for delayed responses and resolves their promises.
+ */
+ #cancelDelayedResponses() {
+ for (let { timer, resolve } of this.#delayedResponseRecords) {
+ timer.cancel();
+ resolve();
+ }
+ this.#delayedResponseRecords.clear();
+ }
+
+ #httpServer = null;
+ #url = null;
+ #baseURL = null;
+ #response = null;
+ #requests = [];
+ #nextRequestDeferred = null;
+ #nextDelayedResponseID = 0;
+ #delayedResponseRecords = new Set();
+}
+
+export var MerinoTestUtils = new _MerinoTestUtils();
diff --git a/browser/components/urlbar/tests/quicksuggest/QuickSuggestTestUtils.sys.mjs b/browser/components/urlbar/tests/quicksuggest/QuickSuggestTestUtils.sys.mjs
new file mode 100644
index 0000000000..34da73e847
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/QuickSuggestTestUtils.sys.mjs
@@ -0,0 +1,1017 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-disable mozilla/valid-lazy */
+
+import {
+ CONTEXTUAL_SERVICES_PING_TYPES,
+ PartnerLinkAttribution,
+} from "resource:///modules/PartnerLinkAttribution.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs",
+ ExperimentFakes: "resource://testing-common/NimbusTestUtils.sys.mjs",
+ NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
+ QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs",
+ QuickSuggestRemoteSettings:
+ "resource:///modules/urlbar/private/QuickSuggestRemoteSettings.sys.mjs",
+ RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
+ SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs",
+ TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
+ TestUtils: "resource://testing-common/TestUtils.sys.mjs",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
+ UrlbarProviderQuickSuggest:
+ "resource:///modules/UrlbarProviderQuickSuggest.sys.mjs",
+ UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+let gTestScope;
+
+// Test utils singletons need special handling. Since they are uninitialized in
+// cleanup functions, they must be re-initialized on each new test. That does
+// not happen automatically inside system modules like this one because system
+// module lifetimes are the app's lifetime, unlike individual browser chrome and
+// xpcshell tests.
+Object.defineProperty(lazy, "UrlbarTestUtils", {
+ get: () => {
+ if (!lazy._UrlbarTestUtils) {
+ const { UrlbarTestUtils: module } = ChromeUtils.importESModule(
+ "resource://testing-common/UrlbarTestUtils.sys.mjs"
+ );
+ module.init(gTestScope);
+ gTestScope.registerCleanupFunction(() => {
+ // Make sure the utils are re-initialized during the next test.
+ lazy._UrlbarTestUtils = null;
+ });
+ lazy._UrlbarTestUtils = module;
+ }
+ return lazy._UrlbarTestUtils;
+ },
+});
+
+// Test utils singletons need special handling. Since they are uninitialized in
+// cleanup functions, they must be re-initialized on each new test. That does
+// not happen automatically inside system modules like this one because system
+// module lifetimes are the app's lifetime, unlike individual browser chrome and
+// xpcshell tests.
+Object.defineProperty(lazy, "MerinoTestUtils", {
+ get: () => {
+ if (!lazy._MerinoTestUtils) {
+ const { MerinoTestUtils: module } = ChromeUtils.importESModule(
+ "resource://testing-common/MerinoTestUtils.sys.mjs"
+ );
+ module.init(gTestScope);
+ gTestScope.registerCleanupFunction(() => {
+ // Make sure the utils are re-initialized during the next test.
+ lazy._MerinoTestUtils = null;
+ });
+ lazy._MerinoTestUtils = module;
+ }
+ return lazy._MerinoTestUtils;
+ },
+});
+
+const DEFAULT_CONFIG = {};
+
+const BEST_MATCH_CONFIG = {
+ best_match: {
+ blocked_suggestion_ids: [],
+ min_search_string_length: 4,
+ },
+};
+
+const DEFAULT_PING_PAYLOADS = {
+ [CONTEXTUAL_SERVICES_PING_TYPES.QS_BLOCK]: {
+ advertiser: "testadvertiser",
+ block_id: 1,
+ context_id: () => actual => !!actual,
+ iab_category: "22 - Shopping",
+ improve_suggest_experience_checked: false,
+ match_type: "firefox-suggest",
+ request_id: null,
+ source: "remote-settings",
+ },
+ [CONTEXTUAL_SERVICES_PING_TYPES.QS_SELECTION]: {
+ advertiser: "testadvertiser",
+ block_id: 1,
+ context_id: () => actual => !!actual,
+ improve_suggest_experience_checked: false,
+ match_type: "firefox-suggest",
+ reporting_url: "https://example.com/click",
+ request_id: null,
+ source: "remote-settings",
+ },
+ [CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION]: {
+ advertiser: "testadvertiser",
+ block_id: 1,
+ context_id: () => actual => !!actual,
+ improve_suggest_experience_checked: false,
+ is_clicked: false,
+ match_type: "firefox-suggest",
+ reporting_url: "https://example.com/impression",
+ request_id: null,
+ source: "remote-settings",
+ },
+};
+
+// The following properties and methods are copied from the test scope to the
+// test utils object so they can be easily accessed. Be careful about assuming a
+// particular property will be defined because depending on the scope -- browser
+// test or xpcshell test -- some may not be.
+const TEST_SCOPE_PROPERTIES = [
+ "Assert",
+ "EventUtils",
+ "info",
+ "registerCleanupFunction",
+];
+
+/**
+ * Mock RemoteSettings.
+ *
+ * @param {object} options
+ * Options object
+ * @param {object} options.config
+ * Dummy config in the RemoteSettings.
+ * @param {Array} options.data
+ * Dummy data in the RemoteSettings.
+ */
+class MockRemoteSettings {
+ constructor({ config = DEFAULT_CONFIG, data = [] }) {
+ this.#config = config;
+ this.#data = data;
+
+ // Make a stub for "get" function to return dummy data.
+ const rs = lazy.RemoteSettings("quicksuggest");
+ this.#sandbox = lazy.sinon.createSandbox();
+ this.#sandbox.stub(rs, "get").callsFake(async query => {
+ return query.filters.type === "configuration"
+ ? [{ configuration: this.#config }]
+ : this.#data.filter(r => r.type === query.filters.type);
+ });
+
+ // Make a stub for "download" in attachments.
+ this.#sandbox.stub(rs.attachments, "download").callsFake(async record => {
+ if (!record.attachment) {
+ throw new Error("No attachmet in the record");
+ }
+ const encoder = new TextEncoder();
+ return {
+ buffer: encoder.encode(JSON.stringify(record.attachment)),
+ };
+ });
+ }
+
+ async sync() {
+ if (!lazy.QuickSuggestRemoteSettings.rs) {
+ // There are no registered features that use remote settings.
+ return;
+ }
+
+ // Observe config-set event to recognize that the config is synced.
+ const onConfigSync = new Promise(resolve => {
+ lazy.QuickSuggestRemoteSettings.emitter.once("config-set", resolve);
+ });
+
+ // Make a stub for each feature to recognize that the features are synced.
+ const features = lazy.QuickSuggestRemoteSettings.features;
+ const onFeatureSyncs = features.map(feature => {
+ return new Promise(resolve => {
+ const stub = this.#sandbox
+ .stub(feature, "onRemoteSettingsSync")
+ .callsFake(async (...args) => {
+ // Call and wait for the original function.
+ await stub.wrappedMethod.apply(feature, args);
+ stub.restore();
+ resolve();
+ });
+ });
+ });
+
+ // Force to sync.
+ const rs = lazy.RemoteSettings("quicksuggest");
+ rs.emit("sync");
+
+ // Wait for sync.
+ await Promise.all([onConfigSync, ...onFeatureSyncs]);
+ }
+
+ /*
+ * Update the config and data in RemoteSettings. If the config or the data are
+ * undefined, use the current one.
+ *
+ * @param {object} options
+ * Options object
+ * @param {object} options.config
+ * Dummy config in the RemoteSettings.
+ * @param {Array} options.data
+ * Dummy data in the RemoteSettings.
+ */
+ async update({ config = this.#config, data = this.#data }) {
+ this.#config = config;
+ this.#data = data;
+
+ await this.sync();
+ }
+
+ cleanup() {
+ this.#sandbox.restore();
+ }
+
+ #config = null;
+ #data = null;
+ #sandbox = null;
+}
+
+/**
+ * Test utils for quick suggest.
+ */
+class _QuickSuggestTestUtils {
+ /**
+ * Initializes the utils.
+ *
+ * @param {object} scope
+ * The global JS scope where tests are being run. This allows the instance
+ * to access test helpers like `Assert` that are available in the scope.
+ */
+ init(scope) {
+ if (!scope) {
+ throw new Error("QuickSuggestTestUtils() must be called with a scope");
+ }
+ gTestScope = scope;
+ for (let p of TEST_SCOPE_PROPERTIES) {
+ this[p] = scope[p];
+ }
+ // If you add other properties to `this`, null them in `uninit()`.
+
+ Services.telemetry.clearScalars();
+
+ scope.registerCleanupFunction?.(() => this.uninit());
+ }
+
+ /**
+ * Uninitializes the utils. If they were created with a test scope that
+ * defines `registerCleanupFunction()`, you don't need to call this yourself
+ * because it will automatically be called as a cleanup function. Otherwise
+ * you'll need to call this.
+ */
+ uninit() {
+ gTestScope = null;
+ for (let p of TEST_SCOPE_PROPERTIES) {
+ this[p] = null;
+ }
+ Services.telemetry.clearScalars();
+ }
+
+ get DEFAULT_CONFIG() {
+ // Return a clone so callers can modify it.
+ return Cu.cloneInto(DEFAULT_CONFIG, this);
+ }
+
+ get BEST_MATCH_CONFIG() {
+ // Return a clone so callers can modify it.
+ return Cu.cloneInto(BEST_MATCH_CONFIG, this);
+ }
+
+ /**
+ * Waits for quick suggest initialization to finish, ensures its data will not
+ * be updated again during the test, and also optionally sets it up with mock
+ * suggestions.
+ *
+ * @param {object} options
+ * Options object
+ * @param {Array} options.remoteSettingsResults
+ * Array of remote settings result objects. If not given, no suggestions
+ * will be present in remote settings.
+ * @param {Array} options.merinoSuggestions
+ * Array of Merino suggestion objects. If given, this function will start
+ * the mock Merino server and set `quicksuggest.dataCollection.enabled` to
+ * true so that `UrlbarProviderQuickSuggest` will fetch suggestions from it.
+ * Otherwise Merino will not serve suggestions, but you can still set up
+ * Merino without using this function by using `MerinoTestUtils` directly.
+ * @param {object} options.config
+ * The quick suggest configuration object.
+ * @returns {Function}
+ * A cleanup function. You only need to call this function if you're in a
+ * browser chrome test and you did not also call `init`. You can ignore it
+ * otherwise.
+ */
+ async ensureQuickSuggestInit({
+ remoteSettingsResults,
+ merinoSuggestions = null,
+ config = DEFAULT_CONFIG,
+ } = {}) {
+ this.#mockRemoteSettings = new MockRemoteSettings({
+ config,
+ data: remoteSettingsResults,
+ });
+
+ this.info?.("ensureQuickSuggestInit calling QuickSuggest.init()");
+ lazy.QuickSuggest.init();
+
+ // Sync with current data.
+ await this.#mockRemoteSettings.sync();
+
+ // Set up Merino.
+ if (merinoSuggestions) {
+ this.info?.("ensureQuickSuggestInit setting up Merino server");
+ await lazy.MerinoTestUtils.server.start();
+ lazy.MerinoTestUtils.server.response.body.suggestions = merinoSuggestions;
+ lazy.UrlbarPrefs.set("quicksuggest.dataCollection.enabled", true);
+ this.info?.("ensureQuickSuggestInit done setting up Merino server");
+ }
+
+ let cleanup = async () => {
+ this.info?.("ensureQuickSuggestInit starting cleanup");
+ this.#mockRemoteSettings.cleanup();
+ if (merinoSuggestions) {
+ lazy.UrlbarPrefs.clear("quicksuggest.dataCollection.enabled");
+ }
+ this.info?.("ensureQuickSuggestInit finished cleanup");
+ };
+ this.registerCleanupFunction?.(cleanup);
+
+ return cleanup;
+ }
+
+ /**
+ * Clears the current remote settings data and adds a new set of data.
+ * This can be used to add remote settings data after
+ * `ensureQuickSuggestInit()` has been called.
+ *
+ * @param {Array} data
+ * Array of remote settings data objects.
+ */
+ async setRemoteSettingsResults(data) {
+ await this.#mockRemoteSettings.update({ data });
+ }
+
+ /**
+ * Sets the quick suggest configuration. You should call this again with
+ * `DEFAULT_CONFIG` before your test finishes. See also `withConfig()`.
+ *
+ * @param {object} config
+ * The config to be applied. See
+ */
+ async setConfig(config) {
+ await this.#mockRemoteSettings.update({ config });
+ }
+
+ /**
+ * Sets the quick suggest configuration, calls your callback, and restores the
+ * previous configuration.
+ *
+ * @param {object} options
+ * The options object.
+ * @param {object} options.config
+ * The configuration that should be used with the callback
+ * @param {Function} options.callback
+ * Will be called with the configuration applied
+ *
+ * @see {@link setConfig}
+ */
+ async withConfig({ config, callback }) {
+ let original = lazy.QuickSuggestRemoteSettings.config;
+ await this.setConfig(config);
+ await callback();
+ await this.setConfig(original);
+ }
+
+ /**
+ * Sets the Firefox Suggest scenario and waits for prefs to be updated.
+ *
+ * @param {string} scenario
+ * Pass falsey to reset the scenario to the default.
+ */
+ async setScenario(scenario) {
+ // If we try to set the scenario before a previous update has finished,
+ // `updateFirefoxSuggestScenario` will bail, so wait.
+ await this.waitForScenarioUpdated();
+ await lazy.UrlbarPrefs.updateFirefoxSuggestScenario({ scenario });
+ }
+
+ /**
+ * Waits for any prior scenario update to finish.
+ */
+ async waitForScenarioUpdated() {
+ await lazy.TestUtils.waitForCondition(
+ () => !lazy.UrlbarPrefs.updatingFirefoxSuggestScenario,
+ "Waiting for updatingFirefoxSuggestScenario to be false"
+ );
+ }
+
+ /**
+ * Asserts a result is a quick suggest result.
+ *
+ * @param {object} [options]
+ * The options object.
+ * @param {string} options.url
+ * The expected URL. At least one of `url` and `originalUrl` must be given.
+ * @param {string} options.originalUrl
+ * The expected original URL (the URL with an unreplaced timestamp
+ * template). At least one of `url` and `originalUrl` must be given.
+ * @param {object} options.window
+ * The window that should be used for this assertion
+ * @param {number} [options.index]
+ * The expected index of the quick suggest result. Pass -1 to use the index
+ * of the last result.
+ * @param {boolean} [options.isSponsored]
+ * Whether the result is expected to be sponsored.
+ * @param {boolean} [options.isBestMatch]
+ * Whether the result is expected to be a best match.
+ * @returns {result}
+ * The quick suggest result.
+ */
+ async assertIsQuickSuggest({
+ url,
+ originalUrl,
+ window,
+ index = -1,
+ isSponsored = true,
+ isBestMatch = false,
+ } = {}) {
+ this.Assert.ok(
+ url || originalUrl,
+ "At least one of url and originalUrl is specified"
+ );
+
+ if (index < 0) {
+ let resultCount = lazy.UrlbarTestUtils.getResultCount(window);
+ if (isBestMatch) {
+ index = 1;
+ this.Assert.greater(
+ resultCount,
+ 1,
+ "Sanity check: Result count should be > 1"
+ );
+ } else {
+ index = resultCount - 1;
+ this.Assert.greater(
+ resultCount,
+ 0,
+ "Sanity check: Result count should be > 0"
+ );
+ }
+ }
+
+ let details = await lazy.UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ index
+ );
+ let { result } = details;
+
+ this.info?.(
+ `Checking actual result at index ${index}: ` + JSON.stringify(result)
+ );
+
+ this.Assert.equal(
+ result.providerName,
+ "UrlbarProviderQuickSuggest",
+ "Result provider name is UrlbarProviderQuickSuggest"
+ );
+ this.Assert.equal(details.type, lazy.UrlbarUtils.RESULT_TYPE.URL);
+ this.Assert.equal(details.isSponsored, isSponsored, "Result isSponsored");
+ if (url) {
+ this.Assert.equal(details.url, url, "Result URL");
+ }
+ if (originalUrl) {
+ this.Assert.equal(
+ result.payload.originalUrl,
+ originalUrl,
+ "Result original URL"
+ );
+ }
+
+ this.Assert.equal(!!result.isBestMatch, isBestMatch, "Result isBestMatch");
+
+ let { row } = details.element;
+
+ let sponsoredElement = isBestMatch
+ ? row._elements.get("bottom")
+ : row._elements.get("action");
+ this.Assert.ok(sponsoredElement, "Result sponsored label element exists");
+ this.Assert.equal(
+ sponsoredElement.textContent,
+ isSponsored ? "Sponsored" : "",
+ "Result sponsored label"
+ );
+
+ this.Assert.equal(
+ result.payload.helpUrl,
+ lazy.QuickSuggest.HELP_URL,
+ "Result helpURL"
+ );
+
+ if (lazy.UrlbarPrefs.get("resultMenu")) {
+ this.Assert.ok(
+ row._buttons.get("menu"),
+ "The menu button should be present"
+ );
+ } else {
+ let helpButton = row._buttons.get("help");
+ this.Assert.ok(helpButton, "The help button should be present");
+
+ let blockButton = row._buttons.get("block");
+ if (!isBestMatch) {
+ this.Assert.equal(
+ !!blockButton,
+ lazy.UrlbarPrefs.get("quickSuggestBlockingEnabled"),
+ "The block button is present iff quick suggest blocking is enabled"
+ );
+ } else {
+ this.Assert.equal(
+ !!blockButton,
+ lazy.UrlbarPrefs.get("bestMatchBlockingEnabled"),
+ "The block button is present iff best match blocking is enabled"
+ );
+ }
+ }
+
+ return details;
+ }
+
+ /**
+ * Asserts a result is not a quick suggest result.
+ *
+ * @param {object} window
+ * The window that should be used for this assertion
+ * @param {number} index
+ * The index of the result.
+ */
+ async assertIsNotQuickSuggest(window, index) {
+ let details = await lazy.UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ index
+ );
+ this.Assert.notEqual(
+ details.result.providerName,
+ "UrlbarProviderQuickSuggest",
+ `Result at index ${index} is not provided by UrlbarProviderQuickSuggest`
+ );
+ }
+
+ /**
+ * Asserts that none of the results are quick suggest results.
+ *
+ * @param {object} window
+ * The window that should be used for this assertion
+ */
+ async assertNoQuickSuggestResults(window) {
+ for (let i = 0; i < lazy.UrlbarTestUtils.getResultCount(window); i++) {
+ await this.assertIsNotQuickSuggest(window, i);
+ }
+ }
+
+ /**
+ * Checks the values of all the quick suggest telemetry keyed scalars and,
+ * if provided, other non-quick-suggest keyed scalars. Scalar values are all
+ * assumed to be 1.
+ *
+ * @param {object} expectedKeysByScalarName
+ * Maps scalar names to keys that are expected to be recorded. The value for
+ * each key is assumed to be 1. If you expect a scalar to be incremented,
+ * include it in this object; otherwise, don't include it.
+ */
+ assertScalars(expectedKeysByScalarName) {
+ let scalars = lazy.TelemetryTestUtils.getProcessScalars(
+ "parent",
+ true,
+ true
+ );
+
+ // Check all quick suggest scalars.
+ expectedKeysByScalarName = { ...expectedKeysByScalarName };
+ for (let scalarName of Object.values(
+ lazy.UrlbarProviderQuickSuggest.TELEMETRY_SCALARS
+ )) {
+ if (scalarName in expectedKeysByScalarName) {
+ lazy.TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ scalarName,
+ expectedKeysByScalarName[scalarName],
+ 1
+ );
+ delete expectedKeysByScalarName[scalarName];
+ } else {
+ this.Assert.ok(
+ !(scalarName in scalars),
+ "Scalar should not be present: " + scalarName
+ );
+ }
+ }
+
+ // Check any other remaining scalars that were passed in.
+ for (let [scalarName, key] of Object.entries(expectedKeysByScalarName)) {
+ lazy.TelemetryTestUtils.assertKeyedScalar(scalars, scalarName, key, 1);
+ }
+ }
+
+ /**
+ * Checks quick suggest telemetry events. This is the same as
+ * `TelemetryTestUtils.assertEvents()` except it filters in only quick suggest
+ * events by default. If you are expecting events that are not in the quick
+ * suggest category, use `TelemetryTestUtils.assertEvents()` directly or pass
+ * in a filter override for `category`.
+ *
+ * @param {Array} expectedEvents
+ * List of expected telemetry events.
+ * @param {object} filterOverrides
+ * Extra properties to set in the filter object.
+ * @param {object} options
+ * The options object to pass to `TelemetryTestUtils.assertEvents()`.
+ */
+ assertEvents(expectedEvents, filterOverrides = {}, options = undefined) {
+ lazy.TelemetryTestUtils.assertEvents(
+ expectedEvents,
+ {
+ category: lazy.QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ ...filterOverrides,
+ },
+ options
+ );
+ }
+
+ /**
+ * Creates a `sinon.sandbox` and `sinon.spy` that can be used to instrument
+ * the quick suggest custom telemetry pings. If `init` was called with a test
+ * scope where `registerCleanupFunction` is defined, the sandbox will
+ * automically be restored at the end of the test.
+ *
+ * @returns {object}
+ * An object: { sandbox, spy, spyCleanup }
+ * `spyCleanup` is a cleanup function that should be called if you're in a
+ * browser chrome test and you did not also call `init`, or if you need to
+ * remove the spy before the test ends for some other reason. You can ignore
+ * it otherwise.
+ */
+ createTelemetryPingSpy() {
+ let sandbox = lazy.sinon.createSandbox();
+ let spy = sandbox.spy(
+ PartnerLinkAttribution._pingCentre,
+ "sendStructuredIngestionPing"
+ );
+ let spyCleanup = () => sandbox.restore();
+ this.registerCleanupFunction?.(spyCleanup);
+ return { sandbox, spy, spyCleanup };
+ }
+
+ /**
+ * Asserts that custom telemetry pings are recorded in the order they appear
+ * in the given `pings` array and that no other pings are recorded.
+ *
+ * @param {object} spy
+ * A `sinon.spy` object. See `createTelemetryPingSpy()`. This method resets
+ * the spy before returning.
+ * @param {Array} pings
+ * The expected pings in the order they are expected to be recorded. Each
+ * item in this array should be an object: `{ type, payload }`
+ *
+ * {string} type
+ * The ping's expected type, one of the `CONTEXTUAL_SERVICES_PING_TYPES`
+ * values.
+ * {object} payload
+ * The ping's expected payload. For convenience, you can leave out
+ * properties whose values are expected to be the default values defined
+ * in `DEFAULT_PING_PAYLOADS`.
+ */
+ assertPings(spy, pings) {
+ let calls = spy.getCalls();
+ this.Assert.equal(
+ calls.length,
+ pings.length,
+ "Expected number of ping calls"
+ );
+
+ for (let i = 0; i < pings.length; i++) {
+ let ping = pings[i];
+ this.info?.(
+ `Checking ping at index ${i}, expected is: ` + JSON.stringify(ping)
+ );
+
+ // Add default properties to the expected payload for any that aren't
+ // already defined.
+ let { type, payload } = ping;
+ let defaultPayload = DEFAULT_PING_PAYLOADS[type];
+ this.Assert.ok(
+ defaultPayload,
+ `Sanity check: Default payload exists for type: ${type}`
+ );
+ payload = { ...defaultPayload, ...payload };
+
+ // Check the endpoint URL.
+ let call = calls[i];
+ let endpointURL = call.args[1];
+ this.Assert.ok(
+ endpointURL.includes(type),
+ `Endpoint URL corresponds to the expected ping type: ${type}`
+ );
+
+ // Check the payload.
+ let actualPayload = call.args[0];
+ this._assertPingPayload(actualPayload, payload);
+ }
+
+ spy.resetHistory();
+ }
+
+ /**
+ * Helper for checking contextual services ping payloads.
+ *
+ * @param {object} actualPayload
+ * The actual payload in the ping.
+ * @param {object} expectedPayload
+ * An object describing the expected payload. Non-function values in this
+ * object are checked for equality against the corresponding actual payload
+ * values. Function values are called and passed the corresponding actual
+ * values and should return true if the actual values are correct.
+ */
+ _assertPingPayload(actualPayload, expectedPayload) {
+ this.info?.(
+ "Checking ping payload. Actual: " +
+ JSON.stringify(actualPayload) +
+ " -- Expected (excluding function properties): " +
+ JSON.stringify(expectedPayload)
+ );
+
+ this.Assert.equal(
+ Object.entries(actualPayload).length,
+ Object.entries(expectedPayload).length,
+ "Payload has expected number of properties"
+ );
+
+ for (let [key, expectedValue] of Object.entries(expectedPayload)) {
+ let actualValue = actualPayload[key];
+ if (typeof expectedValue == "function") {
+ this.Assert.ok(expectedValue(actualValue), "Payload property: " + key);
+ } else {
+ this.Assert.equal(
+ actualValue,
+ expectedValue,
+ "Payload property: " + key
+ );
+ }
+ }
+ }
+
+ /**
+ * Asserts that URLs in a result's payload have the timestamp template
+ * substring replaced with real timestamps.
+ *
+ * @param {UrlbarResult} result The results to check
+ * @param {object} urls
+ * An object that contains the expected payload properties with template
+ * substrings. For example:
+ * ```js
+ * {
+ * url: "http://example.com/foo-%YYYYMMDDHH%",
+ * sponsoredClickUrl: "http://example.com/bar-%YYYYMMDDHH%",
+ * }
+ * ```
+ */
+ assertTimestampsReplaced(result, urls) {
+ let { TIMESTAMP_TEMPLATE, TIMESTAMP_LENGTH } = lazy.QuickSuggest;
+
+ // Parse the timestamp strings from each payload property and save them in
+ // `urls[key].timestamp`.
+ urls = { ...urls };
+ for (let [key, url] of Object.entries(urls)) {
+ let index = url.indexOf(TIMESTAMP_TEMPLATE);
+ this.Assert.ok(
+ index >= 0,
+ `Timestamp template ${TIMESTAMP_TEMPLATE} is in URL ${url} for key ${key}`
+ );
+ let value = result.payload[key];
+ this.Assert.ok(value, "Key is in result payload: " + key);
+ let timestamp = value.substring(index, index + TIMESTAMP_LENGTH);
+
+ // Set `urls[key]` to an object that's helpful in the logged info message
+ // below.
+ urls[key] = { url, value, timestamp };
+ }
+
+ this.info?.("Parsed timestamps: " + JSON.stringify(urls));
+
+ // Make a set of unique timestamp strings. There should only be one.
+ let { timestamp } = Object.values(urls)[0];
+ this.Assert.deepEqual(
+ [...new Set(Object.values(urls).map(o => o.timestamp))],
+ [timestamp],
+ "There's only one unique timestamp string"
+ );
+
+ // Parse the parts of the timestamp string.
+ let year = timestamp.slice(0, -6);
+ let month = timestamp.slice(-6, -4);
+ let day = timestamp.slice(-4, -2);
+ let hour = timestamp.slice(-2);
+ let date = new Date(year, month - 1, day, hour);
+
+ // The timestamp should be no more than two hours in the past. Typically it
+ // will be the same as the current hour, but since its resolution is in
+ // terms of hours and it's possible the test may have crossed over into a
+ // new hour as it was running, allow for the previous hour.
+ this.Assert.less(
+ Date.now() - 2 * 60 * 60 * 1000,
+ date.getTime(),
+ "Timestamp is within the past two hours"
+ );
+ }
+
+ /**
+ * Calls a callback while enrolled in a mock Nimbus experiment. The experiment
+ * is automatically unenrolled and cleaned up after the callback returns.
+ *
+ * @param {object} options
+ * Options for the mock experiment.
+ * @param {Function} options.callback
+ * The callback to call while enrolled in the mock experiment.
+ * @param {object} options.options
+ * See {@link enrollExperiment}.
+ */
+ async withExperiment({ callback, ...options }) {
+ let doExperimentCleanup = await this.enrollExperiment(options);
+ await callback();
+ await doExperimentCleanup();
+ }
+
+ /**
+ * Enrolls in a mock Nimbus experiment.
+ *
+ * @param {object} options
+ * Options for the mock experiment.
+ * @param {object} [options.valueOverrides]
+ * Values for feature variables.
+ * @returns {Promise<Function>}
+ * The experiment cleanup function (async).
+ */
+ async enrollExperiment({ valueOverrides = {} }) {
+ this.info?.("Awaiting ExperimentAPI.ready");
+ await lazy.ExperimentAPI.ready();
+
+ // Wait for any prior scenario updates to finish. If updates are ongoing,
+ // UrlbarPrefs will ignore the Nimbus update when the experiment is
+ // installed. This shouldn't be a problem in practice because in reality
+ // scenario updates are triggered only on app startup and Nimbus
+ // enrollments, but tests can trigger lots of updates back to back.
+ await this.waitForScenarioUpdated();
+
+ let doExperimentCleanup =
+ await lazy.ExperimentFakes.enrollWithFeatureConfig({
+ enabled: true,
+ featureId: "urlbar",
+ value: valueOverrides,
+ });
+
+ // Wait for the pref updates triggered by the experiment enrollment.
+ this.info?.("Awaiting update after enrolling in experiment");
+ await this.waitForScenarioUpdated();
+
+ return async () => {
+ this.info?.("Awaiting experiment cleanup");
+ await doExperimentCleanup();
+
+ // The same pref updates will be triggered by unenrollment, so wait for
+ // them again.
+ this.info?.("Awaiting update after unenrolling in experiment");
+ await this.waitForScenarioUpdated();
+ };
+ }
+
+ /**
+ * Clears the Nimbus exposure event.
+ */
+ async clearExposureEvent() {
+ // Exposure event recording is queued to the idle thread, so wait for idle
+ // before we start so any events from previous tasks will have been recorded
+ // and won't interfere with this task.
+ await new Promise(resolve => Services.tm.idleDispatchToMainThread(resolve));
+
+ Services.telemetry.clearEvents();
+ lazy.NimbusFeatures.urlbar._didSendExposureEvent = false;
+ lazy.QuickSuggest._recordedExposureEvent = false;
+ }
+
+ /**
+ * Asserts the Nimbus exposure event is recorded or not as expected.
+ *
+ * @param {boolean} expectedRecorded
+ * Whether the event is expected to be recorded.
+ */
+ async assertExposureEvent(expectedRecorded) {
+ this.Assert.equal(
+ lazy.QuickSuggest._recordedExposureEvent,
+ expectedRecorded,
+ "_recordedExposureEvent is correct"
+ );
+
+ let filter = {
+ category: "normandy",
+ method: "expose",
+ object: "nimbus_experiment",
+ };
+
+ let expectedEvents = [];
+ if (expectedRecorded) {
+ expectedEvents.push({
+ ...filter,
+ extra: {
+ branchSlug: "control",
+ featureId: "urlbar",
+ },
+ });
+ }
+
+ // The event recording is queued to the idle thread when the search starts,
+ // so likewise queue the assert to idle instead of doing it immediately.
+ await new Promise(resolve => {
+ Services.tm.idleDispatchToMainThread(() => {
+ lazy.TelemetryTestUtils.assertEvents(expectedEvents, filter);
+ resolve();
+ });
+ });
+ }
+
+ /**
+ * Sets the app's locales, calls your callback, and resets locales.
+ *
+ * @param {Array} locales
+ * An array of locale strings. The entire array will be set as the available
+ * locales, and the first locale in the array will be set as the requested
+ * locale.
+ * @param {Function} callback
+ * The callback to be called with the {@link locales} set. This function can
+ * be async.
+ */
+ async withLocales(locales, callback) {
+ let promiseChanges = async desiredLocales => {
+ this.info?.(
+ "Changing locales from " +
+ JSON.stringify(Services.locale.requestedLocales) +
+ " to " +
+ JSON.stringify(desiredLocales)
+ );
+
+ if (desiredLocales[0] == Services.locale.requestedLocales[0]) {
+ // Nothing happens when the locale doesn't actually change.
+ return;
+ }
+
+ this.info?.("Waiting for intl:requested-locales-changed");
+ await lazy.TestUtils.topicObserved("intl:requested-locales-changed");
+ this.info?.("Observed intl:requested-locales-changed");
+
+ // Wait for the search service to reload engines. Otherwise tests can fail
+ // in strange ways due to internal search service state during shutdown.
+ // It won't always reload engines but it's hard to tell in advance when it
+ // won't, so also set a timeout.
+ this.info?.("Waiting for TOPIC_SEARCH_SERVICE");
+ await Promise.race([
+ lazy.TestUtils.topicObserved(
+ lazy.SearchUtils.TOPIC_SEARCH_SERVICE,
+ (subject, data) => {
+ this.info?.("Observed TOPIC_SEARCH_SERVICE with data: " + data);
+ return data == "engines-reloaded";
+ }
+ ),
+ new Promise(resolve => {
+ lazy.setTimeout(() => {
+ this.info?.("Timed out waiting for TOPIC_SEARCH_SERVICE");
+ resolve();
+ }, 2000);
+ }),
+ ]);
+
+ this.info?.("Done waiting for locale changes");
+ };
+
+ let available = Services.locale.availableLocales;
+ let requested = Services.locale.requestedLocales;
+
+ let newRequested = locales.slice(0, 1);
+ let promise = promiseChanges(newRequested);
+ Services.locale.availableLocales = locales;
+ Services.locale.requestedLocales = newRequested;
+ await promise;
+
+ this.Assert.equal(
+ Services.locale.appLocaleAsBCP47,
+ locales[0],
+ "App locale is now " + locales[0]
+ );
+
+ await callback();
+
+ promise = promiseChanges(requested);
+ Services.locale.availableLocales = available;
+ Services.locale.requestedLocales = requested;
+ await promise;
+ }
+
+ #mockRemoteSettings = null;
+}
+
+export var QuickSuggestTestUtils = new _QuickSuggestTestUtils();
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser.ini b/browser/components/urlbar/tests/quicksuggest/browser/browser.ini
new file mode 100644
index 0000000000..a29ef67770
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser.ini
@@ -0,0 +1,37 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+[DEFAULT]
+support-files =
+ head.js
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+ subdialog.xhtml
+
+[browser_quicksuggest.js]
+[browser_quicksuggest_addons.js]
+[browser_quicksuggest_block.js]
+[browser_quicksuggest_configuration.js]
+[browser_quicksuggest_indexes.js]
+skip-if =
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[browser_quicksuggest_merinoSessions.js]
+[browser_quicksuggest_onboardingDialog.js]
+skip-if =
+ os == 'linux' && bits == 64 # Bug 1773830
+[browser_telemetry_dynamicWikipedia.js]
+tags = search-telemetry
+[browser_telemetry_impressionEdgeCases.js]
+tags = search-telemetry
+[browser_telemetry_navigationalSuggestions.js]
+tags = search-telemetry
+[browser_telemetry_nonsponsored.js]
+tags = search-telemetry
+[browser_telemetry_other.js]
+tags = search-telemetry
+[browser_telemetry_sponsored.js]
+tags = search-telemetry
+[browser_telemetry_weather.js]
+tags = search-telemetry
+[browser_weather.js]
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest.js
new file mode 100644
index 0000000000..d11f6d6386
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests browser quick suggestions.
+ */
+
+const TEST_URL = "http://example.com/quicksuggest";
+
+const REMOTE_SETTINGS_RESULTS = [
+ {
+ id: 1,
+ url: `${TEST_URL}?q=frabbits`,
+ title: "frabbits",
+ keywords: ["fra", "frab"],
+ click_url: "http://click.reporting.test.com/",
+ impression_url: "http://impression.reporting.test.com/",
+ advertiser: "TestAdvertiser",
+ },
+ {
+ id: 2,
+ url: `${TEST_URL}?q=nonsponsored`,
+ title: "Non-Sponsored",
+ keywords: ["nonspon"],
+ click_url: "http://click.reporting.test.com/nonsponsored",
+ impression_url: "http://impression.reporting.test.com/nonsponsored",
+ advertiser: "TestAdvertiserNonSponsored",
+ iab_category: "5 - Education",
+ },
+];
+
+add_setup(async function () {
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await UrlbarTestUtils.formHistory.clear();
+
+ await QuickSuggestTestUtils.ensureQuickSuggestInit({
+ remoteSettingsResults: [
+ {
+ type: "data",
+ attachment: REMOTE_SETTINGS_RESULTS,
+ },
+ ],
+ });
+});
+
+// Tests a sponsored result and keyword highlighting.
+add_task(async function sponsored() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "fra",
+ });
+ await QuickSuggestTestUtils.assertIsQuickSuggest({
+ window,
+ index: 1,
+ isSponsored: true,
+ url: `${TEST_URL}?q=frabbits`,
+ });
+ let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1);
+ Assert.equal(
+ row.querySelector(".urlbarView-title").firstChild.textContent,
+ "fra",
+ "The part of the keyword that matches users input is not bold."
+ );
+ Assert.equal(
+ row.querySelector(".urlbarView-title > strong").textContent,
+ "b",
+ "The auto completed section of the keyword is bolded."
+ );
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+// Tests a non-sponsored result.
+add_task(async function nonSponsored() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "nonspon",
+ });
+ await QuickSuggestTestUtils.assertIsQuickSuggest({
+ window,
+ index: 1,
+ isSponsored: false,
+ url: `${TEST_URL}?q=nonsponsored`,
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+});
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_addons.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_addons.js
new file mode 100644
index 0000000000..e0b75d0e9b
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_addons.js
@@ -0,0 +1,560 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for addon suggestions.
+
+const TEST_MERINO_SUGGESTIONS = [
+ {
+ provider: "amo",
+ icon: "https://example.com/first.svg",
+ url: "https://example.com/first-addon",
+ title: "First Addon",
+ description: "This is a first addon",
+ custom_details: {
+ amo: {
+ rating: "5",
+ number_of_ratings: "1234567",
+ guid: "first@addon",
+ },
+ },
+ is_top_pick: true,
+ },
+ {
+ provider: "amo",
+ icon: "https://example.com/second.png",
+ url: "https://example.com/second-addon",
+ title: "Second Addon",
+ description: "This is a second addon",
+ custom_details: {
+ amo: {
+ rating: "4.5",
+ number_of_ratings: "123",
+ guid: "second@addon",
+ },
+ },
+ is_sponsored: true,
+ is_top_pick: false,
+ },
+ {
+ provider: "amo",
+ icon: "https://example.com/third.svg",
+ url: "https://example.com/third-addon",
+ title: "Third Addon",
+ description: "This is a third addon",
+ custom_details: {
+ amo: {
+ rating: "0",
+ number_of_ratings: "0",
+ guid: "third@addon",
+ },
+ },
+ is_top_pick: false,
+ },
+ {
+ provider: "amo",
+ icon: "https://example.com/fourth.svg",
+ url: "https://example.com/fourth-addon",
+ title: "Fourth Addon",
+ description: "This is a fourth addon",
+ custom_details: {
+ amo: {
+ rating: "4",
+ number_of_ratings: "4",
+ guid: "fourth@addon",
+ },
+ },
+ },
+];
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.quicksuggest.enabled", true],
+ ["browser.urlbar.quicksuggest.remoteSettings.enabled", false],
+ ["browser.urlbar.bestMatch.enabled", true],
+ ],
+ });
+
+ await SearchTestUtils.installSearchExtension({}, { setAsDefault: true });
+
+ await QuickSuggestTestUtils.ensureQuickSuggestInit({
+ merinoSuggestions: TEST_MERINO_SUGGESTIONS,
+ });
+});
+
+add_task(async function basic() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.addons.featureGate", true]],
+ });
+
+ for (const merinoSuggestion of TEST_MERINO_SUGGESTIONS) {
+ MerinoTestUtils.server.response.body.suggestions = [merinoSuggestion];
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "only match the Merino suggestion",
+ });
+ Assert.equal(UrlbarTestUtils.getResultCount(window), 2);
+
+ const { element, result } = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ 1
+ );
+ const row = element.row;
+ const icon = row.querySelector(".urlbarView-dynamic-addons-icon");
+ Assert.equal(icon.src, merinoSuggestion.icon);
+ const url = row.querySelector(".urlbarView-dynamic-addons-url");
+ Assert.equal(url.textContent, merinoSuggestion.url);
+ const title = row.querySelector(".urlbarView-dynamic-addons-title");
+ Assert.equal(title.textContent, merinoSuggestion.title);
+ const description = row.querySelector(
+ ".urlbarView-dynamic-addons-description"
+ );
+ Assert.equal(description.textContent, merinoSuggestion.description);
+ const reviews = row.querySelector(".urlbarView-dynamic-addons-reviews");
+ Assert.equal(
+ reviews.textContent,
+ `${new Intl.NumberFormat().format(
+ Number(merinoSuggestion.custom_details.amo.number_of_ratings)
+ )} reviews`
+ );
+
+ const isTopPick = merinoSuggestion.is_top_pick ?? true;
+ if (isTopPick) {
+ Assert.equal(result.suggestedIndex, 1);
+ } else if (merinoSuggestion.is_sponsored) {
+ Assert.equal(
+ result.suggestedIndex,
+ UrlbarPrefs.get("quickSuggestSponsoredIndex")
+ );
+ } else {
+ Assert.equal(
+ result.suggestedIndex,
+ UrlbarPrefs.get("quickSuggestNonSponsoredIndex")
+ );
+ }
+
+ const onLoad = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ merinoSuggestion.url
+ );
+ EventUtils.synthesizeMouseAtCenter(row, {});
+ await onLoad;
+ Assert.ok(true, "Expected page is loaded");
+
+ await PlacesUtils.history.clear();
+ }
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function ratings() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.addons.featureGate", true]],
+ });
+
+ const testRating = [
+ "0",
+ "0.24",
+ "0.25",
+ "0.74",
+ "0.75",
+ "1",
+ "1.24",
+ "1.25",
+ "1.74",
+ "1.75",
+ "2",
+ "2.24",
+ "2.25",
+ "2.74",
+ "2.75",
+ "3",
+ "3.24",
+ "3.25",
+ "3.74",
+ "3.75",
+ "4",
+ "4.24",
+ "4.25",
+ "4.74",
+ "4.75",
+ "5",
+ ];
+ const baseMerinoSuggestion = JSON.parse(
+ JSON.stringify(TEST_MERINO_SUGGESTIONS[0])
+ );
+
+ for (const rating of testRating) {
+ baseMerinoSuggestion.custom_details.amo.rating = rating;
+ MerinoTestUtils.server.response.body.suggestions = [baseMerinoSuggestion];
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "only match the Merino suggestion",
+ });
+ Assert.equal(UrlbarTestUtils.getResultCount(window), 2);
+
+ const { element } = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+
+ const ratingElements = element.row.querySelectorAll(
+ ".urlbarView-dynamic-addons-rating"
+ );
+ Assert.equal(ratingElements.length, 5);
+
+ for (let i = 0; i < ratingElements.length; i++) {
+ const ratingElement = ratingElements[i];
+
+ const distanceToFull = Number(rating) - i;
+ let fill = "full";
+ if (distanceToFull < 0.25) {
+ fill = "empty";
+ } else if (distanceToFull < 0.75) {
+ fill = "half";
+ }
+ Assert.equal(ratingElement.getAttribute("fill"), fill);
+ }
+ }
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function disable() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.addons.featureGate", false]],
+ });
+
+ // Restore AdmWikipedia suggestions.
+ MerinoTestUtils.server.reset();
+ // Add one Addon suggestion that is higher score than AdmWikipedia.
+ MerinoTestUtils.server.response.body.suggestions.push(
+ Object.assign({}, TEST_MERINO_SUGGESTIONS[0], { score: 2 })
+ );
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "only match the Merino suggestion",
+ });
+ Assert.equal(UrlbarTestUtils.getResultCount(window), 2);
+
+ const { result } = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.payload.telemetryType, "adm_sponsored");
+
+ MerinoTestUtils.server.response.body.suggestions = TEST_MERINO_SUGGESTIONS;
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function resultMenu_showLessFrequently() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.addons.featureGate", true],
+ ["browser.urlbar.addons.showLessFrequentlyCount", 0],
+ ],
+ });
+
+ const cleanUpNimbus = await UrlbarTestUtils.initNimbusFeature({
+ addonsShowLessFrequentlyCap: 3,
+ });
+
+ // Sanity check.
+ Assert.equal(UrlbarPrefs.get("addonsShowLessFrequentlyCap"), 3);
+ Assert.equal(UrlbarPrefs.get("addons.showLessFrequentlyCount"), 0);
+
+ await doShowLessFrequently({
+ input: "aaa b",
+ expected: {
+ isSuggestionShown: true,
+ isMenuItemShown: true,
+ },
+ });
+ Assert.equal(UrlbarPrefs.get("addons.showLessFrequentlyCount"), 1);
+
+ await doShowLessFrequently({
+ input: "aaa b",
+ expected: {
+ isSuggestionShown: true,
+ isMenuItemShown: true,
+ },
+ });
+ Assert.equal(UrlbarPrefs.get("addons.showLessFrequentlyCount"), 2);
+
+ await doShowLessFrequently({
+ input: "aaa b",
+ expected: {
+ isSuggestionShown: true,
+ isMenuItemShown: true,
+ },
+ });
+ Assert.equal(UrlbarPrefs.get("addons.showLessFrequentlyCount"), 3);
+
+ await doShowLessFrequently({
+ input: "aaa b",
+ expected: {
+ // The suggestion should not display since addons.showLessFrequentlyCount
+ // is 3 and the substring (" b") after the first word ("aaa") is 2 chars
+ // long.
+ isSuggestionShown: false,
+ },
+ });
+
+ await doShowLessFrequently({
+ input: "aaa bb",
+ expected: {
+ // The suggestion should display, but item should not shown since the
+ // addons.showLessFrequentlyCount reached to addonsShowLessFrequentlyCap
+ // already.
+ isSuggestionShown: true,
+ isMenuItemShown: false,
+ },
+ });
+
+ await cleanUpNimbus();
+ await SpecialPowers.popPrefEnv();
+});
+
+// Tests the "Not interested" result menu dismissal command.
+add_task(async function resultMenu_notInterested() {
+ await doDismissTest("not_interested");
+});
+
+// Tests the "Not relevant" result menu dismissal command.
+add_task(async function notRelevant() {
+ await doDismissTest("not_relevant");
+});
+
+add_task(async function rowLabel() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.addons.featureGate", true]],
+ });
+
+ const testCases = [
+ {
+ bestMatch: true,
+ expected: "Firefox extension",
+ },
+ {
+ bestMatch: false,
+ expected: "Firefox Suggest",
+ },
+ ];
+
+ for (const { bestMatch, expected } of testCases) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.bestMatch.enabled", bestMatch]],
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "only match the Merino suggestion",
+ });
+ Assert.equal(UrlbarTestUtils.getResultCount(window), 2);
+
+ const { element } = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ const row = element.row;
+ Assert.equal(row.getAttribute("label"), expected);
+
+ await SpecialPowers.popPrefEnv();
+ }
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function treatmentB() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.addons.featureGate", true]],
+ });
+
+ const cleanUpNimbus = await UrlbarTestUtils.initNimbusFeature({
+ addonsUITreatment: "b",
+ });
+ // Sanity check.
+ Assert.equal(UrlbarPrefs.get("addonsUITreatment"), "b");
+
+ const merinoSuggestion = TEST_MERINO_SUGGESTIONS[0];
+ MerinoTestUtils.server.response.body.suggestions = [merinoSuggestion];
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "only match the Merino suggestion",
+ });
+ Assert.equal(UrlbarTestUtils.getResultCount(window), 2);
+
+ const { element } = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ const row = element.row;
+ const icon = row.querySelector(".urlbarView-dynamic-addons-icon");
+ Assert.equal(icon.src, merinoSuggestion.icon);
+ const url = row.querySelector(".urlbarView-dynamic-addons-url");
+ Assert.equal(url.textContent, merinoSuggestion.url);
+ const title = row.querySelector(".urlbarView-dynamic-addons-title");
+ Assert.equal(title.textContent, merinoSuggestion.title);
+ const description = row.querySelector(
+ ".urlbarView-dynamic-addons-description"
+ );
+ Assert.equal(description.textContent, merinoSuggestion.description);
+ const ratingContainer = row.querySelector(
+ ".urlbarView-dynamic-addons-ratingContainer"
+ );
+ Assert.ok(BrowserTestUtils.is_hidden(ratingContainer));
+ const reviews = row.querySelector(".urlbarView-dynamic-addons-reviews");
+ Assert.equal(reviews.textContent, "Recommended");
+
+ await cleanUpNimbus();
+ await SpecialPowers.popPrefEnv();
+});
+
+async function doShowLessFrequently({ input, expected }) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: input,
+ });
+
+ if (!expected.isSuggestionShown) {
+ for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) {
+ const details = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ Assert.notEqual(
+ details.result.payload.dynamicType,
+ "addons",
+ `Addons suggestion should be absent (checking index ${i})`
+ );
+ }
+
+ return;
+ }
+
+ const resultIndex = 1;
+ const details = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ resultIndex
+ );
+ Assert.equal(
+ details.result.payload.dynamicType,
+ "addons",
+ `Addons suggestion should be present at expected index after ${input} search`
+ );
+
+ // Click the command.
+ try {
+ await UrlbarTestUtils.openResultMenuAndClickItem(
+ window,
+ "show_less_frequently",
+ {
+ resultIndex,
+ }
+ );
+ Assert.ok(expected.isMenuItemShown);
+ Assert.ok(
+ gURLBar.view.isOpen,
+ "The view should remain open clicking the command"
+ );
+ Assert.ok(
+ details.element.row.hasAttribute("feedback-acknowledgment"),
+ "Row should have feedback acknowledgment after clicking command"
+ );
+ } catch (e) {
+ Assert.ok(!expected.isMenuItemShown);
+ Assert.ok(
+ !details.element.row.hasAttribute("feedback-acknowledgment"),
+ "Row should not have feedback acknowledgment after clicking command"
+ );
+ Assert.equal(
+ e.message,
+ "Menu item not found for command: show_less_frequently"
+ );
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window);
+}
+
+async function doDismissTest(command) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.addons.featureGate", true]],
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "123",
+ });
+
+ const resultCount = UrlbarTestUtils.getResultCount(window);
+ const resultIndex = 1;
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex);
+ Assert.equal(
+ details.result.payload.dynamicType,
+ "addons",
+ "Addons suggestion should be present"
+ );
+
+ // Sanity check.
+ Assert.ok(UrlbarPrefs.get("suggest.addons"));
+
+ // Click the command.
+ await UrlbarTestUtils.openResultMenuAndClickItem(
+ window,
+ ["[data-l10n-id=firefox-suggest-command-dont-show-this]", command],
+ { resultIndex, openByMouse: true }
+ );
+
+ Assert.ok(
+ !UrlbarPrefs.get("suggest.addons"),
+ "suggest.addons pref should be set to false after dismissal"
+ );
+
+ // The row should be a tip now.
+ Assert.ok(gURLBar.view.isOpen, "The view should remain open after dismissal");
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ resultCount,
+ "The result count should not haved changed after dismissal"
+ );
+ details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex);
+ Assert.equal(
+ details.type,
+ UrlbarUtils.RESULT_TYPE.TIP,
+ "Row should be a tip after dismissal"
+ );
+ Assert.equal(
+ details.result.payload.type,
+ "dismissalAcknowledgment",
+ "Tip type should be dismissalAcknowledgment"
+ );
+ Assert.ok(
+ !details.element.row.hasAttribute("feedback-acknowledgment"),
+ "Row should not have feedback acknowledgment after dismissal"
+ );
+
+ // Get the dismissal acknowledgment's "Got it" button and click it.
+ let gotItButton = UrlbarTestUtils.getButtonForResultIndex(
+ window,
+ "0",
+ resultIndex
+ );
+ Assert.ok(gotItButton, "Row should have a 'Got it' button");
+ EventUtils.synthesizeMouseAtCenter(gotItButton, {}, window);
+
+ // The view should remain open and the tip row should be gone.
+ Assert.ok(
+ gURLBar.view.isOpen,
+ "The view should remain open clicking the 'Got it' button"
+ );
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ resultCount - 1,
+ "The result count should be one less after clicking 'Got it' button"
+ );
+ for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) {
+ details = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ Assert.ok(
+ details.type != UrlbarUtils.RESULT_TYPE.TIP &&
+ details.result.payload.dynamicType !== "addons",
+ "Tip result and addon result should not be present"
+ );
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ await SpecialPowers.popPrefEnv();
+ UrlbarPrefs.clear("suggest.addons");
+}
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_block.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_block.js
new file mode 100644
index 0000000000..081818c02b
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_block.js
@@ -0,0 +1,445 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests blocking quick suggest results, including best matches. See also:
+//
+// browser_bestMatch.js
+// Includes tests for blocking best match rows independent of quick suggest,
+// especially the superficial UI part that should be common to all types of
+// best matches
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ CONTEXTUAL_SERVICES_PING_TYPES:
+ "resource:///modules/PartnerLinkAttribution.sys.mjs",
+});
+
+const { TELEMETRY_SCALARS } = UrlbarProviderQuickSuggest;
+const { TIMESTAMP_TEMPLATE } = QuickSuggest;
+
+// Include the timestamp template in the suggestion URLs so we can make sure
+// their original URLs with the unreplaced templates are blocked and not their
+// URLs with timestamps.
+const REMOTE_SETTINGS_RESULTS = [
+ {
+ id: 1,
+ url: `https://example.com/sponsored?t=${TIMESTAMP_TEMPLATE}`,
+ title: "Sponsored suggestion",
+ keywords: ["sponsored"],
+ click_url: "https://example.com/click",
+ impression_url: "https://example.com/impression",
+ advertiser: "TestAdvertiser",
+ iab_category: "22 - Shopping",
+ },
+ {
+ id: 2,
+ url: `https://example.com/nonsponsored?t=${TIMESTAMP_TEMPLATE}`,
+ title: "Non-sponsored suggestion",
+ keywords: ["nonsponsored"],
+ click_url: "https://example.com/click",
+ impression_url: "https://example.com/impression",
+ advertiser: "TestAdvertiser",
+ iab_category: "5 - Education",
+ },
+];
+
+// Spy for the custom impression/click sender
+let spy;
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.bestMatch.blockingEnabled", true],
+ ["browser.urlbar.quicksuggest.blockingEnabled", true],
+ ],
+ });
+
+ ({ spy } = QuickSuggestTestUtils.createTelemetryPingSpy());
+
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await UrlbarTestUtils.formHistory.clear();
+
+ await QuickSuggest.blockedSuggestions._test_readyPromise;
+ await QuickSuggest.blockedSuggestions.clear();
+
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+
+ await QuickSuggestTestUtils.ensureQuickSuggestInit({
+ remoteSettingsResults: [
+ {
+ type: "data",
+ attachment: REMOTE_SETTINGS_RESULTS,
+ },
+ ],
+ config: QuickSuggestTestUtils.BEST_MATCH_CONFIG,
+ });
+});
+
+/**
+ * Adds a test task that runs the given callback with combinations of the
+ * following:
+ *
+ * - Best match disabled and enabled
+ * - Each result in `REMOTE_SETTINGS_RESULTS`
+ *
+ * @param {Function} fn
+ * The callback function. It's passed: `{ isBestMatch, suggestion }`
+ */
+function add_combo_task(fn) {
+ let taskFn = async () => {
+ for (let isBestMatch of [false, true]) {
+ UrlbarPrefs.set("bestMatch.enabled", isBestMatch);
+ for (let result of REMOTE_SETTINGS_RESULTS) {
+ info(`Running ${fn.name}: ${JSON.stringify({ isBestMatch, result })}`);
+ await fn({ isBestMatch, result });
+ }
+ UrlbarPrefs.clear("bestMatch.enabled");
+ }
+ };
+ Object.defineProperty(taskFn, "name", { value: fn.name });
+ add_task(taskFn);
+}
+
+// Picks the block button with the keyboard.
+add_combo_task(async function basic_keyboard({ result, isBestMatch }) {
+ await doBasicBlockTest({
+ result,
+ isBestMatch,
+ block: async () => {
+ if (UrlbarPrefs.get("resultMenu")) {
+ await UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "D", {
+ resultIndex: 1,
+ });
+ } else {
+ // TAB twice to select the block button: once to select the main
+ // part of the row, once to select the block button.
+ EventUtils.synthesizeKey("KEY_Tab", { repeat: 2 });
+ EventUtils.synthesizeKey("KEY_Enter");
+ }
+ },
+ });
+});
+
+// Picks the block button with the mouse.
+add_combo_task(async function basic_mouse({ result, isBestMatch }) {
+ await doBasicBlockTest({
+ result,
+ isBestMatch,
+ block: async () => {
+ if (UrlbarPrefs.get("resultMenu")) {
+ await UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "D", {
+ resultIndex: 1,
+ openByMouse: true,
+ });
+ } else {
+ EventUtils.synthesizeMouseAtCenter(
+ UrlbarTestUtils.getButtonForResultIndex(window, "block", 1),
+ {}
+ );
+ }
+ },
+ });
+});
+
+// Uses the key shortcut to block a suggestion.
+add_combo_task(async function basic_keyShortcut({ result, isBestMatch }) {
+ await doBasicBlockTest({
+ result,
+ isBestMatch,
+ block: () => {
+ // Arrow down once to select the row.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_Delete", { shiftKey: true });
+ },
+ });
+});
+
+async function doBasicBlockTest({ result, isBestMatch, block }) {
+ spy.resetHistory();
+
+ // Do a search that triggers the suggestion.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: result.keywords[0],
+ });
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 2,
+ "Two rows are present after searching (heuristic + suggestion)"
+ );
+
+ let isSponsored = result.keywords[0] == "sponsored";
+ await QuickSuggestTestUtils.assertIsQuickSuggest({
+ window,
+ isBestMatch,
+ isSponsored,
+ originalUrl: result.url,
+ });
+
+ // Block the suggestion.
+ await block();
+
+ // The row should have been removed.
+ Assert.ok(
+ UrlbarTestUtils.isPopupOpen(window),
+ "View remains open after blocking result"
+ );
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 1,
+ "Only one row after blocking suggestion"
+ );
+ await QuickSuggestTestUtils.assertNoQuickSuggestResults(window);
+
+ // The URL should be blocked.
+ Assert.ok(
+ await QuickSuggest.blockedSuggestions.has(result.url),
+ "Suggestion is blocked"
+ );
+
+ // Check telemetry scalars.
+ let index = 2;
+ let scalars = {};
+ if (isSponsored) {
+ scalars[TELEMETRY_SCALARS.IMPRESSION_SPONSORED] = index;
+ scalars[TELEMETRY_SCALARS.BLOCK_SPONSORED] = index;
+ } else {
+ scalars[TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED] = index;
+ scalars[TELEMETRY_SCALARS.BLOCK_NONSPONSORED] = index;
+ }
+ if (isBestMatch) {
+ if (isSponsored) {
+ scalars = {
+ ...scalars,
+ [TELEMETRY_SCALARS.IMPRESSION_SPONSORED_BEST_MATCH]: index,
+ [TELEMETRY_SCALARS.BLOCK_SPONSORED_BEST_MATCH]: index,
+ };
+ } else {
+ scalars = {
+ ...scalars,
+ [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED_BEST_MATCH]: index,
+ [TELEMETRY_SCALARS.BLOCK_NONSPONSORED_BEST_MATCH]: index,
+ };
+ }
+ }
+ QuickSuggestTestUtils.assertScalars(scalars);
+
+ // Check the engagement event.
+ let match_type = isBestMatch ? "best-match" : "firefox-suggest";
+ QuickSuggestTestUtils.assertEvents([
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "block",
+ extra: {
+ match_type,
+ position: String(index),
+ suggestion_type: isSponsored ? "sponsored" : "nonsponsored",
+ },
+ },
+ ]);
+
+ // Check the custom telemetry pings.
+ QuickSuggestTestUtils.assertPings(spy, [
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION,
+ payload: {
+ match_type,
+ block_id: result.id,
+ is_clicked: false,
+ position: index,
+ },
+ },
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_BLOCK,
+ payload: {
+ match_type,
+ block_id: result.id,
+ iab_category: result.iab_category,
+ position: index,
+ },
+ },
+ ]);
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ await QuickSuggest.blockedSuggestions.clear();
+}
+
+// Blocks multiple suggestions one after the other.
+add_task(async function blockMultiple() {
+ for (let isBestMatch of [false, true]) {
+ UrlbarPrefs.set("bestMatch.enabled", isBestMatch);
+ info(`Testing with best match enabled: ${isBestMatch}`);
+
+ for (let i = 0; i < REMOTE_SETTINGS_RESULTS.length; i++) {
+ // Do a search that triggers the i'th suggestion.
+ let { keywords, url } = REMOTE_SETTINGS_RESULTS[i];
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: keywords[0],
+ });
+ await QuickSuggestTestUtils.assertIsQuickSuggest({
+ window,
+ isBestMatch,
+ originalUrl: url,
+ isSponsored: keywords[0] == "sponsored",
+ });
+
+ // Block it.
+ if (UrlbarPrefs.get("resultMenu")) {
+ await UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "D", {
+ resultIndex: 1,
+ });
+ } else {
+ EventUtils.synthesizeKey("KEY_Tab", { repeat: 2 });
+ EventUtils.synthesizeKey("KEY_Enter");
+ }
+ Assert.ok(
+ await QuickSuggest.blockedSuggestions.has(url),
+ "Suggestion is blocked after picking block button"
+ );
+
+ // Make sure all previous suggestions remain blocked and no other
+ // suggestions are blocked yet.
+ for (let j = 0; j < REMOTE_SETTINGS_RESULTS.length; j++) {
+ Assert.equal(
+ await QuickSuggest.blockedSuggestions.has(
+ REMOTE_SETTINGS_RESULTS[j].url
+ ),
+ j <= i,
+ `Suggestion at index ${j} is blocked or not as expected`
+ );
+ }
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ await QuickSuggest.blockedSuggestions.clear();
+ UrlbarPrefs.clear("bestMatch.enabled");
+ }
+});
+
+// Tests with blocking disabled for both best matches and non-best-matches.
+add_combo_task(async function disabled_both({ result, isBestMatch }) {
+ await doDisabledTest({
+ result,
+ isBestMatch,
+ quickSuggestBlockingEnabled: false,
+ bestMatchBlockingEnabled: false,
+ });
+});
+
+// Tests with blocking disabled only for non-best-matches.
+add_combo_task(async function disabled_quickSuggest({ result, isBestMatch }) {
+ await doDisabledTest({
+ result,
+ isBestMatch,
+ quickSuggestBlockingEnabled: false,
+ bestMatchBlockingEnabled: true,
+ });
+});
+
+// Tests with blocking disabled only for best matches.
+add_combo_task(async function disabled_bestMatch({ result, isBestMatch }) {
+ await doDisabledTest({
+ result,
+ isBestMatch,
+ quickSuggestBlockingEnabled: true,
+ bestMatchBlockingEnabled: false,
+ });
+});
+
+async function doDisabledTest({
+ result,
+ isBestMatch,
+ bestMatchBlockingEnabled,
+ quickSuggestBlockingEnabled,
+}) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.bestMatch.blockingEnabled", bestMatchBlockingEnabled],
+ [
+ "browser.urlbar.quicksuggest.blockingEnabled",
+ quickSuggestBlockingEnabled,
+ ],
+ ],
+ });
+
+ // Do a search to show a suggestion.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: result.keywords[0],
+ });
+ let expectedResultCount = 2;
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ expectedResultCount,
+ "Two rows are present after searching (heuristic + suggestion)"
+ );
+ let details = await QuickSuggestTestUtils.assertIsQuickSuggest({
+ window,
+ isBestMatch,
+ originalUrl: result.url,
+ isSponsored: result.keywords[0] == "sponsored",
+ });
+
+ // Arrow down to select the suggestion and press the key shortcut to block.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_Delete", { shiftKey: true });
+ Assert.ok(
+ UrlbarTestUtils.isPopupOpen(window),
+ "View remains open after trying to block result"
+ );
+
+ if (
+ (isBestMatch && !bestMatchBlockingEnabled) ||
+ (!isBestMatch && !quickSuggestBlockingEnabled)
+ ) {
+ // Blocking is disabled. The key shortcut shouldn't have done anything.
+ if (!UrlbarPrefs.get("resultMenu")) {
+ Assert.ok(
+ !details.element.row._buttons.get("block"),
+ "Block button is not present"
+ );
+ }
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ expectedResultCount,
+ "Same number of results after key shortcut"
+ );
+ await QuickSuggestTestUtils.assertIsQuickSuggest({
+ window,
+ isBestMatch,
+ originalUrl: result.url,
+ isSponsored: result.keywords[0] == "sponsored",
+ });
+ Assert.ok(
+ !(await QuickSuggest.blockedSuggestions.has(result.url)),
+ "Suggestion is not blocked"
+ );
+ } else {
+ // Blocking is enabled. The suggestion should have been blocked.
+ if (!UrlbarPrefs.get("resultMenu")) {
+ Assert.ok(
+ details.element.row._buttons.get("block"),
+ "Block button is present"
+ );
+ }
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 1,
+ "Only one row after blocking suggestion"
+ );
+ await QuickSuggestTestUtils.assertNoQuickSuggestResults(window);
+ Assert.ok(
+ await QuickSuggest.blockedSuggestions.has(result.url),
+ "Suggestion is blocked"
+ );
+ await QuickSuggest.blockedSuggestions.clear();
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ await SpecialPowers.popPrefEnv();
+}
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_configuration.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_configuration.js
new file mode 100644
index 0000000000..066ffecd51
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_configuration.js
@@ -0,0 +1,2101 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests QuickSuggest configurations.
+ */
+
+ChromeUtils.defineESModuleGetters(this, {
+ EnterprisePolicyTesting:
+ "resource://testing-common/EnterprisePolicyTesting.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+// We use this pref in enterprise preference policy tests. We specifically use a
+// pref that's sticky and exposed in the UI to make sure it can be set properly.
+const POLICY_PREF = "suggest.quicksuggest.nonsponsored";
+
+let gDefaultBranch = Services.prefs.getDefaultBranch("browser.urlbar.");
+let gUserBranch = Services.prefs.getBranch("browser.urlbar.");
+
+add_setup(async function () {
+ await QuickSuggestTestUtils.ensureQuickSuggestInit();
+});
+
+// Makes sure `QuickSuggest._updateFeatureState()` is called when the
+// `browser.urlbar.quicksuggest.enabled` pref is changed.
+add_task(async function test_updateFeatureState_pref() {
+ Assert.ok(
+ UrlbarPrefs.get("quicksuggest.enabled"),
+ "Sanity check: quicksuggest.enabled is true by default"
+ );
+
+ let sandbox = sinon.createSandbox();
+ let spy = sandbox.spy(QuickSuggest, "_updateFeatureState");
+
+ UrlbarPrefs.set("quicksuggest.enabled", false);
+ Assert.equal(
+ spy.callCount,
+ 1,
+ "_updateFeatureState called once after changing pref"
+ );
+
+ UrlbarPrefs.clear("quicksuggest.enabled");
+ Assert.equal(
+ spy.callCount,
+ 2,
+ "_updateFeatureState called again after clearing pref"
+ );
+
+ sandbox.restore();
+});
+
+// Makes sure `QuickSuggest._updateFeatureState()` is called when a Nimbus
+// experiment is installed and uninstalled.
+add_task(async function test_updateFeatureState_experiment() {
+ let sandbox = sinon.createSandbox();
+ let spy = sandbox.spy(QuickSuggest, "_updateFeatureState");
+
+ await QuickSuggestTestUtils.withExperiment({
+ callback: () => {
+ Assert.equal(
+ spy.callCount,
+ 1,
+ "_updateFeatureState called once after installing experiment"
+ );
+ },
+ });
+
+ Assert.equal(
+ spy.callCount,
+ 2,
+ "_updateFeatureState called again after uninstalling experiment"
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_indexes() {
+ await QuickSuggestTestUtils.withExperiment({
+ valueOverrides: {
+ quickSuggestNonSponsoredIndex: 99,
+ quickSuggestSponsoredIndex: -1337,
+ },
+ callback: () => {
+ Assert.equal(
+ UrlbarPrefs.get("quickSuggestNonSponsoredIndex"),
+ 99,
+ "quickSuggestNonSponsoredIndex"
+ );
+ Assert.equal(
+ UrlbarPrefs.get("quickSuggestSponsoredIndex"),
+ -1337,
+ "quickSuggestSponsoredIndex"
+ );
+ },
+ });
+});
+
+add_task(async function test_merino() {
+ await QuickSuggestTestUtils.withExperiment({
+ valueOverrides: {
+ merinoEnabled: true,
+ merinoEndpointURL: "http://example.com/test_merino_config",
+ merinoClientVariants: "test-client-variants",
+ merinoProviders: "test-providers",
+ },
+ callback: () => {
+ Assert.equal(UrlbarPrefs.get("merinoEnabled"), true, "merinoEnabled");
+ Assert.equal(
+ UrlbarPrefs.get("merinoEndpointURL"),
+ "http://example.com/test_merino_config",
+ "merinoEndpointURL"
+ );
+ Assert.equal(
+ UrlbarPrefs.get("merinoClientVariants"),
+ "test-client-variants",
+ "merinoClientVariants"
+ );
+ Assert.equal(
+ UrlbarPrefs.get("merinoProviders"),
+ "test-providers",
+ "merinoProviders"
+ );
+ },
+ });
+});
+
+add_task(async function test_scenario_online() {
+ await doBasicScenarioTest("online", {
+ urlbarPrefs: {
+ // prefs
+ "quicksuggest.scenario": "online",
+ "quicksuggest.enabled": true,
+ "quicksuggest.dataCollection.enabled": false,
+ "quicksuggest.shouldShowOnboardingDialog": true,
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+
+ // Nimbus variables
+ quickSuggestScenario: "online",
+ quickSuggestEnabled: true,
+ quickSuggestShouldShowOnboardingDialog: true,
+ },
+ defaults: [
+ {
+ name: "browser.urlbar.quicksuggest.enabled",
+ value: true,
+ },
+ {
+ name: "browser.urlbar.quicksuggest.dataCollection.enabled",
+ value: false,
+ },
+ {
+ name: "browser.urlbar.quicksuggest.shouldShowOnboardingDialog",
+ value: true,
+ },
+ {
+ name: "browser.urlbar.suggest.quicksuggest.nonsponsored",
+ value: true,
+ },
+ {
+ name: "browser.urlbar.suggest.quicksuggest.sponsored",
+ value: true,
+ },
+ ],
+ });
+});
+
+add_task(async function test_scenario_offline() {
+ await doBasicScenarioTest("offline", {
+ urlbarPrefs: {
+ // prefs
+ "quicksuggest.scenario": "offline",
+ "quicksuggest.enabled": true,
+ "quicksuggest.dataCollection.enabled": false,
+ "quicksuggest.shouldShowOnboardingDialog": false,
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+
+ // Nimbus variables
+ quickSuggestScenario: "offline",
+ quickSuggestEnabled: true,
+ quickSuggestShouldShowOnboardingDialog: false,
+ },
+ defaults: [
+ {
+ name: "browser.urlbar.quicksuggest.enabled",
+ value: true,
+ },
+ {
+ name: "browser.urlbar.quicksuggest.dataCollection.enabled",
+ value: false,
+ },
+ {
+ name: "browser.urlbar.quicksuggest.shouldShowOnboardingDialog",
+ value: false,
+ },
+ {
+ name: "browser.urlbar.suggest.quicksuggest.nonsponsored",
+ value: true,
+ },
+ {
+ name: "browser.urlbar.suggest.quicksuggest.sponsored",
+ value: true,
+ },
+ ],
+ });
+});
+
+add_task(async function test_scenario_history() {
+ await doBasicScenarioTest("history", {
+ urlbarPrefs: {
+ // prefs
+ "quicksuggest.scenario": "history",
+ "quicksuggest.enabled": false,
+
+ // Nimbus variables
+ quickSuggestScenario: "history",
+ quickSuggestEnabled: false,
+ },
+ defaults: [
+ {
+ name: "browser.urlbar.quicksuggest.enabled",
+ value: false,
+ },
+ ],
+ });
+});
+
+async function doBasicScenarioTest(scenario, expectedPrefs) {
+ await QuickSuggestTestUtils.withExperiment({
+ valueOverrides: {
+ quickSuggestScenario: scenario,
+ },
+ callback: () => {
+ // Pref updates should always settle down by the time enrollment is done.
+ Assert.ok(
+ !UrlbarPrefs.updatingFirefoxSuggestPrefs,
+ "updatingFirefoxSuggestPrefs is false"
+ );
+
+ assertScenarioPrefs(expectedPrefs);
+ },
+ });
+
+ // Similarly, pref updates should always settle down by the time unenrollment
+ // is done.
+ Assert.ok(
+ !UrlbarPrefs.updatingFirefoxSuggestPrefs,
+ "updatingFirefoxSuggestPrefs is false"
+ );
+
+ assertDefaultScenarioPrefs();
+}
+
+function assertScenarioPrefs({ urlbarPrefs, defaults }) {
+ for (let [name, value] of Object.entries(urlbarPrefs)) {
+ Assert.equal(UrlbarPrefs.get(name), value, `UrlbarPrefs.get("${name}")`);
+ }
+
+ let prefs = Services.prefs.getDefaultBranch("");
+ for (let { name, getter, value } of defaults) {
+ Assert.equal(
+ prefs[getter || "getBoolPref"](name),
+ value,
+ `Default branch pref: ${name}`
+ );
+ }
+}
+
+function assertDefaultScenarioPrefs() {
+ assertScenarioPrefs({
+ urlbarPrefs: {
+ "quicksuggest.scenario": "offline",
+ "quicksuggest.enabled": true,
+ "quicksuggest.dataCollection.enabled": false,
+ "quicksuggest.shouldShowOnboardingDialog": false,
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+
+ // No Nimbus variables since they're only available when an experiment is
+ // installed.
+ },
+ defaults: [
+ {
+ name: "browser.urlbar.quicksuggest.enabled",
+ value: true,
+ },
+ {
+ name: "browser.urlbar.quicksuggest.dataCollection.enabled",
+ value: false,
+ },
+ {
+ name: "browser.urlbar.quicksuggest.shouldShowOnboardingDialog",
+ value: false,
+ },
+ {
+ name: "browser.urlbar.suggest.quicksuggest.nonsponsored",
+ value: true,
+ },
+ {
+ name: "browser.urlbar.suggest.quicksuggest.sponsored",
+ value: true,
+ },
+ ],
+ });
+}
+
+function clearOnboardingPrefs() {
+ UrlbarPrefs.clear("suggest.quicksuggest.nonsponsored");
+ UrlbarPrefs.clear("suggest.quicksuggest.sponsored");
+ UrlbarPrefs.clear("quicksuggest.dataCollection.enabled");
+ UrlbarPrefs.clear("quicksuggest.shouldShowOnboardingDialog");
+ UrlbarPrefs.clear("quicksuggest.showedOnboardingDialog");
+ UrlbarPrefs.clear("quicksuggest.seenRestarts");
+}
+
+// The following tasks test Nimbus enrollments
+
+// Initial state:
+// * History (quick suggest feature disabled)
+//
+// Enrollment:
+// * History
+//
+// Expected:
+// * All history prefs set on the default branch
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.history,
+ },
+ valueOverrides: {
+ quickSuggestScenario: "history",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.history,
+ },
+ });
+});
+
+// Initial state:
+// * History (quick suggest feature disabled)
+//
+// Enrollment:
+// * Offline
+//
+// Expected:
+// * All offline prefs set on the default branch
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.history,
+ },
+ valueOverrides: {
+ quickSuggestScenario: "offline",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ },
+ });
+});
+
+// Initial state:
+// * History (quick suggest feature disabled)
+//
+// Enrollment:
+// * Online
+//
+// Expected:
+// * All online prefs set on the default branch
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.history,
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ },
+ });
+});
+
+// The following tasks test OFFLINE TO OFFLINE
+
+// Initial state:
+// * Offline (suggestions on and data collection off by default)
+// * User did not override any defaults
+//
+// Enrollment:
+// * Offline
+//
+// Expected:
+// * All offline prefs set on the default branch
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ },
+ valueOverrides: {
+ quickSuggestScenario: "offline",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ },
+ });
+});
+
+// Initial state:
+// * Offline (suggestions on and data collection off by default)
+// * Non-sponsored suggestions: user left on
+// * Sponsored suggestions: user turned off
+// * Data collection: user left off
+//
+// Enrollment:
+// * Offline
+//
+// Expected:
+// * Non-sponsored suggestions: remain on
+// * Sponsored suggestions: remain off
+// * Data collection: remains off
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "offline",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Offline (suggestions on and data collection off by default)
+// * Non-sponsored suggestions: user turned off
+// * Sponsored suggestions: user left on
+// * Data collection: user left off
+//
+// Enrollment:
+// * Offline
+//
+// Expected:
+// * Non-sponsored suggestions: remain off
+// * Sponsored suggestions: remain on
+// * Data collection: remains off
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "offline",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Offline (suggestions on and data collection off by default)
+// * Non-sponsored suggestions: user turned off
+// * Sponsored suggestions: user turned off
+// * Data collection: user left off
+//
+// Enrollment:
+// * Offline
+//
+// Expected:
+// * Non-sponsored suggestions: remain off
+// * Sponsored suggestions: remain off
+// * Data collection: remains off
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "offline",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Offline (suggestions on and data collection off by default)
+// * Non-sponsored suggestions: user left on
+// * Sponsored suggestions: user left on
+// * Data collection: user turned on
+//
+// Enrollment:
+// * Offline
+//
+// Expected:
+// * Non-sponsored suggestions: remain on
+// * Sponsored suggestions: remain on
+// * Data collection: remains on
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ userBranch: {
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "offline",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ userBranch: {
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Offline (suggestions on and data collection off by default)
+// * Non-sponsored suggestions: user turned off
+// * Sponsored suggestions: user turned off
+// * Data collection: user turned on
+//
+// Enrollment:
+// * Offline
+//
+// Expected:
+// * Non-sponsored suggestions: remain off
+// * Sponsored suggestions: remain off
+// * Data collection: remains on
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "offline",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ });
+});
+
+// The following tasks test OFFLINE TO ONLINE
+
+// Initial state:
+// * Offline (suggestions on and data collection off by default)
+// * User did not override any defaults
+//
+// Enrollment:
+// * Online
+//
+// Expected:
+// * All online prefs set on the default branch
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ },
+ });
+});
+
+// Initial state:
+// * Offline (suggestions on and data collection off by default)
+// * Non-sponsored suggestions: user left on
+// * Sponsored suggestions: user turned off
+// * Data collection: user left off
+//
+// Enrollment:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: remain on
+// * Sponsored suggestions: remain off
+// * Data collection: remains off
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Offline (suggestions on and data collection off by default)
+// * Non-sponsored suggestions: user turned off
+// * Sponsored suggestions: user left on
+// * Data collection: user left off
+//
+// Enrollment:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: remain off
+// * Sponsored suggestions: remain on
+// * Data collection: remains off
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Offline (suggestions on and data collection off by default)
+// * Non-sponsored suggestions: user turned off
+// * Sponsored suggestions: user turned off
+// * Data collection: user left off
+//
+// Enrollment:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: remain off
+// * Sponsored suggestions: remain off
+// * Data collection: remains off
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Offline (suggestions on and data collection off by default)
+// * Non-sponsored suggestions: user left on
+// * Sponsored suggestions: user left on
+// * Data collection: user turned on
+//
+// Enrollment:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: remain on
+// * Sponsored suggestions: remain on
+// * Data collection: remains on
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ userBranch: {
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Offline (suggestions on and data collection off by default)
+// * Non-sponsored suggestions: user left on
+// * Sponsored suggestions: user turned off
+// * Data collection: user turned on
+//
+// Enrollment:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: remain on
+// * Sponsored suggestions: remain off
+// * Data collection: remains on
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.sponsored": false,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.sponsored": false,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Offline (suggestions on and data collection off by default)
+// * Non-sponsored suggestions: user turned off
+// * Sponsored suggestions: user left on
+// * Data collection: user turned on
+//
+// Enrollment:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: remain off
+// * Sponsored suggestions: remain on
+// * Data collection: remains on
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Offline (suggestions on and data collection off by default)
+// * Non-sponsored suggestions: user turned off
+// * Sponsored suggestions: user turned off
+// * Data collection: user turned on
+//
+// Enrollment:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: remain off
+// * Sponsored suggestions: remain off
+// * Data collection: remains on
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ });
+});
+
+// The following tasks test ONLINE TO ONLINE
+
+// Initial state:
+// * Online (suggestions on and data collection off by default)
+// * User did not override any defaults
+//
+// Enrollment:
+// * Online
+//
+// Expected:
+// * All online prefs set on the default branch
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ },
+ });
+});
+
+// Initial state:
+// * Online (suggestions on and data collection off by default)
+// * Non-sponsored suggestions: user left on
+// * Sponsored suggestions: user turned off
+// * Data collection: user left off
+//
+// Enrollment:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: remain on
+// * Sponsored suggestions: remain off
+// * Data collection: remains off
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Online (suggestions on and data collection off by default)
+// * Non-sponsored suggestions: user turned off
+// * Sponsored suggestions: user left on
+// * Data collection: user left off
+//
+// Enrollment:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: remain off
+// * Sponsored suggestions: remain on
+// * Data collection: remains off
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Online (suggestions on and data collection off by default)
+// * Non-sponsored suggestions: user turned off
+// * Sponsored suggestions: user turned off
+// * Data collection: user left off
+//
+// Enrollment:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: remain off
+// * Sponsored suggestions: remain off
+// * Data collection: remains off
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Online (suggestions on and data collection off by default)
+// * Non-sponsored suggestions: user left on
+// * Sponsored suggestions: user left on
+// * Data collection: user turned on
+//
+// Enrollment:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: remain on
+// * Sponsored suggestions: remain on
+// * Data collection: remains on
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Online (suggestions on and data collection off by default)
+// * Non-sponsored suggestions: user left on
+// * Sponsored suggestions: user turned off
+// * Data collection: user turned on
+//
+// Enrollment:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: remain on
+// * Sponsored suggestions: remain off
+// * Data collection: remains on
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.sponsored": false,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.sponsored": false,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Online (suggestions on and data collection off by default)
+// * Non-sponsored suggestions: user turned off
+// * Sponsored suggestions: user left on
+// * Data collection: user turned on
+//
+// Enrollment:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: remain off
+// * Sponsored suggestions: remain on
+// * Data collection: remains on
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Online (suggestions on and data collection off by default)
+// * Non-sponsored suggestions: user turned off
+// * Sponsored suggestions: user turned off
+// * Data collection: user turned on
+//
+// Enrollment:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: remain off
+// * Sponsored suggestions: remain off
+// * Data collection: remains on
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ });
+});
+
+// The following tasks test scenarios in conjunction with individual Nimbus
+// variables
+
+// Initial state:
+// * Offline (suggestions on and data collection off by default)
+// * User did not override any defaults
+//
+// Enrollment:
+// * Offline
+// * Sponsored suggestions individually forced on
+//
+// Expected:
+// * Sponsored suggestions: on
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ },
+ valueOverrides: {
+ quickSuggestScenario: "offline",
+ quickSuggestSponsoredEnabled: true,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ ...UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ "suggest.quicksuggest.sponsored": true,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Offline (suggestions on and data collection off by default)
+// * Sponsored suggestions: user turned off
+//
+// Enrollment:
+// * Offline
+// * Sponsored suggestions individually forced on
+//
+// Expected:
+// * Sponsored suggestions: remain off
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "offline",
+ quickSuggestSponsoredEnabled: true,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ ...UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ "suggest.quicksuggest.sponsored": true,
+ },
+ userBranch: {
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Offline (suggestions on and data collection off by default)
+// * User did not override any defaults
+//
+// Enrollment:
+// * Offline
+// * Data collection individually forced on
+//
+// Expected:
+// * Data collection: on
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ },
+ valueOverrides: {
+ quickSuggestScenario: "offline",
+ quickSuggestDataCollectionEnabled: true,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ ...UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Offline (suggestions on and data collection off by default)
+// * Data collection: user turned off (it's off by default, so this simulates
+// when the user toggled it on and then back off)
+//
+// Enrollment:
+// * Offline
+// * Data collection individually forced on
+//
+// Expected:
+// * Data collection: remains off
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ userBranch: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "offline",
+ quickSuggestDataCollectionEnabled: true,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ ...UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ userBranch: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Online (suggestions on and data collection off by default)
+// * User did not override any defaults
+//
+// Enrollment:
+// * Online
+// * Sponsored suggestions individually forced off
+//
+// Expected:
+// * Sponsored suggestions: off
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ quickSuggestSponsoredEnabled: false,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ ...UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Online (suggestions on and data collection off by default)
+// * Sponsored suggestions: user turned on (they're on by default, so this
+// simulates when the user toggled them off and then back on)
+//
+// Enrollment:
+// * Online
+// * Sponsored suggestions individually forced off
+//
+// Expected:
+// * Sponsored suggestions: remain on
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.sponsored": true,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ quickSuggestSponsoredEnabled: false,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ ...UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ userBranch: {
+ "suggest.quicksuggest.sponsored": true,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Online (suggestions on and data collection off by default)
+// * User did not override any defaults
+//
+// Enrollment:
+// * Online
+// * Data collection individually forced on
+//
+// Expected:
+// * Data collection: on
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ quickSuggestDataCollectionEnabled: true,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ ...UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Online (suggestions on and data collection off by default)
+// * Data collection: user turned off (it's off by default, so this simulates
+// when the user toggled it on and then back off)
+//
+// Enrollment:
+// * Online
+// * Data collection individually forced on
+//
+// Expected:
+// * Data collection: remains off
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ quickSuggestDataCollectionEnabled: true,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ ...UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ userBranch: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ },
+ });
+});
+
+// The following tasks test individual Nimbus variables without scenarios
+
+// Initial state:
+// * Suggestions on by default and user left them on
+//
+// 1. First enrollment:
+// * Suggestions forced off
+//
+// Expected:
+// * Suggestions off
+//
+// 2. User turns on suggestions
+// 3. Second enrollment:
+// * Suggestions forced off again
+//
+// Expected:
+// * Suggestions remain on
+add_task(async function () {
+ await checkEnrollments([
+ {
+ initialPrefsToSet: {
+ defaultBranch: {
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+ },
+ },
+ valueOverrides: {
+ quickSuggestNonSponsoredEnabled: false,
+ quickSuggestSponsoredEnabled: false,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ },
+ {
+ initialPrefsToSet: {
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+ },
+ },
+ valueOverrides: {
+ quickSuggestNonSponsoredEnabled: false,
+ quickSuggestSponsoredEnabled: false,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+ },
+ },
+ },
+ ]);
+});
+
+// Initial state:
+// * Suggestions on by default but user turned them off
+//
+// Enrollment:
+// * Suggestions forced on
+//
+// Expected:
+// * Suggestions remain off
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: {
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+ },
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ valueOverrides: {
+ quickSuggestNonSponsoredEnabled: true,
+ quickSuggestSponsoredEnabled: true,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+ },
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Suggestions off by default and user left them off
+//
+// 1. First enrollment:
+// * Suggestions forced on
+//
+// Expected:
+// * Suggestions on
+//
+// 2. User turns off suggestions
+// 3. Second enrollment:
+// * Suggestions forced on again
+//
+// Expected:
+// * Suggestions remain off
+add_task(async function () {
+ await checkEnrollments([
+ {
+ initialPrefsToSet: {
+ defaultBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ valueOverrides: {
+ quickSuggestNonSponsoredEnabled: true,
+ quickSuggestSponsoredEnabled: true,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+ },
+ },
+ },
+ {
+ initialPrefsToSet: {
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ valueOverrides: {
+ quickSuggestNonSponsoredEnabled: true,
+ quickSuggestSponsoredEnabled: true,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+ },
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ },
+ ]);
+});
+
+// Initial state:
+// * Suggestions off by default but user turned them on
+//
+// Enrollment:
+// * Suggestions forced off
+//
+// Expected:
+// * Suggestions remain on
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+ },
+ },
+ valueOverrides: {
+ quickSuggestNonSponsoredEnabled: false,
+ quickSuggestSponsoredEnabled: false,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Data collection on by default and user left them on
+//
+// 1. First enrollment:
+// * Data collection forced off
+//
+// Expected:
+// * Data collection off
+//
+// 2. User turns on data collection
+// 3. Second enrollment:
+// * Data collection forced off again
+//
+// Expected:
+// * Data collection remains on
+add_task(async function () {
+ await checkEnrollments(
+ [
+ {
+ initialPrefsToSet: {
+ defaultBranch: {
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ valueOverrides: {
+ quickSuggestDataCollectionEnabled: false,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ },
+ },
+ ],
+ [
+ {
+ initialPrefsToSet: {
+ userBranch: {
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ valueOverrides: {
+ quickSuggestDataCollectionEnabled: false,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ userBranch: {
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ },
+ ]
+ );
+});
+
+// Initial state:
+// * Data collection on by default but user turned it off
+//
+// Enrollment:
+// * Data collection forced on
+//
+// Expected:
+// * Data collection remains off
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: {
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ userBranch: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ },
+ valueOverrides: {
+ quickSuggestDataCollectionEnabled: true,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ userBranch: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Data collection off by default and user left it off
+//
+// 1. First enrollment:
+// * Data collection forced on
+//
+// Expected:
+// * Data collection on
+//
+// 2. User turns off data collection
+// 3. Second enrollment:
+// * Data collection forced on again
+//
+// Expected:
+// * Data collection remains off
+add_task(async function () {
+ await checkEnrollments(
+ [
+ {
+ initialPrefsToSet: {
+ defaultBranch: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ },
+ valueOverrides: {
+ quickSuggestDataCollectionEnabled: true,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ },
+ ],
+ [
+ {
+ initialPrefsToSet: {
+ userBranch: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ },
+ valueOverrides: {
+ quickSuggestDataCollectionEnabled: true,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ userBranch: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ },
+ },
+ ]
+ );
+});
+
+// Initial state:
+// * Data collection off by default but user turned it on
+//
+// Enrollment:
+// * Data collection forced off
+//
+// Expected:
+// * Data collection remains on
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ userBranch: {
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ valueOverrides: {
+ quickSuggestDataCollectionEnabled: false,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ userBranch: {
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ });
+});
+
+/**
+ * Tests one or more enrollments. Sets an initial set of prefs on the default
+ * and/or user branches, enrolls in a mock Nimbus experiment, checks expected
+ * pref values, unenrolls, and finally checks prefs again.
+ *
+ * The given `options` value may be an object as described below or an array of
+ * such objects, one per enrollment.
+ *
+ * @param {object} options
+ * Function options.
+ * @param {object} options.initialPrefsToSet
+ * An object: { userBranch, defaultBranch }
+ * `userBranch` and `defaultBranch` are objects that map pref names (relative
+ * to `browser.urlbar`) to values. These prefs will be set on the appropriate
+ * branch before enrollment. Both `userBranch` and `defaultBranch` are
+ * optional.
+ * @param {object} options.valueOverrides
+ * The `valueOverrides` object passed to the mock experiment. It should map
+ * Nimbus variable names to values.
+ * @param {object} options.expectedPrefs
+ * Preferences that should be set after enrollment. It has the same shape as
+ * `options.initialPrefsToSet`.
+ */
+async function checkEnrollments(options) {
+ info("Testing: " + JSON.stringify(options));
+
+ let enrollments;
+ if (Array.isArray(options)) {
+ enrollments = options;
+ } else {
+ enrollments = [options];
+ }
+
+ // Do each enrollment.
+ for (let i = 0; i < enrollments.length; i++) {
+ info(
+ `Starting setup for enrollment ${i}: ` + JSON.stringify(enrollments[i])
+ );
+
+ let { initialPrefsToSet, valueOverrides, expectedPrefs } = enrollments[i];
+
+ // Set initial prefs.
+ UrlbarPrefs._updatingFirefoxSuggestScenario = true;
+ let { defaultBranch: initialDefaultBranch, userBranch: initialUserBranch } =
+ initialPrefsToSet;
+ initialDefaultBranch = initialDefaultBranch || {};
+ initialUserBranch = initialUserBranch || {};
+ for (let name of Object.keys(initialDefaultBranch)) {
+ // Clear user-branch values on the default prefs so the defaults aren't
+ // masked.
+ gUserBranch.clearUserPref(name);
+ }
+ for (let [branch, prefs] of [
+ [gDefaultBranch, initialDefaultBranch],
+ [gUserBranch, initialUserBranch],
+ ]) {
+ for (let [name, value] of Object.entries(prefs)) {
+ branch.setBoolPref(name, value);
+ }
+ }
+ UrlbarPrefs._updatingFirefoxSuggestScenario = false;
+
+ let {
+ defaultBranch: expectedDefaultBranch,
+ userBranch: expectedUserBranch,
+ } = expectedPrefs;
+ expectedDefaultBranch = expectedDefaultBranch || {};
+ expectedUserBranch = expectedUserBranch || {};
+
+ // Install the experiment.
+ info(`Installing experiment for enrollment ${i}`);
+ await QuickSuggestTestUtils.withExperiment({
+ valueOverrides,
+ callback: () => {
+ info(`Installed experiment for enrollment ${i}, now checking prefs`);
+
+ // Check expected pref values. Store expected effective values as we go
+ // so we can check them afterward. For a given pref, the expected
+ // effective value is the user value, or if there's not a user value,
+ // the default value.
+ let expectedEffectivePrefs = {};
+ for (let [branch, prefs, branchType] of [
+ [gDefaultBranch, expectedDefaultBranch, "default"],
+ [gUserBranch, expectedUserBranch, "user"],
+ ]) {
+ for (let [name, value] of Object.entries(prefs)) {
+ expectedEffectivePrefs[name] = value;
+ Assert.equal(
+ branch.getBoolPref(name),
+ value,
+ `Pref ${name} on ${branchType} branch`
+ );
+ if (branch == gUserBranch) {
+ Assert.ok(
+ gUserBranch.prefHasUserValue(name),
+ `Pref ${name} is on user branch`
+ );
+ }
+ }
+ }
+ for (let name of Object.keys(initialDefaultBranch)) {
+ if (!expectedUserBranch.hasOwnProperty(name)) {
+ Assert.ok(
+ !gUserBranch.prefHasUserValue(name),
+ `Pref ${name} is not on user branch`
+ );
+ }
+ }
+ for (let [name, value] of Object.entries(expectedEffectivePrefs)) {
+ Assert.equal(
+ UrlbarPrefs.get(name),
+ value,
+ `Pref ${name} effective value`
+ );
+ }
+
+ info(`Uninstalling experiment for enrollment ${i}`);
+ },
+ });
+
+ info(`Uninstalled experiment for enrollment ${i}, now checking prefs`);
+
+ // Check expected effective values after unenrollment. The expected
+ // effective value for a pref at this point is the value on the user branch,
+ // or if there's not a user value, the original value on the default branch
+ // before enrollment. This assumes the default values reflect the offline
+ // scenario (the case for the U.S. region).
+ let effectivePrefs = Object.assign(
+ {},
+ UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline
+ );
+ for (let [name, value] of Object.entries(expectedUserBranch)) {
+ effectivePrefs[name] = value;
+ }
+ for (let [name, value] of Object.entries(effectivePrefs)) {
+ Assert.equal(
+ UrlbarPrefs.get(name),
+ value,
+ `Pref ${name} effective value after unenrolling`
+ );
+ }
+
+ // Clean up.
+ UrlbarPrefs._updatingFirefoxSuggestScenario = true;
+ for (let name of Object.keys(expectedUserBranch)) {
+ UrlbarPrefs.clear(name);
+ }
+ UrlbarPrefs._updatingFirefoxSuggestScenario = false;
+ }
+}
+
+// The following tasks test enterprise preference policies
+
+// Preference policy test for the following:
+// * Status: locked
+// * Value: false
+add_task(async function () {
+ await doPolicyTest({
+ prefPolicy: {
+ Status: "locked",
+ Value: false,
+ },
+ expectedDefault: false,
+ expectedUser: undefined,
+ expectedLocked: true,
+ });
+});
+
+// Preference policy test for the following:
+// * Status: locked
+// * Value: true
+add_task(async function () {
+ await doPolicyTest({
+ prefPolicy: {
+ Status: "locked",
+ Value: true,
+ },
+ expectedDefault: true,
+ expectedUser: undefined,
+ expectedLocked: true,
+ });
+});
+
+// Preference policy test for the following:
+// * Status: default
+// * Value: false
+add_task(async function () {
+ await doPolicyTest({
+ prefPolicy: {
+ Status: "default",
+ Value: false,
+ },
+ expectedDefault: false,
+ expectedUser: undefined,
+ expectedLocked: false,
+ });
+});
+
+// Preference policy test for the following:
+// * Status: default
+// * Value: true
+add_task(async function () {
+ await doPolicyTest({
+ prefPolicy: {
+ Status: "default",
+ Value: true,
+ },
+ expectedDefault: true,
+ expectedUser: undefined,
+ expectedLocked: false,
+ });
+});
+
+// Preference policy test for the following:
+// * Status: user
+// * Value: false
+add_task(async function () {
+ await doPolicyTest({
+ prefPolicy: {
+ Status: "user",
+ Value: false,
+ },
+ expectedDefault: true,
+ expectedUser: false,
+ expectedLocked: false,
+ });
+});
+
+// Preference policy test for the following:
+// * Status: user
+// * Value: true
+add_task(async function () {
+ await doPolicyTest({
+ prefPolicy: {
+ Status: "user",
+ Value: true,
+ },
+ expectedDefault: true,
+ // Because the pref is sticky, it's true on the user branch even though it's
+ // also true on the default branch. Sticky prefs retain their user-branch
+ // values even when they're the same as their default-branch values.
+ expectedUser: true,
+ expectedLocked: false,
+ });
+});
+
+/**
+ * This tests an enterprise preference policy with one of the quick suggest
+ * sticky prefs (defined by `POLICY_PREF`). Pref policies should apply to the
+ * quick suggest sticky prefs just as they do to non-sticky prefs.
+ *
+ * @param {object} options
+ * Options object.
+ * @param {object} options.prefPolicy
+ * An object `{ Status, Value }` that will be included in the policy.
+ * @param {boolean} options.expectedDefault
+ * The expected default-branch pref value after setting the policy.
+ * @param {boolean} options.expectedUser
+ * The expected user-branch pref value after setting the policy or undefined
+ * if the pref should not exist on the user branch.
+ * @param {boolean} options.expectedLocked
+ * Whether the pref is expected to be locked after setting the policy.
+ */
+async function doPolicyTest({
+ prefPolicy,
+ expectedDefault,
+ expectedUser,
+ expectedLocked,
+}) {
+ info(
+ "Starting pref policy test: " +
+ JSON.stringify({
+ prefPolicy,
+ expectedDefault,
+ expectedUser,
+ expectedLocked,
+ })
+ );
+
+ let pref = POLICY_PREF;
+
+ // Check initial state.
+ Assert.ok(
+ gDefaultBranch.getBoolPref(pref),
+ `${pref} is initially true on default branch (assuming en-US)`
+ );
+ Assert.ok(
+ !gUserBranch.prefHasUserValue(pref),
+ `${pref} does not have initial user value`
+ );
+
+ // Set up the policy.
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson({
+ policies: {
+ Preferences: {
+ [`browser.urlbar.${pref}`]: prefPolicy,
+ },
+ },
+ });
+ Assert.equal(
+ Services.policies.status,
+ Ci.nsIEnterprisePolicies.ACTIVE,
+ "Policy engine is active"
+ );
+
+ // Check the default branch.
+ Assert.equal(
+ gDefaultBranch.getBoolPref(pref),
+ expectedDefault,
+ `${pref} has expected default-branch value after setting policy`
+ );
+
+ // Check the user branch.
+ Assert.equal(
+ gUserBranch.prefHasUserValue(pref),
+ expectedUser !== undefined,
+ `${pref} is on user branch as expected after setting policy`
+ );
+ if (expectedUser !== undefined) {
+ Assert.equal(
+ gUserBranch.getBoolPref(pref),
+ expectedUser,
+ `${pref} has expected user-branch value after setting policy`
+ );
+ }
+
+ // Check the locked state.
+ Assert.equal(
+ gDefaultBranch.prefIsLocked(pref),
+ expectedLocked,
+ `${pref} is locked as expected after setting policy`
+ );
+
+ // Clean up.
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson("");
+ Assert.equal(
+ Services.policies.status,
+ Ci.nsIEnterprisePolicies.INACTIVE,
+ "Policy engine is inactive"
+ );
+
+ gDefaultBranch.unlockPref(pref);
+ gUserBranch.clearUserPref(pref);
+ await QuickSuggestTestUtils.setScenario(null);
+
+ Assert.ok(
+ !gDefaultBranch.prefIsLocked(pref),
+ `${pref} is not locked after cleanup`
+ );
+ Assert.ok(
+ gDefaultBranch.getBoolPref(pref),
+ `${pref} is true on default branch after cleanup (assuming en-US)`
+ );
+ Assert.ok(
+ !gUserBranch.prefHasUserValue(pref),
+ `${pref} does not have user value after cleanup`
+ );
+}
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_indexes.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_indexes.js
new file mode 100644
index 0000000000..282e1a2ba0
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_indexes.js
@@ -0,0 +1,425 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests the configurable indexes of sponsored and non-sponsored ("Firefox
+// Suggest") quick suggest results.
+
+"use strict";
+
+const SUGGESTIONS_FIRST_PREF = "browser.urlbar.showSearchSuggestionsFirst";
+const SUGGESTIONS_PREF = "browser.urlbar.suggest.searches";
+
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+const MAX_RESULTS = UrlbarPrefs.get("maxRichResults");
+
+const SPONSORED_INDEX_PREF = "browser.urlbar.quicksuggest.sponsoredIndex";
+const NON_SPONSORED_INDEX_PREF =
+ "browser.urlbar.quicksuggest.nonSponsoredIndex";
+
+const SPONSORED_SEARCH_STRING = "frabbits";
+const NON_SPONSORED_SEARCH_STRING = "nonspon";
+
+const TEST_URL = "http://example.com/quicksuggest";
+
+const REMOTE_SETTINGS_RESULTS = [
+ {
+ id: 1,
+ url: `${TEST_URL}?q=${SPONSORED_SEARCH_STRING}`,
+ title: "frabbits",
+ keywords: [SPONSORED_SEARCH_STRING],
+ click_url: "http://click.reporting.test.com/",
+ impression_url: "http://impression.reporting.test.com/",
+ advertiser: "TestAdvertiser",
+ },
+ {
+ id: 2,
+ url: `${TEST_URL}?q=${NON_SPONSORED_SEARCH_STRING}`,
+ title: "Non-Sponsored",
+ keywords: [NON_SPONSORED_SEARCH_STRING],
+ click_url: "http://click.reporting.test.com/nonsponsored",
+ impression_url: "http://impression.reporting.test.com/nonsponsored",
+ advertiser: "TestAdvertiserNonSponsored",
+ iab_category: "5 - Education",
+ },
+];
+
+add_setup(async function () {
+ // This test intermittently times out on Mac TV WebRender.
+ if (AppConstants.platform == "macosx") {
+ requestLongerTimeout(3);
+ }
+
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await UrlbarTestUtils.formHistory.clear();
+
+ // Add a mock engine so we don't hit the network.
+ await SearchTestUtils.installSearchExtension({}, { setAsDefault: true });
+
+ await QuickSuggestTestUtils.ensureQuickSuggestInit({
+ remoteSettingsResults: [
+ {
+ type: "data",
+ attachment: REMOTE_SETTINGS_RESULTS,
+ },
+ ],
+ });
+
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ });
+});
+
+// Tests with history only
+add_task(async function noSuggestions() {
+ await doTestPermutations(({ withHistory, generalIndex }) => ({
+ expectedResultCount: withHistory ? MAX_RESULTS : 2,
+ expectedIndex: generalIndex == 0 || !withHistory ? 1 : MAX_RESULTS - 1,
+ }));
+});
+
+// Tests with suggestions followed by history
+add_task(async function suggestionsFirst() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[SUGGESTIONS_FIRST_PREF, true]],
+ });
+ await withSuggestions(async () => {
+ await doTestPermutations(({ withHistory, generalIndex }) => ({
+ expectedResultCount: withHistory ? MAX_RESULTS : 4,
+ expectedIndex: generalIndex == 0 || !withHistory ? 3 : MAX_RESULTS - 1,
+ }));
+ });
+ await SpecialPowers.popPrefEnv();
+});
+
+// Tests with history followed by suggestions
+add_task(async function suggestionsLast() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[SUGGESTIONS_FIRST_PREF, false]],
+ });
+ await withSuggestions(async () => {
+ await doTestPermutations(({ withHistory, generalIndex }) => ({
+ expectedResultCount: withHistory ? MAX_RESULTS : 4,
+ expectedIndex: generalIndex == 0 || !withHistory ? 1 : MAX_RESULTS - 3,
+ }));
+ });
+ await SpecialPowers.popPrefEnv();
+});
+
+// Tests with history only plus a suggestedIndex result with a resultSpan
+add_task(async function otherSuggestedIndex_noSuggestions() {
+ await doSuggestedIndexTest([
+ // heuristic
+ { heuristic: true },
+ // TestProvider result
+ { suggestedIndex: 1, resultSpan: 2 },
+ // history
+ { type: UrlbarUtils.RESULT_TYPE.URL },
+ { type: UrlbarUtils.RESULT_TYPE.URL },
+ { type: UrlbarUtils.RESULT_TYPE.URL },
+ { type: UrlbarUtils.RESULT_TYPE.URL },
+ { type: UrlbarUtils.RESULT_TYPE.URL },
+ { type: UrlbarUtils.RESULT_TYPE.URL },
+ // quick suggest
+ {
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ providerName: UrlbarProviderQuickSuggest.name,
+ },
+ ]);
+});
+
+// Tests with suggestions followed by history plus a suggestedIndex result with
+// a resultSpan
+add_task(async function otherSuggestedIndex_suggestionsFirst() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[SUGGESTIONS_FIRST_PREF, true]],
+ });
+ await withSuggestions(async () => {
+ await doSuggestedIndexTest([
+ // heuristic
+ { heuristic: true },
+ // TestProvider result
+ { suggestedIndex: 1, resultSpan: 2 },
+ // search suggestions
+ {
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ payload: { suggestion: SPONSORED_SEARCH_STRING + "foo" },
+ },
+ {
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ payload: { suggestion: SPONSORED_SEARCH_STRING + "bar" },
+ },
+ // history
+ { type: UrlbarUtils.RESULT_TYPE.URL },
+ { type: UrlbarUtils.RESULT_TYPE.URL },
+ { type: UrlbarUtils.RESULT_TYPE.URL },
+ { type: UrlbarUtils.RESULT_TYPE.URL },
+ // quick suggest
+ {
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ providerName: UrlbarProviderQuickSuggest.name,
+ },
+ ]);
+ });
+ await SpecialPowers.popPrefEnv();
+});
+
+// Tests with history followed by suggestions plus a suggestedIndex result with
+// a resultSpan
+add_task(async function otherSuggestedIndex_suggestionsLast() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[SUGGESTIONS_FIRST_PREF, false]],
+ });
+ await withSuggestions(async () => {
+ await doSuggestedIndexTest([
+ // heuristic
+ { heuristic: true },
+ // TestProvider result
+ { suggestedIndex: 1, resultSpan: 2 },
+ // history
+ { type: UrlbarUtils.RESULT_TYPE.URL },
+ { type: UrlbarUtils.RESULT_TYPE.URL },
+ { type: UrlbarUtils.RESULT_TYPE.URL },
+ { type: UrlbarUtils.RESULT_TYPE.URL },
+ // quick suggest
+ {
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ providerName: UrlbarProviderQuickSuggest.name,
+ },
+ // search suggestions
+ {
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ payload: { suggestion: SPONSORED_SEARCH_STRING + "foo" },
+ },
+ {
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ payload: { suggestion: SPONSORED_SEARCH_STRING + "bar" },
+ },
+ ]);
+ });
+ await SpecialPowers.popPrefEnv();
+});
+
+/**
+ * A test provider that returns one result with a suggestedIndex and resultSpan.
+ */
+class TestProvider extends UrlbarTestUtils.TestProvider {
+ constructor() {
+ super({
+ results: [
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ { url: "http://example.com/test" }
+ ),
+ {
+ suggestedIndex: 1,
+ resultSpan: 2,
+ }
+ ),
+ ],
+ });
+ }
+}
+
+/**
+ * Does a round of test permutations.
+ *
+ * @param {Function} callback
+ * For each permutation, this will be called with the arguments of `doTest()`,
+ * and it should return an object with the appropriate values of
+ * `expectedResultCount` and `expectedIndex`.
+ */
+async function doTestPermutations(callback) {
+ for (let isSponsored of [true, false]) {
+ for (let withHistory of [true, false]) {
+ for (let generalIndex of [0, -1]) {
+ let opts = {
+ isSponsored,
+ withHistory,
+ generalIndex,
+ };
+ await doTest(Object.assign(opts, callback(opts)));
+ }
+ }
+ }
+}
+
+/**
+ * Does one test run.
+ *
+ * @param {object} options
+ * Options for the test.
+ * @param {boolean} options.isSponsored
+ * True to use a sponsored result, false to use a non-sponsored result.
+ * @param {boolean} options.withHistory
+ * True to run with a bunch of history, false to run with no history.
+ * @param {number} options.generalIndex
+ * The value to set as the relevant index pref, i.e., the index within the
+ * general group of the quick suggest result.
+ * @param {number} options.expectedResultCount
+ * The expected total result count for sanity checking.
+ * @param {number} options.expectedIndex
+ * The expected index of the quick suggest result in the whole results list.
+ */
+async function doTest({
+ isSponsored,
+ withHistory,
+ generalIndex,
+ expectedResultCount,
+ expectedIndex,
+}) {
+ info(
+ "Running test with options: " +
+ JSON.stringify({
+ isSponsored,
+ withHistory,
+ generalIndex,
+ expectedResultCount,
+ expectedIndex,
+ })
+ );
+
+ // Set the index pref.
+ let indexPref = isSponsored ? SPONSORED_INDEX_PREF : NON_SPONSORED_INDEX_PREF;
+ await SpecialPowers.pushPrefEnv({
+ set: [[indexPref, generalIndex]],
+ });
+
+ // Add history.
+ if (withHistory) {
+ await addHistory();
+ }
+
+ // Do a search.
+ let value = isSponsored
+ ? SPONSORED_SEARCH_STRING
+ : NON_SPONSORED_SEARCH_STRING;
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value,
+ });
+
+ // Check the result count and quick suggest result.
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ expectedResultCount,
+ "Expected result count"
+ );
+ await QuickSuggestTestUtils.assertIsQuickSuggest({
+ window,
+ isSponsored,
+ index: expectedIndex,
+ url: isSponsored
+ ? `${TEST_URL}?q=${SPONSORED_SEARCH_STRING}`
+ : `${TEST_URL}?q=${NON_SPONSORED_SEARCH_STRING}`,
+ });
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ await PlacesUtils.history.clear();
+ await SpecialPowers.popPrefEnv();
+}
+
+/**
+ * Adds history that matches the sponsored and non-sponsored search strings.
+ */
+async function addHistory() {
+ for (let i = 0; i < MAX_RESULTS; i++) {
+ await PlacesTestUtils.addVisits([
+ "http://example.com/" + SPONSORED_SEARCH_STRING + i,
+ "http://example.com/" + NON_SPONSORED_SEARCH_STRING + i,
+ ]);
+ }
+}
+
+/**
+ * Adds a search engine that provides suggestions, calls your callback, and then
+ * removes the engine.
+ *
+ * @param {Function} callback
+ * Your callback function.
+ */
+async function withSuggestions(callback) {
+ await SpecialPowers.pushPrefEnv({
+ set: [[SUGGESTIONS_PREF, true]],
+ });
+ let engine = await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME,
+ });
+ let oldDefaultEngine = await Services.search.getDefault();
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ try {
+ await callback(engine);
+ } finally {
+ await Services.search.setDefault(
+ oldDefaultEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ await Services.search.removeEngine(engine);
+ await SpecialPowers.popPrefEnv();
+ }
+}
+
+/**
+ * Registers a test provider that returns a result with a suggestedIndex and
+ * resultSpan and asserts the given expected results match the actual results.
+ *
+ * @param {Array} expectedProps
+ * See `checkResults()`.
+ */
+async function doSuggestedIndexTest(expectedProps) {
+ await addHistory();
+ let provider = new TestProvider();
+ UrlbarProvidersManager.registerProvider(provider);
+
+ let context = await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: SPONSORED_SEARCH_STRING,
+ });
+ checkResults(context.results, expectedProps);
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ UrlbarProvidersManager.unregisterProvider(provider);
+ await PlacesUtils.history.clear();
+}
+
+/**
+ * Asserts the given actual and expected results match.
+ *
+ * @param {Array} actualResults
+ * Array of actual results.
+ * @param {Array} expectedProps
+ * Array of expected result-like objects. Only the properties defined in each
+ * of these objects are compared against the corresponding actual result.
+ */
+function checkResults(actualResults, expectedProps) {
+ Assert.equal(
+ actualResults.length,
+ expectedProps.length,
+ "Expected result count"
+ );
+
+ let actualProps = actualResults.map((actual, i) => {
+ if (expectedProps.length <= i) {
+ return actual;
+ }
+ let props = {};
+ let expected = expectedProps[i];
+ for (let [key, expectedValue] of Object.entries(expected)) {
+ if (key != "payload") {
+ props[key] = actual[key];
+ } else {
+ props.payload = {};
+ for (let pkey of Object.keys(expectedValue)) {
+ props.payload[pkey] = actual.payload[pkey];
+ }
+ }
+ }
+ return props;
+ });
+ Assert.deepEqual(actualProps, expectedProps);
+}
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_merinoSessions.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_merinoSessions.js
new file mode 100644
index 0000000000..050ea31e12
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_merinoSessions.js
@@ -0,0 +1,142 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// End-to-end browser smoke test for Merino sessions. More comprehensive tests
+// are in test_quicksuggest_merinoSessions.js. This test essentially makes sure
+// engagements occur as expected when interacting with the urlbar. If you need
+// to add tests that do not depend on a new definition of "engagement", consider
+// adding them to test_quicksuggest_merinoSessions.js instead.
+
+"use strict";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.merino.enabled", true],
+ ["browser.urlbar.quicksuggest.remoteSettings.enabled", false],
+ ["browser.urlbar.quicksuggest.dataCollection.enabled", true],
+ ],
+ });
+
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await UrlbarTestUtils.formHistory.clear();
+
+ // Install a mock default engine so we don't hit the network.
+ await SearchTestUtils.installSearchExtension({}, { setAsDefault: true });
+
+ await MerinoTestUtils.server.start();
+});
+
+// In a single engagement, all requests should use the same session ID and the
+// sequence number should be incremented.
+add_task(async function singleEngagement() {
+ for (let i = 0; i < 3; i++) {
+ let searchString = "search" + i;
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: searchString,
+ fireInputEvent: true,
+ });
+
+ MerinoTestUtils.server.checkAndClearRequests([
+ {
+ params: {
+ [MerinoTestUtils.SEARCH_PARAMS.QUERY]: searchString,
+ [MerinoTestUtils.SEARCH_PARAMS.SEQUENCE_NUMBER]: i,
+ },
+ },
+ ]);
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+});
+
+// In a single engagement, all requests should use the same session ID and the
+// sequence number should be incremented. This task closes the panel between
+// searches but keeps the input focused, so the engagement should not end.
+add_task(async function singleEngagement_panelClosed() {
+ for (let i = 0; i < 3; i++) {
+ let searchString = "search" + i;
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: searchString,
+ fireInputEvent: true,
+ });
+
+ MerinoTestUtils.server.checkAndClearRequests([
+ {
+ params: {
+ [MerinoTestUtils.SEARCH_PARAMS.QUERY]: searchString,
+ [MerinoTestUtils.SEARCH_PARAMS.SEQUENCE_NUMBER]: i,
+ },
+ },
+ ]);
+
+ EventUtils.synthesizeKey("KEY_Escape");
+ Assert.ok(!UrlbarTestUtils.isPopupOpen(window), "Panel is closed");
+ Assert.ok(gURLBar.focused, "Input remains focused");
+ }
+
+ // End the engagement to reset the session for the next test.
+ gURLBar.blur();
+});
+
+// New engagements should not use the same session ID as previous engagements
+// and the sequence number should be reset. This task completes each engagement
+// successfully.
+add_task(async function manyEngagements_engagement() {
+ for (let i = 0; i < 3; i++) {
+ // Open a new tab since we'll load the mock default search engine page.
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ let searchString = "search" + i;
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: searchString,
+ fireInputEvent: true,
+ });
+
+ MerinoTestUtils.server.checkAndClearRequests([
+ {
+ params: {
+ [MerinoTestUtils.SEARCH_PARAMS.QUERY]: searchString,
+ [MerinoTestUtils.SEARCH_PARAMS.SEQUENCE_NUMBER]: 0,
+ },
+ },
+ ]);
+
+ // Press enter on the heuristic result to load the search engine page and
+ // complete the engagement.
+ let loadPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser
+ );
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+ });
+ }
+});
+
+// New engagements should not use the same session ID as previous engagements
+// and the sequence number should be reset. This task abandons each engagement.
+add_task(async function manyEngagements_abandonment() {
+ for (let i = 0; i < 3; i++) {
+ let searchString = "search" + i;
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: searchString,
+ fireInputEvent: true,
+ });
+
+ MerinoTestUtils.server.checkAndClearRequests([
+ {
+ params: {
+ [MerinoTestUtils.SEARCH_PARAMS.QUERY]: searchString,
+ [MerinoTestUtils.SEARCH_PARAMS.SEQUENCE_NUMBER]: 0,
+ },
+ },
+ ]);
+
+ // Blur the urlbar to abandon the engagement.
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+ }
+});
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_onboardingDialog.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_onboardingDialog.js
new file mode 100644
index 0000000000..92c3c7c95d
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_onboardingDialog.js
@@ -0,0 +1,1596 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests the buttons in the onboarding dialog for quick suggest/Firefox Suggest.
+ */
+
+ChromeUtils.defineESModuleGetters(this, {
+ TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.sys.mjs",
+});
+
+const OTHER_DIALOG_URI = getRootDirectory(gTestPath) + "subdialog.xhtml";
+
+// Default-branch pref values in the offline scenario.
+const OFFLINE_DEFAULT_PREFS = {
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+ "quicksuggest.dataCollection.enabled": false,
+};
+
+let gDefaultBranch = Services.prefs.getDefaultBranch("browser.urlbar.");
+let gUserBranch = Services.prefs.getBranch("browser.urlbar.");
+
+// Allow more time for Mac and Linux machines so they don't time out in verify mode.
+if (AppConstants.platform === "macosx") {
+ requestLongerTimeout(4);
+} else if (AppConstants.platform === "linux") {
+ requestLongerTimeout(2);
+}
+
+// Whether the tab key can move the focus. On macOS with full keyboard access
+// disabled (which is default), this will be false. See `canTabMoveFocus`.
+let gCanTabMoveFocus;
+add_setup(async function () {
+ gCanTabMoveFocus = await canTabMoveFocus();
+});
+
+// When the user has already enabled the data-collection pref, the dialog should
+// not appear.
+add_task(async function dataCollectionAlreadyEnabled() {
+ setDialogPrereqPrefs();
+ UrlbarPrefs.set("quicksuggest.dataCollection.enabled", true);
+
+ info("Calling maybeShowOnboardingDialog");
+ let showed = await QuickSuggest.maybeShowOnboardingDialog();
+ Assert.ok(!showed, "The dialog was not shown");
+
+ UrlbarPrefs.clear("quicksuggest.dataCollection.enabled");
+});
+
+// When the current tab is about:welcome, the dialog should not appear.
+add_task(async function aboutWelcome() {
+ setDialogPrereqPrefs();
+ await BrowserTestUtils.withNewTab("about:welcome", async () => {
+ info("Calling maybeShowOnboardingDialog");
+ let showed = await QuickSuggest.maybeShowOnboardingDialog();
+ Assert.ok(!showed, "The dialog was not shown");
+ });
+});
+
+// The Escape key should dismiss the dialog without opting in. This task tests
+// when Escape is pressed while the focus is inside the dialog.
+add_task(async function escKey_focusInsideDialog() {
+ await doDialogTest({
+ callback: async () => {
+ const { maybeShowPromise } = await showOnboardingDialog({
+ skipIntroduction: true,
+ });
+
+ const tabCount = gBrowser.tabs.length;
+ Assert.ok(
+ document.activeElement.classList.contains("dialogFrame"),
+ "dialogFrame is focused in the browser window"
+ );
+
+ info("Close the dialog");
+ EventUtils.synthesizeKey("KEY_Escape");
+
+ await maybeShowPromise;
+
+ Assert.equal(
+ gBrowser.currentURI.spec,
+ "about:blank",
+ "Nothing loaded in the current tab"
+ );
+ Assert.equal(gBrowser.tabs.length, tabCount, "No news tabs were opened");
+ },
+ onboardingDialogVersion: JSON.stringify({ version: 1 }),
+ onboardingDialogChoice: "dismiss_2",
+ expectedUserBranchPrefs: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ telemetryEvents: [
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "data_collect_toggled",
+ object: "disabled",
+ },
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "opt_in_dialog",
+ object: "dismiss_2",
+ },
+ ],
+ });
+});
+
+// The Escape key should dismiss the dialog without opting in. This task tests
+// when Escape is pressed while the focus is outside the dialog.
+add_task(async function escKey_focusOutsideDialog() {
+ await doDialogTest({
+ callback: async () => {
+ const { maybeShowPromise } = await showOnboardingDialog({
+ skipIntroduction: true,
+ });
+
+ document.documentElement.focus();
+ Assert.ok(
+ !document.activeElement.classList.contains("dialogFrame"),
+ "dialogFrame is not focused in the browser window"
+ );
+
+ info("Close the dialog");
+ EventUtils.synthesizeKey("KEY_Escape");
+
+ await maybeShowPromise;
+ },
+ onboardingDialogVersion: JSON.stringify({ version: 1 }),
+ onboardingDialogChoice: "dismiss_2",
+ expectedUserBranchPrefs: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ telemetryEvents: [
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "data_collect_toggled",
+ object: "disabled",
+ },
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "opt_in_dialog",
+ object: "dismiss_2",
+ },
+ ],
+ });
+});
+
+// The Escape key should dismiss the dialog without opting in when another
+// dialog is queued and shown before the onboarding. This task dismisses the
+// other dialog by pressing the Escape key.
+add_task(async function escKey_queued_esc() {
+ await doQueuedEscKeyTest("KEY_Escape");
+});
+
+// The Escape key should dismiss the dialog without opting in when another
+// dialog is queued and shown before the onboarding. This task dismisses the
+// other dialog by pressing the Enter key.
+add_task(async function escKey_queued_enter() {
+ await doQueuedEscKeyTest("KEY_Enter");
+});
+
+async function doQueuedEscKeyTest(otherDialogKey) {
+ await doDialogTest({
+ callback: async () => {
+ // Create promises that will resolve when each dialog is opened.
+ let uris = [OTHER_DIALOG_URI, QuickSuggest.ONBOARDING_URI];
+ let [otherOpenedPromise, onboardingOpenedPromise] = uris.map(uri =>
+ TestUtils.topicObserved(
+ "subdialog-loaded",
+ contentWin => contentWin.document.documentURI == uri
+ ).then(async ([contentWin]) => {
+ if (contentWin.document.readyState != "complete") {
+ await BrowserTestUtils.waitForEvent(contentWin, "load");
+ }
+ })
+ );
+
+ info("Queuing dialogs for opening");
+ let otherClosedPromise = gDialogBox.open(OTHER_DIALOG_URI);
+ let onboardingClosedPromise = QuickSuggest.maybeShowOnboardingDialog();
+
+ info("Waiting for the other dialog to open");
+ await otherOpenedPromise;
+
+ info(`Pressing ${otherDialogKey} and waiting for other dialog to close`);
+ EventUtils.synthesizeKey(otherDialogKey);
+ await otherClosedPromise;
+
+ info("Waiting for the onboarding dialog to open");
+ await onboardingOpenedPromise;
+
+ info("Pressing Escape and waiting for onboarding dialog to close");
+ EventUtils.synthesizeKey("KEY_Escape");
+ await onboardingClosedPromise;
+ },
+ onboardingDialogVersion: JSON.stringify({ version: 1 }),
+ onboardingDialogChoice: "dismiss_1",
+ expectedUserBranchPrefs: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ telemetryEvents: [
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "data_collect_toggled",
+ object: "disabled",
+ },
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "opt_in_dialog",
+ object: "dismiss_1",
+ },
+ ],
+ });
+}
+
+// Tests `dismissed_other` by closing the dialog programmatically.
+add_task(async function dismissed_other_on_introduction() {
+ await doDialogTest({
+ callback: async () => {
+ const { maybeShowPromise } = await showOnboardingDialog();
+ gDialogBox._dialog.close();
+ await maybeShowPromise;
+ },
+ onboardingDialogVersion: JSON.stringify({ version: 1 }),
+ onboardingDialogChoice: "dismiss_1",
+ expectedUserBranchPrefs: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ telemetryEvents: [
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "data_collect_toggled",
+ object: "disabled",
+ },
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "opt_in_dialog",
+ object: "dismiss_1",
+ },
+ ],
+ });
+});
+
+// The default is to wait for no browser restarts to show the onboarding dialog
+// on the first restart. This tests that we can override it by configuring the
+// `showOnboardingDialogOnNthRestart`
+add_task(async function nimbus_override_wait_after_n_restarts() {
+ UrlbarPrefs.clear("quicksuggest.shouldShowOnboardingDialog");
+ UrlbarPrefs.clear("quicksuggest.showedOnboardingDialog");
+ UrlbarPrefs.clear("quicksuggest.seenRestarts", 0);
+
+ await QuickSuggestTestUtils.withExperiment({
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ // Wait for 1 browser restart
+ quickSuggestShowOnboardingDialogAfterNRestarts: 1,
+ },
+ callback: async () => {
+ let prefPromise = TestUtils.waitForPrefChange(
+ "browser.urlbar.quicksuggest.showedOnboardingDialog",
+ value => value === true
+ ).then(() => info("Saw pref change"));
+
+ // Simulate 2 restarts. this function is only called by BrowserGlue
+ // on startup, the first restart would be where MR1 was shown then
+ // we will show onboarding the 2nd restart after that.
+ info("Simulating first restart");
+ await QuickSuggest.maybeShowOnboardingDialog();
+
+ info("Simulating second restart");
+ const dialogPromise = BrowserTestUtils.promiseAlertDialogOpen(
+ null,
+ QuickSuggest.ONBOARDING_URI,
+ { isSubDialog: true }
+ );
+ const maybeShowPromise = QuickSuggest.maybeShowOnboardingDialog();
+ const win = await dialogPromise;
+ if (win.document.readyState != "complete") {
+ await BrowserTestUtils.waitForEvent(win, "load");
+ }
+ // Close dialog.
+ EventUtils.synthesizeKey("KEY_Escape");
+
+ info("Waiting for maybeShowPromise and pref change");
+ await Promise.all([maybeShowPromise, prefPromise]);
+ },
+ });
+});
+
+add_task(async function nimbus_skip_onboarding_dialog() {
+ UrlbarPrefs.clear("quicksuggest.shouldShowOnboardingDialog");
+ UrlbarPrefs.clear("quicksuggest.showedOnboardingDialog");
+ UrlbarPrefs.clear("quicksuggest.seenRestarts", 0);
+
+ await QuickSuggestTestUtils.withExperiment({
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ quickSuggestShouldShowOnboardingDialog: false,
+ },
+ callback: async () => {
+ // Simulate 3 restarts.
+ for (let i = 0; i < 3; i++) {
+ info(`Simulating restart ${i + 1}`);
+ await QuickSuggest.maybeShowOnboardingDialog();
+ }
+ Assert.ok(
+ !Services.prefs.getBoolPref(
+ "browser.urlbar.quicksuggest.showedOnboardingDialog",
+ false
+ ),
+ "The showed onboarding dialog pref should not be set"
+ );
+ },
+ });
+});
+
+add_task(async function nimbus_exposure_event() {
+ const testData = [
+ {
+ experimentType: "modal",
+ expectedRecorded: true,
+ },
+ {
+ experimentType: "best-match",
+ expectedRecorded: false,
+ },
+ {
+ expectedRecorded: false,
+ },
+ ];
+
+ for (const { experimentType, expectedRecorded } of testData) {
+ info(`Nimbus exposure event test for type:[${experimentType}]`);
+ UrlbarPrefs.clear("quicksuggest.shouldShowOnboardingDialog");
+ UrlbarPrefs.clear("quicksuggest.showedOnboardingDialog");
+ UrlbarPrefs.clear("quicksuggest.seenRestarts", 0);
+
+ await QuickSuggestTestUtils.clearExposureEvent();
+
+ await QuickSuggestTestUtils.withExperiment({
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ experimentType,
+ },
+ callback: async () => {
+ info("Calling showOnboardingDialog");
+ const { maybeShowPromise } = await showOnboardingDialog();
+ EventUtils.synthesizeKey("KEY_Escape");
+ await maybeShowPromise;
+
+ info("Check the event");
+ await QuickSuggestTestUtils.assertExposureEvent(expectedRecorded);
+ },
+ });
+ }
+});
+
+const LOGO_TYPE = {
+ FIREFOX: 1,
+ MAGGLASS: 2,
+ ANIMATION_MAGGLASS: 3,
+};
+
+const VARIATION_TEST_DATA = [
+ {
+ name: "A",
+ introductionSection: {
+ logoType: LOGO_TYPE.ANIMATION_MAGGLASS,
+ l10n: {
+ onboardingNext: "firefox-suggest-onboarding-introduction-next-button-1",
+ "introduction-title": "firefox-suggest-onboarding-introduction-title-1",
+ },
+ visibility: {
+ "#onboardingLearnMoreOnIntroduction": false,
+ ".description-section": false,
+ ".pager": true,
+ },
+ defaultFocusOrder: [
+ "onboardingNext",
+ "onboardingClose",
+ "onboardingNext",
+ ],
+ actions: ["onboardingClose", "onboardingNext"],
+ },
+ mainSection: {
+ logoType: LOGO_TYPE.MAGGLASS,
+ l10n: {
+ "main-title": "firefox-suggest-onboarding-main-title-1",
+ "main-description": "firefox-suggest-onboarding-main-description-1",
+ "main-accept-option-label":
+ "firefox-suggest-onboarding-main-accept-option-label",
+ "main-accept-option-description":
+ "firefox-suggest-onboarding-main-accept-option-description-1",
+ "main-reject-option-label":
+ "firefox-suggest-onboarding-main-reject-option-label",
+ "main-reject-option-description":
+ "firefox-suggest-onboarding-main-reject-option-description-1",
+ },
+ visibility: {
+ "#main-privacy-first": false,
+ ".description-section #onboardingLearnMore": false,
+ ".accept #onboardingLearnMore": true,
+ ".pager": true,
+ },
+ defaultFocusOrder: [
+ "onboardingNext",
+ "onboardingAccept",
+ "onboardingLearnMore",
+ "onboardingReject",
+ "onboardingSkipLink",
+ "onboardingAccept",
+ ],
+ acceptFocusOrder: [
+ "onboardingAccept",
+ "onboardingLearnMore",
+ "onboardingSubmit",
+ "onboardingSkipLink",
+ "onboardingAccept",
+ ],
+ rejectFocusOrder: [
+ "onboardingReject",
+ "onboardingSubmit",
+ "onboardingSkipLink",
+ "onboardingLearnMore",
+ "onboardingReject",
+ ],
+ actions: [
+ "onboardingAccept",
+ "onboardingReject",
+ "onboardingSkipLink",
+ "onboardingLearnMore",
+ ],
+ },
+ },
+ {
+ // We don't need to test the focus order and actions because the layout of
+ // variation B-H is as same as A.
+ name: "B",
+ introductionSection: {
+ logoType: LOGO_TYPE.ANIMATION_MAGGLASS,
+ l10n: {
+ onboardingNext: "firefox-suggest-onboarding-introduction-next-button-1",
+ "introduction-title": "firefox-suggest-onboarding-introduction-title-2",
+ },
+ visibility: {
+ "#onboardingLearnMoreOnIntroduction": false,
+ ".description-section": false,
+ ".pager": true,
+ },
+ },
+ mainSection: {
+ logoType: LOGO_TYPE.MAGGLASS,
+ l10n: {
+ "main-title": "firefox-suggest-onboarding-main-title-2",
+ "main-description": "firefox-suggest-onboarding-main-description-2",
+ "main-accept-option-label":
+ "firefox-suggest-onboarding-main-accept-option-label",
+ "main-accept-option-description":
+ "firefox-suggest-onboarding-main-accept-option-description-1",
+ "main-reject-option-label":
+ "firefox-suggest-onboarding-main-reject-option-label",
+ "main-reject-option-description":
+ "firefox-suggest-onboarding-main-reject-option-description-1",
+ },
+ visibility: {
+ "#main-privacy-first": false,
+ ".description-section #onboardingLearnMore": false,
+ ".accept #onboardingLearnMore": true,
+ ".pager": true,
+ },
+ },
+ },
+ {
+ name: "C",
+ introductionSection: {
+ logoType: LOGO_TYPE.FIREFOX,
+ l10n: {
+ onboardingNext: "firefox-suggest-onboarding-introduction-next-button-1",
+ "introduction-title": "firefox-suggest-onboarding-introduction-title-3",
+ },
+ visibility: {
+ "#onboardingLearnMoreOnIntroduction": false,
+ ".description-section": false,
+ ".pager": true,
+ },
+ },
+ mainSection: {
+ logoType: LOGO_TYPE.FIREFOX,
+ l10n: {
+ "main-title": "firefox-suggest-onboarding-main-title-3",
+ "main-description": "firefox-suggest-onboarding-main-description-3",
+ "main-accept-option-label":
+ "firefox-suggest-onboarding-main-accept-option-label",
+ "main-accept-option-description":
+ "firefox-suggest-onboarding-main-accept-option-description-1",
+ "main-reject-option-label":
+ "firefox-suggest-onboarding-main-reject-option-label",
+ "main-reject-option-description":
+ "firefox-suggest-onboarding-main-reject-option-description-1",
+ },
+ visibility: {
+ "#main-privacy-first": false,
+ ".description-section #onboardingLearnMore": false,
+ ".accept #onboardingLearnMore": true,
+ ".pager": true,
+ },
+ },
+ },
+ {
+ name: "D",
+ introductionSection: {
+ logoType: LOGO_TYPE.ANIMATION_MAGGLASS,
+ l10n: {
+ onboardingNext: "firefox-suggest-onboarding-introduction-next-button-1",
+ "introduction-title": "firefox-suggest-onboarding-introduction-title-4",
+ },
+ visibility: {
+ "#onboardingLearnMoreOnIntroduction": false,
+ ".description-section": false,
+ ".pager": true,
+ },
+ },
+ mainSection: {
+ logoType: LOGO_TYPE.MAGGLASS,
+ l10n: {
+ "main-title": "firefox-suggest-onboarding-main-title-4",
+ "main-description": "firefox-suggest-onboarding-main-description-4",
+ "main-accept-option-label":
+ "firefox-suggest-onboarding-main-accept-option-label",
+ "main-accept-option-description":
+ "firefox-suggest-onboarding-main-accept-option-description-2",
+ "main-reject-option-label":
+ "firefox-suggest-onboarding-main-reject-option-label",
+ "main-reject-option-description":
+ "firefox-suggest-onboarding-main-reject-option-description-2",
+ },
+ visibility: {
+ "#main-privacy-first": false,
+ ".description-section #onboardingLearnMore": false,
+ ".accept #onboardingLearnMore": true,
+ ".pager": true,
+ },
+ },
+ },
+ {
+ name: "E",
+ introductionSection: {
+ logoType: LOGO_TYPE.FIREFOX,
+ l10n: {
+ onboardingNext: "firefox-suggest-onboarding-introduction-next-button-1",
+ "introduction-title": "firefox-suggest-onboarding-introduction-title-5",
+ },
+ visibility: {
+ "#onboardingLearnMoreOnIntroduction": false,
+ ".description-section": false,
+ ".pager": true,
+ },
+ },
+ mainSection: {
+ logoType: LOGO_TYPE.FIREFOX,
+ l10n: {
+ "main-title": "firefox-suggest-onboarding-main-title-5",
+ "main-description": "firefox-suggest-onboarding-main-description-5",
+ "main-accept-option-label":
+ "firefox-suggest-onboarding-main-accept-option-label",
+ "main-accept-option-description":
+ "firefox-suggest-onboarding-main-accept-option-description-2",
+ "main-reject-option-label":
+ "firefox-suggest-onboarding-main-reject-option-label",
+ "main-reject-option-description":
+ "firefox-suggest-onboarding-main-reject-option-description-2",
+ },
+ visibility: {
+ "#main-privacy-first": false,
+ ".description-section #onboardingLearnMore": false,
+ ".accept #onboardingLearnMore": true,
+ ".pager": true,
+ },
+ },
+ },
+ {
+ name: "F",
+ introductionSection: {
+ logoType: LOGO_TYPE.ANIMATION_MAGGLASS,
+ l10n: {
+ onboardingNext: "firefox-suggest-onboarding-introduction-next-button-2",
+ "introduction-title": "firefox-suggest-onboarding-introduction-title-6",
+ },
+ visibility: {
+ "#onboardingLearnMoreOnIntroduction": false,
+ ".description-section": false,
+ ".pager": true,
+ },
+ },
+ mainSection: {
+ logoType: LOGO_TYPE.MAGGLASS,
+ l10n: {
+ "main-title": "firefox-suggest-onboarding-main-title-6",
+ "main-description": "firefox-suggest-onboarding-main-description-6",
+ "main-accept-option-label":
+ "firefox-suggest-onboarding-main-accept-option-label",
+ "main-accept-option-description":
+ "firefox-suggest-onboarding-main-accept-option-description-2",
+ "main-reject-option-label":
+ "firefox-suggest-onboarding-main-reject-option-label",
+ "main-reject-option-description":
+ "firefox-suggest-onboarding-main-reject-option-description-2",
+ },
+ visibility: {
+ "#main-privacy-first": false,
+ ".description-section #onboardingLearnMore": false,
+ ".accept #onboardingLearnMore": true,
+ ".pager": true,
+ },
+ },
+ },
+ {
+ name: "G",
+ introductionSection: {
+ logoType: LOGO_TYPE.ANIMATION_MAGGLASS,
+ l10n: {
+ onboardingNext: "firefox-suggest-onboarding-introduction-next-button-1",
+ "introduction-title": "firefox-suggest-onboarding-introduction-title-7",
+ },
+ visibility: {
+ "#onboardingLearnMoreOnIntroduction": false,
+ ".description-section": false,
+ ".pager": true,
+ },
+ },
+ mainSection: {
+ logoType: LOGO_TYPE.MAGGLASS,
+ l10n: {
+ "main-title": "firefox-suggest-onboarding-main-title-7",
+ "main-description": "firefox-suggest-onboarding-main-description-7",
+ "main-accept-option-label":
+ "firefox-suggest-onboarding-main-accept-option-label",
+ "main-accept-option-description":
+ "firefox-suggest-onboarding-main-accept-option-description-2",
+ "main-reject-option-label":
+ "firefox-suggest-onboarding-main-reject-option-label",
+ "main-reject-option-description":
+ "firefox-suggest-onboarding-main-reject-option-description-2",
+ },
+ visibility: {
+ "#main-privacy-first": true,
+ ".description-section #onboardingLearnMore": false,
+ ".accept #onboardingLearnMore": true,
+ ".pager": true,
+ },
+ },
+ },
+ {
+ name: "H",
+ introductionSection: {
+ logoType: LOGO_TYPE.FIREFOX,
+ l10n: {
+ onboardingNext: "firefox-suggest-onboarding-introduction-next-button-1",
+ "introduction-title": "firefox-suggest-onboarding-introduction-title-2",
+ },
+ visibility: {
+ "#onboardingLearnMoreOnIntroduction": false,
+ ".description-section": false,
+ ".pager": true,
+ },
+ },
+ mainSection: {
+ logoType: LOGO_TYPE.FIREFOX,
+ l10n: {
+ "main-title": "firefox-suggest-onboarding-main-title-8",
+ "main-description": "firefox-suggest-onboarding-main-description-8",
+ "main-accept-option-label":
+ "firefox-suggest-onboarding-main-accept-option-label",
+ "main-accept-option-description":
+ "firefox-suggest-onboarding-main-accept-option-description-1",
+ "main-reject-option-label":
+ "firefox-suggest-onboarding-main-reject-option-label",
+ "main-reject-option-description":
+ "firefox-suggest-onboarding-main-reject-option-description-1",
+ },
+ visibility: {
+ "#main-privacy-first": false,
+ ".description-section #onboardingLearnMore": false,
+ ".accept #onboardingLearnMore": true,
+ ".pager": true,
+ },
+ },
+ },
+ {
+ name: "100-A",
+ introductionSection: {
+ logoType: LOGO_TYPE.FIREFOX,
+ l10n: {
+ onboardingNext: "firefox-suggest-onboarding-introduction-next-button-3",
+ "introduction-title": "firefox-suggest-onboarding-main-title-9",
+ },
+ visibility: {
+ "#onboardingLearnMoreOnIntroduction": true,
+ ".description-section": true,
+ ".pager": true,
+ },
+ defaultFocusOrder: [
+ "onboardingNext",
+ "onboardingLearnMoreOnIntroduction",
+ "onboardingClose",
+ "onboardingNext",
+ ],
+ actions: [
+ "onboardingClose",
+ "onboardingNext",
+ "onboardingLearnMoreOnIntroduction",
+ ],
+ },
+ mainSection: {
+ logoType: LOGO_TYPE.FIREFOX,
+ l10n: {
+ "main-title": "firefox-suggest-onboarding-main-title-9",
+ "main-description": "firefox-suggest-onboarding-main-description-9",
+ "main-accept-option-label":
+ "firefox-suggest-onboarding-main-accept-option-label-2",
+ "main-accept-option-description":
+ "firefox-suggest-onboarding-main-accept-option-description-3",
+ "main-reject-option-label":
+ "firefox-suggest-onboarding-main-reject-option-label-2",
+ "main-reject-option-description":
+ "firefox-suggest-onboarding-main-reject-option-description-3",
+ },
+ visibility: {
+ "#main-privacy-first": true,
+ ".description-section #onboardingLearnMore": true,
+ ".accept #onboardingLearnMore": false,
+ ".pager": false,
+ },
+ defaultFocusOrder: [
+ "onboardingNext",
+ "onboardingLearnMore",
+ "onboardingAccept",
+ "onboardingReject",
+ "onboardingSkipLink",
+ "onboardingLearnMore",
+ ],
+ acceptFocusOrder: [
+ "onboardingAccept",
+ "onboardingSubmit",
+ "onboardingSkipLink",
+ "onboardingLearnMore",
+ "onboardingAccept",
+ ],
+ rejectFocusOrder: [
+ "onboardingReject",
+ "onboardingSubmit",
+ "onboardingSkipLink",
+ "onboardingLearnMore",
+ "onboardingReject",
+ ],
+ actions: [
+ "onboardingAccept",
+ "onboardingReject",
+ "onboardingSkipLink",
+ "onboardingLearnMore",
+ ],
+ },
+ },
+ {
+ name: "100-B",
+ mainSection: {
+ logoType: LOGO_TYPE.FIREFOX,
+ l10n: {
+ "main-title": "firefox-suggest-onboarding-main-title-9",
+ "main-description": "firefox-suggest-onboarding-main-description-9",
+ "main-accept-option-label":
+ "firefox-suggest-onboarding-main-accept-option-label-2",
+ "main-accept-option-description":
+ "firefox-suggest-onboarding-main-accept-option-description-3",
+ "main-reject-option-label":
+ "firefox-suggest-onboarding-main-reject-option-label-2",
+ "main-reject-option-description":
+ "firefox-suggest-onboarding-main-reject-option-description-3",
+ },
+ visibility: {
+ "#main-privacy-first": true,
+ ".description-section #onboardingLearnMore": true,
+ ".accept #onboardingLearnMore": false,
+ ".pager": false,
+ },
+ // Layout of 100-B is same as 100-A, but since there is no the introduction
+ // pane, only the default focus order on the main pane is a bit diffrence.
+ defaultFocusOrder: [
+ "onboardingLearnMore",
+ "onboardingAccept",
+ "onboardingReject",
+ "onboardingSkipLink",
+ "onboardingLearnMore",
+ ],
+ },
+ },
+];
+
+/**
+ * This test checks for differences due to variations in logo type, l10n text,
+ * element visibility, order of focus, actions, etc. The designation is on
+ * VARIATION_TEST_DATA. The items that can be specified are below.
+ *
+ * name: Specify the variation name.
+ *
+ * The following items are specified for each section.
+ * (introductionSection, mainSection).
+ *
+ * logoType:
+ * Specify the expected logo type. Please refer to LOGO_TYPE about the type.
+ *
+ * l10n:
+ * Specify the expected l10n id applied to elements.
+ *
+ * visibility:
+ * Specify the expected visibility of elements. The way to specify the element
+ * is using selector.
+ *
+ * defaultFocusOrder:
+ * Specify the expected focus order right after the section is appeared. The
+ * way to specify the element is using id.
+ *
+ * acceptFocusOrder:
+ * Specify the expected focus order after selecting accept option.
+ *
+ * rejectFocusOrder:
+ * Specify the expected focus order after selecting reject option.
+ *
+ * actions:
+ * Specify the action we want to verify such as clicking the close button. The
+ * available actions are below.
+ * - onboardingClose:
+ * Action of the close button “x” by mouse/keyboard.
+ * - onboardingNext:
+ * Action of the next button that transits from the introduction section to
+ * the main section by mouse/keyboard.
+ * - onboardingAccept:
+ * Action of the submit button by mouse/keyboard after selecting accept
+ * option by mouse/keyboard.
+ * - onboardingReject:
+ * Action of the submit button by mouse/keyboard after selecting reject
+ * option by mouse/keyboard.
+ * - onboardingSkipLink:
+ * Action of the skip link by mouse/keyboard.
+ * - onboardingLearnMore:
+ * Action of the learn more link by mouse/keyboard.
+ * - onboardingLearnMoreOnIntroduction:
+ * Action of the learn more link on the introduction section by
+ * mouse/keyboard.
+ */
+add_task(async function variation_test() {
+ for (const variation of VARIATION_TEST_DATA) {
+ info(`Test for variation [${variation.name}]`);
+
+ info("Do layout test");
+ await doLayoutTest(variation);
+
+ for (const action of variation.introductionSection?.actions || []) {
+ info(
+ `${action} test on the introduction section for variation [${variation.name}]`
+ );
+ await this[action](variation);
+ }
+
+ for (const action of variation.mainSection?.actions || []) {
+ info(
+ `${action} test on the main section for variation [${variation.name}]`
+ );
+ await this[action](variation, !!variation.introductionSection);
+ }
+ }
+});
+
+async function doLayoutTest(variation) {
+ UrlbarPrefs.clear("quicksuggest.shouldShowOnboardingDialog");
+ UrlbarPrefs.clear("quicksuggest.showedOnboardingDialog");
+ UrlbarPrefs.clear("quicksuggest.seenRestarts", 0);
+
+ await QuickSuggestTestUtils.withExperiment({
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ quickSuggestOnboardingDialogVariation: variation.name,
+ },
+ callback: async () => {
+ info("Calling showOnboardingDialog");
+ const { win, maybeShowPromise } = await showOnboardingDialog();
+
+ const introductionSection = win.document.getElementById(
+ "introduction-section"
+ );
+ const mainSection = win.document.getElementById("main-section");
+
+ if (variation.introductionSection) {
+ info("Check the section visibility");
+ Assert.ok(BrowserTestUtils.is_visible(introductionSection));
+ Assert.ok(BrowserTestUtils.is_hidden(mainSection));
+
+ info("Check the introduction section");
+ await assertSection(introductionSection, variation.introductionSection);
+
+ info("Transition to the main section");
+ win.document.getElementById("onboardingNext").click();
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ BrowserTestUtils.is_hidden(introductionSection) &&
+ BrowserTestUtils.is_visible(mainSection)
+ );
+ } else {
+ info("Check the section visibility");
+ Assert.ok(BrowserTestUtils.is_hidden(introductionSection));
+ Assert.ok(BrowserTestUtils.is_visible(mainSection));
+ }
+
+ info("Check the main section");
+ await assertSection(mainSection, variation.mainSection);
+
+ info("Close the dialog");
+ EventUtils.synthesizeKey("KEY_Escape", {}, win);
+ await maybeShowPromise;
+ },
+ });
+}
+
+async function assertSection(sectionElement, expectedSection) {
+ info("Check the logo");
+ assertLogo(sectionElement, expectedSection.logoType);
+
+ info("Check the l10n");
+ assertL10N(sectionElement, expectedSection.l10n);
+
+ info("Check the visibility");
+ assertVisibility(sectionElement, expectedSection.visibility);
+
+ if (!gCanTabMoveFocus) {
+ Assert.ok(true, "Tab key can't move focus, skipping test for focus order");
+ return;
+ }
+
+ if (expectedSection.defaultFocusOrder) {
+ info("Check the default focus order");
+ assertFocusOrder(sectionElement, expectedSection.defaultFocusOrder);
+ }
+
+ if (expectedSection.acceptFocusOrder) {
+ info("Check the focus order after selecting accept option");
+ sectionElement.querySelector("#onboardingAccept").focus();
+ EventUtils.synthesizeKey("VK_SPACE", {}, sectionElement.ownerGlobal);
+ assertFocusOrder(sectionElement, expectedSection.acceptFocusOrder);
+ }
+
+ if (expectedSection.rejectFocusOrder) {
+ info("Check the focus order after selecting reject option");
+ sectionElement.querySelector("#onboardingReject").focus();
+ EventUtils.synthesizeKey("VK_SPACE", {}, sectionElement.ownerGlobal);
+ assertFocusOrder(sectionElement, expectedSection.rejectFocusOrder);
+ }
+}
+
+function assertLogo(sectionElement, expectedLogoType) {
+ let expectedLogoImage;
+ switch (expectedLogoType) {
+ case LOGO_TYPE.FIREFOX: {
+ expectedLogoImage = 'url("chrome://branding/content/about-logo.svg")';
+ break;
+ }
+ case LOGO_TYPE.MAGGLASS: {
+ expectedLogoImage =
+ 'url("chrome://browser/content/urlbar/quicksuggestOnboarding_magglass.svg")';
+ break;
+ }
+ case LOGO_TYPE.ANIMATION_MAGGLASS: {
+ const mediaQuery = sectionElement.ownerGlobal.matchMedia(
+ "(prefers-reduced-motion: no-preference)"
+ );
+ expectedLogoImage = mediaQuery.matches
+ ? 'url("chrome://browser/content/urlbar/quicksuggestOnboarding_magglass_animation.svg")'
+ : 'url("chrome://browser/content/urlbar/quicksuggestOnboarding_magglass.svg")';
+ break;
+ }
+ default: {
+ Assert.ok(false, `Unexpected image type ${expectedLogoType}`);
+ break;
+ }
+ }
+
+ const logo = sectionElement.querySelector(".logo");
+ Assert.ok(BrowserTestUtils.is_visible(logo));
+ const logoImage =
+ sectionElement.ownerGlobal.getComputedStyle(logo).backgroundImage;
+ Assert.equal(logoImage, expectedLogoImage);
+}
+
+function assertL10N(sectionElement, expectedL10N) {
+ for (const [id, l10n] of Object.entries(expectedL10N)) {
+ const element = sectionElement.querySelector("#" + id);
+ Assert.equal(element.getAttribute("data-l10n-id"), l10n);
+ }
+}
+
+function assertVisibility(sectionElement, expectedVisibility) {
+ for (const [selector, visibility] of Object.entries(expectedVisibility)) {
+ const element = sectionElement.querySelector(selector);
+ if (visibility) {
+ Assert.ok(BrowserTestUtils.is_visible(element));
+ } else {
+ if (!element) {
+ Assert.ok(true);
+ return;
+ }
+ Assert.ok(BrowserTestUtils.is_hidden(element));
+ }
+ }
+}
+
+function assertFocusOrder(sectionElement, expectedFocusOrder) {
+ const win = sectionElement.ownerGlobal;
+
+ // Check initial active element.
+ Assert.equal(win.document.activeElement.id, expectedFocusOrder[0]);
+
+ for (const next of expectedFocusOrder.slice(1)) {
+ EventUtils.synthesizeKey("KEY_Tab", {}, win);
+ Assert.equal(win.document.activeElement.id, next);
+ }
+}
+
+async function onboardingClose(variation) {
+ await doActionTest({
+ callback: async (win, userAction, maybeShowPromise) => {
+ info("Check the status of the close button");
+ const closeButton = win.document.getElementById("onboardingClose");
+ Assert.ok(BrowserTestUtils.is_visible(closeButton));
+ Assert.equal(closeButton.getAttribute("title"), "Close");
+
+ info("Commit the close button");
+ userAction(closeButton);
+
+ info("Waiting for maybeShowOnboardingDialog to finish");
+ await maybeShowPromise;
+ },
+ variation,
+ onboardingDialogVersion: JSON.stringify({
+ version: 1,
+ variation: variation.name.toLowerCase(),
+ }),
+ onboardingDialogChoice: "close_1",
+ expectedUserBranchPrefs: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ telemetryEvents: [
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "data_collect_toggled",
+ object: "disabled",
+ },
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "opt_in_dialog",
+ object: "close_1",
+ },
+ ],
+ });
+}
+
+async function onboardingNext(variation) {
+ await doActionTest({
+ callback: async (win, userAction, maybeShowPromise) => {
+ info("Check the status of the next button");
+ const nextButton = win.document.getElementById("onboardingNext");
+ Assert.ok(BrowserTestUtils.is_visible(nextButton));
+
+ info("Commit the next button");
+ userAction(nextButton);
+
+ const introductionSection = win.document.getElementById(
+ "introduction-section"
+ );
+ const mainSection = win.document.getElementById("main-section");
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ BrowserTestUtils.is_hidden(introductionSection) &&
+ BrowserTestUtils.is_visible(mainSection),
+ "Wait for the transition"
+ );
+
+ info("Exit");
+ EventUtils.synthesizeKey("KEY_Escape", {}, win);
+
+ info("Waiting for maybeShowOnboardingDialog to finish");
+ await maybeShowPromise;
+ },
+ variation,
+ onboardingDialogVersion: JSON.stringify({
+ version: 1,
+ variation: variation.name.toLowerCase(),
+ }),
+ onboardingDialogChoice: "dismiss_2",
+ expectedUserBranchPrefs: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ telemetryEvents: [
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "data_collect_toggled",
+ object: "disabled",
+ },
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "opt_in_dialog",
+ object: "dismiss_2",
+ },
+ ],
+ });
+}
+
+async function onboardingAccept(variation, skipIntroduction) {
+ await doActionTest({
+ callback: async (win, userAction, maybeShowPromise) => {
+ info("Check the status of the accept option and submit button");
+ const acceptOption = win.document.getElementById("onboardingAccept");
+ const submitButton = win.document.getElementById("onboardingSubmit");
+ Assert.ok(acceptOption);
+ Assert.ok(submitButton.disabled);
+
+ info("Select the accept option");
+ userAction(acceptOption);
+
+ info("Commit the submit button");
+ Assert.ok(!submitButton.disabled);
+ userAction(submitButton);
+
+ info("Waiting for maybeShowOnboardingDialog to finish");
+ await maybeShowPromise;
+ },
+ variation,
+ skipIntroduction,
+ onboardingDialogVersion: JSON.stringify({
+ version: 1,
+ variation: variation.name.toLowerCase(),
+ }),
+ onboardingDialogChoice: "accept_2",
+ expectedUserBranchPrefs: {
+ "quicksuggest.onboardingDialogVersion": JSON.stringify({ version: 1 }),
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ telemetryEvents: [
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "data_collect_toggled",
+ object: "enabled",
+ },
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "opt_in_dialog",
+ object: "accept_2",
+ },
+ ],
+ });
+}
+
+async function onboardingReject(variation, skipIntroduction) {
+ await doActionTest({
+ callback: async (win, userAction, maybeShowPromise) => {
+ info("Check the status of the reject option and submit button");
+ const rejectOption = win.document.getElementById("onboardingReject");
+ const submitButton = win.document.getElementById("onboardingSubmit");
+ Assert.ok(rejectOption);
+ Assert.ok(submitButton.disabled);
+
+ info("Select the reject option");
+ userAction(rejectOption);
+
+ info("Commit the submit button");
+ Assert.ok(!submitButton.disabled);
+ userAction(submitButton);
+
+ info("Waiting for maybeShowOnboardingDialog to finish");
+ await maybeShowPromise;
+ },
+ variation,
+ skipIntroduction,
+ onboardingDialogVersion: JSON.stringify({
+ version: 1,
+ variation: variation.name.toLowerCase(),
+ }),
+ onboardingDialogChoice: "reject_2",
+ expectedUserBranchPrefs: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ telemetryEvents: [
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "data_collect_toggled",
+ object: "disabled",
+ },
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "opt_in_dialog",
+ object: "reject_2",
+ },
+ ],
+ });
+}
+
+async function onboardingSkipLink(variation, skipIntroduction) {
+ await doActionTest({
+ callback: async (win, userAction, maybeShowPromise) => {
+ info("Check the status of the skip link");
+ const skipLink = win.document.getElementById("onboardingSkipLink");
+ Assert.ok(BrowserTestUtils.is_visible(skipLink));
+
+ info("Commit the skip link");
+ const tabCount = gBrowser.tabs.length;
+ userAction(skipLink);
+
+ info("Waiting for maybeShowOnboardingDialog to finish");
+ await maybeShowPromise;
+
+ info("Check the current tab status");
+ Assert.equal(
+ gBrowser.currentURI.spec,
+ "about:blank",
+ "Nothing loaded in the current tab"
+ );
+ Assert.equal(gBrowser.tabs.length, tabCount, "No news tabs were opened");
+ },
+ variation,
+ skipIntroduction,
+ onboardingDialogVersion: JSON.stringify({
+ version: 1,
+ variation: variation.name.toLowerCase(),
+ }),
+ onboardingDialogChoice: "not_now_2",
+ expectedUserBranchPrefs: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ telemetryEvents: [
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "data_collect_toggled",
+ object: "disabled",
+ },
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "opt_in_dialog",
+ object: "not_now_2",
+ },
+ ],
+ });
+}
+
+async function onboardingLearnMore(variation, skipIntroduction) {
+ await doLearnMoreTest(
+ variation,
+ skipIntroduction,
+ "onboardingLearnMore",
+ "learn_more_2"
+ );
+}
+
+async function onboardingLearnMoreOnIntroduction(variation, skipIntroduction) {
+ await doLearnMoreTest(
+ variation,
+ skipIntroduction,
+ "onboardingLearnMoreOnIntroduction",
+ "learn_more_1"
+ );
+}
+
+async function doLearnMoreTest(variation, skipIntroduction, target, telemetry) {
+ await doActionTest({
+ callback: async (win, userAction, maybeShowPromise) => {
+ info("Check the status of the learn more link");
+ const learnMoreLink = win.document.getElementById(target);
+ Assert.ok(BrowserTestUtils.is_visible(learnMoreLink));
+
+ info("Commit the learn more link");
+ const loadPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ QuickSuggest.HELP_URL
+ ).then(tab => {
+ info("Saw new tab");
+ return tab;
+ });
+ userAction(learnMoreLink);
+
+ info("Waiting for maybeShowOnboardingDialog to finish");
+ await maybeShowPromise;
+
+ info("Waiting for new tab");
+ let tab = await loadPromise;
+
+ info("Check the current tab status");
+ Assert.equal(gBrowser.selectedTab, tab, "Current tab is the new tab");
+ Assert.equal(
+ gBrowser.currentURI.spec,
+ QuickSuggest.HELP_URL,
+ "Current tab is the support page"
+ );
+ BrowserTestUtils.removeTab(tab);
+ },
+ variation,
+ skipIntroduction,
+ onboardingDialogChoice: telemetry,
+ onboardingDialogVersion: JSON.stringify({
+ version: 1,
+ variation: variation.name.toLowerCase(),
+ }),
+ expectedUserBranchPrefs: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ telemetryEvents: [
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "data_collect_toggled",
+ object: "disabled",
+ },
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "opt_in_dialog",
+ object: telemetry,
+ },
+ ],
+ });
+}
+
+async function doActionTest({
+ variation,
+ skipIntroduction,
+ callback,
+ onboardingDialogVersion,
+ onboardingDialogChoice,
+ expectedUserBranchPrefs,
+ telemetryEvents,
+}) {
+ const userClick = target => {
+ info("Click on the target");
+ target.click();
+ };
+ const userEnter = target => {
+ target.focus();
+ if (target.type === "radio") {
+ info("Space on the target");
+ EventUtils.synthesizeKey("VK_SPACE", {}, target.ownerGlobal);
+ } else {
+ info("Enter on the target");
+ EventUtils.synthesizeKey("KEY_Enter", {}, target.ownerGlobal);
+ }
+ };
+
+ for (const userAction of [userClick, userEnter]) {
+ UrlbarPrefs.clear("quicksuggest.shouldShowOnboardingDialog");
+ UrlbarPrefs.clear("quicksuggest.showedOnboardingDialog");
+ UrlbarPrefs.clear("quicksuggest.seenRestarts", 0);
+
+ await QuickSuggestTestUtils.withExperiment({
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ quickSuggestOnboardingDialogVariation: variation.name,
+ },
+ callback: async () => {
+ await doDialogTest({
+ callback: async () => {
+ info("Calling showOnboardingDialog");
+ const { win, maybeShowPromise } = await showOnboardingDialog({
+ skipIntroduction,
+ });
+
+ await callback(win, userAction, maybeShowPromise);
+ },
+ onboardingDialogVersion,
+ onboardingDialogChoice,
+ expectedUserBranchPrefs,
+ telemetryEvents,
+ });
+ },
+ });
+ }
+}
+
+async function doDialogTest({
+ callback,
+ onboardingDialogVersion,
+ onboardingDialogChoice,
+ telemetryEvents,
+ expectedUserBranchPrefs,
+}) {
+ setDialogPrereqPrefs();
+
+ // Set initial prefs on the default branch.
+ let initialDefaultBranch = OFFLINE_DEFAULT_PREFS;
+ let originalDefaultBranch = {};
+ for (let [name, value] of Object.entries(initialDefaultBranch)) {
+ originalDefaultBranch = gDefaultBranch.getBoolPref(name);
+ gDefaultBranch.setBoolPref(name, value);
+ gUserBranch.clearUserPref(name);
+ }
+
+ // Setting the prefs just now triggered telemetry events, so clear them
+ // before calling the callback.
+ Services.telemetry.clearEvents();
+
+ // Call the callback, which should trigger the dialog and interact with it.
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await callback();
+ });
+
+ // Now check all pref values on the default and user branches.
+ for (let [name, value] of Object.entries(initialDefaultBranch)) {
+ Assert.equal(
+ gDefaultBranch.getBoolPref(name),
+ value,
+ "Default-branch value for pref did not change after modal: " + name
+ );
+
+ let effectiveValue;
+ if (name in expectedUserBranchPrefs) {
+ effectiveValue = expectedUserBranchPrefs[name];
+ Assert.equal(
+ gUserBranch.getBoolPref(name),
+ effectiveValue,
+ "User-branch value for pref has expected value: " + name
+ );
+ } else {
+ effectiveValue = value;
+ Assert.ok(
+ !gUserBranch.prefHasUserValue(name),
+ "User-branch value for pref does not exist: " + name
+ );
+ }
+
+ // For good measure, check the value returned by UrlbarPrefs.
+ Assert.equal(
+ UrlbarPrefs.get(name),
+ effectiveValue,
+ "Effective value for pref is correct: " + name
+ );
+ }
+
+ Assert.equal(
+ UrlbarPrefs.get("quicksuggest.onboardingDialogVersion"),
+ onboardingDialogVersion,
+ "onboardingDialogVersion"
+ );
+ Assert.equal(
+ UrlbarPrefs.get("quicksuggest.onboardingDialogChoice"),
+ onboardingDialogChoice,
+ "onboardingDialogChoice"
+ );
+ Assert.equal(
+ TelemetryEnvironment.currentEnvironment.settings.userPrefs[
+ "browser.urlbar.quicksuggest.onboardingDialogChoice"
+ ],
+ onboardingDialogChoice,
+ "onboardingDialogChoice is correct in TelemetryEnvironment"
+ );
+
+ QuickSuggestTestUtils.assertEvents(telemetryEvents);
+
+ Assert.ok(
+ UrlbarPrefs.get("quicksuggest.showedOnboardingDialog"),
+ "quicksuggest.showedOnboardingDialog is true after showing dialog"
+ );
+
+ // Clean up.
+ for (let [name, value] of Object.entries(originalDefaultBranch)) {
+ gDefaultBranch.setBoolPref(name, value);
+ }
+ for (let name of Object.keys(expectedUserBranchPrefs)) {
+ gUserBranch.clearUserPref(name);
+ }
+}
+
+/**
+ * Show onbaording dialog.
+ *
+ * @param {object} [options]
+ * The object options.
+ * @param {boolean} [options.skipIntroduction]
+ * If true, return dialog with skipping the introduction section.
+ * @returns {{ window, maybeShowPromise: Promise }}
+ * win: window object of the dialog.
+ * maybeShowPromise: Promise of QuickSuggest.maybeShowOnboardingDialog().
+ */
+async function showOnboardingDialog({ skipIntroduction } = {}) {
+ const dialogPromise = BrowserTestUtils.promiseAlertDialogOpen(
+ null,
+ QuickSuggest.ONBOARDING_URI,
+ { isSubDialog: true }
+ );
+
+ const maybeShowPromise = QuickSuggest.maybeShowOnboardingDialog();
+
+ const win = await dialogPromise;
+ if (win.document.readyState != "complete") {
+ await BrowserTestUtils.waitForEvent(win, "load");
+ }
+
+ // Wait until all listers on onboarding dialog are ready.
+ await window._quicksuggestOnboardingReady;
+
+ if (!skipIntroduction) {
+ return { win, maybeShowPromise };
+ }
+
+ // Trigger the transition by pressing Enter on the Next button.
+ EventUtils.synthesizeKey("KEY_Enter");
+
+ const introductionSection = win.document.getElementById(
+ "introduction-section"
+ );
+ const mainSection = win.document.getElementById("main-section");
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ BrowserTestUtils.is_hidden(introductionSection) &&
+ BrowserTestUtils.is_visible(mainSection)
+ );
+
+ return { win, maybeShowPromise };
+}
+
+/**
+ * Sets all the required prefs for showing the onboarding dialog except for the
+ * prefs that are set when the dialog is accepted.
+ */
+function setDialogPrereqPrefs() {
+ UrlbarPrefs.set("quicksuggest.shouldShowOnboardingDialog", true);
+ UrlbarPrefs.set("quicksuggest.showedOnboardingDialog", false);
+}
+
+/**
+ * This is a real hacky way of determining whether the tab key can move focus.
+ * Windows and Linux both support it but macOS does not unless full keyboard
+ * access is enabled, so practically this is only useful on macOS. Gecko seems
+ * to know whether full keyboard access is enabled because it affects focus in
+ * Firefox and some code in nsXULElement.cpp and other places mention it, but
+ * there doesn't seem to be a way to access that information from JS. There is
+ * `Services.focus.elementIsFocusable`, but it returns true regardless of
+ * whether full access is enabled.
+ *
+ * So what we do here is open the dialog and synthesize a tab key. If the focus
+ * doesn't change, then we assume moving the focus via the tab key is not
+ * supported.
+ *
+ * Why not just always skip the focus tasks on Mac? Because individual
+ * developers (like the one writing this comment) may be running macOS with full
+ * keyboard access enabled and want to actually run the tasks on their machines.
+ *
+ * @returns {boolean}
+ */
+async function canTabMoveFocus() {
+ if (AppConstants.platform != "macosx") {
+ return true;
+ }
+
+ let canMove = false;
+ await doDialogTest({
+ callback: async () => {
+ const { win, maybeShowPromise } = await showOnboardingDialog({
+ skipIntroduction: true,
+ });
+
+ let doc = win.document;
+ doc.getElementById("onboardingAccept").focus();
+ EventUtils.synthesizeKey("KEY_Tab");
+
+ // Whether or not the focus can move to the link.
+ canMove = doc.activeElement.id === "onboardingLearnMore";
+
+ EventUtils.synthesizeKey("KEY_Escape");
+ await maybeShowPromise;
+ },
+ onboardingDialogVersion: JSON.stringify({ version: 1 }),
+ onboardingDialogChoice: "dismiss_2",
+ expectedUserBranchPrefs: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ telemetryEvents: [
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "data_collect_toggled",
+ object: "disabled",
+ },
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "opt_in_dialog",
+ object: "dismiss_2",
+ },
+ ],
+ });
+
+ return canMove;
+}
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_dynamicWikipedia.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_dynamicWikipedia.js
new file mode 100644
index 0000000000..ece6239953
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_dynamicWikipedia.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests primary telemetry for dynamic Wikipedia suggestions.
+ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ CONTEXTUAL_SERVICES_PING_TYPES:
+ "resource:///modules/PartnerLinkAttribution.sys.mjs",
+});
+
+const { TELEMETRY_SCALARS } = UrlbarProviderQuickSuggest;
+
+const MERINO_SUGGESTION = {
+ block_id: 1,
+ url: "https://example.com/dynamic-wikipedia",
+ title: "Dynamic Wikipedia suggestion",
+ click_url: "https://example.com/click",
+ impression_url: "https://example.com/impression",
+ advertiser: "dynamic-wikipedia",
+ provider: "wikipedia",
+ iab_category: "5 - Education",
+};
+
+const suggestion_type = "dynamic-wikipedia";
+const match_type = "firefox-suggest";
+const index = 1;
+const position = index + 1;
+
+add_setup(async function () {
+ await setUpTelemetryTest({
+ merinoSuggestions: [MERINO_SUGGESTION],
+ });
+});
+
+add_task(async function () {
+ await doTelemetryTest({
+ index,
+ suggestion: MERINO_SUGGESTION,
+ // impression-only
+ impressionOnly: {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_DYNAMIC_WIKIPEDIA]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "impression_only",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ },
+ selectables: {
+ // click
+ "urlbarView-row-inner": {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_DYNAMIC_WIKIPEDIA]: position,
+ [TELEMETRY_SCALARS.CLICK_DYNAMIC_WIKIPEDIA]: position,
+ "urlbar.picked.dynamic_wikipedia": index.toString(),
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "click",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ },
+ // block
+ "urlbarView-button-block": {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_DYNAMIC_WIKIPEDIA]: position,
+ [TELEMETRY_SCALARS.BLOCK_DYNAMIC_WIKIPEDIA]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "block",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ },
+ // help
+ "urlbarView-button-help": {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_DYNAMIC_WIKIPEDIA]: position,
+ [TELEMETRY_SCALARS.HELP_DYNAMIC_WIKIPEDIA]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "help",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ },
+ },
+ });
+});
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_impressionEdgeCases.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_impressionEdgeCases.js
new file mode 100644
index 0000000000..c52d22a886
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_impressionEdgeCases.js
@@ -0,0 +1,477 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests abandonment and edge cases related to impressions.
+ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ CONTEXTUAL_SERVICES_PING_TYPES:
+ "resource:///modules/PartnerLinkAttribution.sys.mjs",
+ UrlbarView: "resource:///modules/UrlbarView.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+const { TELEMETRY_SCALARS } = UrlbarProviderQuickSuggest;
+
+const REMOTE_SETTINGS_RESULTS = [
+ {
+ id: 1,
+ url: "https://example.com/sponsored",
+ title: "Sponsored suggestion",
+ keywords: ["sponsored"],
+ click_url: "https://example.com/click",
+ impression_url: "https://example.com/impression",
+ advertiser: "testadvertiser",
+ },
+ {
+ id: 2,
+ url: "https://example.com/nonsponsored",
+ title: "Non-sponsored suggestion",
+ keywords: ["nonsponsored"],
+ click_url: "https://example.com/click",
+ impression_url: "https://example.com/impression",
+ advertiser: "testadvertiser",
+ iab_category: "5 - Education",
+ },
+];
+
+const SPONSORED_RESULT = REMOTE_SETTINGS_RESULTS[0];
+
+// Spy for the custom impression/click sender
+let spy;
+
+add_setup(async function () {
+ ({ spy } = QuickSuggestTestUtils.createTelemetryPingSpy());
+
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await UrlbarTestUtils.formHistory.clear();
+
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+
+ // Add a mock engine so we don't hit the network.
+ await SearchTestUtils.installSearchExtension({}, { setAsDefault: true });
+
+ await QuickSuggestTestUtils.ensureQuickSuggestInit({
+ remoteSettingsResults: [
+ {
+ type: "data",
+ attachment: REMOTE_SETTINGS_RESULTS,
+ },
+ ],
+ });
+});
+
+// Makes sure impression telemetry is not recorded when the urlbar engagement is
+// abandoned.
+add_task(async function abandonment() {
+ Services.telemetry.clearEvents();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "sponsored",
+ fireInputEvent: true,
+ });
+ await QuickSuggestTestUtils.assertIsQuickSuggest({
+ window,
+ url: SPONSORED_RESULT.url,
+ });
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ gURLBar.blur();
+ });
+ QuickSuggestTestUtils.assertScalars({});
+ QuickSuggestTestUtils.assertEvents([]);
+ QuickSuggestTestUtils.assertPings(spy, []);
+});
+
+// Makes sure impression telemetry is not recorded when a quick suggest result
+// is not present.
+add_task(async function noQuickSuggestResult() {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ Services.telemetry.clearEvents();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "noImpression_noQuickSuggestResult",
+ fireInputEvent: true,
+ });
+ await QuickSuggestTestUtils.assertNoQuickSuggestResults(window);
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeKey("KEY_Enter");
+ });
+ QuickSuggestTestUtils.assertScalars({});
+ QuickSuggestTestUtils.assertEvents([]);
+ QuickSuggestTestUtils.assertPings(spy, []);
+ });
+ await PlacesUtils.history.clear();
+});
+
+// When a quick suggest result is added to the view but hidden during the view
+// update, impression telemetry should not be recorded for it.
+add_task(async function hiddenRow() {
+ Services.telemetry.clearEvents();
+
+ // Increase the timeout of the remove-stale-rows timer so that it doesn't
+ // interfere with this task.
+ let originalRemoveStaleRowsTimeout = UrlbarView.removeStaleRowsTimeout;
+ UrlbarView.removeStaleRowsTimeout = 30000;
+ registerCleanupFunction(() => {
+ UrlbarView.removeStaleRowsTimeout = originalRemoveStaleRowsTimeout;
+ });
+
+ // Set up a test provider that doesn't add any results until we resolve its
+ // `finishQueryPromise`. For the first search below, it will add many search
+ // suggestions.
+ let maxCount = UrlbarPrefs.get("maxRichResults");
+ let results = [];
+ for (let i = 0; i < maxCount; i++) {
+ results.push(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ {
+ engine: "Example",
+ suggestion: "suggestion " + i,
+ lowerCaseSuggestion: "suggestion " + i,
+ query: "test",
+ }
+ )
+ );
+ }
+ let provider = new DelayingTestProvider({ results });
+ UrlbarProvidersManager.registerProvider(provider);
+
+ // Open a new tab since we'll load a page below.
+ let tab = await BrowserTestUtils.openNewForegroundTab({ gBrowser });
+
+ // Do a normal search and allow the test provider to finish.
+ provider.finishQueryPromise = Promise.resolve();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ fireInputEvent: true,
+ });
+
+ // Sanity check the rows. After the heuristic, the remaining rows should be
+ // the search results added by the test provider.
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ maxCount,
+ "Row count after first search"
+ );
+ for (let i = 1; i < maxCount; i++) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "Expected result type at index " + i
+ );
+ Assert.equal(
+ result.source,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ "Expected result source at index " + i
+ );
+ }
+
+ // Now set up a second search that triggers a quick suggest result. Add a
+ // mutation listener to the view so we can tell when the quick suggest row is
+ // added.
+ let mutationPromise = new Promise(resolve => {
+ let observer = new MutationObserver(mutations => {
+ let rows = UrlbarTestUtils.getResultsContainer(window).children;
+ for (let row of rows) {
+ if (row.result.providerName == "UrlbarProviderQuickSuggest") {
+ observer.disconnect();
+ resolve(row);
+ return;
+ }
+ }
+ });
+ observer.observe(UrlbarTestUtils.getResultsContainer(window), {
+ childList: true,
+ });
+ });
+
+ // Set the test provider's `finishQueryPromise` to a promise that doesn't
+ // resolve. That will prevent the search from completing, which will prevent
+ // the view from removing stale rows and showing the quick suggest row.
+ let resolveQuery;
+ provider.finishQueryPromise = new Promise(
+ resolve => (resolveQuery = resolve)
+ );
+
+ // Start the second search but don't wait for it to finish.
+ gURLBar.focus();
+ let queryPromise = UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: REMOTE_SETTINGS_RESULTS[0].keywords[0],
+ fireInputEvent: true,
+ });
+
+ // Wait for the quick suggest row to be added to the view. It should be hidden
+ // because (a) quick suggest results have a `suggestedIndex`, and rows with
+ // suggested indexes can't replace rows without suggested indexes, and (b) the
+ // view already contains the maximum number of rows due to the first search.
+ // It should remain hidden until the search completes or the remove-stale-rows
+ // timer fires. Next, we'll hit enter, which will cancel the search and close
+ // the view, so the row should never appear.
+ let quickSuggestRow = await mutationPromise;
+ Assert.ok(
+ BrowserTestUtils.is_hidden(quickSuggestRow),
+ "Quick suggest row is hidden"
+ );
+
+ // Hit enter to pick the heuristic search result. This will cancel the search
+ // and notify the quick suggest provider that an engagement occurred.
+ let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeKey("KEY_Enter");
+ });
+ await loadPromise;
+
+ // Resolve the test provider's promise finally.
+ resolveQuery();
+ await queryPromise;
+
+ // The quick suggest provider added a result but it wasn't visible in the
+ // view. No impression telemetry should be recorded for it.
+ QuickSuggestTestUtils.assertScalars({});
+ QuickSuggestTestUtils.assertEvents([]);
+ QuickSuggestTestUtils.assertPings(spy, []);
+
+ BrowserTestUtils.removeTab(tab);
+ UrlbarProvidersManager.unregisterProvider(provider);
+ UrlbarView.removeStaleRowsTimeout = originalRemoveStaleRowsTimeout;
+});
+
+// When a quick suggest result has not been added to the view, impression
+// telemetry should not be recorded for it even if it's the result most recently
+// returned by the provider.
+add_task(async function notAddedToView() {
+ Services.telemetry.clearEvents();
+
+ // Open a new tab since we'll load a page.
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ // Do an initial search that doesn't match any suggestions to make sure
+ // there aren't any quick suggest results in the view to start.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "this doesn't match anything",
+ fireInputEvent: true,
+ });
+ await QuickSuggestTestUtils.assertNoQuickSuggestResults(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ // Now do a search for a suggestion and hit enter after the provider adds it
+ // but before it appears in the view.
+ await doEngagementWithoutAddingResultToView(
+ REMOTE_SETTINGS_RESULTS[0].keywords[0]
+ );
+
+ // The quick suggest provider added a result but it wasn't visible in the
+ // view, and no other quick suggest results were visible in the view. No
+ // impression telemetry should be recorded.
+ QuickSuggestTestUtils.assertScalars({});
+ QuickSuggestTestUtils.assertEvents([]);
+ QuickSuggestTestUtils.assertPings(spy, []);
+ });
+});
+
+// When a quick suggest result is visible in the view, impression telemetry
+// should be recorded for it even if it's not the result most recently returned
+// by the provider.
+add_task(async function previousResultStillVisible() {
+ Services.telemetry.clearEvents();
+
+ // Open a new tab since we'll load a page.
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ // Do a search for the first suggestion.
+ let firstSuggestion = REMOTE_SETTINGS_RESULTS[0];
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: firstSuggestion.keywords[0],
+ fireInputEvent: true,
+ });
+
+ let index = 1;
+ await QuickSuggestTestUtils.assertIsQuickSuggest({
+ window,
+ index,
+ url: firstSuggestion.url,
+ });
+
+ // Without closing the view, do a second search for the second suggestion
+ // and hit enter after the provider adds it but before it appears in the
+ // view.
+ await doEngagementWithoutAddingResultToView(
+ REMOTE_SETTINGS_RESULTS[1].keywords[0],
+ index
+ );
+
+ // An impression for the first suggestion should be recorded since it's
+ // still visible in the view, not the second suggestion.
+ QuickSuggestTestUtils.assertScalars({
+ [TELEMETRY_SCALARS.IMPRESSION_SPONSORED]: index + 1,
+ });
+ QuickSuggestTestUtils.assertEvents([
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "impression_only",
+ extra: {
+ match_type: "firefox-suggest",
+ position: String(index + 1),
+ suggestion_type: "sponsored",
+ },
+ },
+ ]);
+ QuickSuggestTestUtils.assertPings(spy, [
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION,
+ payload: {
+ improve_suggest_experience_checked: false,
+ block_id: firstSuggestion.id,
+ is_clicked: false,
+ match_type: "firefox-suggest",
+ position: index + 1,
+ },
+ },
+ ]);
+ });
+});
+
+/**
+ * Does a search that causes the quick suggest provider to return a result
+ * without adding it to the view and then hits enter to load a SERP and create
+ * an engagement.
+ *
+ * @param {string} searchString
+ * The search string.
+ * @param {number} previousResultIndex
+ * If the view is already open and showing a quick suggest result, pass its
+ * index here. Otherwise pass -1.
+ */
+async function doEngagementWithoutAddingResultToView(
+ searchString,
+ previousResultIndex = -1
+) {
+ // Set the timeout of the chunk timer to a really high value so that it will
+ // not fire. The view updates when the timer fires, which we specifically want
+ // to avoid here.
+ let originalChunkDelayMs = UrlbarProvidersManager._chunkResultsDelayMs;
+ UrlbarProvidersManager._chunkResultsDelayMs = 30000;
+ registerCleanupFunction(() => {
+ UrlbarProvidersManager._chunkResultsDelayMs = originalChunkDelayMs;
+ });
+
+ // Stub `UrlbarProviderQuickSuggest.getPriority()` to return Infinity.
+ let sandbox = sinon.createSandbox();
+ let getPriorityStub = sandbox.stub(UrlbarProviderQuickSuggest, "getPriority");
+ getPriorityStub.returns(Infinity);
+
+ // Spy on `UrlbarProviderQuickSuggest.onEngagement()`.
+ let onEngagementSpy = sandbox.spy(UrlbarProviderQuickSuggest, "onEngagement");
+
+ let sandboxCleanup = () => {
+ getPriorityStub?.restore();
+ getPriorityStub = null;
+ sandbox?.restore();
+ sandbox = null;
+ };
+ registerCleanupFunction(sandboxCleanup);
+
+ // In addition to setting the chunk timeout to a large value above, in order
+ // to prevent the view from updating there also needs to be a heuristic
+ // provider that takes a long time to add results. Set one up that doesn't add
+ // any results until we resolve its `finishQueryPromise`. Set its priority to
+ // Infinity too so that only it and the quick suggest provider will be active.
+ let provider = new DelayingTestProvider({
+ results: [],
+ priority: Infinity,
+ type: UrlbarUtils.PROVIDER_TYPE.HEURISTIC,
+ });
+ UrlbarProvidersManager.registerProvider(provider);
+
+ let resolveQuery;
+ provider.finishQueryPromise = new Promise(r => (resolveQuery = r));
+
+ // Add a query listener so we can grab the query context.
+ let context;
+ let queryListener = {
+ onQueryStarted: c => (context = c),
+ };
+ gURLBar.controller.addQueryListener(queryListener);
+
+ // Do a search but don't wait for it to finish.
+ gURLBar.focus();
+ UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: searchString,
+ fireInputEvent: true,
+ });
+
+ // Wait for the quick suggest provider to add its result to `context.unsortedResults`.
+ let result = await TestUtils.waitForCondition(() => {
+ let query = UrlbarProvidersManager.queries.get(context);
+ return query?.unsortedResults.find(
+ r => r.providerName == "UrlbarProviderQuickSuggest"
+ );
+ }, "Waiting for quick suggest result to be added to context.unsortedResults");
+
+ gURLBar.controller.removeQueryListener(queryListener);
+
+ // The view should not have updated, so the result's `rowIndex` should still
+ // have its initial value of -1.
+ Assert.equal(result.rowIndex, -1, "result.rowIndex is still -1");
+
+ // If there's a result from the previous query, assert it's still in the
+ // view. Otherwise assume that the view should be closed. These are mostly
+ // sanity checks because they should only fail if the telemetry assertions
+ // below also fail.
+ if (previousResultIndex >= 0) {
+ let rows = gURLBar.view.panel.querySelector(".urlbarView-results");
+ Assert.equal(
+ rows.children[previousResultIndex].result.providerName,
+ "UrlbarProviderQuickSuggest",
+ "Result already in view is a quick suggest"
+ );
+ } else {
+ Assert.ok(!gURLBar.view.isOpen, "View is closed");
+ }
+
+ // Hit enter to load a SERP for the search string. This should notify the
+ // quick suggest provider that an engagement occurred.
+ let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeKey("KEY_Enter");
+ });
+ await loadPromise;
+
+ let engagementCalls = onEngagementSpy.getCalls().filter(call => {
+ let state = call.args[1];
+ return state == "engagement";
+ });
+ Assert.equal(engagementCalls.length, 1, "One engagement occurred");
+
+ // Clean up.
+ resolveQuery();
+ UrlbarProvidersManager.unregisterProvider(provider);
+ UrlbarProvidersManager._chunkResultsDelayMs = originalChunkDelayMs;
+ sandboxCleanup();
+}
+
+/**
+ * A test provider that doesn't finish `startQuery()` until `finishQueryPromise`
+ * is resolved.
+ */
+class DelayingTestProvider extends UrlbarTestUtils.TestProvider {
+ finishQueryPromise = null;
+ async startQuery(context, addCallback) {
+ for (let result of this._results) {
+ addCallback(this, result);
+ }
+ await this.finishQueryPromise;
+ }
+}
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_navigationalSuggestions.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_navigationalSuggestions.js
new file mode 100644
index 0000000000..defb9bb76d
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_navigationalSuggestions.js
@@ -0,0 +1,353 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests primary telemetry for navigational suggestions, a.k.a.
+ * navigational top picks.
+ */
+
+"use strict";
+
+const { TELEMETRY_SCALARS } = UrlbarProviderQuickSuggest;
+
+const MERINO_SUGGESTION = {
+ title: "Navigational suggestion",
+ url: "https://example.com/navigational-suggestion",
+ provider: "top_picks",
+ is_sponsored: false,
+ score: 0.25,
+ block_id: 0,
+ is_top_pick: true,
+};
+
+const suggestion_type = "navigational";
+const index = 1;
+const position = index + 1;
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // `bestMatch.enabled` must be set to show nav suggestions with the best
+ // match UI treatment.
+ ["browser.urlbar.bestMatch.enabled", true],
+ // Disable tab-to-search since like best match it's also shown with
+ // `suggestedIndex` = 1.
+ ["browser.urlbar.suggest.engines", false],
+ ],
+ });
+
+ await setUpTelemetryTest({
+ merinoSuggestions: [MERINO_SUGGESTION],
+ });
+});
+
+// Clicks the heuristic when a nav suggestion is not matched
+add_task(async function notMatched_clickHeuristic() {
+ await doTest({
+ suggestion: null,
+ shouldBeShown: false,
+ pickRowIndex: 0,
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_NAV_NOTMATCHED]: "search_engine",
+ [TELEMETRY_SCALARS.CLICK_NAV_NOTMATCHED]: "search_engine",
+ },
+ events: [],
+ });
+});
+
+// Clicks a non-heuristic row when a nav suggestion is not matched
+add_task(async function notMatched_clickOther() {
+ await PlacesTestUtils.addVisits("http://mochi.test:8888/example");
+ await doTest({
+ suggestion: null,
+ shouldBeShown: false,
+ pickRowIndex: 1,
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_NAV_NOTMATCHED]: "search_engine",
+ },
+ events: [],
+ });
+});
+
+// Clicks the heuristic when a nav suggestion is shown
+add_task(async function shown_clickHeuristic() {
+ await doTest({
+ suggestion: MERINO_SUGGESTION,
+ shouldBeShown: true,
+ pickRowIndex: 0,
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_NAV_SHOWN]: "search_engine",
+ [TELEMETRY_SCALARS.CLICK_NAV_SHOWN_HEURISTIC]: "search_engine",
+ },
+ events: [
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "impression_only",
+ extra: {
+ suggestion_type,
+ match_type: "best-match",
+ position: position.toString(),
+ source: "merino",
+ },
+ },
+ ],
+ });
+});
+
+// Clicks the nav suggestion
+add_task(async function shown_clickNavSuggestion() {
+ await doTest({
+ suggestion: MERINO_SUGGESTION,
+ shouldBeShown: true,
+ pickRowIndex: index,
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_NAV_SHOWN]: "search_engine",
+ [TELEMETRY_SCALARS.CLICK_NAV_SHOWN_NAV]: "search_engine",
+ "urlbar.picked.navigational": "1",
+ },
+ events: [
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "click",
+ extra: {
+ suggestion_type,
+ match_type: "best-match",
+ position: position.toString(),
+ source: "merino",
+ },
+ },
+ ],
+ });
+});
+
+// Clicks a non-heuristic non-nav-suggestion row when the nav suggestion is
+// shown
+add_task(async function shown_clickOther() {
+ await PlacesTestUtils.addVisits("http://mochi.test:8888/example");
+ await doTest({
+ suggestion: MERINO_SUGGESTION,
+ shouldBeShown: true,
+ pickRowIndex: 2,
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_NAV_SHOWN]: "search_engine",
+ },
+ events: [
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "impression_only",
+ extra: {
+ suggestion_type,
+ match_type: "best-match",
+ position: position.toString(),
+ source: "merino",
+ },
+ },
+ ],
+ });
+});
+
+// Clicks the heuristic when it dupes the nav suggestion
+add_task(async function duped_clickHeuristic() {
+ // Add enough visits to example.com so it autofills.
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits("https://example.com/");
+ }
+
+ // Set the nav suggestion's URL to the same URL, example.com.
+ let suggestion = {
+ ...MERINO_SUGGESTION,
+ url: "https://example.com/",
+ };
+
+ await doTest({
+ suggestion,
+ shouldBeShown: false,
+ pickRowIndex: 0,
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_NAV_SUPERCEDED]: "autofill_origin",
+ [TELEMETRY_SCALARS.CLICK_NAV_SUPERCEDED]: "autofill_origin",
+ },
+ events: [],
+ });
+});
+
+// Clicks a non-heuristic row when the heuristic dupes the nav suggestion
+add_task(async function duped_clickOther() {
+ // Add enough visits to example.com so it autofills.
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits("https://example.com/");
+ }
+
+ // Set the nav suggestion's URL to the same URL, example.com.
+ let suggestion = {
+ ...MERINO_SUGGESTION,
+ url: "https://example.com/",
+ };
+
+ // Add a visit to another URL so it appears in the search below.
+ await PlacesTestUtils.addVisits("https://example.com/some-other-url");
+
+ await doTest({
+ suggestion,
+ shouldBeShown: false,
+ pickRowIndex: 1,
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_NAV_SUPERCEDED]: "autofill_origin",
+ },
+ events: [],
+ });
+});
+
+// Telemetry specific to nav suggestions should not be recorded when the
+// `recordNavigationalSuggestionTelemetry` Nimbus variable is false.
+add_task(async function recordNavigationalSuggestionTelemetry_false() {
+ await doTest({
+ valueOverrides: {
+ recordNavigationalSuggestionTelemetry: false,
+ },
+ suggestion: MERINO_SUGGESTION,
+ shouldBeShown: true,
+ pickRowIndex: index,
+ scalars: {},
+ events: [
+ // The legacy engagement event should still be recorded as it is for all
+ // quick suggest results.
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "click",
+ extra: {
+ suggestion_type,
+ match_type: "best-match",
+ position: position.toString(),
+ source: "merino",
+ },
+ },
+ ],
+ });
+});
+
+// Telemetry specific to nav suggestions should not be recorded when the
+// `recordNavigationalSuggestionTelemetry` Nimbus variable is left out.
+add_task(async function recordNavigationalSuggestionTelemetry_undefined() {
+ await doTest({
+ valueOverrides: {},
+ suggestion: MERINO_SUGGESTION,
+ shouldBeShown: true,
+ pickRowIndex: index,
+ scalars: {},
+ events: [
+ // The legacy engagement event should still be recorded as it is for all
+ // quick suggest results.
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "click",
+ extra: {
+ suggestion_type,
+ match_type: "best-match",
+ position: position.toString(),
+ source: "merino",
+ },
+ },
+ ],
+ });
+});
+
+/**
+ * Does the following:
+ *
+ * 1. Sets up a Merino nav suggestion
+ * 2. Enrolls in a Nimbus experiment with the specified variables
+ * 3. Does a search
+ * 4. Makes sure the nav suggestion is or isn't shown as expected
+ * 5. Clicks a specified row
+ * 6. Makes sure the expected telemetry is recorded
+ *
+ * @param {object} options
+ * Options object
+ * @param {object} options.suggestion
+ * The nav suggestion or null if Merino shouldn't serve one.
+ * @param {boolean} options.shouldBeShown
+ * Whether the nav suggestion is expected to be shown.
+ * @param {number} options.pickRowIndex
+ * The index of the row to pick.
+ * @param {object} options.scalars
+ * An object that specifies the nav suggest keyed scalars that are expected to
+ * be recorded.
+ * @param {Array} options.events
+ * An object that specifies the legacy engagement events that are expected to
+ * be recorded.
+ * @param {object} options.valueOverrides
+ * The Nimbus variables to use.
+ */
+async function doTest({
+ suggestion,
+ shouldBeShown,
+ pickRowIndex,
+ scalars,
+ events,
+ valueOverrides = {
+ recordNavigationalSuggestionTelemetry: true,
+ },
+}) {
+ MerinoTestUtils.server.response.body.suggestions = suggestion
+ ? [suggestion]
+ : [];
+
+ Services.telemetry.clearEvents();
+ let { spy, spyCleanup } = QuickSuggestTestUtils.createTelemetryPingSpy();
+
+ await QuickSuggestTestUtils.withExperiment({
+ valueOverrides,
+ callback: async () => {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ gURLBar.focus();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "example",
+ fireInputEvent: true,
+ });
+
+ if (shouldBeShown) {
+ await QuickSuggestTestUtils.assertIsQuickSuggest({
+ window,
+ index,
+ url: suggestion.url,
+ isBestMatch: true,
+ isSponsored: false,
+ });
+ } else {
+ await QuickSuggestTestUtils.assertNoQuickSuggestResults(window);
+ }
+
+ let loadPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser
+ );
+ if (pickRowIndex > 0) {
+ info("Arrowing down to row index " + pickRowIndex);
+ EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: pickRowIndex });
+ }
+ info("Pressing Enter and waiting for page load");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+ });
+ },
+ });
+
+ info("Checking scalars");
+ QuickSuggestTestUtils.assertScalars(scalars);
+
+ info("Checking events");
+ QuickSuggestTestUtils.assertEvents(events);
+
+ info("Checking pings");
+ QuickSuggestTestUtils.assertPings(spy, []);
+
+ await spyCleanup();
+ await PlacesUtils.history.clear();
+ MerinoTestUtils.server.response.body.suggestions = [MERINO_SUGGESTION];
+}
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_nonsponsored.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_nonsponsored.js
new file mode 100644
index 0000000000..c7ddcdd2ce
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_nonsponsored.js
@@ -0,0 +1,368 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests primary telemetry for nonsponsored suggestions.
+ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ CONTEXTUAL_SERVICES_PING_TYPES:
+ "resource:///modules/PartnerLinkAttribution.sys.mjs",
+});
+
+const { TELEMETRY_SCALARS } = UrlbarProviderQuickSuggest;
+
+const REMOTE_SETTINGS_RESULT = {
+ id: 1,
+ url: "https://example.com/nonsponsored",
+ title: "Non-sponsored suggestion",
+ keywords: ["nonsponsored"],
+ click_url: "https://example.com/click",
+ impression_url: "https://example.com/impression",
+ advertiser: "testadvertiser",
+ iab_category: "5 - Education",
+};
+
+const suggestion_type = "nonsponsored";
+const index = 1;
+const position = index + 1;
+
+add_setup(async function () {
+ await setUpTelemetryTest({
+ remoteSettingsResults: [
+ {
+ type: "data",
+ attachment: [REMOTE_SETTINGS_RESULT],
+ },
+ ],
+ });
+});
+
+// nonsponsored
+add_task(async function nonsponsored() {
+ let match_type = "firefox-suggest";
+
+ // Make sure `improve_suggest_experience_checked` is recorded correctly
+ // depending on the value of the related pref.
+ for (let improve_suggest_experience_checked of [false, true]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "browser.urlbar.quicksuggest.dataCollection.enabled",
+ improve_suggest_experience_checked,
+ ],
+ ],
+ });
+ await doTelemetryTest({
+ index,
+ suggestion: REMOTE_SETTINGS_RESULT,
+ // impression-only
+ impressionOnly: {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "impression_only",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ ping: {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION,
+ payload: {
+ match_type,
+ position,
+ improve_suggest_experience_checked,
+ is_clicked: false,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ },
+ selectables: {
+ // click
+ "urlbarView-row-inner": {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED]: position,
+ [TELEMETRY_SCALARS.CLICK_NONSPONSORED]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "click",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ pings: [
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION,
+ payload: {
+ match_type,
+ position,
+ improve_suggest_experience_checked,
+ is_clicked: true,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_SELECTION,
+ payload: {
+ match_type,
+ position,
+ improve_suggest_experience_checked,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ ],
+ },
+ // block
+ "urlbarView-button-block": {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED]: position,
+ [TELEMETRY_SCALARS.BLOCK_NONSPONSORED]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "block",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ pings: [
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION,
+ payload: {
+ match_type,
+ position,
+ improve_suggest_experience_checked,
+ is_clicked: false,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_BLOCK,
+ payload: {
+ match_type,
+ position,
+ improve_suggest_experience_checked,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ iab_category: REMOTE_SETTINGS_RESULT.iab_category,
+ },
+ },
+ ],
+ },
+ // help
+ "urlbarView-button-help": {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED]: position,
+ [TELEMETRY_SCALARS.HELP_NONSPONSORED]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "help",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ pings: [
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION,
+ payload: {
+ match_type,
+ position,
+ improve_suggest_experience_checked,
+ is_clicked: false,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ ],
+ },
+ },
+ });
+ await SpecialPowers.popPrefEnv();
+ }
+});
+
+// nonsponsored best match
+add_task(async function nonsponsoredBestMatch() {
+ let match_type = "best-match";
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.bestMatch.enabled", true]],
+ });
+ await QuickSuggestTestUtils.setConfig(
+ QuickSuggestTestUtils.BEST_MATCH_CONFIG
+ );
+ await doTelemetryTest({
+ index,
+ suggestion: REMOTE_SETTINGS_RESULT,
+ // impression-only
+ impressionOnly: {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED]: position,
+ [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED_BEST_MATCH]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "impression_only",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ ping: {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION,
+ payload: {
+ match_type,
+ position,
+ is_clicked: false,
+ improve_suggest_experience_checked: false,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ },
+ selectables: {
+ // click
+ "urlbarView-row-inner": {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED]: position,
+ [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED_BEST_MATCH]: position,
+ [TELEMETRY_SCALARS.CLICK_NONSPONSORED]: position,
+ [TELEMETRY_SCALARS.CLICK_NONSPONSORED_BEST_MATCH]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "click",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ pings: [
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION,
+ payload: {
+ match_type,
+ position,
+ is_clicked: true,
+ improve_suggest_experience_checked: false,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_SELECTION,
+ payload: {
+ match_type,
+ position,
+ improve_suggest_experience_checked: false,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ ],
+ },
+ // block
+ "urlbarView-button-block": {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED]: position,
+ [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED_BEST_MATCH]: position,
+ [TELEMETRY_SCALARS.BLOCK_NONSPONSORED]: position,
+ [TELEMETRY_SCALARS.BLOCK_NONSPONSORED_BEST_MATCH]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "block",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ pings: [
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION,
+ payload: {
+ match_type,
+ position,
+ is_clicked: false,
+ improve_suggest_experience_checked: false,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_BLOCK,
+ payload: {
+ match_type,
+ position,
+ improve_suggest_experience_checked: false,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ iab_category: REMOTE_SETTINGS_RESULT.iab_category,
+ },
+ },
+ ],
+ },
+ // help
+ "urlbarView-button-help": {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED]: position,
+ [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED_BEST_MATCH]: position,
+ [TELEMETRY_SCALARS.HELP_NONSPONSORED]: position,
+ [TELEMETRY_SCALARS.HELP_NONSPONSORED_BEST_MATCH]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "help",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ pings: [
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION,
+ payload: {
+ match_type,
+ position,
+ is_clicked: false,
+ improve_suggest_experience_checked: false,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ ],
+ },
+ },
+ });
+ await QuickSuggestTestUtils.setConfig(QuickSuggestTestUtils.DEFAULT_CONFIG);
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_other.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_other.js
new file mode 100644
index 0000000000..d151ca81ad
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_other.js
@@ -0,0 +1,409 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests ancillary quick suggest telemetry, i.e., telemetry that's not
+ * strongly related to showing suggestions in the urlbar.
+ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.sys.mjs",
+});
+
+const REMOTE_SETTINGS_RESULTS = [
+ {
+ id: 1,
+ url: "https://example.com/sponsored",
+ title: "Sponsored suggestion",
+ keywords: ["sponsored"],
+ click_url: "https://example.com/click",
+ impression_url: "https://example.com/impression",
+ advertiser: "testadvertiser",
+ },
+];
+
+add_setup(async function () {
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await UrlbarTestUtils.formHistory.clear();
+
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+
+ // Add a mock engine so we don't hit the network.
+ await SearchTestUtils.installSearchExtension({}, { setAsDefault: true });
+
+ await QuickSuggestTestUtils.ensureQuickSuggestInit({
+ remoteSettingsResults: [
+ {
+ type: "data",
+ attachment: REMOTE_SETTINGS_RESULTS,
+ },
+ ],
+ });
+});
+
+// Tests telemetry recorded when toggling the
+// `suggest.quicksuggest.nonsponsored` pref:
+// * contextservices.quicksuggest enable_toggled event telemetry
+// * TelemetryEnvironment
+add_task(async function enableToggled() {
+ Services.telemetry.clearEvents();
+
+ // Toggle the suggest.quicksuggest.nonsponsored pref twice. We should get two
+ // events.
+ let enabled = UrlbarPrefs.get("suggest.quicksuggest.nonsponsored");
+ for (let i = 0; i < 2; i++) {
+ enabled = !enabled;
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", enabled);
+ QuickSuggestTestUtils.assertEvents([
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "enable_toggled",
+ object: enabled ? "enabled" : "disabled",
+ },
+ ]);
+ Assert.equal(
+ TelemetryEnvironment.currentEnvironment.settings.userPrefs[
+ "browser.urlbar.suggest.quicksuggest.nonsponsored"
+ ],
+ enabled,
+ "suggest.quicksuggest.nonsponsored is correct in TelemetryEnvironment"
+ );
+ }
+
+ // Set the main quicksuggest.enabled pref to false and toggle the
+ // suggest.quicksuggest.nonsponsored pref again. We shouldn't get any events.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.quicksuggest.enabled", false]],
+ });
+ enabled = !enabled;
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", enabled);
+ QuickSuggestTestUtils.assertEvents([]);
+ await SpecialPowers.popPrefEnv();
+
+ // Set the pref back to what it was at the start of the task.
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", !enabled);
+});
+
+// Tests telemetry recorded when toggling the `suggest.quicksuggest.sponsored`
+// pref:
+// * contextservices.quicksuggest enable_toggled event telemetry
+// * TelemetryEnvironment
+add_task(async function sponsoredToggled() {
+ Services.telemetry.clearEvents();
+
+ // Toggle the suggest.quicksuggest.sponsored pref twice. We should get two
+ // events.
+ let enabled = UrlbarPrefs.get("suggest.quicksuggest.sponsored");
+ for (let i = 0; i < 2; i++) {
+ enabled = !enabled;
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", enabled);
+ QuickSuggestTestUtils.assertEvents([
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "sponsored_toggled",
+ object: enabled ? "enabled" : "disabled",
+ },
+ ]);
+ Assert.equal(
+ TelemetryEnvironment.currentEnvironment.settings.userPrefs[
+ "browser.urlbar.suggest.quicksuggest.sponsored"
+ ],
+ enabled,
+ "suggest.quicksuggest.sponsored is correct in TelemetryEnvironment"
+ );
+ }
+
+ // Set the main quicksuggest.enabled pref to false and toggle the
+ // suggest.quicksuggest.sponsored pref again. We shouldn't get any events.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.quicksuggest.enabled", false]],
+ });
+ enabled = !enabled;
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", enabled);
+ QuickSuggestTestUtils.assertEvents([]);
+ await SpecialPowers.popPrefEnv();
+
+ // Set the pref back to what it was at the start of the task.
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", !enabled);
+});
+
+// Tests telemetry recorded when toggling the
+// `quicksuggest.dataCollection.enabled` pref:
+// * contextservices.quicksuggest data_collect_toggled event telemetry
+// * TelemetryEnvironment
+add_task(async function dataCollectionToggled() {
+ Services.telemetry.clearEvents();
+
+ // Toggle the quicksuggest.dataCollection.enabled pref twice. We should get
+ // two events.
+ let enabled = UrlbarPrefs.get("quicksuggest.dataCollection.enabled");
+ for (let i = 0; i < 2; i++) {
+ enabled = !enabled;
+ UrlbarPrefs.set("quicksuggest.dataCollection.enabled", enabled);
+ QuickSuggestTestUtils.assertEvents([
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "data_collect_toggled",
+ object: enabled ? "enabled" : "disabled",
+ },
+ ]);
+ Assert.equal(
+ TelemetryEnvironment.currentEnvironment.settings.userPrefs[
+ "browser.urlbar.quicksuggest.dataCollection.enabled"
+ ],
+ enabled,
+ "quicksuggest.dataCollection.enabled is correct in TelemetryEnvironment"
+ );
+ }
+
+ // Set the main quicksuggest.enabled pref to false and toggle the data
+ // collection pref again. We shouldn't get any events.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.quicksuggest.enabled", false]],
+ });
+ enabled = !enabled;
+ UrlbarPrefs.set("quicksuggest.dataCollection.enabled", enabled);
+ QuickSuggestTestUtils.assertEvents([]);
+ await SpecialPowers.popPrefEnv();
+
+ // Set the pref back to what it was at the start of the task.
+ UrlbarPrefs.set("quicksuggest.dataCollection.enabled", !enabled);
+});
+
+// Tests telemetry recorded when clicking the checkbox for best match in
+// preferences UI. The telemetry will be stored as following keyed scalar.
+// scalar: browser.ui.interaction.preferences_panePrivacy
+// key: firefoxSuggestBestMatch
+add_task(async function bestmatchCheckbox() {
+ // Set the initial enabled status.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.bestMatch.enabled", true]],
+ });
+
+ // Open preferences page for best match.
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:preferences#privacy",
+ true
+ );
+
+ for (let i = 0; i < 2; i++) {
+ Services.telemetry.clearScalars();
+
+ // Click on the checkbox.
+ const doc = gBrowser.selectedBrowser.contentDocument;
+ const checkboxId = "firefoxSuggestBestMatch";
+ const checkbox = doc.getElementById(checkboxId);
+ checkbox.scrollIntoView();
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#" + checkboxId,
+ {},
+ gBrowser.selectedBrowser
+ );
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "browser.ui.interaction.preferences_panePrivacy",
+ checkboxId,
+ 1
+ );
+ }
+
+ // Clean up.
+ gBrowser.removeCurrentTab();
+ await SpecialPowers.popPrefEnv();
+});
+
+// Tests telemetry recorded when opening the learn more link for best match in
+// the preferences UI. The telemetry will be stored as following keyed scalar.
+// scalar: browser.ui.interaction.preferences_panePrivacy
+// key: firefoxSuggestBestMatchLearnMore
+add_task(async function bestmatchLearnMore() {
+ // Set the initial enabled status.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.bestMatch.enabled", true]],
+ });
+
+ // Open preferences page for best match.
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:preferences#privacy",
+ true
+ );
+
+ // Click on the learn more link.
+ Services.telemetry.clearScalars();
+ const learnMoreLinkId = "firefoxSuggestBestMatchLearnMore";
+ const doc = gBrowser.selectedBrowser.contentDocument;
+ const link = doc.getElementById(learnMoreLinkId);
+ link.scrollIntoView();
+ const onLearnMoreOpenedByClick = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ QuickSuggest.HELP_URL
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#" + learnMoreLinkId,
+ {},
+ gBrowser.selectedBrowser
+ );
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "browser.ui.interaction.preferences_panePrivacy",
+ "firefoxSuggestBestMatchLearnMore",
+ 1
+ );
+ await onLearnMoreOpenedByClick;
+ gBrowser.removeCurrentTab();
+
+ // Type enter key on the learm more link.
+ Services.telemetry.clearScalars();
+ link.focus();
+ const onLearnMoreOpenedByKey = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ QuickSuggest.HELP_URL
+ );
+ await BrowserTestUtils.synthesizeKey(
+ "KEY_Enter",
+ {},
+ gBrowser.selectedBrowser
+ );
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "browser.ui.interaction.preferences_panePrivacy",
+ "firefoxSuggestBestMatchLearnMore",
+ 1
+ );
+ await onLearnMoreOpenedByKey;
+ gBrowser.removeCurrentTab();
+
+ // Clean up.
+ gBrowser.removeCurrentTab();
+ await SpecialPowers.popPrefEnv();
+});
+
+// Simulates the race on startup between telemetry environment initialization
+// and the initial update of the Suggest scenario. After startup is done,
+// telemetry environment should record the correct values for startup prefs.
+add_task(async function telemetryEnvironmentOnStartup() {
+ await QuickSuggestTestUtils.setScenario(null);
+
+ // Restart telemetry environment so we know it's watching its default set of
+ // prefs.
+ await TelemetryEnvironment.testCleanRestart().onInitialized();
+
+ // Get the prefs that UrlbarPrefs sets when the Suggest scenario is updated on
+ // startup. They're the union of the prefs exposed in the UI and the prefs
+ // that are set on the default branch per scenario.
+ let prefs = [
+ ...new Set([
+ ...Object.values(UrlbarPrefs.FIREFOX_SUGGEST_UI_PREFS_BY_VARIABLE),
+ ...Object.values(UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS)
+ .map(valuesByPrefName => Object.keys(valuesByPrefName))
+ .flat(),
+ ]),
+ ];
+
+ // Not all of the prefs are recorded in telemetry environment. Filter in the
+ // ones that are.
+ prefs = prefs.filter(
+ p =>
+ `browser.urlbar.${p}` in
+ TelemetryEnvironment.currentEnvironment.settings.userPrefs
+ );
+
+ info("Got startup prefs: " + JSON.stringify(prefs));
+
+ // Sanity check the expected prefs. This isn't strictly necessary since we
+ // programmatically get the prefs above, but it's an extra layer of defense,
+ // for example in case we accidentally filtered out some expected prefs above.
+ // If this fails, you might have added a startup pref but didn't update this
+ // array here.
+ Assert.deepEqual(
+ prefs.sort(),
+ [
+ "quicksuggest.dataCollection.enabled",
+ "suggest.quicksuggest.nonsponsored",
+ "suggest.quicksuggest.sponsored",
+ ],
+ "Expected startup prefs"
+ );
+
+ // Make sure the prefs don't have user values that would mask the default
+ // values.
+ for (let p of prefs) {
+ UrlbarPrefs.clear(p);
+ }
+
+ // Build a map of default values.
+ let defaultValues = Object.fromEntries(
+ prefs.map(p => [p, UrlbarPrefs.get(p)])
+ );
+
+ // Now simulate startup. Restart telemetry environment but don't wait for it
+ // to finish before calling `updateFirefoxSuggestScenario()`. This simulates
+ // startup where telemetry environment's initialization races the intial
+ // update of the Suggest scenario.
+ let environmentInitPromise =
+ TelemetryEnvironment.testCleanRestart().onInitialized();
+
+ // Update the scenario and force the startup prefs to take on values that are
+ // the inverse of what they are now.
+ await UrlbarPrefs.updateFirefoxSuggestScenario({
+ isStartup: true,
+ scenario: "online",
+ defaultPrefs: {
+ online: Object.fromEntries(
+ Object.entries(defaultValues).map(([p, value]) => [p, !value])
+ ),
+ },
+ });
+
+ // At this point telemetry environment should be done initializing since
+ // `updateFirefoxSuggestScenario()` waits for it, but await our promise now.
+ await environmentInitPromise;
+
+ // TelemetryEnvironment should have cached the new values.
+ for (let [p, value] of Object.entries(defaultValues)) {
+ let expected = !value;
+ Assert.strictEqual(
+ TelemetryEnvironment.currentEnvironment.settings.userPrefs[
+ `browser.urlbar.${p}`
+ ],
+ expected,
+ `Check 1: ${p} is ${expected} in TelemetryEnvironment`
+ );
+ }
+
+ // Simulate another startup and set all prefs back to their original default
+ // values.
+ environmentInitPromise =
+ TelemetryEnvironment.testCleanRestart().onInitialized();
+
+ await UrlbarPrefs.updateFirefoxSuggestScenario({
+ isStartup: true,
+ scenario: "online",
+ defaultPrefs: {
+ online: defaultValues,
+ },
+ });
+
+ await environmentInitPromise;
+
+ // TelemetryEnvironment should have cached the new (original) values.
+ for (let [p, value] of Object.entries(defaultValues)) {
+ let expected = value;
+ Assert.strictEqual(
+ TelemetryEnvironment.currentEnvironment.settings.userPrefs[
+ `browser.urlbar.${p}`
+ ],
+ expected,
+ `Check 2: ${p} is ${expected} in TelemetryEnvironment`
+ );
+ }
+
+ await TelemetryEnvironment.testCleanRestart().onInitialized();
+});
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_sponsored.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_sponsored.js
new file mode 100644
index 0000000000..498b942a12
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_sponsored.js
@@ -0,0 +1,367 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests primary telemetry for sponsored suggestions.
+ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ CONTEXTUAL_SERVICES_PING_TYPES:
+ "resource:///modules/PartnerLinkAttribution.sys.mjs",
+});
+
+const { TELEMETRY_SCALARS } = UrlbarProviderQuickSuggest;
+
+const REMOTE_SETTINGS_RESULT = {
+ id: 1,
+ url: "https://example.com/sponsored",
+ title: "Sponsored suggestion",
+ keywords: ["sponsored"],
+ click_url: "https://example.com/click",
+ impression_url: "https://example.com/impression",
+ advertiser: "testadvertiser",
+};
+
+const suggestion_type = "sponsored";
+const index = 1;
+const position = index + 1;
+
+add_setup(async function () {
+ await setUpTelemetryTest({
+ remoteSettingsResults: [
+ {
+ type: "data",
+ attachment: [REMOTE_SETTINGS_RESULT],
+ },
+ ],
+ });
+});
+
+// sponsored
+add_task(async function sponsored() {
+ let match_type = "firefox-suggest";
+
+ // Make sure `improve_suggest_experience_checked` is recorded correctly
+ // depending on the value of the related pref.
+ for (let improve_suggest_experience_checked of [false, true]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "browser.urlbar.quicksuggest.dataCollection.enabled",
+ improve_suggest_experience_checked,
+ ],
+ ],
+ });
+ await doTelemetryTest({
+ index,
+ suggestion: REMOTE_SETTINGS_RESULT,
+ // impression-only
+ impressionOnly: {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_SPONSORED]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "impression_only",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ ping: {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION,
+ payload: {
+ match_type,
+ position,
+ improve_suggest_experience_checked,
+ is_clicked: false,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ },
+ selectables: {
+ // click
+ "urlbarView-row-inner": {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_SPONSORED]: position,
+ [TELEMETRY_SCALARS.CLICK_SPONSORED]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "click",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ pings: [
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION,
+ payload: {
+ match_type,
+ position,
+ improve_suggest_experience_checked,
+ is_clicked: true,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_SELECTION,
+ payload: {
+ match_type,
+ position,
+ improve_suggest_experience_checked,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ ],
+ },
+ // block
+ "urlbarView-button-block": {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_SPONSORED]: position,
+ [TELEMETRY_SCALARS.BLOCK_SPONSORED]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "block",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ pings: [
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION,
+ payload: {
+ match_type,
+ position,
+ improve_suggest_experience_checked,
+ is_clicked: false,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_BLOCK,
+ payload: {
+ match_type,
+ position,
+ improve_suggest_experience_checked,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ iab_category: REMOTE_SETTINGS_RESULT.iab_category,
+ },
+ },
+ ],
+ },
+ // help
+ "urlbarView-button-help": {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_SPONSORED]: position,
+ [TELEMETRY_SCALARS.HELP_SPONSORED]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "help",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ pings: [
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION,
+ payload: {
+ match_type,
+ position,
+ improve_suggest_experience_checked,
+ is_clicked: false,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ ],
+ },
+ },
+ });
+ await SpecialPowers.popPrefEnv();
+ }
+});
+
+// sponsored best match
+add_task(async function sponsoredBestMatch() {
+ let match_type = "best-match";
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.bestMatch.enabled", true]],
+ });
+ await QuickSuggestTestUtils.setConfig(
+ QuickSuggestTestUtils.BEST_MATCH_CONFIG
+ );
+ await doTelemetryTest({
+ index,
+ suggestion: REMOTE_SETTINGS_RESULT,
+ // impression-only
+ impressionOnly: {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_SPONSORED]: position,
+ [TELEMETRY_SCALARS.IMPRESSION_SPONSORED_BEST_MATCH]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "impression_only",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ ping: {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION,
+ payload: {
+ match_type,
+ position,
+ is_clicked: false,
+ improve_suggest_experience_checked: false,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ },
+ selectables: {
+ // click
+ "urlbarView-row-inner": {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_SPONSORED]: position,
+ [TELEMETRY_SCALARS.IMPRESSION_SPONSORED_BEST_MATCH]: position,
+ [TELEMETRY_SCALARS.CLICK_SPONSORED]: position,
+ [TELEMETRY_SCALARS.CLICK_SPONSORED_BEST_MATCH]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "click",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ pings: [
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION,
+ payload: {
+ match_type,
+ position,
+ is_clicked: true,
+ improve_suggest_experience_checked: false,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_SELECTION,
+ payload: {
+ match_type,
+ position,
+ improve_suggest_experience_checked: false,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ ],
+ },
+ // block
+ "urlbarView-button-block": {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_SPONSORED]: position,
+ [TELEMETRY_SCALARS.IMPRESSION_SPONSORED_BEST_MATCH]: position,
+ [TELEMETRY_SCALARS.BLOCK_SPONSORED]: position,
+ [TELEMETRY_SCALARS.BLOCK_SPONSORED_BEST_MATCH]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "block",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ pings: [
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION,
+ payload: {
+ match_type,
+ position,
+ is_clicked: false,
+ improve_suggest_experience_checked: false,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_BLOCK,
+ payload: {
+ match_type,
+ position,
+ improve_suggest_experience_checked: false,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ iab_category: REMOTE_SETTINGS_RESULT.iab_category,
+ },
+ },
+ ],
+ },
+ // help
+ "urlbarView-button-help": {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_SPONSORED]: position,
+ [TELEMETRY_SCALARS.IMPRESSION_SPONSORED_BEST_MATCH]: position,
+ [TELEMETRY_SCALARS.HELP_SPONSORED]: position,
+ [TELEMETRY_SCALARS.HELP_SPONSORED_BEST_MATCH]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "help",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ pings: [
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION,
+ payload: {
+ match_type,
+ position,
+ is_clicked: false,
+ improve_suggest_experience_checked: false,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ ],
+ },
+ },
+ });
+ await QuickSuggestTestUtils.setConfig(QuickSuggestTestUtils.DEFAULT_CONFIG);
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_weather.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_weather.js
new file mode 100644
index 0000000000..b60fa9fe85
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_weather.js
@@ -0,0 +1,152 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests primary telemetry for weather suggestions.
+ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarProviderWeather: "resource:///modules/UrlbarProviderWeather.sys.mjs",
+});
+
+const suggestion_type = "weather";
+const match_type = "firefox-suggest";
+const index = 1;
+const position = index + 1;
+
+const { TELEMETRY_SCALARS: WEATHER_SCALARS } = UrlbarProviderWeather;
+const { WEATHER_SUGGESTION: suggestion, WEATHER_RS_DATA } = MerinoTestUtils;
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Make sure quick actions are disabled because showing them in the top
+ // sites view interferes with this test.
+ ["browser.urlbar.suggest.quickactions", false],
+ ],
+ });
+
+ await setUpTelemetryTest({
+ suggestions: [],
+ remoteSettingsResults: [
+ {
+ type: "weather",
+ weather: WEATHER_RS_DATA,
+ },
+ ],
+ });
+ await MerinoTestUtils.initWeather();
+ await updateTopSitesAndAwaitChanged();
+});
+
+add_task(async function () {
+ await doTelemetryTest({
+ index,
+ suggestion,
+ providerName: UrlbarProviderWeather.name,
+ showSuggestion: async () => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: MerinoTestUtils.WEATHER_KEYWORD,
+ });
+ },
+ teardown: async () => {
+ // Picking the block button sets this pref to false and disables weather
+ // suggestions. We need to flip it back to true and wait for the
+ // suggestion to be fetched again before continuing to the next selectable
+ // test. The view also also stay open, so close it afterward.
+ if (!UrlbarPrefs.get("suggest.weather")) {
+ await UrlbarTestUtils.promisePopupClose(window);
+ gURLBar.handleRevert();
+ let fetchPromise = QuickSuggest.weather.waitForFetches();
+ UrlbarPrefs.clear("suggest.weather");
+ await fetchPromise;
+ }
+ },
+ // impression-only
+ impressionOnly: {
+ scalars: {
+ [WEATHER_SCALARS.IMPRESSION]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "impression_only",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ },
+ selectables: {
+ // click
+ "urlbarView-row-inner": {
+ scalars: {
+ [WEATHER_SCALARS.IMPRESSION]: position,
+ [WEATHER_SCALARS.CLICK]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "click",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ },
+ // block
+ "urlbarView-button-block": {
+ scalars: {
+ [WEATHER_SCALARS.IMPRESSION]: position,
+ [WEATHER_SCALARS.BLOCK]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "block",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ },
+ // help
+ "urlbarView-button-help": {
+ scalars: {
+ [WEATHER_SCALARS.IMPRESSION]: position,
+ [WEATHER_SCALARS.HELP]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "help",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ },
+ },
+ });
+});
+
+async function updateTopSitesAndAwaitChanged() {
+ let url = "http://mochi.test:8888/topsite";
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits(url);
+ }
+
+ info("Updating top sites and awaiting newtab-top-sites-changed");
+ let changedPromise = TestUtils.topicObserved("newtab-top-sites-changed").then(
+ () => info("Observed newtab-top-sites-changed")
+ );
+ await updateTopSites(sites => sites?.length);
+ await changedPromise;
+}
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_weather.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_weather.js
new file mode 100644
index 0000000000..049b25dd09
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_weather.js
@@ -0,0 +1,379 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Browser test for the weather suggestion.
+ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarProviderWeather: "resource:///modules/UrlbarProviderWeather.sys.mjs",
+});
+
+add_setup(async function () {
+ await QuickSuggestTestUtils.ensureQuickSuggestInit({
+ remoteSettingsResults: [
+ {
+ type: "weather",
+ weather: MerinoTestUtils.WEATHER_RS_DATA,
+ },
+ ],
+ });
+ await MerinoTestUtils.initWeather();
+});
+
+// This test ensures the browser navigates to the weather webpage after
+// the weather result is selected.
+add_task(async function test_weather_result_selection() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false
+ );
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: MerinoTestUtils.WEATHER_KEYWORD,
+ });
+
+ info(`Select the weather result`);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+
+ info(`Navigate to the weather url`);
+ EventUtils.synthesizeKey("KEY_Enter");
+
+ await browserLoadedPromise;
+
+ Assert.equal(
+ gBrowser.currentURI.spec,
+ "https://example.com/weather",
+ "Assert the page navigated to the weather webpage after selecting the weather result."
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ await PlacesUtils.history.clear();
+});
+
+// Does a search, clicks the "Show less frequently" result menu command, and
+// repeats both steps until the min keyword length cap is reached.
+add_task(async function showLessFrequentlyCapReached_manySearches() {
+ // Set up a min keyword length and cap.
+ await QuickSuggestTestUtils.setRemoteSettingsResults([
+ {
+ type: "weather",
+ weather: {
+ keywords: ["weather"],
+ min_keyword_length: 3,
+ min_keyword_length_cap: 4,
+ },
+ },
+ ]);
+
+ // Trigger the suggestion.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "wea",
+ });
+
+ let resultIndex = 1;
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex);
+ Assert.equal(
+ details.result.providerName,
+ UrlbarProviderWeather.name,
+ "Weather suggestion should be present at expected index after 'wea' search"
+ );
+
+ // Click the command.
+ let command = "show_less_frequently";
+ await UrlbarTestUtils.openResultMenuAndClickItem(window, command, {
+ resultIndex,
+ });
+
+ Assert.ok(
+ gURLBar.view.isOpen,
+ "The view should remain open clicking the command"
+ );
+ Assert.ok(
+ details.element.row.hasAttribute("feedback-acknowledgment"),
+ "Row should have feedback acknowledgment after clicking command"
+ );
+ Assert.equal(
+ UrlbarPrefs.get("weather.minKeywordLength"),
+ 4,
+ "weather.minKeywordLength should be incremented once"
+ );
+
+ // Do the same search again. The suggestion should not appear.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "wea",
+ });
+
+ for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) {
+ details = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ Assert.notEqual(
+ details.result.providerName,
+ UrlbarProviderWeather.name,
+ `Weather suggestion should be absent (checking index ${i})`
+ );
+ }
+
+ // Do a search using one more character. The suggestion should appear.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "weat",
+ });
+
+ details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex);
+ Assert.equal(
+ details.result.providerName,
+ UrlbarProviderWeather.name,
+ "Weather suggestion should be present at expected index after 'weat' search"
+ );
+ Assert.ok(
+ !details.element.row.hasAttribute("feedback-acknowledgment"),
+ "Row should not have feedback acknowledgment after 'weat' search"
+ );
+
+ // Since the cap has been reached, the command should no longer appear in the
+ // result menu.
+ await UrlbarTestUtils.openResultMenu(window, { resultIndex });
+ let menuitem = gURLBar.view.resultMenu.querySelector(
+ `menuitem[data-command=${command}]`
+ );
+ Assert.ok(!menuitem, "Menuitem should be absent");
+ gURLBar.view.resultMenu.hidePopup(true);
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ await QuickSuggestTestUtils.setRemoteSettingsResults([
+ {
+ type: "weather",
+ weather: MerinoTestUtils.WEATHER_RS_DATA,
+ },
+ ]);
+ UrlbarPrefs.clear("weather.minKeywordLength");
+});
+
+// Repeatedly clicks the "Show less frequently" result menu command after doing
+// a single search until the min keyword length cap is reached.
+add_task(async function showLessFrequentlyCapReached_oneSearch() {
+ // Set up a min keyword length and cap.
+ await QuickSuggestTestUtils.setRemoteSettingsResults([
+ {
+ type: "weather",
+ weather: {
+ keywords: ["weather"],
+ min_keyword_length: 3,
+ min_keyword_length_cap: 6,
+ },
+ },
+ ]);
+
+ // Trigger the suggestion.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "wea",
+ });
+
+ let resultIndex = 1;
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex);
+ Assert.equal(
+ details.result.providerName,
+ UrlbarProviderWeather.name,
+ "Weather suggestion should be present at expected index after 'wea' search"
+ );
+
+ let command = "show_less_frequently";
+
+ for (let i = 0; i < 3; i++) {
+ await UrlbarTestUtils.openResultMenuAndClickItem(window, command, {
+ resultIndex,
+ openByMouse: true,
+ });
+
+ Assert.ok(
+ gURLBar.view.isOpen,
+ "The view should remain open clicking the command"
+ );
+ Assert.ok(
+ details.element.row.hasAttribute("feedback-acknowledgment"),
+ "Row should have feedback acknowledgment after clicking command"
+ );
+ Assert.equal(
+ UrlbarPrefs.get("weather.minKeywordLength"),
+ 4 + i,
+ "weather.minKeywordLength should be incremented once"
+ );
+ }
+
+ let menuitem = await UrlbarTestUtils.openResultMenuAndGetItem({
+ window,
+ command,
+ resultIndex,
+ });
+ Assert.ok(
+ !menuitem,
+ "The menuitem should not exist after the cap is reached"
+ );
+
+ gURLBar.view.resultMenu.hidePopup(true);
+ await UrlbarTestUtils.promisePopupClose(window);
+ await QuickSuggestTestUtils.setRemoteSettingsResults([
+ {
+ type: "weather",
+ weather: MerinoTestUtils.WEATHER_RS_DATA,
+ },
+ ]);
+ UrlbarPrefs.clear("weather.minKeywordLength");
+});
+
+// Tests the "Not interested" result menu dismissal command.
+add_task(async function notInterested() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: MerinoTestUtils.WEATHER_KEYWORD,
+ });
+ await doDismissTest("not_interested");
+});
+
+// Tests the "Not relevant" result menu dismissal command.
+add_task(async function notRelevant() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: MerinoTestUtils.WEATHER_KEYWORD,
+ });
+ await doDismissTest("not_relevant");
+});
+
+async function doDismissTest(command) {
+ let resultCount = UrlbarTestUtils.getResultCount(window);
+
+ let resultIndex = 1;
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex);
+ Assert.equal(
+ details.result.providerName,
+ UrlbarProviderWeather.name,
+ "Weather suggestion should be present"
+ );
+
+ // Click the command.
+ await UrlbarTestUtils.openResultMenuAndClickItem(
+ window,
+ ["[data-l10n-id=firefox-suggest-command-dont-show-this]", command],
+ { resultIndex, openByMouse: true }
+ );
+
+ Assert.ok(
+ !UrlbarPrefs.get("suggest.weather"),
+ "suggest.weather pref should be set to false after dismissal"
+ );
+
+ // The row should be a tip now.
+ Assert.ok(gURLBar.view.isOpen, "The view should remain open after dismissal");
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ resultCount,
+ "The result count should not haved changed after dismissal"
+ );
+ details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex);
+ Assert.equal(
+ details.type,
+ UrlbarUtils.RESULT_TYPE.TIP,
+ "Row should be a tip after dismissal"
+ );
+ Assert.equal(
+ details.result.payload.type,
+ "dismissalAcknowledgment",
+ "Tip type should be dismissalAcknowledgment"
+ );
+ Assert.ok(
+ !details.element.row.hasAttribute("feedback-acknowledgment"),
+ "Row should not have feedback acknowledgment after dismissal"
+ );
+
+ // Get the dismissal acknowledgment's "Got it" button and click it.
+ let gotItButton = UrlbarTestUtils.getButtonForResultIndex(
+ window,
+ "0",
+ resultIndex
+ );
+ Assert.ok(gotItButton, "Row should have a 'Got it' button");
+ EventUtils.synthesizeMouseAtCenter(gotItButton, {}, window);
+
+ // The view should remain open and the tip row should be gone.
+ Assert.ok(
+ gURLBar.view.isOpen,
+ "The view should remain open clicking the 'Got it' button"
+ );
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ resultCount - 1,
+ "The result count should be one less after clicking 'Got it' button"
+ );
+ for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) {
+ details = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ Assert.ok(
+ details.type != UrlbarUtils.RESULT_TYPE.TIP &&
+ details.result.providerName != UrlbarProviderWeather.name,
+ "Tip result and weather result should not be present"
+ );
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ // Enable the weather suggestion again and wait for it to be fetched.
+ let fetchPromise = QuickSuggest.weather.waitForFetches();
+ UrlbarPrefs.clear("suggest.weather");
+ info("Waiting for weather fetch after re-enabling the suggestion");
+ await fetchPromise;
+ info("Got weather fetch");
+}
+
+// Tests the "Report inaccurate location" result menu command immediately
+// followed by a dismissal command to make sure other commands still work
+// properly while the urlbar session remains ongoing.
+add_task(async function inaccurateLocationAndDismissal() {
+ await doSessionOngoingCommandTest("inaccurate_location");
+});
+
+// Tests the "Show less frequently" result menu command immediately followed by
+// a dismissal command to make sure other commands still work properly while the
+// urlbar session remains ongoing.
+add_task(async function showLessFrequentlyAndDismissal() {
+ await doSessionOngoingCommandTest("show_less_frequently");
+ UrlbarPrefs.clear("weather.minKeywordLength");
+});
+
+async function doSessionOngoingCommandTest(command) {
+ // Trigger the suggestion.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: MerinoTestUtils.WEATHER_KEYWORD,
+ });
+
+ let resultIndex = 1;
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex);
+ Assert.equal(
+ details.result.providerName,
+ UrlbarProviderWeather.name,
+ "Weather suggestion should be present at expected index after search"
+ );
+
+ // Click the command.
+ await UrlbarTestUtils.openResultMenuAndClickItem(window, command, {
+ resultIndex,
+ });
+
+ Assert.ok(
+ gURLBar.view.isOpen,
+ "The view should remain open clicking the command"
+ );
+ Assert.ok(
+ details.element.row.hasAttribute("feedback-acknowledgment"),
+ "Row should have feedback acknowledgment after clicking command"
+ );
+
+ info("Doing dismissal");
+ await doDismissTest("not_interested");
+}
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/head.js b/browser/components/urlbar/tests/quicksuggest/browser/head.js
new file mode 100644
index 0000000000..07979080fd
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/head.js
@@ -0,0 +1,569 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let sandbox;
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/urlbar/tests/browser/head-common.js",
+ this
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ CONTEXTUAL_SERVICES_PING_TYPES:
+ "resource:///modules/PartnerLinkAttribution.jsm",
+ QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs",
+ TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
+ UrlbarProviderQuickSuggest:
+ "resource:///modules/UrlbarProviderQuickSuggest.sys.mjs",
+});
+
+XPCOMUtils.defineLazyGetter(this, "QuickSuggestTestUtils", () => {
+ const { QuickSuggestTestUtils: module } = ChromeUtils.importESModule(
+ "resource://testing-common/QuickSuggestTestUtils.sys.mjs"
+ );
+ module.init(this);
+ return module;
+});
+
+XPCOMUtils.defineLazyGetter(this, "MerinoTestUtils", () => {
+ const { MerinoTestUtils: module } = ChromeUtils.importESModule(
+ "resource://testing-common/MerinoTestUtils.sys.mjs"
+ );
+ module.init(this);
+ return module;
+});
+
+registerCleanupFunction(async () => {
+ // Ensure the popup is always closed at the end of each test to avoid
+ // interfering with the next test.
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+/**
+ * Updates the Top Sites feed.
+ *
+ * @param {Function} condition
+ * A callback that returns true after Top Sites are successfully updated.
+ * @param {boolean} searchShortcuts
+ * True if Top Sites search shortcuts should be enabled.
+ */
+async function updateTopSites(condition, searchShortcuts = false) {
+ // Toggle the pref to clear the feed cache and force an update.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "browser.newtabpage.activity-stream.discoverystream.endpointSpocsClear",
+ "",
+ ],
+ ["browser.newtabpage.activity-stream.feeds.system.topsites", false],
+ ["browser.newtabpage.activity-stream.feeds.system.topsites", true],
+ [
+ "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts",
+ searchShortcuts,
+ ],
+ ],
+ });
+
+ // Wait for the feed to be updated.
+ await TestUtils.waitForCondition(() => {
+ let sites = AboutNewTab.getTopSites();
+ return condition(sites);
+ }, "Waiting for top sites to be updated");
+}
+
+/**
+ * Call this in your setup task if you use `doTelemetryTest()`.
+ *
+ * @param {object} options
+ * Options
+ * @param {Array} options.remoteSettingsResults
+ * Array of remote settings result objects. If not given, no suggestions
+ * will be present in remote settings.
+ * @param {Array} options.merinoSuggestions
+ * Array of Merino suggestion objects. If given, this function will start
+ * the mock Merino server and set `quicksuggest.dataCollection.enabled` to
+ * true so that `UrlbarProviderQuickSuggest` will fetch suggestions from it.
+ * Otherwise Merino will not serve suggestions, but you can still set up
+ * Merino without using this function by using `MerinoTestUtils` directly.
+ * @param {Array} options.config
+ * Quick suggest will be initialized with this config. Leave undefined to use
+ * the default config. See `QuickSuggestTestUtils` for details.
+ */
+async function setUpTelemetryTest({
+ remoteSettingsResults,
+ merinoSuggestions = null,
+ config = QuickSuggestTestUtils.DEFAULT_CONFIG,
+}) {
+ if (UrlbarPrefs.get("resultMenu")) {
+ todo(
+ false,
+ "telemetry for the result menu to be implemented in bug 1790020"
+ );
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.resultMenu", false]],
+ });
+ }
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Enable blocking on primary sponsored and nonsponsored suggestions so we
+ // can test the block button.
+ ["browser.urlbar.quicksuggest.blockingEnabled", true],
+ ["browser.urlbar.bestMatch.blockingEnabled", true],
+ // Switch-to-tab results can sometimes appear after the test clicks a help
+ // button and closes the new tab, which interferes with the expected
+ // indexes of quick suggest results, so disable them.
+ ["browser.urlbar.suggest.openpage", false],
+ // Disable the persisted-search-terms search tip because it can interfere.
+ ["browser.urlbar.tipShownCount.searchTip_persist", 999],
+ ],
+ });
+
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await UrlbarTestUtils.formHistory.clear();
+
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+
+ // Add a mock engine so we don't hit the network.
+ await SearchTestUtils.installSearchExtension({}, { setAsDefault: true });
+
+ await QuickSuggestTestUtils.ensureQuickSuggestInit({
+ remoteSettingsResults,
+ merinoSuggestions,
+ config,
+ });
+}
+
+/**
+ * Main entry point for testing primary telemetry for quick suggest suggestions:
+ * impressions, clicks, helps, and blocks. This can be used to declaratively
+ * test all primary telemetry for any suggestion type.
+ *
+ * @param {object} options
+ * Options
+ * @param {number} options.index
+ * The expected index of the suggestion in the results list.
+ * @param {object} options.suggestion
+ * The suggestion being tested.
+ * @param {object} options.impressionOnly
+ * An object describing the expected impression-only telemetry, i.e.,
+ * telemetry recorded when an impression occurs but not a click. It must have
+ * the following properties:
+ * {object} scalars
+ * An object that maps expected scalar names to values.
+ * {object} event
+ * The expected recorded event.
+ * {object} ping
+ * The expected recorded custom telemetry ping. If no ping is expected,
+ * leave this undefined or pass null.
+ * @param {object} options.selectables
+ * An object describing the telemetry that's expected to be recorded when each
+ * selectable element in the suggestion's row is picked. This object maps HTML
+ * class names to objects. Each property's name must be an HTML class name
+ * that uniquely identifies a selectable element within the row. The value
+ * must be an object that describes the telemetry that's expected to be
+ * recorded when that element is picked, and this inner object must have the
+ * following properties:
+ * {object} scalars
+ * An object that maps expected scalar names to values.
+ * {object} event
+ * The expected recorded event.
+ * {Array} pings
+ * A list of expected recorded custom telemetry pings. If no pings are
+ * expected, pass an empty array.
+ * @param {string} options.providerName
+ * The name of the provider that is expected to create the UrlbarResult for
+ * the suggestion.
+ * @param {Function} options.teardown
+ * If given, this function will be called after each selectable test. If
+ * picking an element causes side effects that need to be cleaned up before
+ * starting the next selectable test, they can be cleaned up here.
+ * @param {Function} options.showSuggestion
+ * This function should open the view and show the suggestion.
+ */
+async function doTelemetryTest({
+ index,
+ suggestion,
+ impressionOnly,
+ selectables,
+ providerName = UrlbarProviderQuickSuggest.name,
+ teardown = null,
+ showSuggestion = () =>
+ UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ // If the suggestion object is a remote settings result, it will have a
+ // `keywords` property. Otherwise the suggestion object must be a Merino
+ // suggestion, and the search string doesn't matter in that case because
+ // the mock Merino server will be set up to return suggestions regardless.
+ value: suggestion.keywords?.[0] || "test",
+ fireInputEvent: true,
+ }),
+}) {
+ // Do the impression-only test. It will return the `classList` values of all
+ // the selectable elements in the row so we can use them below.
+ let selectableClassLists = await doImpressionOnlyTest({
+ index,
+ suggestion,
+ providerName,
+ showSuggestion,
+ expected: impressionOnly,
+ });
+ if (!selectableClassLists) {
+ Assert.ok(
+ false,
+ "Impression test didn't complete successfully, stopping telemetry test"
+ );
+ return;
+ }
+
+ info(
+ "Got classLists of actual selectable elements in the row: " +
+ JSON.stringify(selectableClassLists)
+ );
+
+ let allMatchedExpectedClasses = new Set();
+
+ // For each actual selectable element in the row, do a selectable test by
+ // picking the element and checking telemetry.
+ for (let classList of selectableClassLists) {
+ info(
+ "Setting up selectable test for actual element with classList " +
+ JSON.stringify(classList)
+ );
+
+ // Each of the actual selectable elements should match exactly one of the
+ // test's expected selectable classes.
+ //
+ // * If an element doesn't match any expected class, then the test does not
+ // account for that element, which is an error in the test.
+ // * If an element matches more than one expected class, then the expected
+ // class is not specific enough, which is also an error in the test.
+
+ // Collect all the expected classes that match the actual element.
+ let matchingExpectedClasses = Object.keys(selectables).filter(className =>
+ classList.includes(className)
+ );
+
+ if (!matchingExpectedClasses.length) {
+ Assert.ok(
+ false,
+ "Actual selectable element doesn't match any expected classes. The element's classList is " +
+ JSON.stringify(classList)
+ );
+ continue;
+ }
+ if (matchingExpectedClasses.length > 1) {
+ Assert.ok(
+ false,
+ "Actual selectable element matches multiple expected classes. The element's classList is " +
+ JSON.stringify(classList)
+ );
+ continue;
+ }
+
+ let className = matchingExpectedClasses[0];
+ allMatchedExpectedClasses.add(className);
+
+ await doSelectableTest({
+ suggestion,
+ providerName,
+ showSuggestion,
+ index,
+ className,
+ expected: selectables[className],
+ });
+
+ if (teardown) {
+ info("Calling teardown");
+ await teardown();
+ info("Finished teardown");
+ }
+ }
+
+ // Finally, if an expected class doesn't match any actual element, then the
+ // test expects an element to be picked that either isn't present or isn't
+ // selectable, which is an error in the test.
+ Assert.deepEqual(
+ Object.keys(selectables).filter(
+ className => !allMatchedExpectedClasses.has(className)
+ ),
+ [],
+ "There should be no expected classes that didn't match actual selectable elements"
+ );
+}
+
+/**
+ * Helper for `doTelemetryTest()` that does an impression-only test.
+ *
+ * @param {object} options
+ * Options
+ * @param {number} options.index
+ * The expected index of the suggestion in the results list.
+ * @param {object} options.suggestion
+ * The suggestion being tested.
+ * @param {string} options.providerName
+ * The name of the provider that is expected to create the UrlbarResult for
+ * the suggestion.
+ * @param {object} options.expected
+ * An object describing the expected impression-only telemetry. It must have
+ * the following properties:
+ * {object} scalars
+ * An object that maps expected scalar names to values.
+ * {object} event
+ * The expected recorded event.
+ * {object} ping
+ * The expected recorded custom telemetry ping. If no ping is expected,
+ * leave this undefined or pass null.
+ * @param {Function} options.showSuggestion
+ * This function should open the view and show the suggestion.
+ * @returns {Array}
+ * The `classList` values of all the selectable elements in the suggestion's
+ * row. Each item in this array is a selectable element's `classList` that has
+ * been converted to an array of strings.
+ */
+async function doImpressionOnlyTest({
+ index,
+ suggestion,
+ providerName,
+ expected,
+ showSuggestion,
+}) {
+ info("Starting impression-only test");
+
+ Services.telemetry.clearEvents();
+ let { spy, spyCleanup } = QuickSuggestTestUtils.createTelemetryPingSpy();
+
+ info("Showing suggestion");
+ await showSuggestion();
+
+ // Get the suggestion row.
+ let row = await validateSuggestionRow(index, suggestion, providerName);
+ if (!row) {
+ Assert.ok(
+ false,
+ "Couldn't get suggestion row, stopping impression-only test"
+ );
+ await spyCleanup();
+ return null;
+ }
+
+ // We need to get a different selectable row so we can pick it to trigger
+ // impression-only telemetry. For simplicity we'll look for a row that will
+ // load a URL when picked. We'll also verify no other rows are from the
+ // expected provider.
+ let otherRow;
+ let rowCount = UrlbarTestUtils.getResultCount(window);
+ for (let i = 0; i < rowCount; i++) {
+ if (i != index) {
+ let r = await UrlbarTestUtils.waitForAutocompleteResultAt(window, i);
+ Assert.notEqual(
+ r.result.providerName,
+ providerName,
+ "No other row should be from expected provider: index = " + i
+ );
+ if (
+ !otherRow &&
+ (r.result.payload.url ||
+ (r.result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
+ (r.result.payload.query || r.result.payload.suggestion))) &&
+ r.hasAttribute("row-selectable")
+ ) {
+ otherRow = r;
+ }
+ }
+ }
+ if (!otherRow) {
+ Assert.ok(
+ false,
+ "Couldn't get a different selectable row with a URL, stopping impression-only test"
+ );
+ await spyCleanup();
+ return null;
+ }
+
+ // Collect the `classList` values for all selectable elements in the row.
+ let selectableClassLists = [];
+ let selectables = row.querySelectorAll(":is([selectable], [role=button])");
+ for (let element of selectables) {
+ selectableClassLists.push([...element.classList]);
+ }
+
+ // Pick the different row. Assumptions:
+ // * The middle of the row is selectable
+ // * Picking the row will load a page
+ info("Clicking different row and waiting for view to close");
+ let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeMouseAtCenter(otherRow, {})
+ );
+
+ info("Waiting for page to load after clicking different row");
+ await loadPromise;
+
+ // Check telemetry.
+ info("Checking scalars. Expected: " + JSON.stringify(expected.scalars));
+ QuickSuggestTestUtils.assertScalars(expected.scalars);
+
+ info("Checking events. Expected: " + JSON.stringify([expected.event]));
+ QuickSuggestTestUtils.assertEvents([expected.event]);
+
+ let expectedPings = expected.ping ? [expected.ping] : [];
+ info("Checking pings. Expected: " + JSON.stringify(expectedPings));
+ QuickSuggestTestUtils.assertPings(spy, expectedPings);
+
+ // Clean up.
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.formHistory.clear();
+ await spyCleanup();
+
+ info("Finished impression-only test");
+
+ return selectableClassLists;
+}
+
+/**
+ * Helper for `doTelemetryTest()` that picks a selectable element in a
+ * suggestion's row and checks telemetry.
+ *
+ * @param {object} options
+ * Options
+ * @param {number} options.index
+ * The expected index of the suggestion in the results list.
+ * @param {object} options.suggestion
+ * The suggestion being tested.
+ * @param {string} options.providerName
+ * The name of the provider that is expected to create the UrlbarResult for
+ * the suggestion.
+ * @param {string} options.className
+ * An HTML class name that should uniquely identify the selectable element
+ * within its row.
+ * @param {object} options.expected
+ * An object describing the telemetry that's expected to be recorded when the
+ * selectable element is picked. It must have the following properties:
+ * {object} scalars
+ * An object that maps expected scalar names to values.
+ * {object} event
+ * The expected recorded event.
+ * {Array} pings
+ * A list of expected recorded custom telemetry pings. If no pings are
+ * expected, leave this undefined or pass an empty array.
+ * @param {Function} options.showSuggestion
+ * This function should open the view and show the suggestion.
+ */
+async function doSelectableTest({
+ index,
+ suggestion,
+ providerName,
+ className,
+ expected,
+ showSuggestion,
+}) {
+ info("Starting selectable test: " + JSON.stringify({ className }));
+
+ Services.telemetry.clearEvents();
+ let { spy, spyCleanup } = QuickSuggestTestUtils.createTelemetryPingSpy();
+
+ info("Showing suggestion");
+ await showSuggestion();
+
+ let row = await validateSuggestionRow(index, suggestion, providerName);
+ if (!row) {
+ Assert.ok(false, "Couldn't get suggestion row, stopping selectable test");
+ await spyCleanup();
+ return;
+ }
+
+ let element = row.querySelector("." + className);
+ Assert.ok(element, "Sanity check: Target selectable element should exist");
+
+ let loadPromise;
+ if (className == "urlbarView-row-inner") {
+ // We assume clicking the row-inner will cause a page to load in the current
+ // browser.
+ loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ } else if (className == "urlbarView-button-help") {
+ loadPromise = BrowserTestUtils.waitForNewTab(gBrowser);
+ }
+
+ info("Clicking element: " + className);
+ EventUtils.synthesizeMouseAtCenter(element, {});
+
+ if (loadPromise) {
+ info("Waiting for load");
+ await loadPromise;
+ await TestUtils.waitForTick();
+ if (className == "urlbarView-button-help") {
+ info("Closing help tab");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+ }
+
+ info("Checking scalars. Expected: " + JSON.stringify(expected.scalars));
+ QuickSuggestTestUtils.assertScalars(expected.scalars);
+
+ info("Checking events. Expected: " + JSON.stringify([expected.event]));
+ QuickSuggestTestUtils.assertEvents([expected.event]);
+
+ let expectedPings = expected.pings ?? [];
+ info("Checking pings. Expected: " + JSON.stringify(expectedPings));
+ QuickSuggestTestUtils.assertPings(spy, expectedPings);
+
+ if (className == "urlbarView-button-block") {
+ await QuickSuggest.blockedSuggestions.clear();
+ }
+ await PlacesUtils.history.clear();
+ await spyCleanup();
+
+ info("Finished selectable test: " + JSON.stringify({ className }));
+}
+
+/**
+ * Gets a row in the view, which is assumed to be open, and asserts that it's a
+ * particular quick suggest row. If it is, the row is returned. If it's not,
+ * null is returned.
+ *
+ * @param {number} index
+ * The expected index of the quick suggest row.
+ * @param {object} suggestion
+ * The expected suggestion.
+ * @param {string} providerName
+ * The name of the provider that is expected to create the UrlbarResult for
+ * the suggestion.
+ * @returns {Element}
+ * If the row is the expected suggestion, the row element is returned.
+ * Otherwise null is returned.
+ */
+async function validateSuggestionRow(index, suggestion, providerName) {
+ let rowCount = UrlbarTestUtils.getResultCount(window);
+ Assert.less(
+ index,
+ rowCount,
+ "Expected suggestion row index should be < row count"
+ );
+ if (rowCount <= index) {
+ return null;
+ }
+
+ let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, index);
+ Assert.equal(
+ row.result.providerName,
+ providerName,
+ "Expected suggestion row should be from expected provider"
+ );
+ Assert.equal(
+ row.result.payload.url,
+ suggestion.url,
+ "The suggestion row should represent the expected suggestion"
+ );
+ if (
+ row.result.providerName != providerName ||
+ row.result.payload.url != suggestion.url
+ ) {
+ return null;
+ }
+
+ return row;
+}
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/searchSuggestionEngine.sjs b/browser/components/urlbar/tests/quicksuggest/browser/searchSuggestionEngine.sjs
new file mode 100644
index 0000000000..145392fcf2
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/searchSuggestionEngine.sjs
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let gTimer;
+
+function handleRequest(req, resp) {
+ // Parse the query params. If the params aren't in the form "foo=bar", then
+ // treat the entire query string as a search string.
+ let params = req.queryString.split("&").reduce((memo, pair) => {
+ let [key, val] = pair.split("=");
+ if (!val) {
+ // This part isn't in the form "foo=bar". Treat it as the search string
+ // (the "query").
+ val = key;
+ key = "query";
+ }
+ memo[decode(key)] = decode(val);
+ return memo;
+ }, {});
+
+ let timeout = parseInt(params.timeout);
+ if (timeout) {
+ // Write the response after a timeout.
+ resp.processAsync();
+ gTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ gTimer.init(
+ () => {
+ writeResponse(params, resp);
+ resp.finish();
+ },
+ timeout,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+ return;
+ }
+
+ writeResponse(params, resp);
+}
+
+function writeResponse(params, resp) {
+ // Echo back the search string with "foo" and "bar" appended.
+ let suffixes = ["foo", "bar"];
+ if (params.count) {
+ // Add more suffixes.
+ let serial = 0;
+ while (suffixes.length < params.count) {
+ suffixes.push(++serial);
+ }
+ }
+ let data = [params.query, suffixes.map(s => params.query + s)];
+ resp.setHeader("Content-Type", "application/json", false);
+ resp.write(JSON.stringify(data));
+}
+
+function decode(str) {
+ return decodeURIComponent(str.replace(/\+/g, encodeURIComponent(" ")));
+}
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/searchSuggestionEngine.xml b/browser/components/urlbar/tests/quicksuggest/browser/searchSuggestionEngine.xml
new file mode 100644
index 0000000000..142c91849c
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/searchSuggestionEngine.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>browser_searchSuggestionEngine searchSuggestionEngine.xml</ShortName>
+<Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/searchSuggestionEngine.sjs?{searchTerms}"/>
+<Url type="text/html" method="GET" template="http://mochi.test:8888/" rel="searchform">
+ <Param name="terms" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/browser/components/urlbar/tests/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 @@
+<?xml version="1.0"?>
+
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Sample sub-dialog">
+<dialog id="subDialog">
+ <description id="desc">A sample sub-dialog for testing</description>
+</dialog>
+</window>
diff --git a/browser/components/urlbar/tests/quicksuggest/unit/head.js b/browser/components/urlbar/tests/quicksuggest/unit/head.js
new file mode 100644
index 0000000000..39f920fce5
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/unit/head.js
@@ -0,0 +1,227 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from ../../unit/head.js */
+
+ChromeUtils.defineESModuleGetters(this, {
+ QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs",
+ QuickSuggestRemoteSettings:
+ "resource:///modules/urlbar/private/QuickSuggestRemoteSettings.sys.mjs",
+ TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
+ UrlbarProviderAutofill: "resource:///modules/UrlbarProviderAutofill.sys.mjs",
+ UrlbarProviderQuickSuggest:
+ "resource:///modules/UrlbarProviderQuickSuggest.sys.mjs",
+});
+
+add_setup(async function setUpQuickSuggestXpcshellTest() {
+ // Initializing TelemetryEnvironment in an xpcshell environment requires
+ // jumping through a bunch of hoops. Suggest's use of TelemetryEnvironment is
+ // tested in browser tests, and there's no other necessary reason to wait for
+ // TelemetryEnvironment initialization in xpcshell tests, so just skip it.
+ UrlbarPrefs._testSkipTelemetryEnvironmentInit = true;
+});
+
+/**
+ * Tests quick suggest prefs migrations.
+ *
+ * @param {object} options
+ * The options object.
+ * @param {object} options.testOverrides
+ * An object that modifies how migration is performed. It has the following
+ * properties, and all are optional:
+ *
+ * {number} migrationVersion
+ * Migration will stop at this version, so for example you can test
+ * migration only up to version 1 even when the current actual version is
+ * larger than 1.
+ * {object} defaultPrefs
+ * An object that maps pref names (relative to `browser.urlbar`) to
+ * default-branch values. These should be the default prefs for the given
+ * `migrationVersion` and will be set as defaults before migration occurs.
+ *
+ * @param {string} options.scenario
+ * The scenario to set at the time migration occurs.
+ * @param {object} options.expectedPrefs
+ * The expected prefs after migration: `{ defaultBranch, userBranch }`
+ * Pref names should be relative to `browser.urlbar`.
+ * @param {object} [options.initialUserBranch]
+ * Prefs to set on the user branch before migration ocurs. Use these to
+ * simulate user actions like disabling prefs or opting in or out of the
+ * online modal. Pref names should be relative to `browser.urlbar`.
+ */
+async function doMigrateTest({
+ testOverrides,
+ scenario,
+ expectedPrefs,
+ initialUserBranch = {},
+}) {
+ info(
+ "Testing migration: " +
+ JSON.stringify({
+ testOverrides,
+ initialUserBranch,
+ scenario,
+ expectedPrefs,
+ })
+ );
+
+ function setPref(branch, name, value) {
+ switch (typeof value) {
+ case "boolean":
+ branch.setBoolPref(name, value);
+ break;
+ case "number":
+ branch.setIntPref(name, value);
+ break;
+ case "string":
+ branch.setCharPref(name, value);
+ break;
+ default:
+ Assert.ok(
+ false,
+ `Pref type not handled for setPref: ${name} = ${value}`
+ );
+ break;
+ }
+ }
+
+ function getPref(branch, name) {
+ let type = typeof UrlbarPrefs.get(name);
+ switch (type) {
+ case "boolean":
+ return branch.getBoolPref(name);
+ case "number":
+ return branch.getIntPref(name);
+ case "string":
+ return branch.getCharPref(name);
+ default:
+ Assert.ok(false, `Pref type not handled for getPref: ${name} ${type}`);
+ break;
+ }
+ return null;
+ }
+
+ let defaultBranch = Services.prefs.getDefaultBranch("browser.urlbar.");
+ let userBranch = Services.prefs.getBranch("browser.urlbar.");
+
+ // Set initial prefs. `initialDefaultBranch` are firefox.js values, i.e.,
+ // defaults immediately after startup and before any scenario update and
+ // migration happens.
+ UrlbarPrefs._updatingFirefoxSuggestScenario = true;
+ UrlbarPrefs.clear("quicksuggest.migrationVersion");
+ let initialDefaultBranch = {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ "quicksuggest.dataCollection.enabled": false,
+ };
+ for (let name of Object.keys(initialDefaultBranch)) {
+ userBranch.clearUserPref(name);
+ }
+ for (let [branch, prefs] of [
+ [defaultBranch, initialDefaultBranch],
+ [userBranch, initialUserBranch],
+ ]) {
+ for (let [name, value] of Object.entries(prefs)) {
+ if (value !== undefined) {
+ setPref(branch, name, value);
+ }
+ }
+ }
+ UrlbarPrefs._updatingFirefoxSuggestScenario = false;
+
+ // Update the scenario and check prefs twice. The first time the migration
+ // should happen, and the second time the migration should not happen and
+ // all the prefs should stay the same.
+ for (let i = 0; i < 2; i++) {
+ info(`Calling updateFirefoxSuggestScenario, i=${i}`);
+
+ // Do the scenario update and set `isStartup` to simulate startup.
+ await UrlbarPrefs.updateFirefoxSuggestScenario({
+ ...testOverrides,
+ scenario,
+ isStartup: true,
+ });
+
+ // Check expected pref values. Store expected effective values as we go so
+ // we can check them afterward. For a given pref, the expected effective
+ // value is the user value, or if there's not a user value, the default
+ // value.
+ let expectedEffectivePrefs = {};
+ let {
+ defaultBranch: expectedDefaultBranch,
+ userBranch: expectedUserBranch,
+ } = expectedPrefs;
+ expectedDefaultBranch = expectedDefaultBranch || {};
+ expectedUserBranch = expectedUserBranch || {};
+ for (let [branch, prefs, branchType] of [
+ [defaultBranch, expectedDefaultBranch, "default"],
+ [userBranch, expectedUserBranch, "user"],
+ ]) {
+ let entries = Object.entries(prefs);
+ if (!entries.length) {
+ continue;
+ }
+
+ info(
+ `Checking expected prefs on ${branchType} branch after updating scenario`
+ );
+ for (let [name, value] of entries) {
+ expectedEffectivePrefs[name] = value;
+ if (branch == userBranch) {
+ Assert.ok(
+ userBranch.prefHasUserValue(name),
+ `Pref ${name} is on user branch`
+ );
+ }
+ Assert.equal(
+ getPref(branch, name),
+ value,
+ `Pref ${name} value on ${branchType} branch`
+ );
+ }
+ }
+
+ info(
+ `Making sure prefs on the default branch without expected user-branch values are not on the user branch`
+ );
+ for (let name of Object.keys(initialDefaultBranch)) {
+ if (!expectedUserBranch.hasOwnProperty(name)) {
+ Assert.ok(
+ !userBranch.prefHasUserValue(name),
+ `Pref ${name} is not on user branch`
+ );
+ }
+ }
+
+ info(`Checking expected effective prefs`);
+ for (let [name, value] of Object.entries(expectedEffectivePrefs)) {
+ Assert.equal(
+ UrlbarPrefs.get(name),
+ value,
+ `Pref ${name} effective value`
+ );
+ }
+
+ let currentVersion =
+ testOverrides?.migrationVersion === undefined
+ ? UrlbarPrefs.FIREFOX_SUGGEST_MIGRATION_VERSION
+ : testOverrides.migrationVersion;
+ Assert.equal(
+ UrlbarPrefs.get("quicksuggest.migrationVersion"),
+ currentVersion,
+ "quicksuggest.migrationVersion is correct after migration"
+ );
+ }
+
+ // Clean up.
+ UrlbarPrefs._updatingFirefoxSuggestScenario = true;
+ UrlbarPrefs.clear("quicksuggest.migrationVersion");
+ let userBranchNames = [
+ ...Object.keys(initialUserBranch),
+ ...Object.keys(expectedPrefs.userBranch || {}),
+ ];
+ for (let name of userBranchNames) {
+ userBranch.clearUserPref(name);
+ }
+ UrlbarPrefs._updatingFirefoxSuggestScenario = false;
+}
diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_merinoClient.js b/browser/components/urlbar/tests/quicksuggest/unit/test_merinoClient.js
new file mode 100644
index 0000000000..cd45cb11a7
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/unit/test_merinoClient.js
@@ -0,0 +1,647 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Test for MerinoClient.
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ ExperimentFakes: "resource://testing-common/NimbusTestUtils.sys.mjs",
+ MerinoClient: "resource:///modules/MerinoClient.sys.mjs",
+ NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
+});
+
+// Set the `merino.timeoutMs` pref to a large value so that the client will not
+// inadvertently time out during fetches. This is especially important on CI and
+// when running this test in verify mode. Tasks that specifically test timeouts
+// may need to set a more reasonable value for their duration.
+const TEST_TIMEOUT_MS = 30000;
+
+// The expected suggestion objects returned from `MerinoClient.fetch()`.
+const EXPECTED_MERINO_SUGGESTIONS = [];
+
+const { SEARCH_PARAMS } = MerinoClient;
+
+let gClient;
+
+add_setup(async function init() {
+ UrlbarPrefs.set("merino.timeoutMs", TEST_TIMEOUT_MS);
+ registerCleanupFunction(() => {
+ UrlbarPrefs.clear("merino.timeoutMs");
+ });
+
+ gClient = new MerinoClient();
+ await MerinoTestUtils.server.start();
+
+ for (let suggestion of MerinoTestUtils.server.response.body.suggestions) {
+ EXPECTED_MERINO_SUGGESTIONS.push({
+ ...suggestion,
+ request_id: MerinoTestUtils.server.response.body.request_id,
+ source: "merino",
+ });
+ }
+});
+
+// Checks client names.
+add_task(async function name() {
+ Assert.equal(
+ gClient.name,
+ "anonymous",
+ "gClient name is 'anonymous' since it wasn't given a name"
+ );
+
+ let client = new MerinoClient("New client");
+ Assert.equal(client.name, "New client", "newClient name is correct");
+});
+
+// Does a successful fetch.
+add_task(async function success() {
+ let histograms = MerinoTestUtils.getAndClearHistograms();
+
+ await fetchAndCheckSuggestions({
+ expected: EXPECTED_MERINO_SUGGESTIONS,
+ });
+
+ Assert.equal(
+ gClient.lastFetchStatus,
+ "success",
+ "The request successfully finished"
+ );
+ MerinoTestUtils.checkAndClearHistograms({
+ histograms,
+ response: "success",
+ latencyRecorded: true,
+ client: gClient,
+ });
+});
+
+// Does a successful fetch that doesn't return any suggestions.
+add_task(async function noSuggestions() {
+ let { suggestions } = MerinoTestUtils.server.response.body;
+ MerinoTestUtils.server.response.body.suggestions = [];
+
+ let histograms = MerinoTestUtils.getAndClearHistograms();
+
+ await fetchAndCheckSuggestions({
+ expected: [],
+ });
+
+ Assert.equal(
+ gClient.lastFetchStatus,
+ "no_suggestion",
+ "The request successfully finished without suggestions"
+ );
+ MerinoTestUtils.checkAndClearHistograms({
+ histograms,
+ response: "no_suggestion",
+ latencyRecorded: true,
+ client: gClient,
+ });
+
+ MerinoTestUtils.server.response.body.suggestions = suggestions;
+});
+
+// Checks a response that's valid but also has some unexpected properties.
+add_task(async function unexpectedResponseProperties() {
+ let histograms = MerinoTestUtils.getAndClearHistograms();
+
+ MerinoTestUtils.server.response.body.unexpectedString = "some value";
+ MerinoTestUtils.server.response.body.unexpectedArray = ["a", "b", "c"];
+ MerinoTestUtils.server.response.body.unexpectedObject = { foo: "bar" };
+
+ await fetchAndCheckSuggestions({
+ expected: EXPECTED_MERINO_SUGGESTIONS,
+ });
+
+ Assert.equal(
+ gClient.lastFetchStatus,
+ "success",
+ "The request successfully finished"
+ );
+ MerinoTestUtils.checkAndClearHistograms({
+ histograms,
+ response: "success",
+ latencyRecorded: true,
+ client: gClient,
+ });
+});
+
+// Checks some responses with unexpected response bodies.
+add_task(async function unexpectedResponseBody() {
+ let histograms = MerinoTestUtils.getAndClearHistograms();
+
+ let responses = [
+ { body: {} },
+ { body: { bogus: [] } },
+ { body: { suggestions: {} } },
+ { body: { suggestions: [] } },
+ { body: "" },
+ { body: "bogus", contentType: "text/html" },
+ ];
+
+ for (let r of responses) {
+ info("Testing response: " + JSON.stringify(r));
+
+ MerinoTestUtils.server.response = r;
+ await fetchAndCheckSuggestions({ expected: [] });
+
+ Assert.equal(
+ gClient.lastFetchStatus,
+ "no_suggestion",
+ "The request successfully finished without suggestions"
+ );
+ MerinoTestUtils.checkAndClearHistograms({
+ histograms,
+ response: "no_suggestion",
+ latencyRecorded: true,
+ client: gClient,
+ });
+ }
+
+ MerinoTestUtils.server.reset();
+});
+
+// Tests with a network error.
+add_task(async function networkError() {
+ let histograms = MerinoTestUtils.getAndClearHistograms();
+
+ // This promise will be resolved when the client processes the network error.
+ let responsePromise = gClient.waitForNextResponse();
+
+ await MerinoTestUtils.server.withNetworkError(async () => {
+ await fetchAndCheckSuggestions({ expected: [] });
+ });
+
+ // The client should have nulled out the timeout timer before `fetch()`
+ // returned.
+ Assert.strictEqual(
+ gClient._test_timeoutTimer,
+ null,
+ "timeoutTimer does not exist after fetch finished"
+ );
+
+ // Wait for the client to process the network error.
+ await responsePromise;
+
+ Assert.equal(
+ gClient.lastFetchStatus,
+ "network_error",
+ "The request failed with a network error"
+ );
+ MerinoTestUtils.checkAndClearHistograms({
+ histograms,
+ response: "network_error",
+ latencyRecorded: false,
+ client: gClient,
+ });
+});
+
+// Tests with an HTTP error.
+add_task(async function httpError() {
+ let histograms = MerinoTestUtils.getAndClearHistograms();
+
+ MerinoTestUtils.server.response = { status: 500 };
+ await fetchAndCheckSuggestions({ expected: [] });
+
+ Assert.equal(
+ gClient.lastFetchStatus,
+ "http_error",
+ "The request failed with an HTTP error"
+ );
+ MerinoTestUtils.checkAndClearHistograms({
+ histograms,
+ response: "http_error",
+ latencyRecorded: true,
+ client: gClient,
+ });
+
+ MerinoTestUtils.server.reset();
+});
+
+// Tests a client timeout.
+add_task(async function clientTimeout() {
+ await doClientTimeoutTest({
+ prefTimeoutMs: 200,
+ responseDelayMs: 400,
+ });
+});
+
+// Tests a client timeout followed by an HTTP error. Only the timeout should be
+// recorded.
+add_task(async function clientTimeoutFollowedByHTTPError() {
+ MerinoTestUtils.server.response = { status: 500 };
+ await doClientTimeoutTest({
+ prefTimeoutMs: 200,
+ responseDelayMs: 400,
+ expectedResponseStatus: 500,
+ });
+});
+
+// Tests a client timeout when a timeout value is passed to `fetch()`, which
+// should override the value in the `merino.timeoutMs` pref.
+add_task(async function timeoutPassedToFetch() {
+ // Set up a timeline like this:
+ //
+ // 1ms: The timeout passed to `fetch()` elapses
+ // 400ms: Merino returns a response
+ // 30000ms: The timeout in the pref elapses
+ //
+ // The expected behavior is that the 1ms timeout is hit, the request fails
+ // with a timeout, and Merino later returns a response. If the 1ms timeout is
+ // not hit, then Merino will return a response before the 30000ms timeout
+ // elapses and the request will complete successfully.
+
+ await doClientTimeoutTest({
+ prefTimeoutMs: 30000,
+ responseDelayMs: 400,
+ fetchArgs: { query: "search", timeoutMs: 1 },
+ });
+});
+
+async function doClientTimeoutTest({
+ prefTimeoutMs,
+ responseDelayMs,
+ fetchArgs = { query: "search" },
+ expectedResponseStatus = 200,
+} = {}) {
+ let histograms = MerinoTestUtils.getAndClearHistograms();
+
+ let originalPrefTimeoutMs = UrlbarPrefs.get("merino.timeoutMs");
+ UrlbarPrefs.set("merino.timeoutMs", prefTimeoutMs);
+
+ // Make the server return a delayed response so the client times out waiting
+ // for it.
+ MerinoTestUtils.server.response.delay = responseDelayMs;
+
+ let responsePromise = gClient.waitForNextResponse();
+ await fetchAndCheckSuggestions({ args: fetchArgs, expected: [] });
+
+ Assert.equal(gClient.lastFetchStatus, "timeout", "The request timed out");
+
+ // The client should have nulled out the timeout timer.
+ Assert.strictEqual(
+ gClient._test_timeoutTimer,
+ null,
+ "timeoutTimer does not exist after fetch finished"
+ );
+
+ // The fetch controller should still exist because the fetch should remain
+ // ongoing.
+ Assert.ok(
+ gClient._test_fetchController,
+ "fetchController still exists after fetch finished"
+ );
+ Assert.ok(
+ !gClient._test_fetchController.signal.aborted,
+ "fetchController is not aborted"
+ );
+
+ // The latency histogram should not be updated since the response has not been
+ // received.
+ MerinoTestUtils.checkAndClearHistograms({
+ histograms,
+ response: "timeout",
+ latencyRecorded: false,
+ latencyStopwatchRunning: true,
+ client: gClient,
+ });
+
+ // Wait for the client to receive the response.
+ let httpResponse = await responsePromise;
+ Assert.ok(httpResponse, "Response was received");
+ Assert.equal(httpResponse.status, expectedResponseStatus, "Response status");
+
+ // The client should have nulled out the fetch controller.
+ Assert.ok(!gClient._test_fetchController, "fetchController no longer exists");
+
+ // The `checkAndClearHistograms()` call above cleared the histograms. After
+ // that, nothing else should have been recorded for the response.
+ MerinoTestUtils.checkAndClearHistograms({
+ histograms,
+ response: null,
+ latencyRecorded: true,
+ client: gClient,
+ });
+
+ MerinoTestUtils.server.reset();
+ UrlbarPrefs.set("merino.timeoutMs", originalPrefTimeoutMs);
+}
+
+// By design, when a fetch times out, the client allows it to finish so we can
+// record its latency. But when a second fetch starts before the first finishes,
+// the client should abort the first so that there is at most one fetch at a
+// time.
+add_task(async function newFetchAbortsPrevious() {
+ let histograms = MerinoTestUtils.getAndClearHistograms();
+
+ // Make the server return a very delayed response so that it would time out
+ // and we can start a second fetch that will abort the first fetch.
+ MerinoTestUtils.server.response.delay =
+ 100 * UrlbarPrefs.get("merino.timeoutMs");
+
+ // Do the first fetch.
+ await fetchAndCheckSuggestions({ expected: [] });
+
+ // At this point, the timeout timer has fired, causing our `fetch()` call to
+ // return. However, the client's internal fetch should still be ongoing.
+
+ Assert.equal(gClient.lastFetchStatus, "timeout", "The request timed out");
+
+ // The client should have nulled out the timeout timer.
+ Assert.strictEqual(
+ gClient._test_timeoutTimer,
+ null,
+ "timeoutTimer does not exist after first fetch finished"
+ );
+
+ // The fetch controller should still exist because the fetch should remain
+ // ongoing.
+ Assert.ok(
+ gClient._test_fetchController,
+ "fetchController still exists after first fetch finished"
+ );
+ Assert.ok(
+ !gClient._test_fetchController.signal.aborted,
+ "fetchController is not aborted"
+ );
+
+ // The latency histogram should not be updated since the fetch is still
+ // ongoing.
+ MerinoTestUtils.checkAndClearHistograms({
+ histograms,
+ response: "timeout",
+ latencyRecorded: false,
+ latencyStopwatchRunning: true,
+ client: gClient,
+ });
+
+ // Do the second fetch. This time don't delay the response.
+ delete MerinoTestUtils.server.response.delay;
+ await fetchAndCheckSuggestions({
+ expected: EXPECTED_MERINO_SUGGESTIONS,
+ });
+
+ Assert.equal(
+ gClient.lastFetchStatus,
+ "success",
+ "The request finished successfully"
+ );
+
+ // The fetch was successful, so the client should have nulled out both
+ // properties.
+ Assert.ok(
+ !gClient._test_fetchController,
+ "fetchController does not exist after second fetch finished"
+ );
+ Assert.strictEqual(
+ gClient._test_timeoutTimer,
+ null,
+ "timeoutTimer does not exist after second fetch finished"
+ );
+
+ MerinoTestUtils.checkAndClearHistograms({
+ histograms,
+ response: "success",
+ latencyRecorded: true,
+ client: gClient,
+ });
+
+ MerinoTestUtils.server.reset();
+});
+
+// The client should not include the `clientVariants` and `providers` search
+// params when they are not set.
+add_task(async function clientVariants_providers_notSet() {
+ UrlbarPrefs.set("merino.clientVariants", "");
+ UrlbarPrefs.set("merino.providers", "");
+
+ await fetchAndCheckSuggestions({
+ expected: EXPECTED_MERINO_SUGGESTIONS,
+ });
+
+ MerinoTestUtils.server.checkAndClearRequests([
+ {
+ params: {
+ [SEARCH_PARAMS.QUERY]: "search",
+ [SEARCH_PARAMS.SEQUENCE_NUMBER]: 0,
+ },
+ },
+ ]);
+
+ UrlbarPrefs.clear("merino.clientVariants");
+ UrlbarPrefs.clear("merino.providers");
+});
+
+// The client should include the `clientVariants` and `providers` search params
+// when they are set using preferences.
+add_task(async function clientVariants_providers_preferences() {
+ UrlbarPrefs.set("merino.clientVariants", "green");
+ UrlbarPrefs.set("merino.providers", "pink");
+
+ await fetchAndCheckSuggestions({
+ expected: EXPECTED_MERINO_SUGGESTIONS,
+ });
+
+ MerinoTestUtils.server.checkAndClearRequests([
+ {
+ params: {
+ [SEARCH_PARAMS.QUERY]: "search",
+ [SEARCH_PARAMS.SEQUENCE_NUMBER]: 0,
+ [SEARCH_PARAMS.CLIENT_VARIANTS]: "green",
+ [SEARCH_PARAMS.PROVIDERS]: "pink",
+ },
+ },
+ ]);
+
+ UrlbarPrefs.clear("merino.clientVariants");
+ UrlbarPrefs.clear("merino.providers");
+});
+
+// The client should include the `providers` search param when it's set by
+// passing in the `providers` argument to `fetch()`. The argument should
+// override the pref. This tests a single provider.
+add_task(async function providers_arg_single() {
+ UrlbarPrefs.set("merino.providers", "prefShouldNotBeUsed");
+
+ await fetchAndCheckSuggestions({
+ args: { query: "search", providers: ["argShouldBeUsed"] },
+ expected: EXPECTED_MERINO_SUGGESTIONS,
+ });
+
+ MerinoTestUtils.server.checkAndClearRequests([
+ {
+ params: {
+ [SEARCH_PARAMS.QUERY]: "search",
+ [SEARCH_PARAMS.SEQUENCE_NUMBER]: 0,
+ [SEARCH_PARAMS.PROVIDERS]: "argShouldBeUsed",
+ },
+ },
+ ]);
+
+ UrlbarPrefs.clear("merino.providers");
+});
+
+// The client should include the `providers` search param when it's set by
+// passing in the `providers` argument to `fetch()`. The argument should
+// override the pref. This tests multiple providers.
+add_task(async function providers_arg_many() {
+ UrlbarPrefs.set("merino.providers", "prefShouldNotBeUsed");
+
+ await fetchAndCheckSuggestions({
+ args: { query: "search", providers: ["one", "two", "three"] },
+ expected: EXPECTED_MERINO_SUGGESTIONS,
+ });
+
+ MerinoTestUtils.server.checkAndClearRequests([
+ {
+ params: {
+ [SEARCH_PARAMS.QUERY]: "search",
+ [SEARCH_PARAMS.SEQUENCE_NUMBER]: 0,
+ [SEARCH_PARAMS.PROVIDERS]: "one,two,three",
+ },
+ },
+ ]);
+
+ UrlbarPrefs.clear("merino.providers");
+});
+
+// The client should include the `providers` search param when it's set by
+// passing in the `providers` argument to `fetch()` even when it's an empty
+// array. The argument should override the pref.
+add_task(async function providers_arg_empty() {
+ UrlbarPrefs.set("merino.providers", "prefShouldNotBeUsed");
+
+ await fetchAndCheckSuggestions({
+ args: { query: "search", providers: [] },
+ expected: EXPECTED_MERINO_SUGGESTIONS,
+ });
+
+ MerinoTestUtils.server.checkAndClearRequests([
+ {
+ params: {
+ [SEARCH_PARAMS.QUERY]: "search",
+ [SEARCH_PARAMS.SEQUENCE_NUMBER]: 0,
+ [SEARCH_PARAMS.PROVIDERS]: "",
+ },
+ },
+ ]);
+
+ UrlbarPrefs.clear("merino.providers");
+});
+
+// Passes invalid `providers` arguments to `fetch()`.
+add_task(async function providers_arg_invalid() {
+ let providersValues = ["", "nonempty", {}];
+
+ for (let providers of providersValues) {
+ info("Calling fetch() with providers: " + JSON.stringify(providers));
+
+ // `Assert.throws()` doesn't seem to work with async functions...
+ let error;
+ try {
+ await gClient.fetch({ providers, query: "search" });
+ } catch (e) {
+ error = e;
+ }
+ Assert.ok(error, "fetch() threw an error");
+ Assert.equal(
+ error.message,
+ "providers must be an array if given",
+ "Expected error was thrown"
+ );
+ }
+});
+
+// Tests setting the endpoint URL and query parameters via Nimbus.
+add_task(async function nimbus() {
+ // Clear the endpoint pref so we know the URL is not being fetched from it.
+ let originalEndpointURL = UrlbarPrefs.get("merino.endpointURL");
+ UrlbarPrefs.set("merino.endpointURL", "");
+
+ await UrlbarTestUtils.initNimbusFeature();
+
+ // First, with the endpoint pref set to an empty string, make sure no Merino
+ // suggestion are returned.
+ await fetchAndCheckSuggestions({ expected: [] });
+
+ // Now install an experiment that sets the endpoint and other Merino-related
+ // variables. Make sure a suggestion is returned and the request includes the
+ // correct query params.
+
+ // `param`: The param name in the request URL
+ // `value`: The value to use for the param
+ // `variable`: The name of the Nimbus variable corresponding to the param
+ let expectedParams = [
+ {
+ param: SEARCH_PARAMS.CLIENT_VARIANTS,
+ value: "test-client-variants",
+ variable: "merinoClientVariants",
+ },
+ {
+ param: SEARCH_PARAMS.PROVIDERS,
+ value: "test-providers",
+ variable: "merinoProviders",
+ },
+ ];
+
+ // Set up the Nimbus variable values to create the experiment with.
+ let experimentValues = {
+ merinoEndpointURL: MerinoTestUtils.server.url.toString(),
+ };
+ for (let { variable, value } of expectedParams) {
+ experimentValues[variable] = value;
+ }
+
+ await withExperiment(experimentValues, async () => {
+ await fetchAndCheckSuggestions({ expected: EXPECTED_MERINO_SUGGESTIONS });
+
+ let params = {
+ [SEARCH_PARAMS.QUERY]: "search",
+ [SEARCH_PARAMS.SEQUENCE_NUMBER]: 0,
+ };
+ for (let { param, value } of expectedParams) {
+ params[param] = value;
+ }
+ MerinoTestUtils.server.checkAndClearRequests([{ params }]);
+ });
+
+ UrlbarPrefs.set("merino.endpointURL", originalEndpointURL);
+});
+
+async function fetchAndCheckSuggestions({
+ expected,
+ args = {
+ query: "search",
+ },
+}) {
+ let actual = await gClient.fetch(args);
+ Assert.deepEqual(actual, expected, "Expected suggestions");
+ gClient.resetSession();
+}
+
+async function withExperiment(values, callback) {
+ let { enrollmentPromise, doExperimentCleanup } =
+ ExperimentFakes.enrollmentHelper(
+ ExperimentFakes.recipe("mock-experiment", {
+ active: true,
+ branches: [
+ {
+ slug: "treatment",
+ features: [
+ {
+ featureId: NimbusFeatures.urlbar.featureId,
+ value: {
+ enabled: true,
+ ...values,
+ },
+ },
+ ],
+ },
+ ],
+ })
+ );
+ await enrollmentPromise;
+ await callback();
+ await doExperimentCleanup();
+}
diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_merinoClient_sessions.js b/browser/components/urlbar/tests/quicksuggest/unit/test_merinoClient_sessions.js
new file mode 100644
index 0000000000..70e970af8a
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/unit/test_merinoClient_sessions.js
@@ -0,0 +1,402 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Test for MerinoClient sessions.
+
+"use strict";
+
+const { MerinoClient } = ChromeUtils.importESModule(
+ "resource:///modules/MerinoClient.sys.mjs"
+);
+
+const { SEARCH_PARAMS } = MerinoClient;
+
+let gClient;
+
+add_task(async function init() {
+ gClient = new MerinoClient();
+ await MerinoTestUtils.server.start();
+});
+
+// In a single session, all requests should use the same session ID and the
+// sequence number should be incremented.
+add_task(async function singleSession() {
+ for (let i = 0; i < 3; i++) {
+ let query = "search" + i;
+ await gClient.fetch({ query });
+
+ MerinoTestUtils.server.checkAndClearRequests([
+ {
+ params: {
+ [SEARCH_PARAMS.QUERY]: query,
+ [SEARCH_PARAMS.SEQUENCE_NUMBER]: i,
+ },
+ },
+ ]);
+ }
+
+ gClient.resetSession();
+});
+
+// Different sessions should use different session IDs and the sequence number
+// should be reset.
+add_task(async function manySessions() {
+ for (let i = 0; i < 3; i++) {
+ let query = "search" + i;
+ await gClient.fetch({ query });
+
+ MerinoTestUtils.server.checkAndClearRequests([
+ {
+ params: {
+ [SEARCH_PARAMS.QUERY]: query,
+ [SEARCH_PARAMS.SEQUENCE_NUMBER]: 0,
+ },
+ },
+ ]);
+
+ gClient.resetSession();
+ }
+});
+
+// Tests two consecutive fetches:
+//
+// 1. Start a fetch
+// 2. Wait for the mock Merino server to receive the request
+// 3. Start a second fetch before the client receives the response
+//
+// The first fetch will be canceled by the second but the sequence number in the
+// second fetch should still be incremented.
+add_task(async function twoFetches_wait() {
+ for (let i = 0; i < 3; i++) {
+ // Send the first response after a delay to make sure the client will not
+ // receive it before we start the second fetch.
+ MerinoTestUtils.server.response.delay = UrlbarPrefs.get("merino.timeoutMs");
+
+ // Start the first fetch but don't wait for it to finish.
+ let requestPromise = MerinoTestUtils.server.waitForNextRequest();
+ let query1 = "search" + i;
+ gClient.fetch({ query: query1 });
+
+ // Wait until the first request is received before starting the second
+ // fetch, which will cancel the first. The response doesn't need to be
+ // delayed, so remove it to make the test run faster.
+ await requestPromise;
+ delete MerinoTestUtils.server.response.delay;
+ let query2 = query1 + "again";
+ await gClient.fetch({ query: query2 });
+
+ // The sequence number should have been incremented for each fetch.
+ MerinoTestUtils.server.checkAndClearRequests([
+ {
+ params: {
+ [SEARCH_PARAMS.QUERY]: query1,
+ [SEARCH_PARAMS.SEQUENCE_NUMBER]: 2 * i,
+ },
+ },
+ {
+ params: {
+ [SEARCH_PARAMS.QUERY]: query2,
+ [SEARCH_PARAMS.SEQUENCE_NUMBER]: 2 * i + 1,
+ },
+ },
+ ]);
+ }
+
+ gClient.resetSession();
+});
+
+// Tests two consecutive fetches:
+//
+// 1. Start a fetch
+// 2. Immediately start a second fetch
+//
+// The first fetch will be canceled by the second but the sequence number in the
+// second fetch should still be incremented.
+add_task(async function twoFetches_immediate() {
+ for (let i = 0; i < 3; i++) {
+ // Send the first response after a delay to make sure the client will not
+ // receive it before we start the second fetch.
+ MerinoTestUtils.server.response.delay =
+ 100 * UrlbarPrefs.get("merino.timeoutMs");
+
+ // Start the first fetch but don't wait for it to finish.
+ let query1 = "search" + i;
+ gClient.fetch({ query: query1 });
+
+ // Immediately do a second fetch that cancels the first. The response
+ // doesn't need to be delayed, so remove it to make the test run faster.
+ delete MerinoTestUtils.server.response.delay;
+ let query2 = query1 + "again";
+ await gClient.fetch({ query: query2 });
+
+ // The sequence number should have been incremented for each fetch, but the
+ // first won't have reached the server since it was immediately canceled.
+ MerinoTestUtils.server.checkAndClearRequests([
+ {
+ params: {
+ [SEARCH_PARAMS.QUERY]: query2,
+ [SEARCH_PARAMS.SEQUENCE_NUMBER]: 2 * i + 1,
+ },
+ },
+ ]);
+ }
+
+ gClient.resetSession();
+});
+
+// When a network error occurs, the sequence number should still be incremented.
+add_task(async function networkError() {
+ for (let i = 0; i < 3; i++) {
+ // Do a fetch that fails with a network error.
+ let query1 = "search" + i;
+ await MerinoTestUtils.server.withNetworkError(async () => {
+ await gClient.fetch({ query: query1 });
+ });
+
+ Assert.equal(
+ gClient.lastFetchStatus,
+ "network_error",
+ "The request failed with a network error"
+ );
+
+ // Do another fetch that successfully finishes.
+ let query2 = query1 + "again";
+ await gClient.fetch({ query: query2 });
+
+ Assert.equal(
+ gClient.lastFetchStatus,
+ "success",
+ "The request completed successfully"
+ );
+
+ // Only the second request should have been received but the sequence number
+ // should have been incremented for each.
+ MerinoTestUtils.server.checkAndClearRequests([
+ {
+ params: {
+ [SEARCH_PARAMS.QUERY]: query2,
+ [SEARCH_PARAMS.SEQUENCE_NUMBER]: 2 * i + 1,
+ },
+ },
+ ]);
+ }
+
+ gClient.resetSession();
+});
+
+// When the server returns a response with an HTTP error, the sequence number
+// should be incremented.
+add_task(async function httpError() {
+ for (let i = 0; i < 3; i++) {
+ // Do a fetch that fails with an HTTP error.
+ MerinoTestUtils.server.response.status = 500;
+ let query1 = "search" + i;
+ await gClient.fetch({ query: query1 });
+
+ Assert.equal(
+ gClient.lastFetchStatus,
+ "http_error",
+ "The last request failed with a network error"
+ );
+
+ // Do another fetch that successfully finishes.
+ MerinoTestUtils.server.response.status = 200;
+ let query2 = query1 + "again";
+ await gClient.fetch({ query: query2 });
+
+ Assert.equal(
+ gClient.lastFetchStatus,
+ "success",
+ "The last request completed successfully"
+ );
+
+ // Both requests should have been received and the sequence number should
+ // have been incremented for each.
+ MerinoTestUtils.server.checkAndClearRequests([
+ {
+ params: {
+ [SEARCH_PARAMS.QUERY]: query1,
+ [SEARCH_PARAMS.SEQUENCE_NUMBER]: 2 * i,
+ },
+ },
+ {
+ params: {
+ [SEARCH_PARAMS.QUERY]: query2,
+ [SEARCH_PARAMS.SEQUENCE_NUMBER]: 2 * i + 1,
+ },
+ },
+ ]);
+
+ MerinoTestUtils.server.reset();
+ }
+
+ gClient.resetSession();
+});
+
+// When the client times out waiting for a response but later receives it and no
+// other fetch happens in the meantime, the sequence number should be
+// incremented.
+add_task(async function clientTimeout_wait() {
+ for (let i = 0; i < 3; i++) {
+ // Do a fetch that causes the client to time out.
+ MerinoTestUtils.server.response.delay =
+ 2 * UrlbarPrefs.get("merino.timeoutMs");
+ let responsePromise = gClient.waitForNextResponse();
+ let query1 = "search" + i;
+ await gClient.fetch({ query: query1 });
+
+ Assert.equal(
+ gClient.lastFetchStatus,
+ "timeout",
+ "The last request failed with a client timeout"
+ );
+
+ // Wait for the client to receive the response.
+ await responsePromise;
+
+ // Do another fetch that successfully finishes.
+ delete MerinoTestUtils.server.response.delay;
+ let query2 = query1 + "again";
+ await gClient.fetch({ query: query2 });
+
+ Assert.equal(
+ gClient.lastFetchStatus,
+ "success",
+ "The last request completed successfully"
+ );
+
+ MerinoTestUtils.server.checkAndClearRequests([
+ {
+ params: {
+ [SEARCH_PARAMS.QUERY]: query1,
+ [SEARCH_PARAMS.SEQUENCE_NUMBER]: 2 * i,
+ },
+ },
+ {
+ params: {
+ [SEARCH_PARAMS.QUERY]: query2,
+ [SEARCH_PARAMS.SEQUENCE_NUMBER]: 2 * i + 1,
+ },
+ },
+ ]);
+ }
+
+ gClient.resetSession();
+});
+
+// When the client times out waiting for a response and a second fetch starts
+// before the response is received, the first fetch should be canceled but the
+// sequence number should still be incremented.
+add_task(async function clientTimeout_canceled() {
+ for (let i = 0; i < 3; i++) {
+ // Do a fetch that causes the client to time out.
+ MerinoTestUtils.server.response.delay =
+ 2 * UrlbarPrefs.get("merino.timeoutMs");
+ let query1 = "search" + i;
+ await gClient.fetch({ query: query1 });
+
+ Assert.equal(
+ gClient.lastFetchStatus,
+ "timeout",
+ "The last request failed with a client timeout"
+ );
+
+ // Do another fetch that successfully finishes.
+ delete MerinoTestUtils.server.response.delay;
+ let query2 = query1 + "again";
+ await gClient.fetch({ query: query2 });
+
+ Assert.equal(
+ gClient.lastFetchStatus,
+ "success",
+ "The last request completed successfully"
+ );
+
+ MerinoTestUtils.server.checkAndClearRequests([
+ {
+ params: {
+ [SEARCH_PARAMS.QUERY]: query1,
+ [SEARCH_PARAMS.SEQUENCE_NUMBER]: 2 * i,
+ },
+ },
+ {
+ params: {
+ [SEARCH_PARAMS.QUERY]: query2,
+ [SEARCH_PARAMS.SEQUENCE_NUMBER]: 2 * i + 1,
+ },
+ },
+ ]);
+ }
+
+ gClient.resetSession();
+});
+
+// When the session times out, the next fetch should use a new session ID and
+// the sequence number should be reset.
+add_task(async function sessionTimeout() {
+ // Set the session timeout to something reasonable to test.
+ let originalTimeoutMs = gClient.sessionTimeoutMs;
+ gClient.sessionTimeoutMs = 500;
+
+ // Do a fetch.
+ let query1 = "search";
+ await gClient.fetch({ query: query1 });
+
+ // Wait for the session to time out.
+ await gClient.waitForNextSessionReset();
+
+ Assert.strictEqual(
+ gClient.sessionID,
+ null,
+ "sessionID is null after session timeout"
+ );
+ Assert.strictEqual(
+ gClient.sequenceNumber,
+ 0,
+ "sequenceNumber is zero after session timeout"
+ );
+ Assert.strictEqual(
+ gClient._test_sessionTimer,
+ null,
+ "sessionTimer is null after session timeout"
+ );
+
+ // Do another fetch.
+ let query2 = query1 + "again";
+ await gClient.fetch({ query: query2 });
+
+ // The second request's sequence number should be zero due to the session
+ // timeout.
+ MerinoTestUtils.server.checkAndClearRequests([
+ {
+ params: {
+ [SEARCH_PARAMS.QUERY]: query1,
+ [SEARCH_PARAMS.SEQUENCE_NUMBER]: 0,
+ },
+ },
+ {
+ params: {
+ [SEARCH_PARAMS.QUERY]: query2,
+ [SEARCH_PARAMS.SEQUENCE_NUMBER]: 0,
+ },
+ },
+ ]);
+
+ Assert.ok(
+ gClient.sessionID,
+ "sessionID is non-null after first request in a new session"
+ );
+ Assert.equal(
+ gClient.sequenceNumber,
+ 1,
+ "sequenceNumber is one after first request in a new session"
+ );
+ Assert.ok(
+ gClient._test_sessionTimer,
+ "sessionTimer is non-null after first request in a new session"
+ );
+
+ gClient.sessionTimeoutMs = originalTimeoutMs;
+ gClient.resetSession();
+});
diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest.js
new file mode 100644
index 0000000000..00e9820fab
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest.js
@@ -0,0 +1,1341 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Basic tests for the quick suggest provider using the remote settings source.
+// See also test_quicksuggest_merino.js.
+
+"use strict";
+
+const TELEMETRY_REMOTE_SETTINGS_LATENCY =
+ "FX_URLBAR_QUICK_SUGGEST_REMOTE_SETTINGS_LATENCY_MS";
+
+const SPONSORED_SEARCH_STRING = "frab";
+const NONSPONSORED_SEARCH_STRING = "nonspon";
+
+const HTTP_SEARCH_STRING = "http prefix";
+const HTTPS_SEARCH_STRING = "https prefix";
+const PREFIX_SUGGESTIONS_STRIPPED_URL = "example.com/prefix-test";
+
+const { TIMESTAMP_TEMPLATE, TIMESTAMP_LENGTH } = QuickSuggest;
+const TIMESTAMP_SEARCH_STRING = "timestamp";
+const TIMESTAMP_SUGGESTION_URL = `http://example.com/timestamp-${TIMESTAMP_TEMPLATE}`;
+const TIMESTAMP_SUGGESTION_CLICK_URL = `http://click.reporting.test.com/timestamp-${TIMESTAMP_TEMPLATE}-foo`;
+
+const REMOTE_SETTINGS_RESULTS = [
+ {
+ id: 1,
+ url: "http://test.com/q=frabbits",
+ title: "frabbits",
+ keywords: [SPONSORED_SEARCH_STRING],
+ click_url: "http://click.reporting.test.com/",
+ impression_url: "http://impression.reporting.test.com/",
+ advertiser: "TestAdvertiser",
+ iab_category: "22 - Shopping",
+ },
+ {
+ id: 2,
+ url: "http://test.com/?q=nonsponsored",
+ title: "Non-Sponsored",
+ keywords: [NONSPONSORED_SEARCH_STRING],
+ click_url: "http://click.reporting.test.com/nonsponsored",
+ impression_url: "http://impression.reporting.test.com/nonsponsored",
+ advertiser: "TestAdvertiserNonSponsored",
+ iab_category: "5 - Education",
+ },
+ {
+ id: 3,
+ url: "http://" + PREFIX_SUGGESTIONS_STRIPPED_URL,
+ title: "http suggestion",
+ keywords: [HTTP_SEARCH_STRING],
+ click_url: "http://click.reporting.test.com/prefix",
+ impression_url: "http://impression.reporting.test.com/prefix",
+ advertiser: "TestAdvertiserPrefix",
+ iab_category: "22 - Shopping",
+ },
+ {
+ id: 4,
+ url: "https://" + PREFIX_SUGGESTIONS_STRIPPED_URL,
+ title: "https suggestion",
+ keywords: [HTTPS_SEARCH_STRING],
+ click_url: "http://click.reporting.test.com/prefix",
+ impression_url: "http://impression.reporting.test.com/prefix",
+ advertiser: "TestAdvertiserPrefix",
+ iab_category: "22 - Shopping",
+ },
+ {
+ id: 5,
+ url: TIMESTAMP_SUGGESTION_URL,
+ title: "Timestamp suggestion",
+ keywords: [TIMESTAMP_SEARCH_STRING],
+ click_url: TIMESTAMP_SUGGESTION_CLICK_URL,
+ impression_url: "http://impression.reporting.test.com/timestamp",
+ advertiser: "TestAdvertiserTimestamp",
+ iab_category: "22 - Shopping",
+ },
+];
+
+const EXPECTED_SPONSORED_RESULT = {
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ heuristic: false,
+ payload: {
+ telemetryType: "adm_sponsored",
+ qsSuggestion: "frab",
+ title: "frabbits",
+ url: "http://test.com/q=frabbits",
+ originalUrl: "http://test.com/q=frabbits",
+ icon: null,
+ sponsoredImpressionUrl: "http://impression.reporting.test.com/",
+ sponsoredClickUrl: "http://click.reporting.test.com/",
+ sponsoredBlockId: 1,
+ sponsoredAdvertiser: "TestAdvertiser",
+ sponsoredIabCategory: "22 - Shopping",
+ isSponsored: true,
+ helpUrl: QuickSuggest.HELP_URL,
+ helpL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-learn-more-about-firefox-suggest"
+ : "firefox-suggest-urlbar-learn-more",
+ },
+ isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"),
+ blockL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-dismiss-firefox-suggest"
+ : "firefox-suggest-urlbar-block",
+ },
+ displayUrl: "http://test.com/q=frabbits",
+ source: "remote-settings",
+ },
+};
+
+const EXPECTED_NONSPONSORED_RESULT = {
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ heuristic: false,
+ payload: {
+ telemetryType: "adm_nonsponsored",
+ qsSuggestion: "nonspon",
+ title: "Non-Sponsored",
+ url: "http://test.com/?q=nonsponsored",
+ originalUrl: "http://test.com/?q=nonsponsored",
+ icon: null,
+ sponsoredImpressionUrl: "http://impression.reporting.test.com/nonsponsored",
+ sponsoredClickUrl: "http://click.reporting.test.com/nonsponsored",
+ sponsoredBlockId: 2,
+ sponsoredAdvertiser: "TestAdvertiserNonSponsored",
+ sponsoredIabCategory: "5 - Education",
+ isSponsored: false,
+ helpUrl: QuickSuggest.HELP_URL,
+ helpL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-learn-more-about-firefox-suggest"
+ : "firefox-suggest-urlbar-learn-more",
+ },
+ isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"),
+ blockL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-dismiss-firefox-suggest"
+ : "firefox-suggest-urlbar-block",
+ },
+ displayUrl: "http://test.com/?q=nonsponsored",
+ source: "remote-settings",
+ },
+};
+
+const EXPECTED_HTTP_RESULT = {
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ heuristic: false,
+ payload: {
+ telemetryType: "adm_sponsored",
+ qsSuggestion: HTTP_SEARCH_STRING,
+ title: "http suggestion",
+ url: "http://" + PREFIX_SUGGESTIONS_STRIPPED_URL,
+ originalUrl: "http://" + PREFIX_SUGGESTIONS_STRIPPED_URL,
+ icon: null,
+ sponsoredImpressionUrl: "http://impression.reporting.test.com/prefix",
+ sponsoredClickUrl: "http://click.reporting.test.com/prefix",
+ sponsoredBlockId: 3,
+ sponsoredAdvertiser: "TestAdvertiserPrefix",
+ sponsoredIabCategory: "22 - Shopping",
+ isSponsored: true,
+ helpUrl: QuickSuggest.HELP_URL,
+ helpL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-learn-more-about-firefox-suggest"
+ : "firefox-suggest-urlbar-learn-more",
+ },
+ isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"),
+ blockL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-dismiss-firefox-suggest"
+ : "firefox-suggest-urlbar-block",
+ },
+ displayUrl: "http://" + PREFIX_SUGGESTIONS_STRIPPED_URL,
+ source: "remote-settings",
+ },
+};
+
+const EXPECTED_HTTPS_RESULT = {
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ heuristic: false,
+ payload: {
+ telemetryType: "adm_sponsored",
+ qsSuggestion: HTTPS_SEARCH_STRING,
+ title: "https suggestion",
+ url: "https://" + PREFIX_SUGGESTIONS_STRIPPED_URL,
+ originalUrl: "https://" + PREFIX_SUGGESTIONS_STRIPPED_URL,
+ icon: null,
+ sponsoredImpressionUrl: "http://impression.reporting.test.com/prefix",
+ sponsoredClickUrl: "http://click.reporting.test.com/prefix",
+ sponsoredBlockId: 4,
+ sponsoredAdvertiser: "TestAdvertiserPrefix",
+ sponsoredIabCategory: "22 - Shopping",
+ isSponsored: true,
+ helpUrl: QuickSuggest.HELP_URL,
+ helpL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-learn-more-about-firefox-suggest"
+ : "firefox-suggest-urlbar-learn-more",
+ },
+ isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"),
+ blockL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-dismiss-firefox-suggest"
+ : "firefox-suggest-urlbar-block",
+ },
+ displayUrl: PREFIX_SUGGESTIONS_STRIPPED_URL,
+ source: "remote-settings",
+ },
+};
+
+add_setup(async function init() {
+ UrlbarPrefs.set("quicksuggest.enabled", true);
+ UrlbarPrefs.set("quicksuggest.shouldShowOnboardingDialog", false);
+ UrlbarPrefs.set("quicksuggest.remoteSettings.enabled", true);
+ UrlbarPrefs.set("merino.enabled", false);
+
+ // Install a default test engine.
+ let engine = await addTestSuggestionsEngine();
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+
+ const testDataTypeResults = [
+ Object.assign({}, REMOTE_SETTINGS_RESULTS[0], { title: "test-data-type" }),
+ ];
+
+ await QuickSuggestTestUtils.ensureQuickSuggestInit({
+ remoteSettingsResults: [
+ {
+ type: "data",
+ attachment: REMOTE_SETTINGS_RESULTS,
+ },
+ {
+ type: "test-data-type",
+ attachment: testDataTypeResults,
+ },
+ ],
+ });
+});
+
+// Tests with only non-sponsored suggestions enabled with a matching search
+// string.
+add_task(async function nonsponsoredOnly_match() {
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", false);
+
+ let context = createContext(NONSPONSORED_SEARCH_STRING, {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [EXPECTED_NONSPONSORED_RESULT],
+ });
+});
+
+// Tests with only non-sponsored suggestions enabled with a non-matching search
+// string.
+add_task(async function nonsponsoredOnly_noMatch() {
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", false);
+
+ let context = createContext(SPONSORED_SEARCH_STRING, {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+ await check_results({ context, matches: [] });
+});
+
+// Tests with only sponsored suggestions enabled with a matching search string.
+add_task(async function sponsoredOnly_sponsored() {
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false);
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
+
+ let context = createContext(SPONSORED_SEARCH_STRING, {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [EXPECTED_SPONSORED_RESULT],
+ });
+});
+
+// Tests with only sponsored suggestions enabled with a non-matching search
+// string.
+add_task(async function sponsoredOnly_nonsponsored() {
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false);
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
+
+ let context = createContext(NONSPONSORED_SEARCH_STRING, {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+ await check_results({ context, matches: [] });
+});
+
+// Tests with both sponsored and non-sponsored suggestions enabled with a
+// search string that matches the sponsored suggestion.
+add_task(async function both_sponsored() {
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
+
+ let context = createContext(SPONSORED_SEARCH_STRING, {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [EXPECTED_SPONSORED_RESULT],
+ });
+});
+
+// Tests with both sponsored and non-sponsored suggestions enabled with a
+// search string that matches the non-sponsored suggestion.
+add_task(async function both_nonsponsored() {
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
+
+ let context = createContext(NONSPONSORED_SEARCH_STRING, {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [EXPECTED_NONSPONSORED_RESULT],
+ });
+});
+
+// Tests with both sponsored and non-sponsored suggestions enabled with a
+// search string that doesn't match either suggestion.
+add_task(async function both_noMatch() {
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
+
+ let context = createContext("this doesn't match anything", {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+ await check_results({ context, matches: [] });
+});
+
+// Tests with both the main and sponsored prefs disabled with a search string
+// that matches the sponsored suggestion.
+add_task(async function neither_sponsored() {
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false);
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", false);
+
+ let context = createContext(SPONSORED_SEARCH_STRING, {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+ await check_results({ context, matches: [] });
+});
+
+// Tests with both the main and sponsored prefs disabled with a search string
+// that matches the non-sponsored suggestion.
+add_task(async function neither_nonsponsored() {
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false);
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", false);
+
+ let context = createContext(NONSPONSORED_SEARCH_STRING, {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+ await check_results({ context, matches: [] });
+});
+
+// Search string matching should be case insensitive and ignore leading spaces.
+add_task(async function caseInsensitiveAndLeadingSpaces() {
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
+
+ let context = createContext(" " + SPONSORED_SEARCH_STRING.toUpperCase(), {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [EXPECTED_SPONSORED_RESULT],
+ });
+});
+
+// The provider should not be active for search strings that are empty or
+// contain only spaces.
+add_task(async function emptySearchStringsAndSpaces() {
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
+
+ let searchStrings = ["", " ", " ", " "];
+ for (let str of searchStrings) {
+ let msg = JSON.stringify(str) + ` (length = ${str.length})`;
+ info("Testing search string: " + msg);
+
+ let context = createContext(str, {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [],
+ });
+ Assert.ok(
+ !UrlbarProviderQuickSuggest.isActive(context),
+ "Provider should not be active for search string: " + msg
+ );
+ }
+});
+
+// Results should be returned even when `browser.search.suggest.enabled` is
+// false.
+add_task(async function browser_search_suggest_enabled() {
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
+ UrlbarPrefs.set("browser.search.suggest.enabled", false);
+
+ let context = createContext(SPONSORED_SEARCH_STRING, {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [EXPECTED_SPONSORED_RESULT],
+ });
+
+ UrlbarPrefs.clear("browser.search.suggest.enabled");
+});
+
+// Results should be returned even when `browser.urlbar.suggest.searches` is
+// false.
+add_task(async function browser_search_suggest_enabled() {
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
+ UrlbarPrefs.set("suggest.searches", false);
+
+ let context = createContext(SPONSORED_SEARCH_STRING, {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [EXPECTED_SPONSORED_RESULT],
+ });
+
+ UrlbarPrefs.clear("suggest.searches");
+});
+
+// Neither sponsored nor non-sponsored results should appear in private contexts
+// even when suggestions in private windows are enabled.
+add_task(async function privateContext() {
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
+
+ for (let privateSuggestionsEnabled of [true, false]) {
+ UrlbarPrefs.set(
+ "browser.search.suggest.enabled.private",
+ privateSuggestionsEnabled
+ );
+ let context = createContext(SPONSORED_SEARCH_STRING, {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: true,
+ });
+ await check_results({
+ context,
+ matches: [],
+ });
+ }
+
+ UrlbarPrefs.clear("browser.search.suggest.enabled.private");
+});
+
+// When search suggestions come before general results and the only general
+// result is a quick suggest result, it should come last.
+add_task(async function suggestionsBeforeGeneral_only() {
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
+ UrlbarPrefs.set("browser.search.suggest.enabled", true);
+ UrlbarPrefs.set("suggest.searches", true);
+ UrlbarPrefs.set("showSearchSuggestionsFirst", true);
+
+ let context = createContext(SPONSORED_SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ heuristic: true,
+ query: SPONSORED_SEARCH_STRING,
+ engineName: Services.search.defaultEngine.name,
+ }),
+ makeSearchResult(context, {
+ query: SPONSORED_SEARCH_STRING,
+ suggestion: SPONSORED_SEARCH_STRING + " foo",
+ engineName: Services.search.defaultEngine.name,
+ }),
+ makeSearchResult(context, {
+ query: SPONSORED_SEARCH_STRING,
+ suggestion: SPONSORED_SEARCH_STRING + " bar",
+ engineName: Services.search.defaultEngine.name,
+ }),
+ EXPECTED_SPONSORED_RESULT,
+ ],
+ });
+
+ UrlbarPrefs.clear("browser.search.suggest.enabled");
+ UrlbarPrefs.clear("suggest.searches");
+ UrlbarPrefs.clear("showSearchSuggestionsFirst");
+});
+
+// When search suggestions come before general results and there are other
+// general results besides quick suggest, the quick suggest result should come
+// last.
+add_task(async function suggestionsBeforeGeneral_others() {
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
+ UrlbarPrefs.set("browser.search.suggest.enabled", true);
+ UrlbarPrefs.set("suggest.searches", true);
+ UrlbarPrefs.set("showSearchSuggestionsFirst", true);
+
+ let context = createContext(SPONSORED_SEARCH_STRING, { isPrivate: false });
+
+ // Add some history that will match our query below.
+ let maxResults = UrlbarPrefs.get("maxRichResults");
+ let historyResults = [];
+ for (let i = 0; i < maxResults; i++) {
+ let url = "http://example.com/" + SPONSORED_SEARCH_STRING + i;
+ historyResults.push(
+ makeVisitResult(context, {
+ uri: url,
+ title: "test visit for " + url,
+ })
+ );
+ await PlacesTestUtils.addVisits(url);
+ }
+ historyResults = historyResults.reverse().slice(0, historyResults.length - 4);
+
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ heuristic: true,
+ query: SPONSORED_SEARCH_STRING,
+ engineName: Services.search.defaultEngine.name,
+ }),
+ makeSearchResult(context, {
+ query: SPONSORED_SEARCH_STRING,
+ suggestion: SPONSORED_SEARCH_STRING + " foo",
+ engineName: Services.search.defaultEngine.name,
+ }),
+ makeSearchResult(context, {
+ query: SPONSORED_SEARCH_STRING,
+ suggestion: SPONSORED_SEARCH_STRING + " bar",
+ engineName: Services.search.defaultEngine.name,
+ }),
+ ...historyResults,
+ EXPECTED_SPONSORED_RESULT,
+ ],
+ });
+
+ UrlbarPrefs.clear("browser.search.suggest.enabled");
+ UrlbarPrefs.clear("suggest.searches");
+ UrlbarPrefs.clear("showSearchSuggestionsFirst");
+ await PlacesUtils.history.clear();
+});
+
+// When general results come before search suggestions and the only general
+// result is a quick suggest result, it should come before suggestions.
+add_task(async function generalBeforeSuggestions_only() {
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
+ UrlbarPrefs.set("browser.search.suggest.enabled", true);
+ UrlbarPrefs.set("suggest.searches", true);
+ UrlbarPrefs.set("showSearchSuggestionsFirst", false);
+
+ let context = createContext(SPONSORED_SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ heuristic: true,
+ query: SPONSORED_SEARCH_STRING,
+ engineName: Services.search.defaultEngine.name,
+ }),
+ EXPECTED_SPONSORED_RESULT,
+ makeSearchResult(context, {
+ query: SPONSORED_SEARCH_STRING,
+ suggestion: SPONSORED_SEARCH_STRING + " foo",
+ engineName: Services.search.defaultEngine.name,
+ }),
+ makeSearchResult(context, {
+ query: SPONSORED_SEARCH_STRING,
+ suggestion: SPONSORED_SEARCH_STRING + " bar",
+ engineName: Services.search.defaultEngine.name,
+ }),
+ ],
+ });
+
+ UrlbarPrefs.clear("browser.search.suggest.enabled");
+ UrlbarPrefs.clear("suggest.searches");
+ UrlbarPrefs.clear("showSearchSuggestionsFirst");
+});
+
+// When general results come before search suggestions and there are other
+// general results besides quick suggest, the quick suggest result should be the
+// last general result.
+add_task(async function generalBeforeSuggestions_others() {
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
+ UrlbarPrefs.set("browser.search.suggest.enabled", true);
+ UrlbarPrefs.set("suggest.searches", true);
+ UrlbarPrefs.set("showSearchSuggestionsFirst", false);
+
+ let context = createContext(SPONSORED_SEARCH_STRING, { isPrivate: false });
+
+ // Add some history that will match our query below.
+ let maxResults = UrlbarPrefs.get("maxRichResults");
+ let historyResults = [];
+ for (let i = 0; i < maxResults; i++) {
+ let url = "http://example.com/" + SPONSORED_SEARCH_STRING + i;
+ historyResults.push(
+ makeVisitResult(context, {
+ uri: url,
+ title: "test visit for " + url,
+ })
+ );
+ await PlacesTestUtils.addVisits(url);
+ }
+ historyResults = historyResults.reverse().slice(0, historyResults.length - 4);
+
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ heuristic: true,
+ query: SPONSORED_SEARCH_STRING,
+ engineName: Services.search.defaultEngine.name,
+ }),
+ ...historyResults,
+ EXPECTED_SPONSORED_RESULT,
+ makeSearchResult(context, {
+ query: SPONSORED_SEARCH_STRING,
+ suggestion: SPONSORED_SEARCH_STRING + " foo",
+ engineName: Services.search.defaultEngine.name,
+ }),
+ makeSearchResult(context, {
+ query: SPONSORED_SEARCH_STRING,
+ suggestion: SPONSORED_SEARCH_STRING + " bar",
+ engineName: Services.search.defaultEngine.name,
+ }),
+ ],
+ });
+
+ UrlbarPrefs.clear("browser.search.suggest.enabled");
+ UrlbarPrefs.clear("suggest.searches");
+ UrlbarPrefs.clear("showSearchSuggestionsFirst");
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function dedupeAgainstURL_samePrefix() {
+ await doDedupeAgainstURLTest({
+ searchString: HTTP_SEARCH_STRING,
+ expectedQuickSuggestResult: EXPECTED_HTTP_RESULT,
+ otherPrefix: "http://",
+ expectOther: false,
+ });
+});
+
+add_task(async function dedupeAgainstURL_higherPrefix() {
+ await doDedupeAgainstURLTest({
+ searchString: HTTPS_SEARCH_STRING,
+ expectedQuickSuggestResult: EXPECTED_HTTPS_RESULT,
+ otherPrefix: "http://",
+ expectOther: false,
+ });
+});
+
+add_task(async function dedupeAgainstURL_lowerPrefix() {
+ await doDedupeAgainstURLTest({
+ searchString: HTTP_SEARCH_STRING,
+ expectedQuickSuggestResult: EXPECTED_HTTP_RESULT,
+ otherPrefix: "https://",
+ expectOther: true,
+ });
+});
+
+/**
+ * Tests how the muxer dedupes URL results against quick suggest results.
+ * Depending on prefix rank, quick suggest results should be preferred over
+ * other URL results with the same stripped URL: Other results should be
+ * discarded when their prefix rank is lower than the prefix rank of the quick
+ * suggest. They should not be discarded when their prefix rank is higher, and
+ * in that case both results should be included.
+ *
+ * This function adds a visit to the URL formed by the given `otherPrefix` and
+ * `PREFIX_SUGGESTIONS_STRIPPED_URL`. The visit's title will be set to the given
+ * `searchString` so that both the visit and the quick suggest will match it.
+ *
+ * @param {object} options
+ * Options object.
+ * @param {string} options.searchString
+ * The search string that should trigger one of the mock prefix-test quick
+ * suggest results.
+ * @param {object} options.expectedQuickSuggestResult
+ * The expected quick suggest result.
+ * @param {string} options.otherPrefix
+ * The visit will be created with a URL with this prefix, e.g., "http://".
+ * @param {boolean} options.expectOther
+ * Whether the visit result should appear in the final results.
+ */
+async function doDedupeAgainstURLTest({
+ searchString,
+ expectedQuickSuggestResult,
+ otherPrefix,
+ expectOther,
+}) {
+ // Disable search suggestions.
+ UrlbarPrefs.set("suggest.searches", false);
+
+ // Add a visit that will match our query below.
+ let otherURL = otherPrefix + PREFIX_SUGGESTIONS_STRIPPED_URL;
+ await PlacesTestUtils.addVisits({ uri: otherURL, title: searchString });
+
+ // First, do a search with quick suggest disabled to make sure the search
+ // string matches the visit.
+ info("Doing first query");
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false);
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", false);
+ let context = createContext(searchString, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ heuristic: true,
+ query: searchString,
+ engineName: Services.search.defaultEngine.name,
+ }),
+ makeVisitResult(context, {
+ uri: otherURL,
+ title: searchString,
+ }),
+ ],
+ });
+
+ // Now do another search with quick suggest enabled.
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
+
+ context = createContext(searchString, { isPrivate: false });
+
+ let expectedResults = [
+ makeSearchResult(context, {
+ heuristic: true,
+ query: searchString,
+ engineName: Services.search.defaultEngine.name,
+ }),
+ ];
+ if (expectOther) {
+ expectedResults.push(
+ makeVisitResult(context, {
+ uri: otherURL,
+ title: searchString,
+ })
+ );
+ }
+ expectedResults.push(expectedQuickSuggestResult);
+
+ info("Doing second query");
+ await check_results({ context, matches: expectedResults });
+
+ UrlbarPrefs.clear("suggest.quicksuggest.nonsponsored");
+ UrlbarPrefs.clear("suggest.quicksuggest.sponsored");
+ UrlbarPrefs.clear("suggest.searches");
+ await PlacesUtils.history.clear();
+}
+
+// Tests the remote settings latency histogram.
+add_task(async function latencyTelemetry() {
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
+
+ let histogram = Services.telemetry.getHistogramById(
+ TELEMETRY_REMOTE_SETTINGS_LATENCY
+ );
+ histogram.clear();
+
+ let context = createContext(SPONSORED_SEARCH_STRING, {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [EXPECTED_SPONSORED_RESULT],
+ });
+
+ // In the latency histogram, there should be a single value across all
+ // buckets.
+ Assert.deepEqual(
+ Object.values(histogram.snapshot().values).filter(v => v > 0),
+ [1],
+ "Latency histogram updated after search"
+ );
+ Assert.ok(
+ !TelemetryStopwatch.running(TELEMETRY_REMOTE_SETTINGS_LATENCY, context),
+ "Stopwatch not running after search"
+ );
+});
+
+// Tests setup and teardown of the remote settings client depending on whether
+// quick suggest is enabled.
+add_task(async function setupAndTeardown() {
+ // Disable the suggest prefs so the settings client starts out torn down.
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false);
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", false);
+ Assert.ok(
+ !QuickSuggestRemoteSettings.rs,
+ "Settings client is null after disabling suggest prefs"
+ );
+
+ // Setting one of the suggest prefs should cause the client to be set up. We
+ // assume all previous tasks left `quicksuggest.enabled` true (from the init
+ // task).
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
+ Assert.ok(
+ QuickSuggestRemoteSettings.rs,
+ "Settings client is non-null after enabling suggest.quicksuggest.nonsponsored"
+ );
+
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false);
+ Assert.ok(
+ !QuickSuggestRemoteSettings.rs,
+ "Settings client is null after disabling suggest.quicksuggest.nonsponsored"
+ );
+
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
+ Assert.ok(
+ QuickSuggestRemoteSettings.rs,
+ "Settings client is non-null after enabling suggest.quicksuggest.sponsored"
+ );
+
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
+ Assert.ok(
+ QuickSuggestRemoteSettings.rs,
+ "Settings client remains non-null after enabling suggest.quicksuggest.nonsponsored"
+ );
+
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false);
+ Assert.ok(
+ QuickSuggestRemoteSettings.rs,
+ "Settings client remains non-null after disabling suggest.quicksuggest.nonsponsored"
+ );
+
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", false);
+ Assert.ok(
+ !QuickSuggestRemoteSettings.rs,
+ "Settings client is null after disabling suggest.quicksuggest.sponsored"
+ );
+
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
+ Assert.ok(
+ QuickSuggestRemoteSettings.rs,
+ "Settings client is non-null after enabling suggest.quicksuggest.nonsponsored"
+ );
+
+ UrlbarPrefs.set("quicksuggest.enabled", false);
+ Assert.ok(
+ !QuickSuggestRemoteSettings.rs,
+ "Settings client is null after disabling quicksuggest.enabled"
+ );
+
+ // Leave the prefs in the same state as when the task started.
+ UrlbarPrefs.clear("suggest.quicksuggest.nonsponsored");
+ UrlbarPrefs.clear("suggest.quicksuggest.sponsored");
+ UrlbarPrefs.set("quicksuggest.enabled", true);
+ Assert.ok(
+ !QuickSuggestRemoteSettings.rs,
+ "Settings client remains null at end of task"
+ );
+});
+
+// Timestamp templates in URLs should be replaced with real timestamps.
+add_task(async function timestamps() {
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
+
+ // Do a search.
+ let context = createContext(TIMESTAMP_SEARCH_STRING, {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+ let controller = UrlbarTestUtils.newMockController({
+ input: {
+ isPrivate: context.isPrivate,
+ onFirstResult() {
+ return false;
+ },
+ getSearchSource() {
+ return "dummy-search-source";
+ },
+ window: {
+ location: {
+ href: AppConstants.BROWSER_CHROME_URL,
+ },
+ },
+ },
+ });
+ await controller.startQuery(context);
+
+ // Should be one quick suggest result.
+ Assert.equal(context.results.length, 1, "One result returned");
+ let result = context.results[0];
+
+ QuickSuggestTestUtils.assertTimestampsReplaced(result, {
+ url: TIMESTAMP_SUGGESTION_URL,
+ sponsoredClickUrl: TIMESTAMP_SUGGESTION_CLICK_URL,
+ });
+});
+
+// Real quick suggest URLs include a timestamp template that
+// UrlbarProviderQuickSuggest fills in when it fetches suggestions. When the
+// user picks a quick suggest, its URL with its particular timestamp is added to
+// history. If the user triggers the quick suggest again later, its new
+// timestamp may be different from the one in the user's history. In that case,
+// the two URLs should be treated as dupes and only the quick suggest should be
+// shown, not the URL from history.
+add_task(async function dedupeAgainstURL_timestamps() {
+ // Disable search suggestions.
+ UrlbarPrefs.set("suggest.searches", false);
+
+ // Add a visit that will match the query below and dupe the quick suggest.
+ let dupeURL = TIMESTAMP_SUGGESTION_URL.replace(
+ TIMESTAMP_TEMPLATE,
+ "2013051113"
+ );
+
+ // Add other visits that will match the query and almost dupe the quick
+ // suggest but not quite because they have invalid timestamps.
+ let badTimestamps = [
+ // not numeric digits
+ "x".repeat(TIMESTAMP_LENGTH),
+ // too few digits
+ "5".repeat(TIMESTAMP_LENGTH - 1),
+ // empty string, too few digits
+ "",
+ ];
+ let badTimestampURLs = badTimestamps.map(str =>
+ TIMESTAMP_SUGGESTION_URL.replace(TIMESTAMP_TEMPLATE, str)
+ );
+
+ await PlacesTestUtils.addVisits(
+ [dupeURL, ...badTimestampURLs].map(uri => ({
+ uri,
+ title: TIMESTAMP_SEARCH_STRING,
+ }))
+ );
+
+ // First, do a search with quick suggest disabled to make sure the search
+ // string matches all the other URLs.
+ info("Doing first query");
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false);
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", false);
+ let context = createContext(TIMESTAMP_SEARCH_STRING, { isPrivate: false });
+
+ let expectedHeuristic = makeSearchResult(context, {
+ heuristic: true,
+ query: TIMESTAMP_SEARCH_STRING,
+ engineName: Services.search.defaultEngine.name,
+ });
+ let expectedDupeResult = makeVisitResult(context, {
+ uri: dupeURL,
+ title: TIMESTAMP_SEARCH_STRING,
+ });
+ let expectedBadTimestampResults = [...badTimestampURLs].reverse().map(uri =>
+ makeVisitResult(context, {
+ uri,
+ title: TIMESTAMP_SEARCH_STRING,
+ })
+ );
+
+ await check_results({
+ context,
+ matches: [
+ expectedHeuristic,
+ ...expectedBadTimestampResults,
+ expectedDupeResult,
+ ],
+ });
+
+ // Now do another search with quick suggest enabled.
+ info("Doing second query");
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
+ context = createContext(TIMESTAMP_SEARCH_STRING, { isPrivate: false });
+
+ // The expected quick suggest result without the timestamp-related payload
+ // properties.
+ let expectedQuickSuggest = {
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ heuristic: false,
+ payload: {
+ telemetryType: "adm_sponsored",
+ originalUrl: TIMESTAMP_SUGGESTION_URL,
+ qsSuggestion: TIMESTAMP_SEARCH_STRING,
+ title: "Timestamp suggestion",
+ icon: null,
+ sponsoredImpressionUrl: "http://impression.reporting.test.com/timestamp",
+ sponsoredBlockId: 5,
+ sponsoredAdvertiser: "TestAdvertiserTimestamp",
+ sponsoredIabCategory: "22 - Shopping",
+ isSponsored: true,
+ helpUrl: QuickSuggest.HELP_URL,
+ helpL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-learn-more-about-firefox-suggest"
+ : "firefox-suggest-urlbar-learn-more",
+ },
+ isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"),
+ blockL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-dismiss-firefox-suggest"
+ : "firefox-suggest-urlbar-block",
+ },
+ source: "remote-settings",
+ },
+ };
+
+ let expectedResults = [
+ expectedHeuristic,
+ ...expectedBadTimestampResults,
+ expectedQuickSuggest,
+ ];
+
+ let controller = UrlbarTestUtils.newMockController({
+ input: {
+ isPrivate: false,
+ onFirstResult() {
+ return false;
+ },
+ getSearchSource() {
+ return "dummy-search-source";
+ },
+ window: {
+ location: {
+ href: AppConstants.BROWSER_CHROME_URL,
+ },
+ },
+ },
+ });
+ await controller.startQuery(context);
+ info("Actual results: " + JSON.stringify(context.results));
+
+ Assert.equal(
+ context.results.length,
+ expectedResults.length,
+ "Found the expected number of results"
+ );
+
+ function getPayload(result, keysToIgnore = []) {
+ let payload = {};
+ for (let [key, value] of Object.entries(result.payload)) {
+ if (value !== undefined && !keysToIgnore.includes(key)) {
+ payload[key] = value;
+ }
+ }
+ return payload;
+ }
+
+ // Check actual vs. expected result properties.
+ for (let i = 0; i < expectedResults.length; i++) {
+ let actual = context.results[i];
+ let expected = expectedResults[i];
+ info(
+ `Comparing results at index ${i}:` +
+ " actual=" +
+ JSON.stringify(actual) +
+ " expected=" +
+ JSON.stringify(expected)
+ );
+ Assert.equal(
+ actual.type,
+ expected.type,
+ `result.type at result index ${i}`
+ );
+ Assert.equal(
+ actual.source,
+ expected.source,
+ `result.source at result index ${i}`
+ );
+ Assert.equal(
+ actual.heuristic,
+ expected.heuristic,
+ `result.heuristic at result index ${i}`
+ );
+
+ // Check payloads except for the last result, which should be the quick
+ // suggest.
+ if (i != expectedResults.length - 1) {
+ Assert.deepEqual(
+ getPayload(context.results[i]),
+ getPayload(expectedResults[i]),
+ "Payload at index " + i
+ );
+ }
+ }
+
+ // Check the quick suggest's payload excluding the timestamp-related
+ // properties.
+ let actualQuickSuggest = context.results[context.results.length - 1];
+ let timestampKeys = [
+ "displayUrl",
+ "sponsoredClickUrl",
+ "url",
+ "urlTimestampIndex",
+ ];
+ Assert.deepEqual(
+ getPayload(actualQuickSuggest, timestampKeys),
+ getPayload(expectedQuickSuggest, timestampKeys),
+ "Quick suggest payload excluding timestamp-related keys"
+ );
+
+ // Now check the timestamps in the payload.
+ QuickSuggestTestUtils.assertTimestampsReplaced(actualQuickSuggest, {
+ url: TIMESTAMP_SUGGESTION_URL,
+ sponsoredClickUrl: TIMESTAMP_SUGGESTION_CLICK_URL,
+ });
+
+ // Clean up.
+ UrlbarPrefs.clear("suggest.quicksuggest.nonsponsored");
+ UrlbarPrefs.clear("suggest.quicksuggest.sponsored");
+ UrlbarPrefs.clear("suggest.searches");
+ await PlacesUtils.history.clear();
+});
+
+// Tests the API for blocking suggestions and the backing pref.
+add_task(async function blockedSuggestionsAPI() {
+ // Start with no blocked suggestions.
+ await QuickSuggest.blockedSuggestions.clear();
+ Assert.equal(
+ QuickSuggest.blockedSuggestions._test_digests.size,
+ 0,
+ "blockedSuggestions._test_digests is empty"
+ );
+ Assert.equal(
+ UrlbarPrefs.get("quicksuggest.blockedDigests"),
+ "",
+ "quicksuggest.blockedDigests is an empty string"
+ );
+
+ // Make some URLs.
+ let urls = [];
+ for (let i = 0; i < 3; i++) {
+ urls.push("http://example.com/" + i);
+ }
+
+ // Block each URL in turn and make sure previously blocked URLs are still
+ // blocked and the remaining URLs are not blocked.
+ for (let i = 0; i < urls.length; i++) {
+ await QuickSuggest.blockedSuggestions.add(urls[i]);
+ for (let j = 0; j < urls.length; j++) {
+ Assert.equal(
+ await QuickSuggest.blockedSuggestions.has(urls[j]),
+ j <= i,
+ `Suggestion at index ${j} is blocked or not as expected`
+ );
+ }
+ }
+
+ // Make sure all URLs are blocked for good measure.
+ for (let url of urls) {
+ Assert.ok(
+ await QuickSuggest.blockedSuggestions.has(url),
+ `Suggestion is blocked: ${url}`
+ );
+ }
+
+ // Check `blockedSuggestions._test_digests` and `quicksuggest.blockedDigests`.
+ Assert.equal(
+ QuickSuggest.blockedSuggestions._test_digests.size,
+ urls.length,
+ "blockedSuggestions._test_digests has correct size"
+ );
+ let array = JSON.parse(UrlbarPrefs.get("quicksuggest.blockedDigests"));
+ Assert.ok(Array.isArray(array), "Parsed value of pref is an array");
+ Assert.equal(array.length, urls.length, "Array has correct length");
+
+ // Write some junk to `quicksuggest.blockedDigests`.
+ // `blockedSuggestions._test_digests` should not be changed and all previously
+ // blocked URLs should remain blocked.
+ UrlbarPrefs.set("quicksuggest.blockedDigests", "not a json array");
+ await QuickSuggest.blockedSuggestions._test_readyPromise;
+ for (let url of urls) {
+ Assert.ok(
+ await QuickSuggest.blockedSuggestions.has(url),
+ `Suggestion remains blocked: ${url}`
+ );
+ }
+ Assert.equal(
+ QuickSuggest.blockedSuggestions._test_digests.size,
+ urls.length,
+ "blockedSuggestions._test_digests still has correct size"
+ );
+
+ // Block a new URL. All URLs should remain blocked and the pref should be
+ // updated.
+ let newURL = "http://example.com/new-block";
+ await QuickSuggest.blockedSuggestions.add(newURL);
+ urls.push(newURL);
+ for (let url of urls) {
+ Assert.ok(
+ await QuickSuggest.blockedSuggestions.has(url),
+ `Suggestion is blocked: ${url}`
+ );
+ }
+ Assert.equal(
+ QuickSuggest.blockedSuggestions._test_digests.size,
+ urls.length,
+ "blockedSuggestions._test_digests has correct size"
+ );
+ array = JSON.parse(UrlbarPrefs.get("quicksuggest.blockedDigests"));
+ Assert.ok(Array.isArray(array), "Parsed value of pref is an array");
+ Assert.equal(array.length, urls.length, "Array has correct length");
+
+ // Add a new URL digest directly to the JSON'ed array in the pref.
+ newURL = "http://example.com/direct-to-pref";
+ urls.push(newURL);
+ array = JSON.parse(UrlbarPrefs.get("quicksuggest.blockedDigests"));
+ array.push(await QuickSuggest.blockedSuggestions._test_getDigest(newURL));
+ UrlbarPrefs.set("quicksuggest.blockedDigests", JSON.stringify(array));
+ await QuickSuggest.blockedSuggestions._test_readyPromise;
+
+ // All URLs should remain blocked and the new URL should be blocked.
+ for (let url of urls) {
+ Assert.ok(
+ await QuickSuggest.blockedSuggestions.has(url),
+ `Suggestion is blocked: ${url}`
+ );
+ }
+ Assert.equal(
+ QuickSuggest.blockedSuggestions._test_digests.size,
+ urls.length,
+ "blockedSuggestions._test_digests has correct size"
+ );
+
+ // Clear the pref. All URLs should be unblocked.
+ UrlbarPrefs.clear("quicksuggest.blockedDigests");
+ await QuickSuggest.blockedSuggestions._test_readyPromise;
+ for (let url of urls) {
+ Assert.ok(
+ !(await QuickSuggest.blockedSuggestions.has(url)),
+ `Suggestion is no longer blocked: ${url}`
+ );
+ }
+ Assert.equal(
+ QuickSuggest.blockedSuggestions._test_digests.size,
+ 0,
+ "blockedSuggestions._test_digests is now empty"
+ );
+
+ // Block all the URLs again and test `blockedSuggestions.clear()`.
+ for (let url of urls) {
+ await QuickSuggest.blockedSuggestions.add(url);
+ }
+ for (let url of urls) {
+ Assert.ok(
+ await QuickSuggest.blockedSuggestions.has(url),
+ `Suggestion is blocked: ${url}`
+ );
+ }
+ await QuickSuggest.blockedSuggestions.clear();
+ for (let url of urls) {
+ Assert.ok(
+ !(await QuickSuggest.blockedSuggestions.has(url)),
+ `Suggestion is no longer blocked: ${url}`
+ );
+ }
+ Assert.equal(
+ QuickSuggest.blockedSuggestions._test_digests.size,
+ 0,
+ "blockedSuggestions._test_digests is now empty"
+ );
+});
+
+// Test whether the blocking for remote settings results works.
+add_task(async function block() {
+ for (const result of REMOTE_SETTINGS_RESULTS) {
+ await QuickSuggest.blockedSuggestions.add(result.url);
+ }
+
+ for (const result of REMOTE_SETTINGS_RESULTS) {
+ const context = createContext(result.keywords[0], {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [],
+ });
+ }
+
+ await QuickSuggest.blockedSuggestions.clear();
+});
+
+// Makes sure remote settings data is fetched using the correct `type` based on
+// the value of the `quickSuggestRemoteSettingsDataType` Nimbus variable.
+add_task(async function remoteSettingsDataType() {
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
+
+ for (let dataType of [undefined, "test-data-type"]) {
+ // Set up a mock Nimbus rollout with the data type.
+ let value = {};
+ if (dataType) {
+ value.quickSuggestRemoteSettingsDataType = dataType;
+ }
+ let cleanUpNimbus = await UrlbarTestUtils.initNimbusFeature(value);
+
+ // Make the result for test data type.
+ let expected = EXPECTED_SPONSORED_RESULT;
+ if (dataType) {
+ expected = JSON.parse(JSON.stringify(expected));
+ expected.payload.title = dataType;
+ }
+
+ // Re-enable to trigger sync from remote settings.
+ UrlbarPrefs.set("quicksuggest.remoteSettings.enabled", false);
+ UrlbarPrefs.set("quicksuggest.remoteSettings.enabled", true);
+
+ let context = createContext(SPONSORED_SEARCH_STRING, {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [expected],
+ });
+
+ await cleanUpNimbus();
+ }
+});
diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_addons.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_addons.js
new file mode 100644
index 0000000000..d667fe35b7
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_addons.js
@@ -0,0 +1,728 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Tests addon quick suggest results.
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
+ ExtensionTestCommon: "resource://testing-common/ExtensionTestCommon.sys.mjs",
+});
+
+const MERINO_SUGGESTIONS = [
+ {
+ provider: "amo",
+ icon: "icon",
+ url: "url",
+ title: "title",
+ description: "description",
+ is_top_pick: true,
+ custom_details: {
+ amo: {
+ rating: "5",
+ number_of_ratings: "1234567",
+ guid: "test@addon",
+ },
+ },
+ },
+];
+
+const REMOTE_SETTINGS_RESULTS = [
+ {
+ type: "amo-suggestions",
+ attachment: [
+ {
+ url: "https://example.com/first-addon",
+ guid: "first@addon",
+ icon: "https://example.com/first-addon.svg",
+ title: "First Addon",
+ rating: "4.7",
+ keywords: ["first", "1st", "two words", "a b c"],
+ description: "Description for the First Addon",
+ number_of_ratings: 1256,
+ is_top_pick: true,
+ },
+ {
+ url: "https://example.com/second-addon",
+ guid: "second@addon",
+ icon: "https://example.com/second-addon.svg",
+ title: "Second Addon",
+ rating: "1.7",
+ keywords: ["second", "2nd"],
+ description: "Description for the Second Addon",
+ number_of_ratings: 256,
+ is_top_pick: false,
+ },
+ {
+ url: "https://example.com/third-addon",
+ guid: "third@addon",
+ icon: "https://example.com/third-addon.svg",
+ title: "Third Addon",
+ rating: "3.7",
+ keywords: ["third", "3rd"],
+ description: "Description for the Third Addon",
+ number_of_ratings: 3,
+ },
+ ],
+ },
+];
+
+add_setup(async function init() {
+ UrlbarPrefs.set("quicksuggest.enabled", true);
+ UrlbarPrefs.set("bestMatch.enabled", true);
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
+ UrlbarPrefs.set("addons.featureGate", true);
+
+ // Disable search suggestions so we don't hit the network.
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", false);
+
+ await QuickSuggestTestUtils.ensureQuickSuggestInit({
+ remoteSettingsResults: REMOTE_SETTINGS_RESULTS,
+ merinoSuggestions: MERINO_SUGGESTIONS,
+ });
+});
+
+// When non-sponsored suggestions are disabled, addon suggestions should be
+// disabled.
+add_task(async function nonsponsoredDisabled() {
+ // Disable sponsored suggestions. Addon suggestions are non-sponsored, so
+ // doing this should not prevent them from being enabled.
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", false);
+
+ // First make sure the suggestion is added when non-sponsored suggestions are
+ // enabled.
+ await check_results({
+ context: createContext("test", {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ }),
+ matches: [
+ makeExpectedResult({
+ suggestion: MERINO_SUGGESTIONS[0],
+ source: "merino",
+ isTopPick: true,
+ }),
+ ],
+ });
+
+ // Now disable them.
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false);
+ await check_results({
+ context: createContext("test", {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ }),
+ matches: [],
+ });
+
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
+ UrlbarPrefs.clear("suggest.quicksuggest.sponsored");
+});
+
+// When addon suggestions specific preference is disabled, addon suggestions
+// should not be added.
+add_task(async function addonSuggestionsSpecificPrefDisabled() {
+ const prefs = ["suggest.addons", "addons.featureGate"];
+ for (const pref of prefs) {
+ // First make sure the suggestion is added.
+ await check_results({
+ context: createContext("test", {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ }),
+ matches: [
+ makeExpectedResult({
+ suggestion: MERINO_SUGGESTIONS[0],
+ source: "merino",
+ isTopPick: true,
+ }),
+ ],
+ });
+
+ // Now disable the pref.
+ UrlbarPrefs.set(pref, false);
+ await check_results({
+ context: createContext("test", {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ }),
+ matches: [],
+ });
+
+ // Revert.
+ UrlbarPrefs.set(pref, true);
+ }
+});
+
+// Check wheather the addon suggestions will be shown by the setup of Nimbus
+// variable.
+add_task(async function nimbus() {
+ // Disable the fature gate.
+ UrlbarPrefs.set("addons.featureGate", false);
+ await check_results({
+ context: createContext("test", {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ }),
+ matches: [],
+ });
+
+ // Enable by Nimbus.
+ const cleanUpNimbusEnable = await UrlbarTestUtils.initNimbusFeature({
+ addonsFeatureGate: true,
+ });
+ await check_results({
+ context: createContext("test", {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ }),
+ matches: [
+ makeExpectedResult({
+ suggestion: MERINO_SUGGESTIONS[0],
+ source: "merino",
+ isTopPick: true,
+ }),
+ ],
+ });
+ await cleanUpNimbusEnable();
+
+ // Enable locally.
+ UrlbarPrefs.set("addons.featureGate", true);
+
+ // Disable by Nimbus.
+ const cleanUpNimbusDisable = await UrlbarTestUtils.initNimbusFeature({
+ addonsFeatureGate: false,
+ });
+ await check_results({
+ context: createContext("test", {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ }),
+ matches: [],
+ });
+ await cleanUpNimbusDisable();
+
+ // Revert.
+ UrlbarPrefs.set("addons.featureGate", true);
+});
+
+add_task(async function hideIfAlreadyInstalled() {
+ // Show suggestion.
+ await check_results({
+ context: createContext("test", {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ }),
+ matches: [
+ makeExpectedResult({
+ suggestion: MERINO_SUGGESTIONS[0],
+ source: "merino",
+ isTopPick: true,
+ }),
+ ],
+ });
+
+ // Install an addon for the suggestion.
+ const xpi = ExtensionTestCommon.generateXPI({
+ manifest: {
+ browser_specific_settings: {
+ gecko: { id: "test@addon" },
+ },
+ },
+ });
+ const addon = await AddonManager.installTemporaryAddon(xpi);
+
+ // Show suggestion for the addon installed.
+ await check_results({
+ context: createContext("test", {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ }),
+ matches: [],
+ });
+
+ await addon.uninstall();
+ xpi.remove(false);
+});
+
+add_task(async function remoteSettings() {
+ const testCases = [
+ {
+ input: "f",
+ expected: null,
+ },
+ {
+ input: "fi",
+ expected: null,
+ },
+ {
+ input: "fir",
+ expected: null,
+ },
+ {
+ input: "firs",
+ expected: null,
+ },
+ {
+ input: "first",
+ expected: makeExpectedResult({
+ suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
+ source: "remote-settings",
+ isTopPick: true,
+ }),
+ },
+ {
+ input: "1st",
+ expected: makeExpectedResult({
+ suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
+ source: "remote-settings",
+ isTopPick: true,
+ }),
+ },
+ {
+ input: "t",
+ expected: null,
+ },
+ {
+ input: "tw",
+ expected: null,
+ },
+ {
+ input: "two",
+ expected: makeExpectedResult({
+ suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
+ source: "remote-settings",
+ isTopPick: true,
+ }),
+ },
+ {
+ input: "two ",
+ expected: makeExpectedResult({
+ suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
+ source: "remote-settings",
+ isTopPick: true,
+ }),
+ },
+ {
+ input: "two w",
+ expected: makeExpectedResult({
+ suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
+ source: "remote-settings",
+ isTopPick: true,
+ }),
+ },
+ {
+ input: "two wo",
+ expected: makeExpectedResult({
+ suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
+ source: "remote-settings",
+ isTopPick: true,
+ }),
+ },
+ {
+ input: "two wor",
+ expected: makeExpectedResult({
+ suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
+ source: "remote-settings",
+ isTopPick: true,
+ }),
+ },
+ {
+ input: "two word",
+ expected: makeExpectedResult({
+ suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
+ source: "remote-settings",
+ isTopPick: true,
+ }),
+ },
+ {
+ input: "two words",
+ expected: makeExpectedResult({
+ suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
+ source: "remote-settings",
+ isTopPick: true,
+ }),
+ },
+ {
+ input: "a",
+ expected: makeExpectedResult({
+ suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
+ source: "remote-settings",
+ isTopPick: true,
+ }),
+ },
+ {
+ input: "a ",
+ expected: makeExpectedResult({
+ suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
+ source: "remote-settings",
+ isTopPick: true,
+ }),
+ },
+ {
+ input: "a b",
+ expected: makeExpectedResult({
+ suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
+ source: "remote-settings",
+ isTopPick: true,
+ }),
+ },
+ {
+ input: "a b ",
+ expected: makeExpectedResult({
+ suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
+ source: "remote-settings",
+ isTopPick: true,
+ }),
+ },
+ {
+ input: "a b c",
+ expected: makeExpectedResult({
+ suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
+ source: "remote-settings",
+ isTopPick: true,
+ }),
+ },
+ {
+ input: "second",
+ expected: makeExpectedResult({
+ suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[1],
+ source: "remote-settings",
+ isTopPick: false,
+ }),
+ },
+ {
+ input: "2nd",
+ expected: makeExpectedResult({
+ suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[1],
+ source: "remote-settings",
+ isTopPick: false,
+ }),
+ },
+ {
+ input: "third",
+ expected: makeExpectedResult({
+ suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[2],
+ source: "remote-settings",
+ isTopPick: true,
+ }),
+ },
+ {
+ input: "3rd",
+ expected: makeExpectedResult({
+ suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[2],
+ source: "remote-settings",
+ isTopPick: true,
+ }),
+ },
+ ];
+
+ // Disable Merino so we trigger only remote settings suggestions.
+ UrlbarPrefs.set("quicksuggest.dataCollection.enabled", false);
+
+ for (const { input, expected } of testCases) {
+ await check_results({
+ context: createContext(input, {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ }),
+ matches: expected ? [expected] : [],
+ });
+ }
+
+ UrlbarPrefs.set("quicksuggest.dataCollection.enabled", true);
+});
+
+add_task(async function merinoIsTopPick() {
+ const suggestion = JSON.parse(JSON.stringify(MERINO_SUGGESTIONS[0]));
+
+ // is_top_pick is specified as false.
+ suggestion.is_top_pick = false;
+ MerinoTestUtils.server.response.body.suggestions = [suggestion];
+ await check_results({
+ context: createContext("test", {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ }),
+ matches: [
+ makeExpectedResult({
+ suggestion,
+ source: "merino",
+ isTopPick: false,
+ }),
+ ],
+ });
+
+ // is_top_pick is undefined.
+ delete suggestion.is_top_pick;
+ MerinoTestUtils.server.response.body.suggestions = [suggestion];
+ await check_results({
+ context: createContext("test", {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ }),
+ matches: [
+ makeExpectedResult({
+ suggestion,
+ source: "merino",
+ isTopPick: true,
+ }),
+ ],
+ });
+});
+
+// Tests "show less frequently" with the cap set in remote settings.
+add_task(async function showLessFrequently_rs() {
+ await doShowLessFrequentlyTest({
+ rs: {
+ show_less_frequently_cap: 3,
+ },
+ tests: [
+ {
+ showLessFrequentlyCount: 0,
+ canShowLessFrequently: true,
+ searches: {
+ f: false,
+ fi: false,
+ fir: false,
+ firs: false,
+ first: true,
+ t: false,
+ tw: false,
+ two: true,
+ "two ": true,
+ "two w": true,
+ "two wo": true,
+ "two wor": true,
+ "two word": true,
+ "two words": true,
+ a: true,
+ "a ": true,
+ "a b": true,
+ "a b ": true,
+ "a b c": true,
+ },
+ },
+ {
+ showLessFrequentlyCount: 1,
+ canShowLessFrequently: true,
+ searches: {
+ first: false,
+ two: false,
+ a: false,
+ },
+ },
+ {
+ showLessFrequentlyCount: 2,
+ canShowLessFrequently: true,
+ searches: {
+ "two ": false,
+ "a ": false,
+ },
+ },
+ {
+ showLessFrequentlyCount: 3,
+ canShowLessFrequently: false,
+ searches: {
+ "two w": false,
+ "a b": false,
+ },
+ },
+ {
+ showLessFrequentlyCount: 3,
+ canShowLessFrequently: false,
+ searches: {},
+ },
+ ],
+ });
+});
+
+// Tests "show less frequently" with the cap set in both Nimbus and remote
+// settings. Nimbus should override remote settings.
+add_task(async function showLessFrequently_nimbus() {
+ await doShowLessFrequentlyTest({
+ nimbus: {
+ addonsShowLessFrequentlyCap: 3,
+ },
+ rs: {
+ show_less_frequently_cap: 10,
+ },
+ tests: [
+ {
+ showLessFrequentlyCount: 0,
+ canShowLessFrequently: true,
+ searches: {
+ a: true,
+ "a ": true,
+ "a b": true,
+ "a b ": true,
+ "a b c": true,
+ },
+ },
+ {
+ showLessFrequentlyCount: 1,
+ canShowLessFrequently: true,
+ searches: {
+ a: false,
+ },
+ },
+ {
+ showLessFrequentlyCount: 2,
+ canShowLessFrequently: true,
+ searches: {
+ "a ": false,
+ },
+ },
+ {
+ showLessFrequentlyCount: 3,
+ canShowLessFrequently: false,
+ searches: {
+ "a b": false,
+ },
+ },
+ {
+ showLessFrequentlyCount: 3,
+ canShowLessFrequently: false,
+ searches: {},
+ },
+ ],
+ });
+});
+
+/**
+ * Does a group of searches, increments the `showLessFrequentlyCount`, and
+ * repeats until all groups are done. The cap can be set by remote settings
+ * config and/or Nimbus.
+ *
+ * @param {object} options
+ * Options object.
+ * @param {object} options.tests
+ * An array where each item describes a group of searches to perform and
+ * expected state. Each item should look like this:
+ * `{ showLessFrequentlyCount, canShowLessFrequently, searches }`
+ *
+ * {number} showLessFrequentlyCount
+ * The expected value of `showLessFrequentlyCount` before the group of
+ * searches is performed.
+ * {boolean} canShowLessFrequently
+ * The expected value of `canShowLessFrequently` before the group of
+ * searches is performed.
+ * {object} searches
+ * An object that maps each search string to a boolean that indicates
+ * whether the first remote settings suggestion should be triggered by the
+ * search string. `searches` objects are cumulative: The intended use is to
+ * pass a large initial group of searches in the first search group, and
+ * then each following `searches` is a diff against the previous.
+ * @param {object} options.rs
+ * The remote settings config to set.
+ * @param {object} options.nimbus
+ * The Nimbus variables to set.
+ */
+async function doShowLessFrequentlyTest({ tests, rs = {}, nimbus = {} }) {
+ // Disable Merino so we trigger only remote settings suggestions.
+ UrlbarPrefs.set("quicksuggest.dataCollection.enabled", false);
+
+ // We'll be testing with the first remote settings suggestion.
+ let suggestion = REMOTE_SETTINGS_RESULTS[0].attachment[0];
+
+ let addonSuggestions = QuickSuggest.getFeature("AddonSuggestions");
+
+ // Set Nimbus variables and RS config.
+ let cleanUpNimbus = await UrlbarTestUtils.initNimbusFeature(nimbus);
+ await QuickSuggestTestUtils.withConfig({
+ config: rs,
+ callback: async () => {
+ let cumulativeSearches = {};
+
+ for (let {
+ showLessFrequentlyCount,
+ canShowLessFrequently,
+ searches,
+ } of tests) {
+ Assert.equal(
+ addonSuggestions.showLessFrequentlyCount,
+ showLessFrequentlyCount,
+ "showLessFrequentlyCount should be correct initially"
+ );
+ Assert.equal(
+ UrlbarPrefs.get("addons.showLessFrequentlyCount"),
+ showLessFrequentlyCount,
+ "Pref should be correct initially"
+ );
+ Assert.equal(
+ addonSuggestions.canShowLessFrequently,
+ canShowLessFrequently,
+ "canShowLessFrequently should be correct initially"
+ );
+
+ // Merge the current `searches` object into the cumulative object.
+ cumulativeSearches = {
+ ...cumulativeSearches,
+ ...searches,
+ };
+
+ for (let [searchString, isExpected] of Object.entries(
+ cumulativeSearches
+ )) {
+ await check_results({
+ context: createContext(searchString, {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ }),
+ matches: !isExpected
+ ? []
+ : [
+ makeExpectedResult({
+ suggestion,
+ source: "remote-settings",
+ isTopPick: true,
+ }),
+ ],
+ });
+ }
+
+ addonSuggestions.incrementShowLessFrequentlyCount();
+ }
+ },
+ });
+
+ await cleanUpNimbus();
+ UrlbarPrefs.clear("addons.showLessFrequentlyCount");
+ UrlbarPrefs.set("quicksuggest.dataCollection.enabled", true);
+}
+
+function makeExpectedResult({ suggestion, source, isTopPick }) {
+ let rating;
+ let number_of_ratings;
+ if (source === "remote-settings") {
+ rating = suggestion.rating;
+ number_of_ratings = suggestion.number_of_ratings;
+ } else {
+ rating = suggestion.custom_details.amo.rating;
+ number_of_ratings = suggestion.custom_details.amo.number_of_ratings;
+ }
+
+ return {
+ isBestMatch: isTopPick,
+ suggestedIndex: isTopPick ? 1 : -1,
+ type: UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ heuristic: false,
+ payload: {
+ telemetryType: "amo",
+ dynamicType: "addons",
+ title: suggestion.title,
+ url: suggestion.url,
+ displayUrl: suggestion.url.replace(/^https:\/\//, ""),
+ icon: suggestion.icon,
+ description: suggestion.description,
+ rating: Number(rating),
+ reviews: Number(number_of_ratings),
+ shouldNavigate: true,
+ helpUrl: QuickSuggest.HELP_URL,
+ source,
+ },
+ };
+}
diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_bestMatch.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_bestMatch.js
new file mode 100644
index 0000000000..853073a6c0
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_bestMatch.js
@@ -0,0 +1,463 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Tests best match quick suggest results. "Best match" refers to two different
+// concepts:
+//
+// (1) The "best match" UI treatment (labeled "top pick" in the UI) that makes a
+// result's row larger than usual and sets `suggestedIndex` to 1.
+// (2) The quick suggest config in remote settings can contain a `best_match`
+// object that tells Firefox to use the best match UI treatment if the
+// user's search string is a certain length.
+//
+// This file tests aspects of both concepts.
+//
+// See also test_quicksuggest_topPicks.js. "Top picks" refer to a similar
+// concept but it is not related to (2).
+
+"use strict";
+
+const MAX_RESULT_COUNT = UrlbarPrefs.get("maxRichResults");
+
+// This search string length needs to be >= 4 to trigger its suggestion as a
+// best match instead of a usual quick suggest.
+const BEST_MATCH_POSITION_SEARCH_STRING = "bestmatchposition";
+const BEST_MATCH_POSITION = Math.round(MAX_RESULT_COUNT / 2);
+
+const REMOTE_SETTINGS_RESULTS = [
+ {
+ id: 1,
+ url: "http://example.com/",
+ title: "Fullkeyword title",
+ keywords: [
+ "fu",
+ "ful",
+ "full",
+ "fullk",
+ "fullke",
+ "fullkey",
+ "fullkeyw",
+ "fullkeywo",
+ "fullkeywor",
+ "fullkeyword",
+ ],
+ click_url: "http://example.com/click",
+ impression_url: "http://example.com/impression",
+ advertiser: "TestAdvertiser",
+ },
+ {
+ id: 2,
+ url: "http://example.com/best-match-position",
+ title: `${BEST_MATCH_POSITION_SEARCH_STRING} title`,
+ keywords: [BEST_MATCH_POSITION_SEARCH_STRING],
+ click_url: "http://example.com/click",
+ impression_url: "http://example.com/impression",
+ advertiser: "TestAdvertiser",
+ position: BEST_MATCH_POSITION,
+ },
+];
+
+const EXPECTED_BEST_MATCH_URLBAR_RESULT = {
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ heuristic: false,
+ isBestMatch: true,
+ payload: {
+ telemetryType: "adm_sponsored",
+ url: "http://example.com/",
+ originalUrl: "http://example.com/",
+ title: "Fullkeyword title",
+ icon: null,
+ isSponsored: true,
+ sponsoredImpressionUrl: "http://example.com/impression",
+ sponsoredClickUrl: "http://example.com/click",
+ sponsoredBlockId: 1,
+ sponsoredAdvertiser: "TestAdvertiser",
+ helpUrl: QuickSuggest.HELP_URL,
+ helpL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-learn-more-about-firefox-suggest"
+ : "firefox-suggest-urlbar-learn-more",
+ },
+ isBlockable: UrlbarPrefs.get("bestMatchBlockingEnabled"),
+ blockL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-dismiss-firefox-suggest"
+ : "firefox-suggest-urlbar-block",
+ },
+ displayUrl: "http://example.com",
+ source: "remote-settings",
+ },
+};
+
+const EXPECTED_NON_BEST_MATCH_URLBAR_RESULT = {
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ heuristic: false,
+ payload: {
+ telemetryType: "adm_sponsored",
+ url: "http://example.com/",
+ originalUrl: "http://example.com/",
+ title: "Fullkeyword title",
+ qsSuggestion: "fullkeyword",
+ icon: null,
+ isSponsored: true,
+ sponsoredImpressionUrl: "http://example.com/impression",
+ sponsoredClickUrl: "http://example.com/click",
+ sponsoredBlockId: 1,
+ sponsoredAdvertiser: "TestAdvertiser",
+ helpUrl: QuickSuggest.HELP_URL,
+ helpL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-learn-more-about-firefox-suggest"
+ : "firefox-suggest-urlbar-learn-more",
+ },
+ isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"),
+ blockL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-dismiss-firefox-suggest"
+ : "firefox-suggest-urlbar-block",
+ },
+ displayUrl: "http://example.com",
+ source: "remote-settings",
+ },
+};
+
+const EXPECTED_BEST_MATCH_POSITION_URLBAR_RESULT = {
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ heuristic: false,
+ isBestMatch: true,
+ payload: {
+ telemetryType: "adm_sponsored",
+ url: "http://example.com/best-match-position",
+ originalUrl: "http://example.com/best-match-position",
+ title: `${BEST_MATCH_POSITION_SEARCH_STRING} title`,
+ icon: null,
+ isSponsored: true,
+ sponsoredImpressionUrl: "http://example.com/impression",
+ sponsoredClickUrl: "http://example.com/click",
+ sponsoredBlockId: 2,
+ sponsoredAdvertiser: "TestAdvertiser",
+ helpUrl: QuickSuggest.HELP_URL,
+ helpL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-learn-more-about-firefox-suggest"
+ : "firefox-suggest-urlbar-learn-more",
+ },
+ isBlockable: UrlbarPrefs.get("bestMatchBlockingEnabled"),
+ blockL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-dismiss-firefox-suggest"
+ : "firefox-suggest-urlbar-block",
+ },
+ displayUrl: "http://example.com/best-match-position",
+ source: "remote-settings",
+ },
+};
+
+add_task(async function init() {
+ UrlbarPrefs.set("quicksuggest.enabled", true);
+ UrlbarPrefs.set("bestMatch.enabled", true);
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
+ UrlbarPrefs.set("suggest.bestmatch", true);
+
+ // Disable search suggestions so we don't hit the network.
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", false);
+
+ await QuickSuggestTestUtils.ensureQuickSuggestInit({
+ remoteSettingsResults: [
+ {
+ type: "data",
+ attachment: REMOTE_SETTINGS_RESULTS,
+ },
+ ],
+ config: QuickSuggestTestUtils.BEST_MATCH_CONFIG,
+ });
+});
+
+// Tests a best match result.
+add_task(async function bestMatch() {
+ let context = createContext("fullkeyword", {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [EXPECTED_BEST_MATCH_URLBAR_RESULT],
+ });
+
+ let result = context.results[0];
+
+ // The title should not include the full keyword and em dash, and the part of
+ // the title that the search string matches should be highlighted.
+ Assert.equal(result.title, "Fullkeyword title", "result.title");
+ Assert.deepEqual(
+ result.titleHighlights,
+ [[0, "fullkeyword".length]],
+ "result.titleHighlights"
+ );
+
+ Assert.equal(result.suggestedIndex, 1, "result.suggestedIndex");
+ Assert.equal(
+ !!result.isSuggestedIndexRelativeToGroup,
+ false,
+ "result.isSuggestedIndexRelativeToGroup"
+ );
+});
+
+// Tests a usual, non-best match quick suggest result.
+add_task(async function nonBestMatch() {
+ // Search for a substring of the full search string so we can test title
+ // highlights.
+ let context = createContext("fu", {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [EXPECTED_NON_BEST_MATCH_URLBAR_RESULT],
+ });
+
+ let result = context.results[0];
+
+ // The title should include the full keyword and em dash, and the part of the
+ // title that the search string does not match should be highlighted.
+ Assert.equal(result.title, "fullkeyword — Fullkeyword title", "result.title");
+ Assert.deepEqual(
+ result.titleHighlights,
+ [["fu".length, "fullkeyword".length - "fu".length]],
+ "result.titleHighlights"
+ );
+
+ Assert.equal(result.suggestedIndex, -1, "result.suggestedIndex");
+ Assert.equal(
+ result.isSuggestedIndexRelativeToGroup,
+ true,
+ "result.isSuggestedIndexRelativeToGroup"
+ );
+});
+
+// Tests prefix keywords leading up to a best match.
+add_task(async function prefixKeywords() {
+ let sawNonBestMatch = false;
+ let sawBestMatch = false;
+ for (let keyword of REMOTE_SETTINGS_RESULTS[0].keywords) {
+ info(`Searching for "${keyword}"`);
+ let context = createContext(keyword, {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+
+ let expectedResult;
+ if (keyword.length < 4) {
+ expectedResult = EXPECTED_NON_BEST_MATCH_URLBAR_RESULT;
+ sawNonBestMatch = true;
+ } else {
+ expectedResult = EXPECTED_BEST_MATCH_URLBAR_RESULT;
+ sawBestMatch = true;
+ }
+
+ await check_results({
+ context,
+ matches: [expectedResult],
+ });
+ }
+
+ Assert.ok(sawNonBestMatch, "Sanity check: Saw a non-best match");
+ Assert.ok(sawBestMatch, "Sanity check: Saw a best match");
+});
+
+// When tab-to-search is shown in the same search, both it and the best match
+// will have a `suggestedIndex` value of 1. The TTS should appear first.
+add_task(async function tabToSearch() {
+ // Disable tab-to-search onboarding results so we get a regular TTS result,
+ // which we can test a little more easily with `makeSearchResult()`.
+ UrlbarPrefs.set("tabToSearch.onboard.interactionsLeft", 0);
+
+ // Install a test engine. The main part of its domain name needs to match the
+ // best match result too so we can trigger both its TTS and the best match.
+ let engineURL = "https://foo.fullkeyword.com/";
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: "Test",
+ search_url: engineURL,
+ },
+ { skipUnload: true }
+ );
+ let engine = Services.search.getEngineByName("Test");
+
+ // Also need to add a visit to trigger TTS.
+ await PlacesTestUtils.addVisits(engineURL);
+
+ let context = createContext("fullkeyword", {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ // search heuristic
+ makeSearchResult(context, {
+ engineName: Services.search.defaultEngine.name,
+ engineIconUri: Services.search.defaultEngine.iconURI?.spec,
+ heuristic: true,
+ }),
+ // tab to search
+ makeSearchResult(context, {
+ engineName: engine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(engine.searchUrlDomain),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ satisfiesAutofillThreshold: true,
+ }),
+ // best match
+ EXPECTED_BEST_MATCH_URLBAR_RESULT,
+ // visit
+ makeVisitResult(context, {
+ uri: engineURL,
+ title: `test visit for ${engineURL}`,
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+ await extension.unload();
+
+ UrlbarPrefs.clear("tabToSearch.onboard.interactionsLeft");
+});
+
+// When the best match feature gate is disabled, quick suggest results should be
+// shown as the usual non-best match results.
+add_task(async function disabled_featureGate() {
+ UrlbarPrefs.set("bestMatch.enabled", false);
+ await doDisabledTest();
+ UrlbarPrefs.set("bestMatch.enabled", true);
+});
+
+// When the best match suggestions are disabled, quick suggest results should be
+// shown as the usual non-best match results.
+add_task(async function disabled_suggestions() {
+ UrlbarPrefs.set("suggest.bestmatch", false);
+ await doDisabledTest();
+ UrlbarPrefs.set("suggest.bestmatch", true);
+});
+
+// When best match is disabled, quick suggest results should be shown as the
+// usual, non-best match results.
+async function doDisabledTest() {
+ let context = createContext("fullkeywor", {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [EXPECTED_NON_BEST_MATCH_URLBAR_RESULT],
+ });
+
+ let result = context.results[0];
+
+ // The title should include the full keyword and em dash, and the part of the
+ // title that the search string does not match should be highlighted.
+ Assert.equal(result.title, "fullkeyword — Fullkeyword title", "result.title");
+ Assert.deepEqual(
+ result.titleHighlights,
+ [["fullkeywor".length, 1]],
+ "result.titleHighlights"
+ );
+
+ Assert.equal(result.suggestedIndex, -1, "result.suggestedIndex");
+ Assert.equal(
+ result.isSuggestedIndexRelativeToGroup,
+ true,
+ "result.isSuggestedIndexRelativeToGroup"
+ );
+}
+
+// `suggestion.position` should be ignored when the suggestion is a best match.
+add_task(async function position() {
+ Assert.greater(
+ BEST_MATCH_POSITION,
+ 1,
+ "Precondition: `suggestion.position` > the best match index"
+ );
+
+ UrlbarPrefs.set("quicksuggest.allowPositionInSuggestions", true);
+
+ let context = createContext(BEST_MATCH_POSITION_SEARCH_STRING, {
+ isPrivate: false,
+ });
+
+ // Add some visits to fill up the view.
+ let maxResultCount = UrlbarPrefs.get("maxRichResults");
+ let visitResults = [];
+ for (let i = 0; i < maxResultCount; i++) {
+ let url = `http://example.com/${BEST_MATCH_POSITION_SEARCH_STRING}-${i}`;
+ await PlacesTestUtils.addVisits(url);
+ visitResults.unshift(
+ makeVisitResult(context, {
+ uri: url,
+ title: `test visit for ${url}`,
+ })
+ );
+ }
+
+ // Do a search.
+ await check_results({
+ context,
+ matches: [
+ // search heuristic
+ makeSearchResult(context, {
+ engineName: Services.search.defaultEngine.name,
+ engineIconUri: Services.search.defaultEngine.iconURI?.spec,
+ heuristic: true,
+ }),
+ // best match whose backing suggestion has a `position`
+ EXPECTED_BEST_MATCH_POSITION_URLBAR_RESULT,
+ // visits
+ ...visitResults.slice(0, MAX_RESULT_COUNT - 2),
+ ],
+ });
+
+ await cleanupPlaces();
+ UrlbarPrefs.clear("quicksuggest.allowPositionInSuggestions");
+});
+
+// Tests a suggestion that is blocked from being a best match.
+add_task(async function blockedAsBestMatch() {
+ let config = QuickSuggestTestUtils.BEST_MATCH_CONFIG;
+ config.best_match.blocked_suggestion_ids = [1];
+ await QuickSuggestTestUtils.withConfig({
+ config,
+ callback: async () => {
+ let context = createContext("fullkeyword", {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [EXPECTED_NON_BEST_MATCH_URLBAR_RESULT],
+ });
+ },
+ });
+});
+
+// Tests without a best_match config to make sure nothing breaks.
+add_task(async function noConfig() {
+ await QuickSuggestTestUtils.withConfig({
+ config: {},
+ callback: async () => {
+ let context = createContext("fullkeyword", {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [EXPECTED_NON_BEST_MATCH_URLBAR_RESULT],
+ });
+ },
+ });
+});
diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_dynamicWikipedia.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_dynamicWikipedia.js
new file mode 100644
index 0000000000..04859c7404
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_dynamicWikipedia.js
@@ -0,0 +1,95 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Tests dynamic Wikipedia quick suggest results.
+
+"use strict";
+
+const MERINO_SUGGESTIONS = [
+ {
+ title: "title",
+ url: "url",
+ is_sponsored: false,
+ score: 0.23,
+ description: "description",
+ icon: "icon",
+ full_keyword: "full_keyword",
+ advertiser: "dynamic-wikipedia",
+ block_id: 0,
+ impression_url: "impression_url",
+ click_url: "click_url",
+ provider: "wikipedia",
+ },
+];
+
+add_setup(async function init() {
+ UrlbarPrefs.set("quicksuggest.enabled", true);
+ UrlbarPrefs.set("bestMatch.enabled", true);
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
+
+ // Disable search suggestions so we don't hit the network.
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", false);
+
+ await QuickSuggestTestUtils.ensureQuickSuggestInit({
+ merinoSuggestions: MERINO_SUGGESTIONS,
+ });
+});
+
+// When non-sponsored suggestions are disabled, dynamic Wikipedia suggestions
+// should be disabled.
+add_task(async function nonsponsoredDisabled() {
+ // Disable sponsored suggestions. Dynamic Wikipedia suggestions are
+ // non-sponsored, so doing this should not prevent them from being enabled.
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", false);
+
+ // First make sure the suggestion is added when non-sponsored suggestions are
+ // enabled.
+ await check_results({
+ context: createContext("test", {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ }),
+ matches: [makeExpectedResult()],
+ });
+
+ // Now disable them.
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false);
+ await check_results({
+ context: createContext("test", {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ }),
+ matches: [],
+ });
+
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
+ UrlbarPrefs.clear("suggest.quicksuggest.sponsored");
+});
+
+function makeExpectedResult() {
+ return {
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ heuristic: false,
+ suggestedIndex: -1,
+ payload: {
+ telemetryType: "wikipedia",
+ title: "title",
+ url: "url",
+ displayUrl: "url",
+ isSponsored: false,
+ icon: "icon",
+ qsSuggestion: "full_keyword",
+ source: "merino",
+ helpUrl: QuickSuggest.HELP_URL,
+ helpL10n: {
+ id: "urlbar-result-menu-learn-more-about-firefox-suggest",
+ },
+ isBlockable: true,
+ blockL10n: {
+ id: "urlbar-result-menu-dismiss-firefox-suggest",
+ },
+ },
+ };
+}
diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_impressionCaps.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_impressionCaps.js
new file mode 100644
index 0000000000..41706dabd8
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_impressionCaps.js
@@ -0,0 +1,3888 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Tests impression frequency capping for quick suggest results.
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+});
+
+const REMOTE_SETTINGS_RESULTS = [
+ {
+ id: 1,
+ url: "http://example.com/sponsored",
+ title: "Sponsored suggestion",
+ keywords: ["sponsored"],
+ click_url: "http://example.com/click",
+ impression_url: "http://example.com/impression",
+ advertiser: "TestAdvertiser",
+ iab_category: "22 - Shopping",
+ },
+ {
+ id: 2,
+ url: "http://example.com/nonsponsored",
+ title: "Non-sponsored suggestion",
+ keywords: ["nonsponsored"],
+ click_url: "http://example.com/click",
+ impression_url: "http://example.com/impression",
+ advertiser: "TestAdvertiser",
+ iab_category: "5 - Education",
+ },
+];
+
+const EXPECTED_SPONSORED_URLBAR_RESULT = {
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ heuristic: false,
+ payload: {
+ telemetryType: "adm_sponsored",
+ url: "http://example.com/sponsored",
+ originalUrl: "http://example.com/sponsored",
+ displayUrl: "http://example.com/sponsored",
+ title: "Sponsored suggestion",
+ qsSuggestion: "sponsored",
+ icon: null,
+ isSponsored: true,
+ sponsoredImpressionUrl: "http://example.com/impression",
+ sponsoredClickUrl: "http://example.com/click",
+ sponsoredBlockId: 1,
+ sponsoredAdvertiser: "TestAdvertiser",
+ sponsoredIabCategory: "22 - Shopping",
+ helpUrl: QuickSuggest.HELP_URL,
+ helpL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-learn-more-about-firefox-suggest"
+ : "firefox-suggest-urlbar-learn-more",
+ },
+ isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"),
+ blockL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-dismiss-firefox-suggest"
+ : "firefox-suggest-urlbar-block",
+ },
+ source: "remote-settings",
+ },
+};
+
+const EXPECTED_NONSPONSORED_URLBAR_RESULT = {
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ heuristic: false,
+ payload: {
+ telemetryType: "adm_nonsponsored",
+ url: "http://example.com/nonsponsored",
+ originalUrl: "http://example.com/nonsponsored",
+ displayUrl: "http://example.com/nonsponsored",
+ title: "Non-sponsored suggestion",
+ qsSuggestion: "nonsponsored",
+ icon: null,
+ isSponsored: false,
+ sponsoredImpressionUrl: "http://example.com/impression",
+ sponsoredClickUrl: "http://example.com/click",
+ sponsoredBlockId: 2,
+ sponsoredAdvertiser: "TestAdvertiser",
+ sponsoredIabCategory: "5 - Education",
+ helpUrl: QuickSuggest.HELP_URL,
+ helpL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-learn-more-about-firefox-suggest"
+ : "firefox-suggest-urlbar-learn-more",
+ },
+ isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"),
+ blockL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-dismiss-firefox-suggest"
+ : "firefox-suggest-urlbar-block",
+ },
+ source: "remote-settings",
+ },
+};
+
+let gSandbox;
+let gDateNowStub;
+let gStartupDateMsStub;
+
+add_task(async function init() {
+ UrlbarPrefs.set("quicksuggest.enabled", true);
+ UrlbarPrefs.set("quicksuggest.impressionCaps.sponsoredEnabled", true);
+ UrlbarPrefs.set("quicksuggest.impressionCaps.nonSponsoredEnabled", true);
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
+ UrlbarPrefs.set("bestMatch.enabled", false);
+
+ // Disable search suggestions so we don't hit the network.
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", false);
+
+ await QuickSuggestTestUtils.ensureQuickSuggestInit({
+ remoteSettingsResults: [
+ {
+ type: "data",
+ attachment: REMOTE_SETTINGS_RESULTS,
+ },
+ ],
+ });
+
+ // Set up a sinon stub for the `Date.now()` implementation inside of
+ // UrlbarProviderQuickSuggest. This lets us test searches performed at
+ // specific times. See `doTimedCallbacks()` for more info.
+ gSandbox = sinon.createSandbox();
+ gDateNowStub = gSandbox.stub(
+ Cu.getGlobalForObject(UrlbarProviderQuickSuggest).Date,
+ "now"
+ );
+
+ // Set up a sinon stub for `UrlbarProviderQuickSuggest._getStartupDateMs()` to
+ // let the test override the startup date.
+ gStartupDateMsStub = gSandbox.stub(
+ QuickSuggest.impressionCaps,
+ "_getStartupDateMs"
+ );
+ gStartupDateMsStub.returns(0);
+});
+
+// Tests a single interval.
+add_task(async function oneInterval() {
+ await doTest({
+ config: {
+ impression_caps: {
+ sponsored: {
+ custom: [{ interval_s: 3, max_count: 1 }],
+ },
+ },
+ },
+ callback: async () => {
+ await doTimedSearches("sponsored", {
+ 0: {
+ results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []],
+ telemetry: {
+ events: [
+ {
+ object: "hit",
+ extra: {
+ eventDate: "0",
+ intervalSeconds: "3",
+ maxCount: "1",
+ startDate: "0",
+ impressionDate: "0",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ 1: {
+ results: [[]],
+ },
+ 2: {
+ results: [[]],
+ },
+ 3: {
+ results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []],
+ telemetry: {
+ events: [
+ {
+ object: "reset",
+ extra: {
+ eventDate: "3000",
+ intervalSeconds: "3",
+ maxCount: "1",
+ startDate: "0",
+ impressionDate: "0",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ {
+ object: "hit",
+ extra: {
+ eventDate: "3000",
+ intervalSeconds: "3",
+ maxCount: "1",
+ startDate: "3000",
+ impressionDate: "3000",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ 4: {
+ results: [[]],
+ },
+ 5: {
+ results: [[]],
+ },
+ });
+ },
+ });
+});
+
+// Tests multiple intervals.
+add_task(async function multipleIntervals() {
+ await doTest({
+ config: {
+ impression_caps: {
+ sponsored: {
+ custom: [
+ { interval_s: 1, max_count: 1 },
+ { interval_s: 5, max_count: 3 },
+ { interval_s: 10, max_count: 5 },
+ ],
+ },
+ },
+ },
+ callback: async () => {
+ await doTimedSearches("sponsored", {
+ // 0s: 1 new impression; 1 impression total
+ 0: {
+ results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []],
+ telemetry: {
+ events: [
+ // hit: interval_s: 1, max_count: 1
+ {
+ object: "hit",
+ extra: {
+ eventDate: "0",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "0",
+ impressionDate: "0",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ // 1s: 1 new impression; 2 impressions total
+ 1: {
+ results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []],
+ telemetry: {
+ events: [
+ // reset: interval_s: 1, max_count: 1
+ {
+ object: "reset",
+ extra: {
+ eventDate: "1000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "0",
+ impressionDate: "0",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ // hit: interval_s: 1, max_count: 1
+ {
+ object: "hit",
+ extra: {
+ eventDate: "1000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "1000",
+ impressionDate: "1000",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ // 2s: 1 new impression; 3 impressions total
+ 2: {
+ results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []],
+ telemetry: {
+ events: [
+ // reset: interval_s: 1, max_count: 1
+ {
+ object: "reset",
+ extra: {
+ eventDate: "2000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "1000",
+ impressionDate: "1000",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ // hit: interval_s: 1, max_count: 1
+ {
+ object: "hit",
+ extra: {
+ eventDate: "2000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "2000",
+ impressionDate: "2000",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ // hit: interval_s: 5, max_count: 3
+ {
+ object: "hit",
+ extra: {
+ eventDate: "2000",
+ intervalSeconds: "5",
+ maxCount: "3",
+ startDate: "0",
+ impressionDate: "2000",
+ count: "3",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ // 3s: no new impressions; 3 impressions total
+ 3: {
+ results: [[]],
+ telemetry: {
+ events: [
+ // reset: interval_s: 1, max_count: 1
+ {
+ object: "reset",
+ extra: {
+ eventDate: "3000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "2000",
+ impressionDate: "2000",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ // 4s: no new impressions; 3 impressions total
+ 4: {
+ results: [[]],
+ telemetry: {
+ events: [
+ // reset: interval_s: 1, max_count: 1
+ {
+ object: "reset",
+ extra: {
+ eventDate: "4000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "3000",
+ impressionDate: "2000",
+ count: "0",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ // 5s: 1 new impression; 4 impressions total
+ 5: {
+ results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []],
+ telemetry: {
+ events: [
+ // reset: interval_s: 1, max_count: 1
+ {
+ object: "reset",
+ extra: {
+ eventDate: "5000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "4000",
+ impressionDate: "2000",
+ count: "0",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ // reset: interval_s: 5, max_count: 3
+ {
+ object: "reset",
+ extra: {
+ eventDate: "5000",
+ intervalSeconds: "5",
+ maxCount: "3",
+ startDate: "0",
+ impressionDate: "2000",
+ count: "3",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ // hit: interval_s: 1, max_count: 1
+ {
+ object: "hit",
+ extra: {
+ eventDate: "5000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "5000",
+ impressionDate: "5000",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ // 6s: 1 new impression; 5 impressions total
+ 6: {
+ results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []],
+ telemetry: {
+ events: [
+ // reset: interval_s: 1, max_count: 1
+ {
+ object: "reset",
+ extra: {
+ eventDate: "6000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "5000",
+ impressionDate: "5000",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ // hit: interval_s: 1, max_count: 1
+ {
+ object: "hit",
+ extra: {
+ eventDate: "6000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "6000",
+ impressionDate: "6000",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ // hit: interval_s: 10, max_count: 5
+ {
+ object: "hit",
+ extra: {
+ eventDate: "6000",
+ intervalSeconds: "10",
+ maxCount: "5",
+ startDate: "0",
+ impressionDate: "6000",
+ count: "5",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ // 7s: no new impressions; 5 impressions total
+ 7: {
+ results: [[]],
+ telemetry: {
+ events: [
+ // reset: interval_s: 1, max_count: 1
+ {
+ object: "reset",
+ extra: {
+ eventDate: "7000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "6000",
+ impressionDate: "6000",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ // 8s: no new impressions; 5 impressions total
+ 8: {
+ results: [[]],
+ telemetry: {
+ events: [
+ // reset: interval_s: 1, max_count: 1
+ {
+ object: "reset",
+ extra: {
+ eventDate: "8000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "7000",
+ impressionDate: "6000",
+ count: "0",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ // 9s: no new impressions; 5 impressions total
+ 9: {
+ results: [[]],
+ telemetry: {
+ events: [
+ // reset: interval_s: 1, max_count: 1
+ {
+ object: "reset",
+ extra: {
+ eventDate: "9000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "8000",
+ impressionDate: "6000",
+ count: "0",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ // 10s: 1 new impression; 6 impressions total
+ 10: {
+ results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []],
+ telemetry: {
+ events: [
+ // reset: interval_s: 1, max_count: 1
+ {
+ object: "reset",
+ extra: {
+ eventDate: "10000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "9000",
+ impressionDate: "6000",
+ count: "0",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ // reset: interval_s: 5, max_count: 3
+ {
+ object: "reset",
+ extra: {
+ eventDate: "10000",
+ intervalSeconds: "5",
+ maxCount: "3",
+ startDate: "5000",
+ impressionDate: "6000",
+ count: "2",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ // reset: interval_s: 10, max_count: 5
+ {
+ object: "reset",
+ extra: {
+ eventDate: "10000",
+ intervalSeconds: "10",
+ maxCount: "5",
+ startDate: "0",
+ impressionDate: "6000",
+ count: "5",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ // hit: interval_s: 1, max_count: 1
+ {
+ object: "hit",
+ extra: {
+ eventDate: "10000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "10000",
+ impressionDate: "10000",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ // 11s: 1 new impression; 7 impressions total
+ 11: {
+ results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []],
+ telemetry: {
+ events: [
+ // reset: interval_s: 1, max_count: 1
+ {
+ object: "reset",
+ extra: {
+ eventDate: "11000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "10000",
+ impressionDate: "10000",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ // hit: interval_s: 1, max_count: 1
+ {
+ object: "hit",
+ extra: {
+ eventDate: "11000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "11000",
+ impressionDate: "11000",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ // 12s: 1 new impression; 8 impressions total
+ 12: {
+ results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []],
+ telemetry: {
+ events: [
+ // reset: interval_s: 1, max_count: 1
+ {
+ object: "reset",
+ extra: {
+ eventDate: "12000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "11000",
+ impressionDate: "11000",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ // hit: interval_s: 1, max_count: 1
+ {
+ object: "hit",
+ extra: {
+ eventDate: "12000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "12000",
+ impressionDate: "12000",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ // hit: interval_s: 5, max_count: 3
+ {
+ object: "hit",
+ extra: {
+ eventDate: "12000",
+ intervalSeconds: "5",
+ maxCount: "3",
+ startDate: "10000",
+ impressionDate: "12000",
+ count: "3",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ // 13s: no new impressions; 8 impressions total
+ 13: {
+ results: [[]],
+ telemetry: {
+ events: [
+ // reset: interval_s: 1, max_count: 1
+ {
+ object: "reset",
+ extra: {
+ eventDate: "13000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "12000",
+ impressionDate: "12000",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ // 14s: no new impressions; 8 impressions total
+ 14: {
+ results: [[]],
+ telemetry: {
+ events: [
+ // reset: interval_s: 1, max_count: 1
+ {
+ object: "reset",
+ extra: {
+ eventDate: "14000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "13000",
+ impressionDate: "12000",
+ count: "0",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ // 15s: 1 new impression; 9 impressions total
+ 15: {
+ results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []],
+ telemetry: {
+ events: [
+ // reset: interval_s: 1, max_count: 1
+ {
+ object: "reset",
+ extra: {
+ eventDate: "15000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "14000",
+ impressionDate: "12000",
+ count: "0",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ // reset: interval_s: 5, max_count: 3
+ {
+ object: "reset",
+ extra: {
+ eventDate: "15000",
+ intervalSeconds: "5",
+ maxCount: "3",
+ startDate: "10000",
+ impressionDate: "12000",
+ count: "3",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ // hit: interval_s: 1, max_count: 1
+ {
+ object: "hit",
+ extra: {
+ eventDate: "15000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "15000",
+ impressionDate: "15000",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ // 16s: 1 new impression; 10 impressions total
+ 16: {
+ results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []],
+ telemetry: {
+ events: [
+ // reset: interval_s: 1, max_count: 1
+ {
+ object: "reset",
+ extra: {
+ eventDate: "16000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "15000",
+ impressionDate: "15000",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ // hit: interval_s: 1, max_count: 1
+ {
+ object: "hit",
+ extra: {
+ eventDate: "16000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "16000",
+ impressionDate: "16000",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ // hit: interval_s: 10, max_count: 5
+ {
+ object: "hit",
+ extra: {
+ eventDate: "16000",
+ intervalSeconds: "10",
+ maxCount: "5",
+ startDate: "10000",
+ impressionDate: "16000",
+ count: "5",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ // 17s: no new impressions; 10 impressions total
+ 17: {
+ results: [[]],
+ telemetry: {
+ events: [
+ // reset: interval_s: 1, max_count: 1
+ {
+ object: "reset",
+ extra: {
+ eventDate: "17000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "16000",
+ impressionDate: "16000",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ // 18s: no new impressions; 10 impressions total
+ 18: {
+ results: [[]],
+ telemetry: {
+ events: [
+ // reset: interval_s: 1, max_count: 1
+ {
+ object: "reset",
+ extra: {
+ eventDate: "18000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "17000",
+ impressionDate: "16000",
+ count: "0",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ // 19s: no new impressions; 10 impressions total
+ 19: {
+ results: [[]],
+ telemetry: {
+ events: [
+ // reset: interval_s: 1, max_count: 1
+ {
+ object: "reset",
+ extra: {
+ eventDate: "19000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "18000",
+ impressionDate: "16000",
+ count: "0",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ // 20s: 1 new impression; 11 impressions total
+ 20: {
+ results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []],
+ telemetry: {
+ events: [
+ // reset: interval_s: 1, max_count: 1
+ {
+ object: "reset",
+ extra: {
+ eventDate: "20000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "19000",
+ impressionDate: "16000",
+ count: "0",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ // reset: interval_s: 5, max_count: 3
+ {
+ object: "reset",
+ extra: {
+ eventDate: "20000",
+ intervalSeconds: "5",
+ maxCount: "3",
+ startDate: "15000",
+ impressionDate: "16000",
+ count: "2",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ // reset: interval_s: 10, max_count: 5
+ {
+ object: "reset",
+ extra: {
+ eventDate: "20000",
+ intervalSeconds: "10",
+ maxCount: "5",
+ startDate: "10000",
+ impressionDate: "16000",
+ count: "5",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ // hit: interval_s: 1, max_count: 1
+ {
+ object: "hit",
+ extra: {
+ eventDate: "20000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "20000",
+ impressionDate: "20000",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ });
+ },
+ });
+});
+
+// Tests a lifetime cap.
+add_task(async function lifetime() {
+ await doTest({
+ config: {
+ impression_caps: {
+ sponsored: {
+ lifetime: 3,
+ },
+ },
+ },
+ callback: async () => {
+ await doTimedSearches("sponsored", {
+ 0: {
+ results: [
+ [EXPECTED_SPONSORED_URLBAR_RESULT],
+ [EXPECTED_SPONSORED_URLBAR_RESULT],
+ [EXPECTED_SPONSORED_URLBAR_RESULT],
+ [],
+ ],
+ telemetry: {
+ events: [
+ {
+ object: "hit",
+ extra: {
+ eventDate: "0",
+ intervalSeconds: "Infinity",
+ maxCount: "3",
+ startDate: "0",
+ impressionDate: "0",
+ count: "3",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ 1: {
+ results: [[]],
+ },
+ });
+ },
+ });
+});
+
+// Tests one interval and a lifetime cap together.
+add_task(async function intervalAndLifetime() {
+ await doTest({
+ config: {
+ impression_caps: {
+ sponsored: {
+ lifetime: 3,
+ custom: [{ interval_s: 1, max_count: 1 }],
+ },
+ },
+ },
+ callback: async () => {
+ await doTimedSearches("sponsored", {
+ // 0s: 1 new impression; 1 impression total
+ 0: {
+ results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []],
+ telemetry: {
+ events: [
+ // hit: interval_s: 1, max_count: 1
+ {
+ object: "hit",
+ extra: {
+ eventDate: "0",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "0",
+ impressionDate: "0",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ // 1s: 1 new impression; 2 impressions total
+ 1: {
+ results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []],
+ telemetry: {
+ events: [
+ // reset: interval_s: 1, max_count: 1
+ {
+ object: "reset",
+ extra: {
+ eventDate: "1000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "0",
+ impressionDate: "0",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ // hit: interval_s: 1, max_count: 1
+ {
+ object: "hit",
+ extra: {
+ eventDate: "1000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "1000",
+ impressionDate: "1000",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ // 2s: 1 new impression; 3 impressions total
+ 2: {
+ results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []],
+ telemetry: {
+ events: [
+ // reset: interval_s: 1, max_count: 1
+ {
+ object: "reset",
+ extra: {
+ eventDate: "2000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "1000",
+ impressionDate: "1000",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ // hit: interval_s: 1, max_count: 1
+ {
+ object: "hit",
+ extra: {
+ eventDate: "2000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "2000",
+ impressionDate: "2000",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ // hit: interval_s: Infinity, max_count: 3
+ {
+ object: "hit",
+ extra: {
+ eventDate: "2000",
+ intervalSeconds: "Infinity",
+ maxCount: "3",
+ startDate: "0",
+ impressionDate: "2000",
+ count: "3",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ 3: {
+ results: [[]],
+ telemetry: {
+ events: [
+ // reset: interval_s: 1, max_count: 1
+ {
+ object: "reset",
+ extra: {
+ eventDate: "3000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "2000",
+ impressionDate: "2000",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ });
+ },
+ });
+});
+
+// Tests multiple intervals and a lifetime cap together.
+add_task(async function multipleIntervalsAndLifetime() {
+ await doTest({
+ config: {
+ impression_caps: {
+ sponsored: {
+ lifetime: 4,
+ custom: [
+ { interval_s: 1, max_count: 1 },
+ { interval_s: 5, max_count: 3 },
+ ],
+ },
+ },
+ },
+ callback: async () => {
+ await doTimedSearches("sponsored", {
+ // 0s: 1 new impression; 1 impression total
+ 0: {
+ results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []],
+ telemetry: {
+ events: [
+ // hit: interval_s: 1, max_count: 1
+ {
+ object: "hit",
+ extra: {
+ eventDate: "0",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "0",
+ impressionDate: "0",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ // 1s: 1 new impression; 2 impressions total
+ 1: {
+ results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []],
+ telemetry: {
+ events: [
+ // reset: interval_s: 1, max_count: 1
+ {
+ object: "reset",
+ extra: {
+ eventDate: "1000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "0",
+ impressionDate: "0",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ // hit: interval_s: 1, max_count: 1
+ {
+ object: "hit",
+ extra: {
+ eventDate: "1000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "1000",
+ impressionDate: "1000",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ // 2s: 1 new impression; 3 impressions total
+ 2: {
+ results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []],
+ telemetry: {
+ events: [
+ // reset: interval_s: 1, max_count: 1
+ {
+ object: "reset",
+ extra: {
+ eventDate: "2000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "1000",
+ impressionDate: "1000",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ // hit: interval_s: 1, max_count: 1
+ {
+ object: "hit",
+ extra: {
+ eventDate: "2000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "2000",
+ impressionDate: "2000",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ // hit: interval_s: 5, max_count: 3
+ {
+ object: "hit",
+ extra: {
+ eventDate: "2000",
+ intervalSeconds: "5",
+ maxCount: "3",
+ startDate: "0",
+ impressionDate: "2000",
+ count: "3",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ // 3s: no new impressions; 3 impressions total
+ 3: {
+ results: [[]],
+ telemetry: {
+ events: [
+ // reset: interval_s: 1, max_count: 1
+ {
+ object: "reset",
+ extra: {
+ eventDate: "3000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "2000",
+ impressionDate: "2000",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ // 4s: no new impressions; 3 impressions total
+ 4: {
+ results: [[]],
+ telemetry: {
+ events: [
+ // reset: interval_s: 1, max_count: 1
+ {
+ object: "reset",
+ extra: {
+ eventDate: "4000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "3000",
+ impressionDate: "2000",
+ count: "0",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ // 5s: 1 new impression; 4 impressions total
+ 5: {
+ results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []],
+ telemetry: {
+ events: [
+ // reset: interval_s: 1, max_count: 1
+ {
+ object: "reset",
+ extra: {
+ eventDate: "5000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "4000",
+ impressionDate: "2000",
+ count: "0",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ // reset: interval_s: 5, max_count: 3
+ {
+ object: "reset",
+ extra: {
+ eventDate: "5000",
+ intervalSeconds: "5",
+ maxCount: "3",
+ startDate: "0",
+ impressionDate: "2000",
+ count: "3",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ // hit: interval_s: 1, max_count: 1
+ {
+ object: "hit",
+ extra: {
+ eventDate: "5000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "5000",
+ impressionDate: "5000",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ // hit: interval_s: Infinity, max_count: 4
+ {
+ object: "hit",
+ extra: {
+ eventDate: "5000",
+ intervalSeconds: "Infinity",
+ maxCount: "4",
+ startDate: "0",
+ impressionDate: "5000",
+ count: "4",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ // 6s: no new impressions; 4 impressions total
+ 6: {
+ results: [[]],
+ telemetry: {
+ events: [
+ // reset: interval_s: 1, max_count: 1
+ {
+ object: "reset",
+ extra: {
+ eventDate: "6000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "5000",
+ impressionDate: "5000",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ // 7s: no new impressions; 4 impressions total
+ 7: {
+ results: [[]],
+ telemetry: {
+ events: [
+ // reset: interval_s: 1, max_count: 1
+ {
+ object: "reset",
+ extra: {
+ eventDate: "7000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "6000",
+ impressionDate: "5000",
+ count: "0",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ });
+ },
+ });
+});
+
+// Smoke test for non-sponsored caps. Most tasks use sponsored results and caps,
+// but sponsored and non-sponsored should behave the same since they use the
+// same code paths.
+add_task(async function nonsponsored() {
+ await doTest({
+ config: {
+ impression_caps: {
+ nonsponsored: {
+ lifetime: 4,
+ custom: [
+ { interval_s: 1, max_count: 1 },
+ { interval_s: 5, max_count: 3 },
+ ],
+ },
+ },
+ },
+ callback: async () => {
+ await doTimedSearches("nonsponsored", {
+ // 0s: 1 new impression; 1 impression total
+ 0: {
+ results: [[EXPECTED_NONSPONSORED_URLBAR_RESULT], []],
+ telemetry: {
+ events: [
+ // hit: interval_s: 1, max_count: 1
+ {
+ object: "hit",
+ extra: {
+ eventDate: "0",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "0",
+ impressionDate: "0",
+ count: "1",
+ type: "nonsponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ // 1s: 1 new impression; 2 impressions total
+ 1: {
+ results: [[EXPECTED_NONSPONSORED_URLBAR_RESULT], []],
+ telemetry: {
+ events: [
+ // reset: interval_s: 1, max_count: 1
+ {
+ object: "reset",
+ extra: {
+ eventDate: "1000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "0",
+ impressionDate: "0",
+ count: "1",
+ type: "nonsponsored",
+ eventCount: "1",
+ },
+ },
+ // hit: interval_s: 1, max_count: 1
+ {
+ object: "hit",
+ extra: {
+ eventDate: "1000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "1000",
+ impressionDate: "1000",
+ count: "1",
+ type: "nonsponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ // 2s: 1 new impression; 3 impressions total
+ 2: {
+ results: [[EXPECTED_NONSPONSORED_URLBAR_RESULT], []],
+ telemetry: {
+ events: [
+ // reset: interval_s: 1, max_count: 1
+ {
+ object: "reset",
+ extra: {
+ eventDate: "2000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "1000",
+ impressionDate: "1000",
+ count: "1",
+ type: "nonsponsored",
+ eventCount: "1",
+ },
+ },
+ // hit: interval_s: 1, max_count: 1
+ {
+ object: "hit",
+ extra: {
+ eventDate: "2000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "2000",
+ impressionDate: "2000",
+ count: "1",
+ type: "nonsponsored",
+ eventCount: "1",
+ },
+ },
+ // hit: interval_s: 5, max_count: 3
+ {
+ object: "hit",
+ extra: {
+ eventDate: "2000",
+ intervalSeconds: "5",
+ maxCount: "3",
+ startDate: "0",
+ impressionDate: "2000",
+ count: "3",
+ type: "nonsponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ // 3s: no new impressions; 3 impressions total
+ 3: {
+ results: [[]],
+ telemetry: {
+ events: [
+ // reset: interval_s: 1, max_count: 1
+ {
+ object: "reset",
+ extra: {
+ eventDate: "3000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "2000",
+ impressionDate: "2000",
+ count: "1",
+ type: "nonsponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ // 4s: no new impressions; 3 impressions total
+ 4: {
+ results: [[]],
+ telemetry: {
+ events: [
+ // reset: interval_s: 1, max_count: 1
+ {
+ object: "reset",
+ extra: {
+ eventDate: "4000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "3000",
+ impressionDate: "2000",
+ count: "0",
+ type: "nonsponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ // 5s: 1 new impression; 4 impressions total
+ 5: {
+ results: [[EXPECTED_NONSPONSORED_URLBAR_RESULT], []],
+ telemetry: {
+ events: [
+ // reset: interval_s: 1, max_count: 1
+ {
+ object: "reset",
+ extra: {
+ eventDate: "5000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "4000",
+ impressionDate: "2000",
+ count: "0",
+ type: "nonsponsored",
+ eventCount: "1",
+ },
+ },
+ // reset: interval_s: 5, max_count: 3
+ {
+ object: "reset",
+ extra: {
+ eventDate: "5000",
+ intervalSeconds: "5",
+ maxCount: "3",
+ startDate: "0",
+ impressionDate: "2000",
+ count: "3",
+ type: "nonsponsored",
+ eventCount: "1",
+ },
+ },
+ // hit: interval_s: 1, max_count: 1
+ {
+ object: "hit",
+ extra: {
+ eventDate: "5000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "5000",
+ impressionDate: "5000",
+ count: "1",
+ type: "nonsponsored",
+ eventCount: "1",
+ },
+ },
+ // hit: interval_s: Infinity, max_count: 4
+ {
+ object: "hit",
+ extra: {
+ eventDate: "5000",
+ intervalSeconds: "Infinity",
+ maxCount: "4",
+ startDate: "0",
+ impressionDate: "5000",
+ count: "4",
+ type: "nonsponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ // 6s: no new impressions; 4 impressions total
+ 6: {
+ results: [[]],
+ telemetry: {
+ events: [
+ // reset: interval_s: 1, max_count: 1
+ {
+ object: "reset",
+ extra: {
+ eventDate: "6000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "5000",
+ impressionDate: "5000",
+ count: "1",
+ type: "nonsponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ // 7s: no new impressions; 4 impressions total
+ 7: {
+ results: [[]],
+ telemetry: {
+ events: [
+ // reset: interval_s: 1, max_count: 1
+ {
+ object: "reset",
+ extra: {
+ eventDate: "7000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "6000",
+ impressionDate: "5000",
+ count: "0",
+ type: "nonsponsored",
+ eventCount: "1",
+ },
+ },
+ ],
+ },
+ },
+ });
+ },
+ });
+});
+
+// Smoke test for sponsored and non-sponsored caps together. Most tasks use only
+// sponsored results and caps, but sponsored and non-sponsored should behave the
+// same since they use the same code paths.
+add_task(async function sponsoredAndNonsponsored() {
+ await doTest({
+ config: {
+ impression_caps: {
+ sponsored: {
+ lifetime: 2,
+ },
+ nonsponsored: {
+ lifetime: 3,
+ },
+ },
+ },
+ callback: async () => {
+ // 1st searches
+ await checkSearch({
+ name: "sponsored 1",
+ searchString: "sponsored",
+ expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT],
+ });
+ await checkSearch({
+ name: "nonsponsored 1",
+ searchString: "nonsponsored",
+ expectedResults: [EXPECTED_NONSPONSORED_URLBAR_RESULT],
+ });
+ await checkTelemetryEvents([]);
+
+ // 2nd searches
+ await checkSearch({
+ name: "sponsored 2",
+ searchString: "sponsored",
+ expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT],
+ });
+ await checkSearch({
+ name: "nonsponsored 2",
+ searchString: "nonsponsored",
+ expectedResults: [EXPECTED_NONSPONSORED_URLBAR_RESULT],
+ });
+ await checkTelemetryEvents([
+ {
+ object: "hit",
+ extra: {
+ eventDate: "0",
+ intervalSeconds: "Infinity",
+ maxCount: "2",
+ startDate: "0",
+ impressionDate: "0",
+ count: "2",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ]);
+
+ // 3rd searches
+ await checkSearch({
+ name: "sponsored 3",
+ searchString: "sponsored",
+ expectedResults: [],
+ });
+ await checkSearch({
+ name: "nonsponsored 3",
+ searchString: "nonsponsored",
+ expectedResults: [EXPECTED_NONSPONSORED_URLBAR_RESULT],
+ });
+ await checkTelemetryEvents([
+ {
+ object: "hit",
+ extra: {
+ eventDate: "0",
+ intervalSeconds: "Infinity",
+ maxCount: "3",
+ startDate: "0",
+ impressionDate: "0",
+ count: "3",
+ type: "nonsponsored",
+ eventCount: "1",
+ },
+ },
+ ]);
+
+ // 4th searches
+ await checkSearch({
+ name: "sponsored 4",
+ searchString: "sponsored",
+ expectedResults: [],
+ });
+ await checkSearch({
+ name: "nonsponsored 4",
+ searchString: "nonsponsored",
+ expectedResults: [],
+ });
+ await checkTelemetryEvents([]);
+ },
+ });
+});
+
+// Tests with an empty config to make sure results are not capped.
+add_task(async function emptyConfig() {
+ await doTest({
+ config: {},
+ callback: async () => {
+ for (let i = 0; i < 2; i++) {
+ await checkSearch({
+ name: "sponsored " + i,
+ searchString: "sponsored",
+ expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT],
+ });
+ await checkSearch({
+ name: "nonsponsored " + i,
+ searchString: "nonsponsored",
+ expectedResults: [EXPECTED_NONSPONSORED_URLBAR_RESULT],
+ });
+ }
+ await checkTelemetryEvents([]);
+ },
+ });
+});
+
+// Tests with sponsored caps disabled. Non-sponsored should still be capped.
+add_task(async function sponsoredCapsDisabled() {
+ UrlbarPrefs.set("quicksuggest.impressionCaps.sponsoredEnabled", false);
+ await doTest({
+ config: {
+ impression_caps: {
+ sponsored: {
+ lifetime: 0,
+ },
+ nonsponsored: {
+ lifetime: 3,
+ },
+ },
+ },
+ callback: async () => {
+ for (let i = 0; i < 3; i++) {
+ await checkSearch({
+ name: "sponsored " + i,
+ searchString: "sponsored",
+ expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT],
+ });
+ await checkSearch({
+ name: "nonsponsored " + i,
+ searchString: "nonsponsored",
+ expectedResults: [EXPECTED_NONSPONSORED_URLBAR_RESULT],
+ });
+ }
+ await checkTelemetryEvents([
+ {
+ object: "hit",
+ extra: {
+ eventDate: "0",
+ intervalSeconds: "Infinity",
+ maxCount: "3",
+ startDate: "0",
+ impressionDate: "0",
+ count: "3",
+ type: "nonsponsored",
+ eventCount: "1",
+ },
+ },
+ ]);
+
+ await checkSearch({
+ name: "sponsored additional",
+ searchString: "sponsored",
+ expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT],
+ });
+ await checkSearch({
+ name: "nonsponsored additional",
+ searchString: "nonsponsored",
+ expectedResults: [],
+ });
+ await checkTelemetryEvents([]);
+ },
+ });
+ UrlbarPrefs.set("quicksuggest.impressionCaps.sponsoredEnabled", true);
+});
+
+// Tests with non-sponsored caps disabled. Sponsored should still be capped.
+add_task(async function nonsponsoredCapsDisabled() {
+ UrlbarPrefs.set("quicksuggest.impressionCaps.nonSponsoredEnabled", false);
+ await doTest({
+ config: {
+ impression_caps: {
+ sponsored: {
+ lifetime: 3,
+ },
+ nonsponsored: {
+ lifetime: 0,
+ },
+ },
+ },
+ callback: async () => {
+ for (let i = 0; i < 3; i++) {
+ await checkSearch({
+ name: "sponsored " + i,
+ searchString: "sponsored",
+ expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT],
+ });
+ await checkSearch({
+ name: "nonsponsored " + i,
+ searchString: "nonsponsored",
+ expectedResults: [EXPECTED_NONSPONSORED_URLBAR_RESULT],
+ });
+ }
+ await checkTelemetryEvents([
+ {
+ object: "hit",
+ extra: {
+ eventDate: "0",
+ intervalSeconds: "Infinity",
+ maxCount: "3",
+ startDate: "0",
+ impressionDate: "0",
+ count: "3",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ]);
+
+ await checkSearch({
+ name: "sponsored additional",
+ searchString: "sponsored",
+ expectedResults: [],
+ });
+ await checkSearch({
+ name: "nonsponsored additional",
+ searchString: "nonsponsored",
+ expectedResults: [EXPECTED_NONSPONSORED_URLBAR_RESULT],
+ });
+ await checkTelemetryEvents([]);
+ },
+ });
+ UrlbarPrefs.set("quicksuggest.impressionCaps.nonSponsoredEnabled", true);
+});
+
+// Tests a config change: 1 interval -> same interval with lower cap, with the
+// old cap already reached
+add_task(async function configChange_sameIntervalLowerCap_1() {
+ await doTest({
+ config: {
+ impression_caps: {
+ sponsored: {
+ custom: [{ interval_s: 3, max_count: 3 }],
+ },
+ },
+ },
+ callback: async () => {
+ await doTimedCallbacks({
+ 0: async () => {
+ for (let i = 0; i < 3; i++) {
+ await checkSearch({
+ name: "0s " + i,
+ searchString: "sponsored",
+ expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT],
+ });
+ }
+ await checkSearch({
+ name: "0s additional",
+ searchString: "sponsored",
+ expectedResults: [],
+ });
+ await checkTelemetryEvents([
+ {
+ object: "hit",
+ extra: {
+ eventDate: "0",
+ intervalSeconds: "3",
+ maxCount: "3",
+ startDate: "0",
+ impressionDate: "0",
+ count: "3",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ]);
+ await QuickSuggestTestUtils.setConfig({
+ impression_caps: {
+ sponsored: {
+ custom: [{ interval_s: 3, max_count: 1 }],
+ },
+ },
+ });
+ },
+ 1: async () => {
+ await checkSearch({
+ name: "1s",
+ searchString: "sponsored",
+ expectedResults: [],
+ });
+ await checkTelemetryEvents([]);
+ },
+ 3: async () => {
+ await checkSearch({
+ name: "3s 0",
+ searchString: "sponsored",
+ expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT],
+ });
+ await checkSearch({
+ name: "3s additional",
+ searchString: "sponsored",
+ expectedResults: [],
+ });
+ await checkTelemetryEvents([
+ {
+ object: "reset",
+ extra: {
+ eventDate: "3000",
+ intervalSeconds: "3",
+ maxCount: "1",
+ startDate: "0",
+ impressionDate: "0",
+ count: "3",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ {
+ object: "hit",
+ extra: {
+ eventDate: "3000",
+ intervalSeconds: "3",
+ maxCount: "1",
+ startDate: "3000",
+ impressionDate: "3000",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ]);
+ },
+ });
+ },
+ });
+});
+
+// Tests a config change: 1 interval -> same interval with lower cap, with the
+// old cap not reached
+add_task(async function configChange_sameIntervalLowerCap_2() {
+ await doTest({
+ config: {
+ impression_caps: {
+ sponsored: {
+ custom: [{ interval_s: 3, max_count: 3 }],
+ },
+ },
+ },
+ callback: async () => {
+ await doTimedCallbacks({
+ 0: async () => {
+ for (let i = 0; i < 2; i++) {
+ await checkSearch({
+ name: "0s " + i,
+ searchString: "sponsored",
+ expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT],
+ });
+ }
+ await checkTelemetryEvents([]);
+ await QuickSuggestTestUtils.setConfig({
+ impression_caps: {
+ sponsored: {
+ custom: [{ interval_s: 3, max_count: 1 }],
+ },
+ },
+ });
+ },
+ 1: async () => {
+ await checkSearch({
+ name: "1s",
+ searchString: "sponsored",
+ expectedResults: [],
+ });
+ },
+ 3: async () => {
+ await checkSearch({
+ name: "3s 0",
+ searchString: "sponsored",
+ expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT],
+ });
+ await checkSearch({
+ name: "3s additional",
+ searchString: "sponsored",
+ expectedResults: [],
+ });
+ await checkTelemetryEvents([
+ {
+ object: "reset",
+ extra: {
+ eventDate: "3000",
+ intervalSeconds: "3",
+ maxCount: "1",
+ startDate: "0",
+ impressionDate: "0",
+ count: "2",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ {
+ object: "hit",
+ extra: {
+ eventDate: "3000",
+ intervalSeconds: "3",
+ maxCount: "1",
+ startDate: "3000",
+ impressionDate: "3000",
+ count: "1",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ]);
+ },
+ });
+ },
+ });
+});
+
+// Tests a config change: 1 interval -> same interval with higher cap
+add_task(async function configChange_sameIntervalHigherCap() {
+ await doTest({
+ config: {
+ impression_caps: {
+ sponsored: {
+ custom: [{ interval_s: 3, max_count: 3 }],
+ },
+ },
+ },
+ callback: async () => {
+ await doTimedCallbacks({
+ 0: async () => {
+ for (let i = 0; i < 3; i++) {
+ await checkSearch({
+ name: "0s " + i,
+ searchString: "sponsored",
+ expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT],
+ });
+ }
+ await checkSearch({
+ name: "0s additional",
+ searchString: "sponsored",
+ expectedResults: [],
+ });
+ await checkTelemetryEvents([
+ {
+ object: "hit",
+ extra: {
+ eventDate: "0",
+ intervalSeconds: "3",
+ maxCount: "3",
+ startDate: "0",
+ impressionDate: "0",
+ count: "3",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ]);
+ await QuickSuggestTestUtils.setConfig({
+ impression_caps: {
+ sponsored: {
+ custom: [{ interval_s: 3, max_count: 5 }],
+ },
+ },
+ });
+ },
+ 1: async () => {
+ for (let i = 0; i < 2; i++) {
+ await checkSearch({
+ name: "1s " + i,
+ searchString: "sponsored",
+ expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT],
+ });
+ }
+ await checkSearch({
+ name: "1s additional",
+ searchString: "sponsored",
+ expectedResults: [],
+ });
+ await checkTelemetryEvents([
+ {
+ object: "hit",
+ extra: {
+ eventDate: "1000",
+ intervalSeconds: "3",
+ maxCount: "5",
+ startDate: "0",
+ impressionDate: "1000",
+ count: "5",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ]);
+ },
+ 3: async () => {
+ for (let i = 0; i < 5; i++) {
+ await checkSearch({
+ name: "3s " + i,
+ searchString: "sponsored",
+ expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT],
+ });
+ }
+ await checkSearch({
+ name: "3s additional",
+ searchString: "sponsored",
+ expectedResults: [],
+ });
+ await checkTelemetryEvents([
+ {
+ object: "reset",
+ extra: {
+ eventDate: "3000",
+ intervalSeconds: "3",
+ maxCount: "5",
+ startDate: "0",
+ impressionDate: "1000",
+ count: "5",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ {
+ object: "hit",
+ extra: {
+ eventDate: "3000",
+ intervalSeconds: "3",
+ maxCount: "5",
+ startDate: "3000",
+ impressionDate: "3000",
+ count: "5",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ]);
+ },
+ });
+ },
+ });
+});
+
+// Tests a config change: 1 interval -> 2 new intervals with higher timeouts.
+// Impression counts for the old interval should contribute to the new
+// intervals.
+add_task(async function configChange_1IntervalTo2NewIntervalsHigher() {
+ await doTest({
+ config: {
+ impression_caps: {
+ sponsored: {
+ custom: [{ interval_s: 3, max_count: 3 }],
+ },
+ },
+ },
+ callback: async () => {
+ await doTimedCallbacks({
+ 0: async () => {
+ for (let i = 0; i < 3; i++) {
+ await checkSearch({
+ name: "0s " + i,
+ searchString: "sponsored",
+ expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT],
+ });
+ }
+ await checkTelemetryEvents([
+ {
+ object: "hit",
+ extra: {
+ eventDate: "0",
+ intervalSeconds: "3",
+ maxCount: "3",
+ startDate: "0",
+ impressionDate: "0",
+ count: "3",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ]);
+ await QuickSuggestTestUtils.setConfig({
+ impression_caps: {
+ sponsored: {
+ custom: [
+ { interval_s: 5, max_count: 3 },
+ { interval_s: 10, max_count: 5 },
+ ],
+ },
+ },
+ });
+ },
+ 3: async () => {
+ await checkSearch({
+ name: "3s",
+ searchString: "sponsored",
+ expectedResults: [],
+ });
+ await checkTelemetryEvents([]);
+ },
+ 4: async () => {
+ await checkSearch({
+ name: "4s",
+ searchString: "sponsored",
+ expectedResults: [],
+ });
+ await checkTelemetryEvents([]);
+ },
+ 5: async () => {
+ for (let i = 0; i < 2; i++) {
+ await checkSearch({
+ name: "5s " + i,
+ searchString: "sponsored",
+ expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT],
+ });
+ }
+ await checkSearch({
+ name: "5s additional",
+ searchString: "sponsored",
+ expectedResults: [],
+ });
+ await checkTelemetryEvents([
+ {
+ object: "reset",
+ extra: {
+ eventDate: "5000",
+ intervalSeconds: "5",
+ maxCount: "3",
+ startDate: "0",
+ impressionDate: "0",
+ count: "3",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ {
+ object: "hit",
+ extra: {
+ eventDate: "5000",
+ intervalSeconds: "10",
+ maxCount: "5",
+ startDate: "0",
+ impressionDate: "5000",
+ count: "5",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ]);
+ },
+ });
+ },
+ });
+});
+
+// Tests a config change: 2 intervals -> 1 new interval with higher timeout.
+// Impression counts for the old intervals should contribute to the new
+// interval.
+add_task(async function configChange_2IntervalsTo1NewIntervalHigher() {
+ await doTest({
+ config: {
+ impression_caps: {
+ sponsored: {
+ custom: [
+ { interval_s: 2, max_count: 2 },
+ { interval_s: 4, max_count: 4 },
+ ],
+ },
+ },
+ },
+ callback: async () => {
+ await doTimedCallbacks({
+ 0: async () => {
+ for (let i = 0; i < 2; i++) {
+ await checkSearch({
+ name: "0s " + i,
+ searchString: "sponsored",
+ expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT],
+ });
+ }
+ await checkTelemetryEvents([
+ {
+ object: "hit",
+ extra: {
+ eventDate: "0",
+ intervalSeconds: "2",
+ maxCount: "2",
+ startDate: "0",
+ impressionDate: "0",
+ count: "2",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ]);
+ },
+ 2: async () => {
+ for (let i = 0; i < 2; i++) {
+ await checkSearch({
+ name: "2s " + i,
+ searchString: "sponsored",
+ expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT],
+ });
+ }
+ await checkTelemetryEvents([
+ {
+ object: "reset",
+ extra: {
+ eventDate: "2000",
+ intervalSeconds: "2",
+ maxCount: "2",
+ startDate: "0",
+ impressionDate: "0",
+ count: "2",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ {
+ object: "hit",
+ extra: {
+ eventDate: "2000",
+ intervalSeconds: "2",
+ maxCount: "2",
+ startDate: "2000",
+ impressionDate: "2000",
+ count: "2",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ {
+ object: "hit",
+ extra: {
+ eventDate: "2000",
+ intervalSeconds: "4",
+ maxCount: "4",
+ startDate: "0",
+ impressionDate: "2000",
+ count: "4",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ]);
+ await QuickSuggestTestUtils.setConfig({
+ impression_caps: {
+ sponsored: {
+ custom: [{ interval_s: 6, max_count: 5 }],
+ },
+ },
+ });
+ },
+ 4: async () => {
+ await checkSearch({
+ name: "4s 0",
+ searchString: "sponsored",
+ expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT],
+ });
+ await checkSearch({
+ name: "4s 1",
+ searchString: "sponsored",
+ expectedResults: [],
+ });
+ await checkTelemetryEvents([
+ {
+ object: "hit",
+ extra: {
+ eventDate: "4000",
+ intervalSeconds: "6",
+ maxCount: "5",
+ startDate: "0",
+ impressionDate: "4000",
+ count: "5",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ]);
+ },
+ 5: async () => {
+ await checkSearch({
+ name: "5s",
+ searchString: "sponsored",
+ expectedResults: [],
+ });
+ await checkTelemetryEvents([]);
+ },
+ 6: async () => {
+ for (let i = 0; i < 5; i++) {
+ await checkSearch({
+ name: "6s " + i,
+ searchString: "sponsored",
+ expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT],
+ });
+ }
+ await checkSearch({
+ name: "6s additional",
+ searchString: "sponsored",
+ expectedResults: [],
+ });
+ await checkTelemetryEvents([
+ {
+ object: "reset",
+ extra: {
+ eventDate: "6000",
+ intervalSeconds: "6",
+ maxCount: "5",
+ startDate: "0",
+ impressionDate: "4000",
+ count: "5",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ {
+ object: "hit",
+ extra: {
+ eventDate: "6000",
+ intervalSeconds: "6",
+ maxCount: "5",
+ startDate: "6000",
+ impressionDate: "6000",
+ count: "5",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ]);
+ },
+ });
+ },
+ });
+});
+
+// Tests a config change: 1 interval -> 1 new interval with lower timeout.
+// Impression counts for the old interval should not contribute to the new
+// interval since the new interval has a lower timeout.
+add_task(async function configChange_1IntervalTo1NewIntervalLower() {
+ await doTest({
+ config: {
+ impression_caps: {
+ sponsored: {
+ custom: [{ interval_s: 5, max_count: 3 }],
+ },
+ },
+ },
+ callback: async () => {
+ await doTimedCallbacks({
+ 0: async () => {
+ for (let i = 0; i < 3; i++) {
+ await checkSearch({
+ name: "0s " + i,
+ searchString: "sponsored",
+ expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT],
+ });
+ }
+ await checkTelemetryEvents([
+ {
+ object: "hit",
+ extra: {
+ eventDate: "0",
+ intervalSeconds: "5",
+ maxCount: "3",
+ startDate: "0",
+ impressionDate: "0",
+ count: "3",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ]);
+ await QuickSuggestTestUtils.setConfig({
+ impression_caps: {
+ sponsored: {
+ custom: [{ interval_s: 3, max_count: 3 }],
+ },
+ },
+ });
+ },
+ 1: async () => {
+ for (let i = 0; i < 3; i++) {
+ await checkSearch({
+ name: "3s " + i,
+ searchString: "sponsored",
+ expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT],
+ });
+ }
+ await checkSearch({
+ name: "3s additional",
+ searchString: "sponsored",
+ expectedResults: [],
+ });
+ await checkTelemetryEvents([
+ {
+ object: "hit",
+ extra: {
+ eventDate: "1000",
+ intervalSeconds: "3",
+ maxCount: "3",
+ startDate: "0",
+ impressionDate: "1000",
+ count: "3",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ]);
+ },
+ });
+ },
+ });
+});
+
+// Tests a config change: 1 interval -> lifetime.
+// Impression counts for the old interval should contribute to the new lifetime
+// cap.
+add_task(async function configChange_1IntervalToLifetime() {
+ await doTest({
+ config: {
+ impression_caps: {
+ sponsored: {
+ custom: [{ interval_s: 3, max_count: 3 }],
+ },
+ },
+ },
+ callback: async () => {
+ await doTimedCallbacks({
+ 0: async () => {
+ for (let i = 0; i < 3; i++) {
+ await checkSearch({
+ name: "0s " + i,
+ searchString: "sponsored",
+ expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT],
+ });
+ }
+ await checkTelemetryEvents([
+ {
+ object: "hit",
+ extra: {
+ eventDate: "0",
+ intervalSeconds: "3",
+ maxCount: "3",
+ startDate: "0",
+ impressionDate: "0",
+ count: "3",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ]);
+ await QuickSuggestTestUtils.setConfig({
+ impression_caps: {
+ sponsored: {
+ lifetime: 3,
+ },
+ },
+ });
+ },
+ 3: async () => {
+ await checkSearch({
+ name: "3s",
+ searchString: "sponsored",
+ expectedResults: [],
+ });
+ await checkTelemetryEvents([]);
+ },
+ });
+ },
+ });
+});
+
+// Tests a config change: lifetime cap -> higher lifetime cap
+add_task(async function configChange_lifetimeCapHigher() {
+ await doTest({
+ config: {
+ impression_caps: {
+ sponsored: {
+ lifetime: 3,
+ },
+ },
+ },
+ callback: async () => {
+ await doTimedCallbacks({
+ 0: async () => {
+ for (let i = 0; i < 3; i++) {
+ await checkSearch({
+ name: "0s " + i,
+ searchString: "sponsored",
+ expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT],
+ });
+ }
+ await checkSearch({
+ name: "0s additional",
+ searchString: "sponsored",
+ expectedResults: [],
+ });
+ await checkTelemetryEvents([
+ {
+ object: "hit",
+ extra: {
+ eventDate: "0",
+ intervalSeconds: "Infinity",
+ maxCount: "3",
+ startDate: "0",
+ impressionDate: "0",
+ count: "3",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ]);
+ await QuickSuggestTestUtils.setConfig({
+ impression_caps: {
+ sponsored: {
+ lifetime: 5,
+ },
+ },
+ });
+ },
+ 1: async () => {
+ for (let i = 0; i < 2; i++) {
+ await checkSearch({
+ name: "1s " + i,
+ searchString: "sponsored",
+ expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT],
+ });
+ }
+ await checkSearch({
+ name: "1s additional",
+ searchString: "sponsored",
+ expectedResults: [],
+ });
+ await checkTelemetryEvents([
+ {
+ object: "hit",
+ extra: {
+ eventDate: "1000",
+ intervalSeconds: "Infinity",
+ maxCount: "5",
+ startDate: "0",
+ impressionDate: "1000",
+ count: "5",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ]);
+ },
+ });
+ },
+ });
+});
+
+// Tests a config change: lifetime cap -> lower lifetime cap
+add_task(async function configChange_lifetimeCapLower() {
+ await doTest({
+ config: {
+ impression_caps: {
+ sponsored: {
+ lifetime: 3,
+ },
+ },
+ },
+ callback: async () => {
+ await doTimedCallbacks({
+ 0: async () => {
+ for (let i = 0; i < 3; i++) {
+ await checkSearch({
+ name: "0s " + i,
+ searchString: "sponsored",
+ expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT],
+ });
+ }
+ await checkSearch({
+ name: "0s additional",
+ searchString: "sponsored",
+ expectedResults: [],
+ });
+ await checkTelemetryEvents([
+ {
+ object: "hit",
+ extra: {
+ eventDate: "0",
+ intervalSeconds: "Infinity",
+ maxCount: "3",
+ startDate: "0",
+ impressionDate: "0",
+ count: "3",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ]);
+ await QuickSuggestTestUtils.setConfig({
+ impression_caps: {
+ sponsored: {
+ lifetime: 1,
+ },
+ },
+ });
+ },
+ 1: async () => {
+ await checkSearch({
+ name: "1s",
+ searchString: "sponsored",
+ expectedResults: [],
+ });
+ await checkTelemetryEvents([]);
+ },
+ });
+ },
+ });
+});
+
+// Makes sure stats are serialized to and from the pref correctly.
+add_task(async function prefSync() {
+ await doTest({
+ config: {
+ impression_caps: {
+ sponsored: {
+ lifetime: 5,
+ custom: [
+ { interval_s: 3, max_count: 2 },
+ { interval_s: 5, max_count: 4 },
+ ],
+ },
+ },
+ },
+ callback: async () => {
+ for (let i = 0; i < 2; i++) {
+ await checkSearch({
+ name: i,
+ searchString: "sponsored",
+ expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT],
+ });
+ }
+
+ let json = UrlbarPrefs.get("quicksuggest.impressionCaps.stats");
+ Assert.ok(json, "JSON is non-empty");
+ Assert.deepEqual(
+ JSON.parse(json),
+ {
+ sponsored: [
+ {
+ intervalSeconds: 3,
+ count: 2,
+ maxCount: 2,
+ startDateMs: 0,
+ impressionDateMs: 0,
+ },
+ {
+ intervalSeconds: 5,
+ count: 2,
+ maxCount: 4,
+ startDateMs: 0,
+ impressionDateMs: 0,
+ },
+ {
+ intervalSeconds: null,
+ count: 2,
+ maxCount: 5,
+ startDateMs: 0,
+ impressionDateMs: 0,
+ },
+ ],
+ },
+ "JSON is correct"
+ );
+
+ QuickSuggest.impressionCaps._test_reloadStats();
+ Assert.deepEqual(
+ QuickSuggest.impressionCaps._test_stats,
+ {
+ sponsored: [
+ {
+ intervalSeconds: 3,
+ count: 2,
+ maxCount: 2,
+ startDateMs: 0,
+ impressionDateMs: 0,
+ },
+ {
+ intervalSeconds: 5,
+ count: 2,
+ maxCount: 4,
+ startDateMs: 0,
+ impressionDateMs: 0,
+ },
+ {
+ intervalSeconds: Infinity,
+ count: 2,
+ maxCount: 5,
+ startDateMs: 0,
+ impressionDateMs: 0,
+ },
+ ],
+ },
+ "Impression stats were properly restored from the pref"
+ );
+ },
+ });
+});
+
+// Tests direct changes to the stats pref.
+add_task(async function prefDirectlyChanged() {
+ await doTest({
+ config: {
+ impression_caps: {
+ sponsored: {
+ lifetime: 5,
+ custom: [{ interval_s: 3, max_count: 3 }],
+ },
+ },
+ },
+ callback: async () => {
+ let expectedStats = {
+ sponsored: [
+ {
+ intervalSeconds: 3,
+ count: 0,
+ maxCount: 3,
+ startDateMs: 0,
+ impressionDateMs: 0,
+ },
+ {
+ intervalSeconds: Infinity,
+ count: 0,
+ maxCount: 5,
+ startDateMs: 0,
+ impressionDateMs: 0,
+ },
+ ],
+ };
+
+ UrlbarPrefs.set("quicksuggest.impressionCaps.stats", "bogus");
+ Assert.deepEqual(
+ QuickSuggest.impressionCaps._test_stats,
+ expectedStats,
+ "Expected stats for 'bogus'"
+ );
+
+ UrlbarPrefs.set("quicksuggest.impressionCaps.stats", JSON.stringify({}));
+ Assert.deepEqual(
+ QuickSuggest.impressionCaps._test_stats,
+ expectedStats,
+ "Expected stats for {}"
+ );
+
+ UrlbarPrefs.set(
+ "quicksuggest.impressionCaps.stats",
+ JSON.stringify({ sponsored: "bogus" })
+ );
+ Assert.deepEqual(
+ QuickSuggest.impressionCaps._test_stats,
+ expectedStats,
+ "Expected stats for { sponsored: 'bogus' }"
+ );
+
+ UrlbarPrefs.set(
+ "quicksuggest.impressionCaps.stats",
+ JSON.stringify({
+ sponsored: [
+ {
+ intervalSeconds: 3,
+ count: 0,
+ maxCount: 3,
+ startDateMs: 0,
+ impressionDateMs: 0,
+ },
+ {
+ intervalSeconds: "bogus",
+ count: 0,
+ maxCount: 99,
+ startDateMs: 0,
+ impressionDateMs: 0,
+ },
+ {
+ intervalSeconds: Infinity,
+ count: 0,
+ maxCount: 5,
+ startDateMs: 0,
+ impressionDateMs: 0,
+ },
+ ],
+ })
+ );
+ Assert.deepEqual(
+ QuickSuggest.impressionCaps._test_stats,
+ expectedStats,
+ "Expected stats with intervalSeconds: 'bogus'"
+ );
+
+ UrlbarPrefs.set(
+ "quicksuggest.impressionCaps.stats",
+ JSON.stringify({
+ sponsored: [
+ {
+ intervalSeconds: 3,
+ count: 0,
+ maxCount: 123,
+ startDateMs: 0,
+ impressionDateMs: 0,
+ },
+ {
+ intervalSeconds: Infinity,
+ count: 0,
+ maxCount: 456,
+ startDateMs: 0,
+ impressionDateMs: 0,
+ },
+ ],
+ })
+ );
+ Assert.deepEqual(
+ QuickSuggest.impressionCaps._test_stats,
+ expectedStats,
+ "Expected stats with `maxCount` values different from caps"
+ );
+
+ let stats = {
+ sponsored: [
+ {
+ intervalSeconds: 3,
+ count: 1,
+ maxCount: 3,
+ startDateMs: 99,
+ impressionDateMs: 99,
+ },
+ {
+ intervalSeconds: Infinity,
+ count: 7,
+ maxCount: 5,
+ startDateMs: 1337,
+ impressionDateMs: 1337,
+ },
+ ],
+ };
+ UrlbarPrefs.set(
+ "quicksuggest.impressionCaps.stats",
+ JSON.stringify(stats)
+ );
+ Assert.deepEqual(
+ QuickSuggest.impressionCaps._test_stats,
+ stats,
+ "Expected stats with valid JSON"
+ );
+ },
+ });
+});
+
+// Tests multiple interval periods where the cap is not hit. Telemetry should be
+// recorded for these periods.
+add_task(async function intervalsElapsedButCapNotHit() {
+ await doTest({
+ config: {
+ impression_caps: {
+ sponsored: {
+ custom: [{ interval_s: 1, max_count: 3 }],
+ },
+ },
+ },
+ callback: async () => {
+ await doTimedCallbacks({
+ // 1s
+ 1: async () => {
+ await checkSearch({
+ name: "1s",
+ searchString: "sponsored",
+ expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT],
+ });
+ },
+ // 10s
+ 10: async () => {
+ QuickSuggest.impressionCaps._test_resetElapsedCounters();
+ let expectedEvents = [
+ // 1s: reset with count = 0
+ {
+ object: "reset",
+ extra: {
+ eventDate: "1000",
+ intervalSeconds: "1",
+ maxCount: "3",
+ startDate: "0",
+ impressionDate: "0",
+ count: "0",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ // 2-10s: reset with count = 1, eventCount = 9
+ {
+ object: "reset",
+ extra: {
+ eventDate: "10000",
+ intervalSeconds: "1",
+ maxCount: "3",
+ startDate: "1000",
+ impressionDate: "1000",
+ count: "1",
+ type: "sponsored",
+ eventCount: "9",
+ },
+ },
+ ];
+ await checkTelemetryEvents(expectedEvents);
+ },
+ });
+ },
+ });
+});
+
+// Simulates reset events across a restart with the following:
+//
+// S S R
+// >----|----|----|----|----|----|----|----|----|----|
+// 0s 1 2 3 4 5 6 7 8 9 10
+//
+// 1. Startup at 0s
+// 2. Caps and stats initialized with interval_s: 1
+// 3. Startup at 4.5s
+// 4. Reset triggered at 10s
+//
+// Expected:
+// At 10s: 6 batched resets for periods starting at 4s
+add_task(async function restart_1() {
+ await doTest({
+ config: {
+ impression_caps: {
+ sponsored: {
+ custom: [{ interval_s: 1, max_count: 1 }],
+ },
+ },
+ },
+ callback: async () => {
+ gStartupDateMsStub.returns(4500);
+ await doTimedCallbacks({
+ // 10s: 6 batched resets for periods starting at 4s
+ 10: async () => {
+ QuickSuggest.impressionCaps._test_resetElapsedCounters();
+ await checkTelemetryEvents([
+ {
+ object: "reset",
+ extra: {
+ eventDate: "10000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "4000",
+ impressionDate: "0",
+ count: "0",
+ type: "sponsored",
+ eventCount: "6",
+ },
+ },
+ ]);
+ },
+ });
+ },
+ });
+ gStartupDateMsStub.returns(0);
+});
+
+// Simulates reset events across a restart with the following:
+//
+// S S R
+// >----|----|----|----|----|----|----|----|----|----|
+// 0s 1 2 3 4 5 6 7 8 9 10
+//
+// 1. Startup at 0s
+// 2. Caps and stats initialized with interval_s: 1
+// 3. Startup at 5s
+// 4. Reset triggered at 10s
+//
+// Expected:
+// At 10s: 5 batched resets for periods starting at 5s
+add_task(async function restart_2() {
+ await doTest({
+ config: {
+ impression_caps: {
+ sponsored: {
+ custom: [{ interval_s: 1, max_count: 1 }],
+ },
+ },
+ },
+ callback: async () => {
+ gStartupDateMsStub.returns(5000);
+ await doTimedCallbacks({
+ // 10s: 5 batched resets for periods starting at 5s
+ 10: async () => {
+ QuickSuggest.impressionCaps._test_resetElapsedCounters();
+ await checkTelemetryEvents([
+ {
+ object: "reset",
+ extra: {
+ eventDate: "10000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "5000",
+ impressionDate: "0",
+ count: "0",
+ type: "sponsored",
+ eventCount: "5",
+ },
+ },
+ ]);
+ },
+ });
+ },
+ });
+ gStartupDateMsStub.returns(0);
+});
+
+// Simulates reset events across a restart with the following:
+//
+// S S R
+// >----|----|----|----|----|----|----|----|----|----|
+// 0s 1 2 3 4 5 6 7 8 9 10
+//
+// 1. Startup at 0s
+// 2. Caps and stats initialized with interval_s: 1
+// 3. Startup at 5.5s
+// 4. Reset triggered at 10s
+//
+// Expected:
+// At 10s: 5 batched resets for periods starting at 5s
+add_task(async function restart_3() {
+ await doTest({
+ config: {
+ impression_caps: {
+ sponsored: {
+ custom: [{ interval_s: 1, max_count: 1 }],
+ },
+ },
+ },
+ callback: async () => {
+ gStartupDateMsStub.returns(5500);
+ await doTimedCallbacks({
+ // 10s: 5 batched resets for periods starting at 5s
+ 10: async () => {
+ QuickSuggest.impressionCaps._test_resetElapsedCounters();
+ await checkTelemetryEvents([
+ {
+ object: "reset",
+ extra: {
+ eventDate: "10000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "5000",
+ impressionDate: "0",
+ count: "0",
+ type: "sponsored",
+ eventCount: "5",
+ },
+ },
+ ]);
+ },
+ });
+ },
+ });
+ gStartupDateMsStub.returns(0);
+});
+
+// Simulates reset events across a restart with the following:
+//
+// S S RR RR
+// >---------|---------|
+// 0s 10 20
+//
+// 1. Startup at 0s
+// 2. Caps and stats initialized with interval_s: 10
+// 3. Startup at 5s
+// 4. Resets triggered at 9s, 10s, 19s, 20s
+//
+// Expected:
+// At 10s: 1 reset for period starting at 0s
+// At 20s: 1 reset for period starting at 10s
+add_task(async function restart_4() {
+ await doTest({
+ config: {
+ impression_caps: {
+ sponsored: {
+ custom: [{ interval_s: 10, max_count: 1 }],
+ },
+ },
+ },
+ callback: async () => {
+ gStartupDateMsStub.returns(5000);
+ await doTimedCallbacks({
+ // 9s: no resets
+ 9: async () => {
+ QuickSuggest.impressionCaps._test_resetElapsedCounters();
+ await checkTelemetryEvents([]);
+ },
+ // 10s: 1 reset for period starting at 0s
+ 10: async () => {
+ QuickSuggest.impressionCaps._test_resetElapsedCounters();
+ await checkTelemetryEvents([
+ {
+ object: "reset",
+ extra: {
+ eventDate: "10000",
+ intervalSeconds: "10",
+ maxCount: "1",
+ startDate: "0",
+ impressionDate: "0",
+ count: "0",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ]);
+ },
+ // 19s: no resets
+ 19: async () => {
+ QuickSuggest.impressionCaps._test_resetElapsedCounters();
+ await checkTelemetryEvents([]);
+ },
+ // 20s: 1 reset for period starting at 10s
+ 20: async () => {
+ QuickSuggest.impressionCaps._test_resetElapsedCounters();
+ await checkTelemetryEvents([
+ {
+ object: "reset",
+ extra: {
+ eventDate: "20000",
+ intervalSeconds: "10",
+ maxCount: "1",
+ startDate: "10000",
+ impressionDate: "0",
+ count: "0",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ]);
+ },
+ });
+ },
+ });
+ gStartupDateMsStub.returns(0);
+});
+
+// Simulates reset events across a restart with the following:
+//
+// S S R
+// >---------|---------|
+// 0s 10 20
+//
+// 1. Startup at 0s
+// 2. Caps and stats initialized with interval_s: 10
+// 3. Startup at 5s
+// 4. Reset triggered at 20s
+//
+// Expected:
+// At 20s: 2 batched resets for periods starting at 0s
+add_task(async function restart_5() {
+ await doTest({
+ config: {
+ impression_caps: {
+ sponsored: {
+ custom: [{ interval_s: 10, max_count: 1 }],
+ },
+ },
+ },
+ callback: async () => {
+ gStartupDateMsStub.returns(5000);
+ await doTimedCallbacks({
+ // 20s: 2 batches resets for periods starting at 0s
+ 20: async () => {
+ QuickSuggest.impressionCaps._test_resetElapsedCounters();
+ await checkTelemetryEvents([
+ {
+ object: "reset",
+ extra: {
+ eventDate: "20000",
+ intervalSeconds: "10",
+ maxCount: "1",
+ startDate: "0",
+ impressionDate: "0",
+ count: "0",
+ type: "sponsored",
+ eventCount: "2",
+ },
+ },
+ ]);
+ },
+ });
+ },
+ });
+ gStartupDateMsStub.returns(0);
+});
+
+// Simulates reset events across a restart with the following:
+//
+// S S RR RR
+// >---------|---------|---------|
+// 0s 10 20 30
+//
+// 1. Startup at 0s
+// 2. Caps and stats initialized with interval_s: 10
+// 3. Startup at 15s
+// 4. Resets triggered at 19s, 20s, 29s, 30s
+//
+// Expected:
+// At 20s: 1 reset for period starting at 10s
+// At 30s: 1 reset for period starting at 20s
+add_task(async function restart_6() {
+ await doTest({
+ config: {
+ impression_caps: {
+ sponsored: {
+ custom: [{ interval_s: 10, max_count: 1 }],
+ },
+ },
+ },
+ callback: async () => {
+ gStartupDateMsStub.returns(15000);
+ await doTimedCallbacks({
+ // 19s: no resets
+ 19: async () => {
+ QuickSuggest.impressionCaps._test_resetElapsedCounters();
+ await checkTelemetryEvents([]);
+ },
+ // 20s: 1 reset for period starting at 10s
+ 20: async () => {
+ QuickSuggest.impressionCaps._test_resetElapsedCounters();
+ await checkTelemetryEvents([
+ {
+ object: "reset",
+ extra: {
+ eventDate: "20000",
+ intervalSeconds: "10",
+ maxCount: "1",
+ startDate: "10000",
+ impressionDate: "0",
+ count: "0",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ]);
+ },
+ // 29s: no resets
+ 29: async () => {
+ QuickSuggest.impressionCaps._test_resetElapsedCounters();
+ await checkTelemetryEvents([]);
+ },
+ // 30s: 1 reset for period starting at 20s
+ 30: async () => {
+ QuickSuggest.impressionCaps._test_resetElapsedCounters();
+ await checkTelemetryEvents([
+ {
+ object: "reset",
+ extra: {
+ eventDate: "30000",
+ intervalSeconds: "10",
+ maxCount: "1",
+ startDate: "20000",
+ impressionDate: "0",
+ count: "0",
+ type: "sponsored",
+ eventCount: "1",
+ },
+ },
+ ]);
+ },
+ });
+ },
+ });
+ gStartupDateMsStub.returns(0);
+});
+
+// Simulates reset events across a restart with the following:
+//
+// S S R
+// >---------|---------|---------|
+// 0s 10 20 30
+//
+// 1. Startup at 0s
+// 2. Caps and stats initialized with interval_s: 10
+// 3. Startup at 15s
+// 4. Reset triggered at 30s
+//
+// Expected:
+// At 30s: 2 batched resets for periods starting at 10s
+add_task(async function restart_7() {
+ await doTest({
+ config: {
+ impression_caps: {
+ sponsored: {
+ custom: [{ interval_s: 10, max_count: 1 }],
+ },
+ },
+ },
+ callback: async () => {
+ gStartupDateMsStub.returns(15000);
+ await doTimedCallbacks({
+ // 30s: 2 batched resets for periods starting at 10s
+ 30: async () => {
+ QuickSuggest.impressionCaps._test_resetElapsedCounters();
+ await checkTelemetryEvents([
+ {
+ object: "reset",
+ extra: {
+ eventDate: "30000",
+ intervalSeconds: "10",
+ maxCount: "1",
+ startDate: "10000",
+ impressionDate: "0",
+ count: "0",
+ type: "sponsored",
+ eventCount: "2",
+ },
+ },
+ ]);
+ },
+ });
+ },
+ });
+ gStartupDateMsStub.returns(0);
+});
+
+// Tests reset telemetry recorded on shutdown.
+add_task(async function shutdown() {
+ await doTest({
+ config: {
+ impression_caps: {
+ sponsored: {
+ custom: [{ interval_s: 1, max_count: 1 }],
+ },
+ },
+ },
+ callback: async () => {
+ // Make `Date.now()` return 10s. Since the cap's `interval_s` is 1s and
+ // before this `Date.now()` returned 0s, 10 reset events should be
+ // recorded on shutdown.
+ gDateNowStub.returns(10000);
+
+ // Simulate shutdown.
+ Services.prefs.setBoolPref("toolkit.asyncshutdown.testing", true);
+ AsyncShutdown.profileChangeTeardown._trigger();
+
+ await checkTelemetryEvents([
+ {
+ object: "reset",
+ extra: {
+ eventDate: "10000",
+ intervalSeconds: "1",
+ maxCount: "1",
+ startDate: "0",
+ impressionDate: "0",
+ count: "0",
+ type: "sponsored",
+ eventCount: "10",
+ },
+ },
+ ]);
+
+ gDateNowStub.returns(0);
+ Services.prefs.clearUserPref("toolkit.asyncshutdown.testing");
+ },
+ });
+});
+
+// Tests the reset interval in realtime.
+add_task(async function resetInterval() {
+ // Remove the test stubs so we can test in realtime.
+ gDateNowStub.restore();
+ gStartupDateMsStub.restore();
+
+ await doTest({
+ config: {
+ impression_caps: {
+ sponsored: {
+ custom: [{ interval_s: 0.1, max_count: 1 }],
+ },
+ },
+ },
+ callback: async () => {
+ // Restart the reset interval now with a 1s period. Since the cap's
+ // `interval_s` is 0.1s, at least 10 reset events should be recorded the
+ // first time the reset interval fires. The exact number depends on timing
+ // and the machine running the test: how many 0.1s intervals elapse
+ // between when the config is set to when the reset interval fires. For
+ // that reason, we allow some leeway when checking `eventCount` below to
+ // avoid intermittent failures.
+ QuickSuggest.impressionCaps._test_setCountersResetInterval(1000);
+
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 1100));
+
+ // Restore the reset interval to its default.
+ QuickSuggest.impressionCaps._test_setCountersResetInterval();
+
+ await checkTelemetryEvents([
+ {
+ object: "reset",
+ extra: {
+ eventDate: /^[0-9]+$/,
+ intervalSeconds: "0.1",
+ maxCount: "1",
+ startDate: /^[0-9]+$/,
+ impressionDate: "0",
+ count: "0",
+ type: "sponsored",
+ // See comment above on allowing leeway for `eventCount`.
+ eventCount: str => {
+ info(`Checking 'eventCount': ${str}`);
+ let count = parseInt(str);
+ return 10 <= count && count < 20;
+ },
+ },
+ },
+ ]);
+ },
+ });
+
+ // Recreate the test stubs.
+ gDateNowStub = gSandbox.stub(
+ Cu.getGlobalForObject(UrlbarProviderQuickSuggest).Date,
+ "now"
+ );
+ gStartupDateMsStub = gSandbox.stub(
+ QuickSuggest.impressionCaps,
+ "_getStartupDateMs"
+ );
+ gStartupDateMsStub.returns(0);
+});
+
+/**
+ * Main test helper. Sets up state, calls your callback, and resets state.
+ *
+ * @param {object} options
+ * Options object.
+ * @param {object} options.config
+ * The quick suggest config to use during the test.
+ * @param {Function} options.callback
+ * The callback that will be run with the {@link config}
+ */
+async function doTest({ config, callback }) {
+ Services.telemetry.clearEvents();
+
+ // Make `Date.now()` return 0 to start with. It's necessary to do this before
+ // calling `withConfig()` because when a new config is set, the provider
+ // validates its impression stats, whose `startDateMs` values depend on
+ // `Date.now()`.
+ gDateNowStub.returns(0);
+
+ info(`Clearing stats and setting config`);
+ UrlbarPrefs.clear("quicksuggest.impressionCaps.stats");
+ QuickSuggest.impressionCaps._test_reloadStats();
+ await QuickSuggestTestUtils.withConfig({ config, callback });
+}
+
+/**
+ * Does a series of timed searches and checks their results and telemetry. This
+ * function relies on `doTimedCallbacks()`, so it may be helpful to look at it
+ * too.
+ *
+ * @param {string} searchString
+ * The query that should be timed
+ * @param {object} expectedBySecond
+ * An object that maps from seconds to objects that describe the searches to
+ * perform, their expected results, and the expected telemetry. For a given
+ * entry `S -> E` in this object, searches are performed S seconds after this
+ * function is called. `E` is an object that looks like this:
+ *
+ * { results, telemetry }
+ *
+ * {array} results
+ * An array of arrays. A search is performed for each sub-array in
+ * `results`, and the contents of the sub-array are the expected results
+ * for that search.
+ * {object} telemetry
+ * An object like this: { events }
+ * {array} events
+ * An array of expected telemetry events after all searches are done.
+ * Telemetry events are cleared after checking these. If not present,
+ * then it will be asserted that no events were recorded.
+ *
+ * Example:
+ *
+ * {
+ * 0: {
+ * results: [[R1], []],
+ * telemetry: {
+ * events: [
+ * someExpectedEvent,
+ * ],
+ * },
+ * }
+ * 1: {
+ * results: [[]],
+ * },
+ * }
+ *
+ * 0 seconds after `doTimedSearches()` is called, two searches are
+ * performed. The first one is expected to return a single result R1, and
+ * the second search is expected to return no results. After the searches
+ * are done, one telemetry event is expected to be recorded.
+ *
+ * 1 second after `doTimedSearches()` is called, one search is performed.
+ * It's expected to return no results, and no telemetry is expected to be
+ * recorded.
+ */
+async function doTimedSearches(searchString, expectedBySecond) {
+ await doTimedCallbacks(
+ Object.entries(expectedBySecond).reduce(
+ (memo, [second, { results, telemetry }]) => {
+ memo[second] = async () => {
+ for (let i = 0; i < results.length; i++) {
+ let expectedResults = results[i];
+ await checkSearch({
+ searchString,
+ expectedResults,
+ name: `${second}s search ${i + 1} of ${results.length}`,
+ });
+ }
+ let { events } = telemetry || {};
+ await checkTelemetryEvents(events || []);
+ };
+ return memo;
+ },
+ {}
+ )
+ );
+}
+
+/**
+ * Takes a series a callbacks and times at which they should be called, and
+ * calls them accordingly. This function is specifically designed for
+ * UrlbarProviderQuickSuggest and its impression capping implementation because
+ * it works by stubbing `Date.now()` within UrlbarProviderQuickSuggest. The
+ * callbacks are not actually called at the given times but instead `Date.now()`
+ * is stubbed so that UrlbarProviderQuickSuggest will think they are being
+ * called at the given times.
+ *
+ * A more general implementation of this helper function that isn't tailored to
+ * UrlbarProviderQuickSuggest is commented out below, and unfortunately it
+ * doesn't work properly on macOS.
+ *
+ * @param {object} callbacksBySecond
+ * An object that maps from seconds to callback functions. For a given entry
+ * `S -> F` in this object, the callback F is called S seconds after
+ * `doTimedCallbacks()` is called.
+ */
+async function doTimedCallbacks(callbacksBySecond) {
+ let entries = Object.entries(callbacksBySecond).sort(([t1], [t2]) => t1 - t2);
+ for (let [timeoutSeconds, callback] of entries) {
+ gDateNowStub.returns(1000 * timeoutSeconds);
+ await callback();
+ }
+}
+
+/*
+// This is the original implementation of `doTimedCallbacks()`, left here for
+// reference or in case the macOS problem described below is fixed. Instead of
+// stubbing `Date.now()` within UrlbarProviderQuickSuggest, it starts parallel
+// timers so that the callbacks are actually called at appropriate times. This
+// version of `doTimedCallbacks()` is therefore more generally useful, but it
+// has the drawback that your test has to run in real time. e.g., if one of your
+// callbacks needs to run 10s from now, the test must actually wait 10s.
+//
+// Unfortunately macOS seems to have some kind of limit of ~33 total 1-second
+// timers during any xpcshell test -- not 33 simultaneous timers but 33 total
+// timers. After that, timers fire randomly and with huge timeout periods that
+// are often exactly 10s greater than the specified period, as if some 10s
+// timeout internal to macOS is being hit. This problem does not seem to happen
+// when running the full browser, only during xpcshell tests. In fact the
+// problem can be reproduced in an xpcshell test that simply creates an interval
+// timer whose period is 1s (e.g., using `setInterval()` from Timer.sys.mjs).
+// After ~33 ticks, the timer's period jumps to ~10s.
+async function doTimedCallbacks(callbacksBySecond) {
+ await Promise.all(
+ Object.entries(callbacksBySecond).map(
+ ([timeoutSeconds, callback]) => new Promise(
+ resolve => setTimeout(
+ () => callback().then(resolve),
+ 1000 * parseInt(timeoutSeconds)
+ )
+ )
+ )
+ );
+}
+*/
+
+/**
+ * Does a search, triggers an engagement, and checks the results.
+ *
+ * @param {object} options
+ * Options object.
+ * @param {string} options.name
+ * This value is the name of the search and will be logged in messages to make
+ * debugging easier.
+ * @param {string} options.searchString
+ * The query that should be searched.
+ * @param {Array} options.expectedResults
+ * The results that are expected from the search.
+ */
+async function checkSearch({ name, searchString, expectedResults }) {
+ info(`Preparing search "${name}" with search string "${searchString}"`);
+ let context = createContext(searchString, {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+ info(`Doing search: ${name}`);
+ await check_results({
+ context,
+ matches: expectedResults,
+ });
+ info(`Finished search: ${name}`);
+
+ // Impression stats are updated only on engagement, so force one now.
+ // `selIndex` doesn't really matter but since we're not trying to simulate a
+ // click on the suggestion, pass in -1 to ensure we don't record a click. Pass
+ // in true for `isPrivate` so we don't attempt to record the impression ping
+ // because otherwise the following PingCentre error is logged:
+ // "Structured Ingestion ping failure with error: undefined"
+ let isPrivate = true;
+ if (UrlbarProviderQuickSuggest._resultFromLastQuery) {
+ UrlbarProviderQuickSuggest._resultFromLastQuery.isVisible = true;
+ }
+ UrlbarProviderQuickSuggest.onEngagement(isPrivate, "engagement", context, {
+ selIndex: -1,
+ });
+}
+
+async function checkTelemetryEvents(expectedEvents) {
+ QuickSuggestTestUtils.assertEvents(
+ expectedEvents.map(event => ({
+ ...event,
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "impression_cap",
+ })),
+ // Filter in only impression_cap events.
+ { method: "impression_cap" }
+ );
+}
diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_merino.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_merino.js
new file mode 100644
index 0000000000..4327890a0d
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_merino.js
@@ -0,0 +1,681 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Tests Merino integration with UrlbarProviderQuickSuggest.
+
+"use strict";
+
+// relative to `browser.urlbar`
+const PREF_DATA_COLLECTION_ENABLED = "quicksuggest.dataCollection.enabled";
+const PREF_MERINO_ENABLED = "merino.enabled";
+const PREF_REMOTE_SETTINGS_ENABLED = "quicksuggest.remoteSettings.enabled";
+
+const SEARCH_STRING = "frab";
+
+const REMOTE_SETTINGS_RESULTS = [
+ {
+ id: 1,
+ url: "http://test.com/q=frabbits",
+ title: "frabbits",
+ keywords: [SEARCH_STRING],
+ click_url: "http://click.reporting.test.com/",
+ impression_url: "http://impression.reporting.test.com/",
+ advertiser: "TestAdvertiser",
+ },
+];
+
+const EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT = {
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ heuristic: false,
+ payload: {
+ telemetryType: "adm_sponsored",
+ qsSuggestion: SEARCH_STRING,
+ title: "frabbits",
+ url: "http://test.com/q=frabbits",
+ originalUrl: "http://test.com/q=frabbits",
+ icon: null,
+ sponsoredImpressionUrl: "http://impression.reporting.test.com/",
+ sponsoredClickUrl: "http://click.reporting.test.com/",
+ sponsoredBlockId: 1,
+ sponsoredAdvertiser: "TestAdvertiser",
+ isSponsored: true,
+ helpUrl: QuickSuggest.HELP_URL,
+ helpL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-learn-more-about-firefox-suggest"
+ : "firefox-suggest-urlbar-learn-more",
+ },
+ isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"),
+ blockL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-dismiss-firefox-suggest"
+ : "firefox-suggest-urlbar-block",
+ },
+ displayUrl: "http://test.com/q=frabbits",
+ source: "remote-settings",
+ },
+};
+
+const EXPECTED_MERINO_URLBAR_RESULT = {
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ heuristic: false,
+ payload: {
+ telemetryType: "adm_sponsored",
+ qsSuggestion: "full_keyword",
+ title: "title",
+ url: "url",
+ originalUrl: "url",
+ icon: null,
+ sponsoredImpressionUrl: "impression_url",
+ sponsoredClickUrl: "click_url",
+ sponsoredBlockId: 1,
+ sponsoredAdvertiser: "advertiser",
+ isSponsored: true,
+ helpUrl: QuickSuggest.HELP_URL,
+ helpL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-learn-more-about-firefox-suggest"
+ : "firefox-suggest-urlbar-learn-more",
+ },
+ isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"),
+ blockL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-dismiss-firefox-suggest"
+ : "firefox-suggest-urlbar-block",
+ },
+ displayUrl: "url",
+ requestId: "request_id",
+ source: "merino",
+ },
+};
+
+// `UrlbarProviderQuickSuggest.#merino` is lazily created on the first Merino
+// fetch, so it's easiest to create `gClient` lazily too.
+XPCOMUtils.defineLazyGetter(
+ this,
+ "gClient",
+ () => UrlbarProviderQuickSuggest._test_merino
+);
+
+add_task(async function init() {
+ UrlbarPrefs.set("quicksuggest.enabled", true);
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
+ UrlbarPrefs.set("quicksuggest.shouldShowOnboardingDialog", false);
+
+ await MerinoTestUtils.server.start();
+
+ // Set up the remote settings client with the test data.
+ await QuickSuggestTestUtils.ensureQuickSuggestInit({
+ remoteSettingsResults: [
+ {
+ type: "data",
+ attachment: REMOTE_SETTINGS_RESULTS,
+ },
+ ],
+ });
+
+ Assert.equal(
+ typeof QuickSuggestRemoteSettings.DEFAULT_SUGGESTION_SCORE,
+ "number",
+ "Sanity check: DEFAULT_SUGGESTION_SCORE is defined"
+ );
+});
+
+// Tests with Merino enabled and remote settings disabled.
+add_task(async function oneEnabled_merino() {
+ UrlbarPrefs.set(PREF_MERINO_ENABLED, true);
+ UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, false);
+ UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true);
+
+ let histograms = MerinoTestUtils.getAndClearHistograms();
+
+ // Use a score lower than the remote settings score to make sure the
+ // suggestion is included regardless.
+ MerinoTestUtils.server.response.body.suggestions[0].score =
+ QuickSuggestRemoteSettings.DEFAULT_SUGGESTION_SCORE / 2;
+
+ let context = createContext(SEARCH_STRING, {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [EXPECTED_MERINO_URLBAR_RESULT],
+ });
+
+ MerinoTestUtils.checkAndClearHistograms({
+ histograms,
+ response: "success",
+ latencyRecorded: true,
+ client: gClient,
+ });
+
+ MerinoTestUtils.server.reset();
+ gClient.resetSession();
+});
+
+// Tests with Merino disabled and remote settings enabled.
+add_task(async function oneEnabled_remoteSettings() {
+ UrlbarPrefs.set(PREF_MERINO_ENABLED, false);
+ UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, true);
+ UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true);
+
+ let histograms = MerinoTestUtils.getAndClearHistograms();
+
+ let context = createContext(SEARCH_STRING, {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT],
+ });
+
+ MerinoTestUtils.checkAndClearHistograms({
+ histograms,
+ response: null,
+ latencyRecorded: false,
+ client: gClient,
+ });
+});
+
+// Tests with Merino enabled but with data collection disabled. Results should
+// not be fetched from Merino in that case. Also tests with remote settings
+// enabled.
+add_task(async function dataCollectionDisabled() {
+ UrlbarPrefs.set(PREF_MERINO_ENABLED, true);
+ UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, true);
+ UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, false);
+
+ let context = createContext(SEARCH_STRING, {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT],
+ });
+});
+
+// When the Merino suggestion has a higher score than the remote settings
+// suggestion, the Merino suggestion should be used.
+add_task(async function higherScore() {
+ UrlbarPrefs.set(PREF_MERINO_ENABLED, true);
+ UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, true);
+ UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true);
+
+ let histograms = MerinoTestUtils.getAndClearHistograms();
+
+ MerinoTestUtils.server.response.body.suggestions[0].score =
+ 2 * QuickSuggestRemoteSettings.DEFAULT_SUGGESTION_SCORE;
+
+ let context = createContext(SEARCH_STRING, {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [EXPECTED_MERINO_URLBAR_RESULT],
+ });
+
+ MerinoTestUtils.checkAndClearHistograms({
+ histograms,
+ response: "success",
+ latencyRecorded: true,
+ client: gClient,
+ });
+
+ MerinoTestUtils.server.reset();
+ gClient.resetSession();
+});
+
+// When the Merino suggestion has a lower score than the remote settings
+// suggestion, the remote settings suggestion should be used.
+add_task(async function lowerScore() {
+ UrlbarPrefs.set(PREF_MERINO_ENABLED, true);
+ UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, true);
+ UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true);
+
+ let histograms = MerinoTestUtils.getAndClearHistograms();
+
+ MerinoTestUtils.server.response.body.suggestions[0].score =
+ QuickSuggestRemoteSettings.DEFAULT_SUGGESTION_SCORE / 2;
+
+ let context = createContext(SEARCH_STRING, {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT],
+ });
+
+ MerinoTestUtils.checkAndClearHistograms({
+ histograms,
+ response: "success",
+ latencyRecorded: true,
+ client: gClient,
+ });
+
+ MerinoTestUtils.server.reset();
+ gClient.resetSession();
+});
+
+// When the Merino and remote settings suggestions have the same score, the
+// remote settings suggestion should be used.
+add_task(async function sameScore() {
+ UrlbarPrefs.set(PREF_MERINO_ENABLED, true);
+ UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, true);
+ UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true);
+
+ let histograms = MerinoTestUtils.getAndClearHistograms();
+
+ MerinoTestUtils.server.response.body.suggestions[0].score =
+ QuickSuggestRemoteSettings.DEFAULT_SUGGESTION_SCORE;
+
+ let context = createContext(SEARCH_STRING, {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT],
+ });
+
+ MerinoTestUtils.checkAndClearHistograms({
+ histograms,
+ response: "success",
+ latencyRecorded: true,
+ client: gClient,
+ });
+
+ MerinoTestUtils.server.reset();
+ gClient.resetSession();
+});
+
+// When the Merino suggestion does not include a score, the remote settings
+// suggestion should be used.
+add_task(async function noMerinoScore() {
+ UrlbarPrefs.set(PREF_MERINO_ENABLED, true);
+ UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, true);
+ UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true);
+
+ let histograms = MerinoTestUtils.getAndClearHistograms();
+
+ Assert.equal(
+ typeof MerinoTestUtils.server.response.body.suggestions[0].score,
+ "number",
+ "Sanity check: First suggestion has a score"
+ );
+ delete MerinoTestUtils.server.response.body.suggestions[0].score;
+
+ let context = createContext(SEARCH_STRING, {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT],
+ });
+
+ MerinoTestUtils.checkAndClearHistograms({
+ histograms,
+ response: "success",
+ latencyRecorded: true,
+ client: gClient,
+ });
+
+ MerinoTestUtils.server.reset();
+ gClient.resetSession();
+});
+
+// When remote settings doesn't return a suggestion but Merino does, the Merino
+// suggestion should be used.
+add_task(async function noSuggestion_remoteSettings() {
+ UrlbarPrefs.set(PREF_MERINO_ENABLED, true);
+ UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, true);
+ UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true);
+
+ let histograms = MerinoTestUtils.getAndClearHistograms();
+
+ let context = createContext("this doesn't match remote settings", {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [EXPECTED_MERINO_URLBAR_RESULT],
+ });
+
+ MerinoTestUtils.checkAndClearHistograms({
+ histograms,
+ response: "success",
+ latencyRecorded: true,
+ client: gClient,
+ });
+
+ MerinoTestUtils.server.reset();
+ gClient.resetSession();
+});
+
+// When Merino doesn't return a suggestion but remote settings does, the remote
+// settings suggestion should be used.
+add_task(async function noSuggestion_merino() {
+ UrlbarPrefs.set(PREF_MERINO_ENABLED, true);
+ UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, true);
+ UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true);
+
+ let histograms = MerinoTestUtils.getAndClearHistograms();
+
+ MerinoTestUtils.server.response.body.suggestions = [];
+
+ let context = createContext(SEARCH_STRING, {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT],
+ });
+
+ MerinoTestUtils.checkAndClearHistograms({
+ histograms,
+ response: "no_suggestion",
+ latencyRecorded: true,
+ client: gClient,
+ });
+
+ MerinoTestUtils.server.reset();
+ gClient.resetSession();
+});
+
+// Tests with both Merino and remote settings disabled.
+add_task(async function bothDisabled() {
+ UrlbarPrefs.set(PREF_MERINO_ENABLED, false);
+ UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, false);
+ UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true);
+
+ let histograms = MerinoTestUtils.getAndClearHistograms();
+
+ let context = createContext(SEARCH_STRING, {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+ await check_results({ context, matches: [] });
+
+ MerinoTestUtils.checkAndClearHistograms({
+ histograms,
+ response: null,
+ latencyRecorded: false,
+ client: gClient,
+ });
+});
+
+// When Merino returns multiple suggestions, the one with the largest score
+// should be used.
+add_task(async function multipleMerinoSuggestions() {
+ UrlbarPrefs.set(PREF_MERINO_ENABLED, true);
+ UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, false);
+ UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true);
+
+ let histograms = MerinoTestUtils.getAndClearHistograms();
+
+ MerinoTestUtils.server.response.body.suggestions = [
+ {
+ provider: "adm",
+ full_keyword: "multipleMerinoSuggestions 0 full_keyword",
+ title: "multipleMerinoSuggestions 0 title",
+ url: "multipleMerinoSuggestions 0 url",
+ icon: "multipleMerinoSuggestions 0 icon",
+ impression_url: "multipleMerinoSuggestions 0 impression_url",
+ click_url: "multipleMerinoSuggestions 0 click_url",
+ block_id: 0,
+ advertiser: "multipleMerinoSuggestions 0 advertiser",
+ is_sponsored: true,
+ score: 0.1,
+ },
+ {
+ provider: "adm",
+ full_keyword: "multipleMerinoSuggestions 1 full_keyword",
+ title: "multipleMerinoSuggestions 1 title",
+ url: "multipleMerinoSuggestions 1 url",
+ icon: "multipleMerinoSuggestions 1 icon",
+ impression_url: "multipleMerinoSuggestions 1 impression_url",
+ click_url: "multipleMerinoSuggestions 1 click_url",
+ block_id: 1,
+ advertiser: "multipleMerinoSuggestions 1 advertiser",
+ is_sponsored: true,
+ score: 1,
+ },
+ {
+ provider: "adm",
+ full_keyword: "multipleMerinoSuggestions 2 full_keyword",
+ title: "multipleMerinoSuggestions 2 title",
+ url: "multipleMerinoSuggestions 2 url",
+ icon: "multipleMerinoSuggestions 2 icon",
+ impression_url: "multipleMerinoSuggestions 2 impression_url",
+ click_url: "multipleMerinoSuggestions 2 click_url",
+ block_id: 2,
+ advertiser: "multipleMerinoSuggestions 2 advertiser",
+ is_sponsored: true,
+ score: 0.2,
+ },
+ ];
+
+ let context = createContext("test", {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ {
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ heuristic: false,
+ payload: {
+ telemetryType: "adm_sponsored",
+ qsSuggestion: "multipleMerinoSuggestions 1 full_keyword",
+ title: "multipleMerinoSuggestions 1 title",
+ url: "multipleMerinoSuggestions 1 url",
+ originalUrl: "multipleMerinoSuggestions 1 url",
+ icon: "multipleMerinoSuggestions 1 icon",
+ sponsoredImpressionUrl: "multipleMerinoSuggestions 1 impression_url",
+ sponsoredClickUrl: "multipleMerinoSuggestions 1 click_url",
+ sponsoredBlockId: 1,
+ sponsoredAdvertiser: "multipleMerinoSuggestions 1 advertiser",
+ isSponsored: true,
+ helpUrl: QuickSuggest.HELP_URL,
+ helpL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-learn-more-about-firefox-suggest"
+ : "firefox-suggest-urlbar-learn-more",
+ },
+ isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"),
+ blockL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-dismiss-firefox-suggest"
+ : "firefox-suggest-urlbar-block",
+ },
+ displayUrl: "multipleMerinoSuggestions 1 url",
+ requestId: "request_id",
+ source: "merino",
+ },
+ },
+ ],
+ });
+
+ MerinoTestUtils.checkAndClearHistograms({
+ histograms,
+ response: "success",
+ latencyRecorded: true,
+ client: gClient,
+ });
+
+ MerinoTestUtils.server.reset();
+ gClient.resetSession();
+});
+
+// Timestamp templates in URLs should be replaced with real timestamps.
+add_task(async function timestamps() {
+ UrlbarPrefs.set(PREF_MERINO_ENABLED, true);
+ UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, false);
+ UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true);
+
+ // Set up the Merino response with template URLs.
+ let suggestion = MerinoTestUtils.server.response.body.suggestions[0];
+ let { TIMESTAMP_TEMPLATE } = QuickSuggest;
+
+ suggestion.url = `http://example.com/time-${TIMESTAMP_TEMPLATE}`;
+ suggestion.click_url = `http://example.com/time-${TIMESTAMP_TEMPLATE}-foo`;
+
+ // Do a search.
+ let context = createContext("test", {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+ let controller = UrlbarTestUtils.newMockController({
+ input: {
+ isPrivate: context.isPrivate,
+ onFirstResult() {
+ return false;
+ },
+ getSearchSource() {
+ return "dummy-search-source";
+ },
+ window: {
+ location: {
+ href: AppConstants.BROWSER_CHROME_URL,
+ },
+ },
+ },
+ });
+ await controller.startQuery(context);
+
+ // Should be one quick suggest result.
+ Assert.equal(context.results.length, 1, "One result returned");
+ let result = context.results[0];
+
+ QuickSuggestTestUtils.assertTimestampsReplaced(result, {
+ url: suggestion.click_url,
+ sponsoredClickUrl: suggestion.click_url,
+ });
+
+ MerinoTestUtils.server.reset();
+ gClient.resetSession();
+});
+
+// When both suggestion types are disabled but data collection is enabled, we
+// should still send requests to Merino, and the requests should include an
+// empty `providers` to tell Merino not to fetch any suggestions.
+add_task(async function suggestedDisabled_dataCollectionEnabled() {
+ UrlbarPrefs.set(PREF_MERINO_ENABLED, true);
+ UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, false);
+ UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true);
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false);
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", false);
+
+ let histograms = MerinoTestUtils.getAndClearHistograms();
+
+ let context = createContext("test", {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [],
+ });
+
+ // Check that the request is received and includes an empty `providers`.
+ MerinoTestUtils.server.checkAndClearRequests([
+ {
+ params: {
+ [MerinoTestUtils.SEARCH_PARAMS.QUERY]: "test",
+ [MerinoTestUtils.SEARCH_PARAMS.SEQUENCE_NUMBER]: 0,
+ [MerinoTestUtils.SEARCH_PARAMS.PROVIDERS]: "",
+ },
+ },
+ ]);
+
+ MerinoTestUtils.checkAndClearHistograms({
+ histograms,
+ response: "success",
+ latencyRecorded: true,
+ client: gClient,
+ });
+
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
+ gClient.resetSession();
+});
+
+// Test whether the blocking for Merino results works.
+add_task(async function block() {
+ UrlbarPrefs.set(PREF_MERINO_ENABLED, true);
+ UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, true);
+ UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true);
+
+ for (const suggestion of MerinoTestUtils.server.response.body.suggestions) {
+ await QuickSuggest.blockedSuggestions.add(suggestion.url);
+ }
+
+ const context = createContext(SEARCH_STRING, {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+
+ await check_results({
+ context,
+ matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT],
+ });
+
+ await QuickSuggest.blockedSuggestions.clear();
+ MerinoTestUtils.server.reset();
+ gClient.resetSession();
+});
+
+// Tests a Merino suggestion that is a best match.
+add_task(async function bestMatch() {
+ UrlbarPrefs.set(PREF_MERINO_ENABLED, true);
+ UrlbarPrefs.set(PREF_REMOTE_SETTINGS_ENABLED, true);
+ UrlbarPrefs.set(PREF_DATA_COLLECTION_ENABLED, true);
+
+ // Simply enabling the best match feature should make the mock suggestion a
+ // best match because the search string length is greater than the required
+ // best match length.
+ UrlbarPrefs.set("bestMatch.enabled", true);
+ UrlbarPrefs.set("suggest.bestmatch", true);
+
+ let expectedResult = { ...EXPECTED_MERINO_URLBAR_RESULT };
+ expectedResult.payload = { ...EXPECTED_MERINO_URLBAR_RESULT.payload };
+ expectedResult.isBestMatch = true;
+ delete expectedResult.payload.qsSuggestion;
+
+ await QuickSuggestTestUtils.withConfig({
+ config: QuickSuggestTestUtils.BEST_MATCH_CONFIG,
+ callback: async () => {
+ let context = createContext(SEARCH_STRING, {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [expectedResult],
+ });
+
+ // This isn't necessary since `check_results()` checks `isBestMatch`, but
+ // check it here explicitly for good measure.
+ Assert.ok(context.results[0].isBestMatch, "Result is a best match");
+ },
+ });
+
+ UrlbarPrefs.clear("bestMatch.enabled");
+ UrlbarPrefs.clear("suggest.bestmatch");
+
+ MerinoTestUtils.server.reset();
+ gClient.resetSession();
+});
diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_merinoSessions.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_merinoSessions.js
new file mode 100644
index 0000000000..935577c36c
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_merinoSessions.js
@@ -0,0 +1,174 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Tests Merino session integration with UrlbarProviderQuickSuggest.
+
+"use strict";
+
+// `UrlbarProviderQuickSuggest.#merino` is lazily created on the first Merino
+// fetch, so it's easiest to create `gClient` lazily too.
+XPCOMUtils.defineLazyGetter(
+ this,
+ "gClient",
+ () => UrlbarProviderQuickSuggest._test_merino
+);
+
+add_task(async function init() {
+ UrlbarPrefs.set("quicksuggest.enabled", true);
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
+ UrlbarPrefs.set("merino.enabled", true);
+ UrlbarPrefs.set("quicksuggest.remoteSettings.enabled", false);
+ UrlbarPrefs.set("quicksuggest.dataCollection.enabled", true);
+
+ await MerinoTestUtils.server.start();
+ await QuickSuggestTestUtils.ensureQuickSuggestInit();
+});
+
+// In a single engagement, all requests should use the same session ID and the
+// sequence number should be incremented.
+add_task(async function singleEngagement() {
+ let controller = UrlbarTestUtils.newMockController();
+
+ for (let i = 0; i < 3; i++) {
+ let searchString = "search" + i;
+ await controller.startQuery(
+ createContext(searchString, {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ })
+ );
+
+ MerinoTestUtils.server.checkAndClearRequests([
+ {
+ params: {
+ [MerinoTestUtils.SEARCH_PARAMS.QUERY]: searchString,
+ [MerinoTestUtils.SEARCH_PARAMS.SEQUENCE_NUMBER]: i,
+ },
+ },
+ ]);
+ }
+
+ // End the engagement to reset the session for the next test.
+ endEngagement();
+});
+
+// New engagements should not use the same session ID as previous engagements
+// and the sequence number should be reset. This task completes each engagement
+// successfully.
+add_task(async function manyEngagements_engagement() {
+ await doManyEngagementsTest("engagement");
+});
+
+// New engagements should not use the same session ID as previous engagements
+// and the sequence number should be reset. This task abandons each engagement.
+add_task(async function manyEngagements_abandonment() {
+ await doManyEngagementsTest("abandonment");
+});
+
+async function doManyEngagementsTest(state) {
+ let controller = UrlbarTestUtils.newMockController();
+
+ for (let i = 0; i < 3; i++) {
+ let searchString = "search" + i;
+ let context = createContext(searchString, {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+ await controller.startQuery(context);
+
+ MerinoTestUtils.server.checkAndClearRequests([
+ {
+ params: {
+ [MerinoTestUtils.SEARCH_PARAMS.QUERY]: searchString,
+ [MerinoTestUtils.SEARCH_PARAMS.SEQUENCE_NUMBER]: 0,
+ },
+ },
+ ]);
+
+ endEngagement(context, state);
+ }
+}
+
+// When a search is canceled after the request is sent and before the Merino
+// response is received, the sequence number should still be incremented.
+add_task(async function canceledQueries() {
+ let controller = UrlbarTestUtils.newMockController();
+
+ for (let i = 0; i < 3; i++) {
+ // Send the first response after a delay to make sure the client will not
+ // receive it before we start the second fetch.
+ MerinoTestUtils.server.response.delay = UrlbarPrefs.get("merino.timeoutMs");
+
+ // Start the first search.
+ let requestPromise = MerinoTestUtils.server.waitForNextRequest();
+ let searchString1 = "search" + i;
+ controller.startQuery(
+ createContext(searchString1, {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ })
+ );
+
+ // Wait until the first request is received before starting the second
+ // search. If we started the second search immediately, the first would be
+ // canceled before the provider is even called due to the urlbar's 50ms
+ // delay (see `browser.urlbar.delay`) so the sequence number would not be
+ // incremented for it. Here we want to test the case where the first search
+ // is canceled after the request is sent and the number is incremented.
+ await requestPromise;
+ delete MerinoTestUtils.server.response.delay;
+
+ // Now do a second search that cancels the first.
+ let searchString2 = searchString1 + "again";
+ await controller.startQuery(
+ createContext(searchString2, {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ })
+ );
+
+ // The sequence number should have been incremented for each search.
+ MerinoTestUtils.server.checkAndClearRequests([
+ {
+ params: {
+ [MerinoTestUtils.SEARCH_PARAMS.QUERY]: searchString1,
+ [MerinoTestUtils.SEARCH_PARAMS.SEQUENCE_NUMBER]: 2 * i,
+ },
+ },
+ {
+ params: {
+ [MerinoTestUtils.SEARCH_PARAMS.QUERY]: searchString2,
+ [MerinoTestUtils.SEARCH_PARAMS.SEQUENCE_NUMBER]: 2 * i + 1,
+ },
+ },
+ ]);
+ }
+
+ // End the engagement to reset the session for the next test.
+ endEngagement();
+});
+
+function endEngagement(context = null, state = "engagement") {
+ UrlbarProviderQuickSuggest.onEngagement(
+ false,
+ state,
+ context ||
+ createContext("endEngagement", {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ }),
+ { selIndex: -1 }
+ );
+
+ Assert.strictEqual(
+ gClient.sessionID,
+ null,
+ "sessionID is null after engagement"
+ );
+ Assert.strictEqual(
+ gClient._test_sessionTimer,
+ null,
+ "sessionTimer is null after engagement"
+ );
+}
diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_migrate_v1.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_migrate_v1.js
new file mode 100644
index 0000000000..8e80f3639a
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_migrate_v1.js
@@ -0,0 +1,490 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Tests quick suggest prefs migration from unversioned prefs to version 1.
+
+"use strict";
+
+// Expected version 1 default-branch prefs
+const DEFAULT_PREFS = {
+ history: {
+ "quicksuggest.enabled": false,
+ },
+ offline: {
+ "quicksuggest.enabled": true,
+ "quicksuggest.dataCollection.enabled": false,
+ "quicksuggest.shouldShowOnboardingDialog": false,
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+ },
+ online: {
+ "quicksuggest.enabled": true,
+ "quicksuggest.dataCollection.enabled": false,
+ "quicksuggest.shouldShowOnboardingDialog": true,
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+};
+
+// Migration will use these values to migrate only up to version 1 instead of
+// the current version.
+const TEST_OVERRIDES = {
+ migrationVersion: 1,
+ defaultPrefs: DEFAULT_PREFS,
+};
+
+add_task(async function init() {
+ await UrlbarTestUtils.initNimbusFeature();
+});
+
+// The following tasks test OFFLINE TO OFFLINE
+
+// Migrating from:
+// * Offline (suggestions on by default)
+// * User did not override any defaults
+//
+// Scenario when migration occurs:
+// * Offline
+//
+// Expected:
+// * Non-sponsored suggestions: remain on
+// * Sponsored suggestions: remain on
+// * Data collection: off
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ scenario: "offline",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.offline,
+ },
+ });
+});
+
+// Migrating from:
+// * Offline (suggestions on by default)
+// * Main suggestions pref: user left on
+// * Sponsored suggestions: user turned off
+//
+// Scenario when migration occurs:
+// * Offline
+//
+// Expected:
+// * Non-sponsored suggestions: on
+// * Sponsored suggestions: remain off
+// * Data collection: off
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "suggest.quicksuggest.sponsored": false,
+ },
+ scenario: "offline",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ });
+});
+
+// Migrating from:
+// * Offline (suggestions on by default)
+// * Main suggestions pref: user turned off
+// * Sponsored suggestions: user left on (but ignored since main was off)
+//
+// Scenario when migration occurs:
+// * Offline
+//
+// Expected:
+// * Non-sponsored suggestions: off
+// * Sponsored suggestions: off
+// * Data collection: off
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "suggest.quicksuggest": false,
+ },
+ scenario: "offline",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ });
+});
+
+// Migrating from:
+// * Offline (suggestions on by default)
+// * Main suggestions pref: user turned off
+// * Sponsored suggestions: user turned off
+//
+// Scenario when migration occurs:
+// * Offline
+//
+// Expected:
+// * Non-sponsored suggestions: off
+// * Sponsored suggestions: off
+// * Data collection: off
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "suggest.quicksuggest": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ scenario: "offline",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ });
+});
+
+// The following tasks test OFFLINE TO ONLINE
+
+// Migrating from:
+// * Offline (suggestions on by default)
+// * User did not override any defaults
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: off
+// * Sponsored suggestions: off
+// * Data collection: off
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ scenario: "online",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.online,
+ },
+ });
+});
+
+// Migrating from:
+// * Offline (suggestions on by default)
+// * Main suggestions pref: user left on
+// * Sponsored suggestions: user turned off
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: off
+// * Sponsored suggestions: off
+// * Data collection: off
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "suggest.quicksuggest.sponsored": false,
+ },
+ scenario: "online",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ });
+});
+
+// Migrating from:
+// * Offline (suggestions on by default)
+// * Main suggestions pref: user turned off
+// * Sponsored suggestions: user left on (but ignored since main was off)
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: off
+// * Sponsored suggestions: off
+// * Data collection: off
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "suggest.quicksuggest": false,
+ },
+ scenario: "online",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ },
+ },
+ });
+});
+
+// Migrating from:
+// * Offline (suggestions on by default)
+// * Main suggestions pref: user turned off
+// * Sponsored suggestions: user turned off
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: off
+// * Sponsored suggestions: off
+// * Data collection: off
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "suggest.quicksuggest": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ scenario: "online",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ });
+});
+
+// The following tasks test ONLINE TO OFFLINE
+
+// Migrating from:
+// * Online (suggestions off by default)
+// * User did not override any defaults
+//
+// Scenario when migration occurs:
+// * Offline
+//
+// Expected:
+// * Non-sponsored suggestions: on (since main pref had default value)
+// * Sponsored suggestions: on (since main & sponsored prefs had default values)
+// * Data collection: off
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ scenario: "offline",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.offline,
+ },
+ });
+});
+
+// Migrating from:
+// * Online (suggestions off by default)
+// * Main suggestions pref: user left off
+// * Sponsored suggestions: user turned on (but ignored since main was off)
+//
+// Scenario when migration occurs:
+// * Offline
+//
+// Expected:
+// * Non-sponsored suggestions: off
+// * Sponsored suggestions: on (see below)
+// * Data collection: off
+//
+// It's unfortunate that sponsored suggestions are ultimately on since before
+// the migration no suggestions were shown to the user. There's nothing we can
+// do about it, aside from forcing off suggestions in more cases than we want.
+// The reason is that at the time of migration we can't tell that the previous
+// scenario was online -- or more precisely that it wasn't history. If we knew
+// it wasn't history, then we'd know to turn sponsored off; if we knew it *was*
+// history, then we'd know to turn sponsored -- and non-sponsored -- on, since
+// the scenario at the time of migration is offline, where suggestions should be
+// enabled by default.
+//
+// This is the reason we now record `quicksuggest.scenario` on the user branch
+// and not the default branch as we previously did.
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "suggest.quicksuggest.sponsored": true,
+ },
+ scenario: "offline",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.sponsored": true,
+ },
+ },
+ });
+});
+
+// Migrating from:
+// * Online (suggestions off by default)
+// * Main suggestions pref: user turned on
+// * Sponsored suggestions: user left off
+//
+// Scenario when migration occurs:
+// * Offline
+//
+// Expected:
+// * Non-sponsored suggestions: remain on
+// * Sponsored suggestions: remain off
+// * Data collection: off (since scenario is offline)
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "suggest.quicksuggest": true,
+ },
+ scenario: "offline",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": true,
+ },
+ },
+ });
+});
+
+// Migrating from:
+// * Online (suggestions off by default)
+// * Main suggestions pref: user turned on
+// * Sponsored suggestions: user turned on
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: remain on
+// * Sponsored suggestions: remain on
+// * Data collection: off (since scenario is offline)
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "suggest.quicksuggest": true,
+ "suggest.quicksuggest.sponsored": true,
+ },
+ scenario: "offline",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+ },
+ },
+ });
+});
+
+// The following tasks test ONLINE TO ONLINE
+
+// Migrating from:
+// * Online (suggestions off by default)
+// * User did not override any defaults
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: off
+// * Sponsored suggestions: off
+// * Data collection: off
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ scenario: "online",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.online,
+ },
+ });
+});
+
+// Migrating from:
+// * Online (suggestions off by default)
+// * Main suggestions pref: user left off
+// * Sponsored suggestions: user turned on (but ignored since main was off)
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: remain off
+// * Sponsored suggestions: off
+// * Data collection: off
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "suggest.quicksuggest.sponsored": true,
+ },
+ scenario: "online",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.online,
+ },
+ });
+});
+
+// Migrating from:
+// * Online (suggestions off by default)
+// * Main suggestions pref: user turned on
+// * Sponsored suggestions: user left off
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: remain on
+// * Sponsored suggestions: remain off
+// * Data collection: ON (since user effectively opted in by turning on
+// suggestions)
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "suggest.quicksuggest": true,
+ },
+ scenario: "online",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": true,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ });
+});
+
+// Migrating from:
+// * Online (suggestions off by default)
+// * Main suggestions pref: user turned on
+// * Sponsored suggestions: user turned on
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: remain on
+// * Sponsored suggestions: remain on
+// * Data collection: ON (since user effectively opted in by turning on
+// suggestions)
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "suggest.quicksuggest": true,
+ "suggest.quicksuggest.sponsored": true,
+ },
+ scenario: "online",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ });
+});
diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_migrate_v2.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_migrate_v2.js
new file mode 100644
index 0000000000..cd4a2149e6
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_migrate_v2.js
@@ -0,0 +1,1355 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Tests quick suggest prefs migration to version 2.
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ ExperimentFakes: "resource://testing-common/NimbusTestUtils.sys.mjs",
+ NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
+});
+
+// Expected version 2 default-branch prefs
+const DEFAULT_PREFS = {
+ history: {
+ "quicksuggest.enabled": false,
+ },
+ offline: {
+ "quicksuggest.enabled": true,
+ "quicksuggest.dataCollection.enabled": false,
+ "quicksuggest.shouldShowOnboardingDialog": false,
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+ },
+ online: {
+ "quicksuggest.enabled": true,
+ "quicksuggest.dataCollection.enabled": false,
+ "quicksuggest.shouldShowOnboardingDialog": true,
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+ },
+};
+
+// Migration will use these values to migrate only up to version 1 instead of
+// the current version.
+// Currently undefined because version 2 is the current migration version and we
+// want migration to use its actual values, not overrides. When version 3 is
+// added, set this to an object like the one in test_quicksuggest_migrate_v1.js.
+const TEST_OVERRIDES = undefined;
+
+add_task(async function init() {
+ await UrlbarTestUtils.initNimbusFeature();
+});
+
+// The following tasks test OFFLINE UNVERSIONED to OFFLINE
+
+// Migrating from:
+// * Unversioned prefs
+// * Offline
+// * Main suggestions pref: user left on
+// * Sponsored suggestions: user left on
+//
+// Scenario when migration occurs:
+// * Offline
+//
+// Expected:
+// * Non-sponsored suggestions: on
+// * Sponsored suggestions: on
+// * Data collection: off
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ scenario: "offline",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.offline,
+ },
+ });
+});
+
+// Migrating from:
+// * Unversioned prefs
+// * Offline
+// * Main suggestions pref: user turned off
+// * Sponsored suggestions: user left on (but ignored since main was off)
+//
+// Scenario when migration occurs:
+// * Offline
+//
+// Expected:
+// * Non-sponsored suggestions: off
+// * Sponsored suggestions: off
+// * Data collection: off
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "suggest.quicksuggest": false,
+ },
+ scenario: "offline",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ });
+});
+
+// Migrating from:
+// * Unversioned prefs
+// * Offline
+// * Main suggestions pref: user left on
+// * Sponsored suggestions: user turned off
+//
+// Scenario when migration occurs:
+// * Offline
+//
+// Expected:
+// * Non-sponsored suggestions: on
+// * Sponsored suggestions: off
+// * Data collection: off
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "suggest.quicksuggest.sponsored": false,
+ },
+ scenario: "offline",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ });
+});
+
+// Migrating from:
+// * Unversioned prefs
+// * Offline
+// * Main suggestions pref: user turned off
+// * Sponsored suggestions: user turned off
+//
+// Scenario when migration occurs:
+// * Offline
+//
+// Expected:
+// * Non-sponsored suggestions: off
+// * Sponsored suggestions: off
+// * Data collection: off
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "suggest.quicksuggest": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ scenario: "offline",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ });
+});
+
+// The following tasks test OFFLINE UNVERSIONED to ONLINE
+
+// Migrating from:
+// * Unversioned prefs
+// * Offline
+// * Main suggestions pref: user left on
+// * Sponsored suggestions: user left on
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: on
+// * Sponsored suggestions: on
+// * Data collection: off
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ scenario: "online",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.online,
+ },
+ });
+});
+
+// Migrating from:
+// * Unversioned prefs
+// * Offline
+// * Main suggestions pref: user turned off
+// * Sponsored suggestions: user left on (but ignored since main was off)
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: off
+// * Sponsored suggestions: off
+// * Data collection: off
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "suggest.quicksuggest": false,
+ },
+ scenario: "online",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ });
+});
+
+// Migrating from:
+// * Unversioned prefs
+// * Offline
+// * Main suggestions pref: user left on
+// * Sponsored suggestions: user turned off
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: on
+// * Sponsored suggestions: off
+// * Data collection: off
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "suggest.quicksuggest.sponsored": false,
+ },
+ scenario: "online",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ });
+});
+
+// Migrating from:
+// * Unversioned prefs
+// * Offline
+// * Main suggestions pref: user turned off
+// * Sponsored suggestions: user turned off
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: off
+// * Sponsored suggestions: off
+// * Data collection: off
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "suggest.quicksuggest": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ scenario: "online",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ });
+});
+
+// The following tasks test ONLINE UNVERSIONED to ONLINE when the user was NOT
+// SHOWN THE MODAL (e.g., because they didn't restart)
+
+// Migrating from:
+// * Unversioned prefs
+// * Online
+// * Modal shown: no
+// * User enrolled in online where suggestions were disabled by default, did not
+// turn on either type of suggestion, was not shown the modal (e.g., because
+// they didn't restart), and upgraded to v2
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: off
+// * Sponsored suggestions: off
+// * Data collection: off
+add_task(async function () {
+ await withOnlineExperiment(async () => {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ scenario: "online",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.online,
+ },
+ });
+ });
+});
+
+// Migrating from:
+// * Unversioned prefs
+// * Online
+// * Modal shown: no
+// * User enrolled in online where suggestions were disabled by default, turned
+// on main suggestions pref and left off sponsored suggestions, was not shown
+// the modal (e.g., because they didn't restart), and upgraded to v2
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: on
+// * Sponsored suggestions: off
+// * Data collection: on (since they opted in by checking the main checkbox
+// while in online)
+add_task(async function () {
+ await withOnlineExperiment(async () => {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "suggest.quicksuggest": true,
+ },
+ scenario: "online",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": true,
+ },
+ },
+ });
+ });
+});
+
+// Migrating from:
+// * Unversioned prefs
+// * Online
+// * Modal shown: no
+// * User enrolled in online where suggestions were disabled by default, left
+// off main suggestions pref and turned on sponsored suggestions, was not
+// shown the modal (e.g., because they didn't restart), and upgraded to v2
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: off
+// * Sponsored suggestions: off
+// * Data collection: off
+add_task(async function () {
+ await withOnlineExperiment(async () => {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "suggest.quicksuggest.sponsored": true,
+ },
+ scenario: "online",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.sponsored": true,
+ },
+ },
+ });
+ });
+});
+
+// Migrating from:
+// * Unversioned prefs
+// * Online
+// * Modal shown: no
+// * User enrolled in online where suggestions were disabled by default, turned
+// on main suggestions pref and sponsored suggestions, was not shown the
+// modal (e.g., because they didn't restart), and upgraded to v2
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: on
+// * Sponsored suggestions: on
+// * Data collection: on (since they opted in by checking the main checkbox
+// while in online)
+add_task(async function () {
+ await withOnlineExperiment(async () => {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "suggest.quicksuggest": true,
+ "suggest.quicksuggest.sponsored": true,
+ },
+ scenario: "online",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+ },
+ },
+ });
+ });
+});
+
+// The following tasks test ONLINE UNVERSIONED to ONLINE when the user WAS SHOWN
+// THE MODAL
+
+// Migrating from:
+// * Unversioned prefs
+// * Online
+// * Modal shown: yes
+// * The following end up with same prefs and are covered by this task:
+// 1. User did not opt in and left off both the main suggestions pref and
+// sponsored suggestions
+// 2. User opted in but then later turned off both the main suggestions pref
+// and sponsored suggestions
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: off
+// * Sponsored suggestions: off
+// * Data collection: off
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "quicksuggest.showedOnboardingDialog": true,
+ },
+ scenario: "online",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ },
+ });
+});
+
+// Migrating from:
+// * Unversioned prefs
+// * Online
+// * Modal shown: yes
+// * The following end up with same prefs and are covered by this task:
+// 1. User did not opt in but then later turned on the main suggestions pref
+// 2. User opted in but then later turned off sponsored suggestions
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: on
+// * Sponsored suggestions: off
+// * Data collection: on
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "quicksuggest.showedOnboardingDialog": true,
+ "suggest.quicksuggest": true,
+ },
+ scenario: "online",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": false,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ });
+});
+
+// Migrating from:
+// * Unversioned prefs
+// * Online
+// * Modal shown: yes
+// * The following end up with same prefs and are covered by this task:
+// 1. User did not opt in but then later turned on sponsored suggestions
+// 2. User opted in but then later turned off the main suggestions pref
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: off
+// * Sponsored suggestions: off
+// * Data collection: off
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "quicksuggest.showedOnboardingDialog": true,
+ "suggest.quicksuggest.sponsored": true,
+ },
+ scenario: "online",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ },
+ });
+});
+
+// Migrating from:
+// * Unversioned prefs
+// * Online
+// * Modal shown: yes
+// * The following end up with same prefs and are covered by this task:
+// 1. User did not opt in but then later turned on both the main suggestions
+// pref and sponsored suggestions
+// 2. User opted in and left on both the main suggestions pref and sponsored
+// suggestions
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: on
+// * Sponsored suggestions: on
+// * Data collection: on
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "quicksuggest.showedOnboardingDialog": true,
+ "suggest.quicksuggest": true,
+ "suggest.quicksuggest.sponsored": true,
+ },
+ scenario: "online",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ });
+});
+
+// The following tasks test OFFLINE VERSION 1 to OFFLINE
+
+// Migrating from:
+// * Version 1 prefs
+// * Offline
+// * Non-sponsored suggestions: user left on
+// * Sponsored suggestions: user left on
+// * Data collection: user left off
+//
+// Scenario when migration occurs:
+// * Offline
+//
+// Expected:
+// * Non-sponsored suggestions: on
+// * Sponsored suggestions: on
+// * Data collection: off
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "quicksuggest.migrationVersion": 1,
+ "quicksuggest.scenario": "offline",
+ },
+ scenario: "offline",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.offline,
+ },
+ });
+});
+
+// Migrating from:
+// * Version 1 prefs
+// * Offline
+// * Non-sponsored suggestions: user left on
+// * Sponsored suggestions: user left on
+// * Data collection: user turned on
+//
+// Scenario when migration occurs:
+// * Offline
+//
+// Expected:
+// * Non-sponsored suggestions: on
+// * Sponsored suggestions: on
+// * Data collection: on
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "quicksuggest.migrationVersion": 1,
+ "quicksuggest.scenario": "offline",
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ scenario: "offline",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.offline,
+ userBranch: {
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ });
+});
+
+// Migrating from:
+// * Version 1 prefs
+// * Offline
+// * Non-sponsored suggestions: user left on
+// * Sponsored suggestions: user turned off
+// * Data collection: user left off
+//
+// Scenario when migration occurs:
+// * Offline
+//
+// Expected:
+// * Non-sponsored suggestions: on
+// * Sponsored suggestions: off
+// * Data collection: off
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "quicksuggest.migrationVersion": 1,
+ "quicksuggest.scenario": "offline",
+ "suggest.quicksuggest.sponsored": false,
+ },
+ scenario: "offline",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ });
+});
+
+// Migrating from:
+// * Version 1 prefs
+// * Offline
+// * Non-sponsored suggestions: user turned off
+// * Sponsored suggestions: user turned off
+// * Data collection: user left off
+//
+// Scenario when migration occurs:
+// * Offline
+//
+// Expected:
+// * Non-sponsored suggestions: off
+// * Sponsored suggestions: off
+// * Data collection: off
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "quicksuggest.migrationVersion": 1,
+ "quicksuggest.scenario": "offline",
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ scenario: "offline",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ });
+});
+
+// The following tasks test OFFLINE VERSION 1 to ONLINE
+
+// Migrating from:
+// * Version 1 prefs
+// * Offline
+// * Non-sponsored suggestions: user left on
+// * Sponsored suggestions: user left on
+// * Data collection: user left off
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: on
+// * Sponsored suggestions: on
+// * Data collection: off
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "quicksuggest.migrationVersion": 1,
+ "quicksuggest.scenario": "offline",
+ },
+ scenario: "online",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.online,
+ },
+ });
+});
+
+// Migrating from:
+// * Version 1 prefs
+// * Offline
+// * Non-sponsored suggestions: user left on
+// * Sponsored suggestions: user turned off
+// * Data collection: user left off
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: on
+// * Sponsored suggestions: off
+// * Data collection: off
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "quicksuggest.migrationVersion": 1,
+ "quicksuggest.scenario": "offline",
+ "suggest.quicksuggest.sponsored": false,
+ },
+ scenario: "online",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ });
+});
+
+// Migrating from:
+// * Version 1 prefs
+// * Offline
+// * Non-sponsored suggestions: user turned off
+// * Sponsored suggestions: user turned off
+// * Data collection: user left off
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: on
+// * Sponsored suggestions: off
+// * Data collection: off
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "quicksuggest.migrationVersion": 1,
+ "quicksuggest.scenario": "offline",
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ scenario: "online",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ });
+});
+
+// The following tasks test ONLINE VERSION 1 to ONLINE when the user was NOT
+// SHOWN THE MODAL (e.g., because they didn't restart)
+
+// Migrating from:
+// * Version 1 prefs
+// * Online
+// * Modal shown: no
+// * Non-sponsored suggestions: user left off
+// * Sponsored suggestions: user left off
+// * Data collection: user left off
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: off
+// * Sponsored suggestions: off
+// * Data collection: off
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "quicksuggest.migrationVersion": 1,
+ "quicksuggest.scenario": "online",
+ },
+ scenario: "online",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ });
+});
+
+// Migrating from:
+// * Version 1 prefs
+// * Online
+// * Modal shown: no
+// * Non-sponsored suggestions: user left off
+// * Sponsored suggestions: user left off
+// * Data collection: user turned on
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: off
+// * Sponsored suggestions: off
+// * Data collection: on
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "quicksuggest.migrationVersion": 1,
+ "quicksuggest.scenario": "online",
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ scenario: "online",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ });
+});
+
+// Migrating from:
+// * Version 1 prefs
+// * Online
+// * Modal shown: no
+// * Non-sponsored suggestions: user left off
+// * Sponsored suggestions: user turned on
+// * Data collection: user left off
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: off
+// * Sponsored suggestions: on
+// * Data collection: off
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "quicksuggest.migrationVersion": 1,
+ "quicksuggest.scenario": "online",
+ "suggest.quicksuggest.sponsored": true,
+ },
+ scenario: "online",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": true,
+ },
+ },
+ });
+});
+
+// Migrating from:
+// * Version 1 prefs
+// * Online
+// * Modal shown: no
+// * Non-sponsored suggestions: user left off
+// * Sponsored suggestions: user turned on
+// * Data collection: user turned on
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: off
+// * Sponsored suggestions: on
+// * Data collection: on
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "quicksuggest.migrationVersion": 1,
+ "quicksuggest.scenario": "online",
+ "suggest.quicksuggest.sponsored": true,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ scenario: "online",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": true,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ });
+});
+
+// Migrating from:
+// * Version 1 prefs
+// * Online
+// * Modal shown: no
+// * Non-sponsored suggestions: user turned on
+// * Sponsored suggestions: user left off
+// * Data collection: user left off
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: on
+// * Sponsored suggestions: off
+// * Data collection: off
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "quicksuggest.migrationVersion": 1,
+ "quicksuggest.scenario": "online",
+ "suggest.quicksuggest.nonsponsored": true,
+ },
+ scenario: "online",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ });
+});
+
+// Migrating from:
+// * Version 1 prefs
+// * Online
+// * Modal shown: no
+// * Non-sponsored suggestions: user turned on
+// * Sponsored suggestions: user left off
+// * Data collection: user turned on
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: on
+// * Sponsored suggestions: off
+// * Data collection: on
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "quicksuggest.migrationVersion": 1,
+ "quicksuggest.scenario": "online",
+ "suggest.quicksuggest.nonsponsored": true,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ scenario: "online",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": false,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ });
+});
+
+// Migrating from:
+// * Version 1 prefs
+// * Online
+// * Modal shown: no
+// * Non-sponsored suggestions: user turned on
+// * Sponsored suggestions: user turned on
+// * Data collection: user left off
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: on
+// * Sponsored suggestions: on
+// * Data collection: off
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "quicksuggest.migrationVersion": 1,
+ "quicksuggest.scenario": "online",
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+ },
+ scenario: "online",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+ },
+ },
+ });
+});
+
+// Migrating from:
+// * Version 1 prefs
+// * Online
+// * Modal shown: no
+// * Non-sponsored suggestions: user turned on
+// * Sponsored suggestions: user turned on
+// * Data collection: user turned on
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: on
+// * Sponsored suggestions: on
+// * Data collection: on
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "quicksuggest.migrationVersion": 1,
+ "quicksuggest.scenario": "online",
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ scenario: "online",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ });
+});
+
+// The following tasks test ONLINE VERSION 1 to ONLINE when the user WAS SHOWN
+// THE MODAL WHILE PREFS WERE UNVERSIONED
+
+// Migrating from:
+// * Version 1 prefs
+// * Online
+// * Modal shown: yes, while prefs were unversioned
+// * User opted in: no
+// * Non-sponsored suggestions: user left off
+// * Sponsored suggestions: user left off
+// * Data collection: user left off
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: off
+// * Sponsored suggestions: off
+// * Data collection: off
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "quicksuggest.migrationVersion": 1,
+ "quicksuggest.scenario": "online",
+ "quicksuggest.showedOnboardingDialog": true,
+ },
+ scenario: "online",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ });
+});
+
+// Migrating from:
+// * Version 1 prefs
+// * Online
+// * Modal shown: yes, while prefs were unversioned
+// * User opted in: no
+// * Non-sponsored suggestions: user turned on
+// * Sponsored suggestions: user left off
+// * Data collection: user left off
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: on
+// * Sponsored suggestions: off
+// * Data collection: off
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "quicksuggest.migrationVersion": 1,
+ "quicksuggest.scenario": "online",
+ "quicksuggest.showedOnboardingDialog": true,
+ "suggest.quicksuggest.nonsponsored": true,
+ },
+ scenario: "online",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ });
+});
+
+// Migrating from:
+// * Version 1 prefs
+// * Online
+// * Modal shown: yes, while prefs were unversioned
+// * User opted in: yes
+// * Non-sponsored suggestions: user left on
+// * Sponsored suggestions: user left on
+// * Data collection: user left on
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: on
+// * Sponsored suggestions: on
+// * Data collection: on
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "quicksuggest.migrationVersion": 1,
+ "quicksuggest.scenario": "online",
+ "quicksuggest.showedOnboardingDialog": true,
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ scenario: "online",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ });
+});
+
+// Migrating from:
+// * Version 1 prefs
+// * Online
+// * Modal shown: yes, while prefs were unversioned
+// * User opted in: yes
+// * Non-sponsored suggestions: user turned off
+// * Sponsored suggestions: user left on
+// * Data collection: user left on
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: off
+// * Sponsored suggestions: on
+// * Data collection: on
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "quicksuggest.migrationVersion": 1,
+ "quicksuggest.scenario": "online",
+ "quicksuggest.showedOnboardingDialog": true,
+ "suggest.quicksuggest.sponsored": true,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ scenario: "online",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": true,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ });
+});
+
+// Migrating from:
+// * Version 1 prefs
+// * Online
+// * Modal shown: yes, while prefs were unversioned
+// * User opted in: yes
+// * Non-sponsored suggestions: user left on
+// * Sponsored suggestions: user left on
+// * Data collection: user turned off
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: on
+// * Sponsored suggestions: on
+// * Data collection: on
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "quicksuggest.migrationVersion": 1,
+ "quicksuggest.scenario": "online",
+ "quicksuggest.showedOnboardingDialog": true,
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ scenario: "online",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ },
+ });
+});
+
+// The following tasks test ONLINE VERSION 1 to ONLINE when the user WAS SHOWN
+// THE MODAL WHILE PREFS WERE VERSION 1
+
+// Migrating from:
+// * Version 1 prefs
+// * Online
+// * Modal shown: yes, while prefs were version 1
+// * User opted in: no
+// * Non-sponsored suggestions: user left off
+// * Sponsored suggestions: user left off
+// * Data collection: user left off
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: off
+// * Sponsored suggestions: off
+// * Data collection: off
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "quicksuggest.migrationVersion": 1,
+ "quicksuggest.scenario": "online",
+ "quicksuggest.showedOnboardingDialog": true,
+ "quicksuggest.onboardingDialogChoice": "not_now_link",
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ scenario: "online",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ },
+ });
+});
+
+// Migrating from:
+// * Version 1 prefs
+// * Online
+// * Modal shown: yes, while prefs were version 1
+// * User opted in: yes
+// * Non-sponsored suggestions: user left on
+// * Sponsored suggestions: user left on
+// * Data collection: user left on
+//
+// Scenario when migration occurs:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: on
+// * Sponsored suggestions: on
+// * Data collection: on
+add_task(async function () {
+ await doMigrateTest({
+ testOverrides: TEST_OVERRIDES,
+ initialUserBranch: {
+ "quicksuggest.migrationVersion": 1,
+ "quicksuggest.scenario": "online",
+ "quicksuggest.showedOnboardingDialog": true,
+ "quicksuggest.onboardingDialogChoice": "accept",
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ scenario: "online",
+ expectedPrefs: {
+ defaultBranch: DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ });
+});
+
+async function withOnlineExperiment(callback) {
+ let { enrollmentPromise, doExperimentCleanup } =
+ ExperimentFakes.enrollmentHelper(
+ ExperimentFakes.recipe("firefox-suggest-offline-vs-online", {
+ active: true,
+ branches: [
+ {
+ slug: "treatment",
+ features: [
+ {
+ featureId: NimbusFeatures.urlbar.featureId,
+ value: {
+ enabled: true,
+ },
+ },
+ ],
+ },
+ ],
+ })
+ );
+ await enrollmentPromise;
+ await callback();
+ await doExperimentCleanup();
+}
diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_nonUniqueKeywords.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_nonUniqueKeywords.js
new file mode 100644
index 0000000000..84d1116e89
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_nonUniqueKeywords.js
@@ -0,0 +1,282 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Tests non-unique keywords, i.e., keywords used by multiple suggestions.
+
+"use strict";
+
+// For each of these objects, the test creates a quick suggest result (the kind
+// stored in the remote settings data, not a urlbar result), the corresponding
+// expected quick suggest suggestion, and the corresponding expected urlbar
+// result. The test assumes results and suggestions are returned in the order
+// listed here.
+let SUGGESTIONS_DATA = [
+ {
+ keywords: ["aaa"],
+ isSponsored: true,
+ },
+ {
+ keywords: ["aaa", "bbb"],
+ isSponsored: false,
+ score: 2 * QuickSuggestRemoteSettings.DEFAULT_SUGGESTION_SCORE,
+ },
+ {
+ keywords: ["bbb"],
+ isSponsored: true,
+ score: 4 * QuickSuggestRemoteSettings.DEFAULT_SUGGESTION_SCORE,
+ },
+ {
+ keywords: ["bbb"],
+ isSponsored: false,
+ score: 3 * QuickSuggestRemoteSettings.DEFAULT_SUGGESTION_SCORE,
+ },
+ {
+ keywords: ["ccc"],
+ isSponsored: true,
+ },
+];
+
+// Test cases. In this object, keywords map to subtest cases. For each keyword,
+// the test calls `query(keyword)` and checks that the indexes (relative to
+// `SUGGESTIONS_DATA`) of the returned quick suggest results are the ones in
+// `expectedIndexes`. Then the test does a series of urlbar searches using the
+// keyword as the search string, one search per object in `searches`. Sponsored
+// and non-sponsored urlbar results are enabled as defined by `sponsored` and
+// `nonsponsored`. `expectedIndex` is the expected index (relative to
+// `SUGGESTIONS_DATA`) of the returned urlbar result.
+let TESTS = {
+ aaa: {
+ // 0: sponsored
+ // 1: nonsponsored, score = 2x
+ expectedIndexes: [0, 1],
+ searches: [
+ {
+ sponsored: true,
+ nonsponsored: true,
+ expectedIndex: 1,
+ },
+ {
+ sponsored: false,
+ nonsponsored: true,
+ expectedIndex: 1,
+ },
+ {
+ sponsored: true,
+ nonsponsored: false,
+ expectedIndex: 0,
+ },
+ {
+ sponsored: false,
+ nonsponsored: false,
+ expectedIndex: undefined,
+ },
+ ],
+ },
+ bbb: {
+ // 1: nonsponsored, score = 2x
+ // 2: sponsored, score = 4x,
+ // 3: nonsponsored, score = 3x
+ expectedIndexes: [1, 2, 3],
+ searches: [
+ {
+ sponsored: true,
+ nonsponsored: true,
+ expectedIndex: 2,
+ },
+ {
+ sponsored: false,
+ nonsponsored: true,
+ expectedIndex: 3,
+ },
+ {
+ sponsored: true,
+ nonsponsored: false,
+ expectedIndex: 2,
+ },
+ {
+ sponsored: false,
+ nonsponsored: false,
+ expectedIndex: undefined,
+ },
+ ],
+ },
+ ccc: {
+ // 4: sponsored
+ expectedIndexes: [4],
+ searches: [
+ {
+ sponsored: true,
+ nonsponsored: true,
+ expectedIndex: 4,
+ },
+ {
+ sponsored: false,
+ nonsponsored: true,
+ expectedIndex: undefined,
+ },
+ {
+ sponsored: true,
+ nonsponsored: false,
+ expectedIndex: 4,
+ },
+ {
+ sponsored: false,
+ nonsponsored: false,
+ expectedIndex: undefined,
+ },
+ ],
+ },
+};
+
+add_task(async function () {
+ UrlbarPrefs.set("quicksuggest.enabled", true);
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
+
+ // Create results and suggestions based on `SUGGESTIONS_DATA`.
+ let qsResults = [];
+ let qsSuggestions = [];
+ let urlbarResults = [];
+ for (let i = 0; i < SUGGESTIONS_DATA.length; i++) {
+ let { keywords, isSponsored, score } = SUGGESTIONS_DATA[i];
+
+ // quick suggest result
+ let qsResult = {
+ keywords,
+ score,
+ id: i,
+ url: "http://example.com/" + i,
+ title: "Title " + i,
+ click_url: "http://example.com/click",
+ impression_url: "http://example.com/impression",
+ advertiser: "TestAdvertiser",
+ iab_category: isSponsored ? "22 - Shopping" : "5 - Education",
+ };
+ qsResults.push(qsResult);
+
+ // expected quick suggest suggestion
+ let qsSuggestion = {
+ ...qsResult,
+ block_id: qsResult.id,
+ is_sponsored: isSponsored,
+ score:
+ typeof score == "number"
+ ? score
+ : QuickSuggestRemoteSettings.DEFAULT_SUGGESTION_SCORE,
+ source: "remote-settings",
+ icon: null,
+ position: undefined,
+ provider: "AdmWikipedia",
+ };
+ delete qsSuggestion.keywords;
+ delete qsSuggestion.id;
+ qsSuggestions.push(qsSuggestion);
+
+ // expected urlbar result
+ urlbarResults.push({
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ heuristic: false,
+ payload: {
+ isSponsored,
+ telemetryType: isSponsored ? "adm_sponsored" : "adm_nonsponsored",
+ sponsoredBlockId: qsResult.id,
+ url: qsResult.url,
+ originalUrl: qsResult.url,
+ displayUrl: qsResult.url,
+ title: qsResult.title,
+ sponsoredClickUrl: qsResult.click_url,
+ sponsoredImpressionUrl: qsResult.impression_url,
+ sponsoredAdvertiser: qsResult.advertiser,
+ sponsoredIabCategory: qsResult.iab_category,
+ icon: null,
+ helpUrl: QuickSuggest.HELP_URL,
+ helpL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-learn-more-about-firefox-suggest"
+ : "firefox-suggest-urlbar-learn-more",
+ },
+ isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"),
+ blockL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-dismiss-firefox-suggest"
+ : "firefox-suggest-urlbar-block",
+ },
+ source: "remote-settings",
+ },
+ });
+ }
+
+ await QuickSuggestTestUtils.ensureQuickSuggestInit({
+ remoteSettingsResults: [
+ {
+ type: "data",
+ attachment: qsResults,
+ },
+ ],
+ });
+
+ // Run a test for each keyword.
+ for (let [keyword, test] of Object.entries(TESTS)) {
+ info("Running subtest " + JSON.stringify({ keyword, test }));
+
+ let { expectedIndexes, searches } = test;
+
+ // Call `query()`.
+ Assert.deepEqual(
+ await QuickSuggestRemoteSettings.query(keyword),
+ expectedIndexes.map(i => ({
+ ...qsSuggestions[i],
+ full_keyword: keyword,
+ })),
+ `query() for keyword ${keyword}`
+ );
+
+ // Now do a urlbar search for the keyword with all possible combinations of
+ // sponsored and non-sponsored suggestions enabled and disabled.
+ for (let sponsored of [true, false]) {
+ for (let nonsponsored of [true, false]) {
+ // Find the matching `searches` object.
+ let search = searches.find(
+ s => s.sponsored == sponsored && s.nonsponsored == nonsponsored
+ );
+ Assert.ok(
+ search,
+ "Sanity check: Search test case specified for " +
+ JSON.stringify({ keyword, sponsored, nonsponsored })
+ );
+
+ info(
+ "Running urlbar search subtest " +
+ JSON.stringify({ keyword, expectedIndexes, search })
+ );
+
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", sponsored);
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", nonsponsored);
+
+ // Set up the search and do it.
+ let context = createContext(keyword, {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+
+ let matches = [];
+ if (search.expectedIndex !== undefined) {
+ matches.push({
+ ...urlbarResults[search.expectedIndex],
+ payload: {
+ ...urlbarResults[search.expectedIndex].payload,
+ qsSuggestion: keyword,
+ },
+ });
+ }
+
+ await check_results({ context, matches });
+ }
+ }
+
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
+ }
+});
diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_offlineDefault.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_offlineDefault.js
new file mode 100644
index 0000000000..7330dd4fd5
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_offlineDefault.js
@@ -0,0 +1,127 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Tests `UrlbarPrefs.updateFirefoxSuggestScenario` in isolation under the
+// assumption that the offline scenario should be enabled by default for US en.
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ Region: "resource://gre/modules/Region.sys.mjs",
+});
+
+// All the prefs that `updateFirefoxSuggestScenario` sets along with the
+// expected default-branch values when offline is enabled and when it's not
+// enabled.
+const PREFS = [
+ {
+ name: "browser.urlbar.quicksuggest.enabled",
+ get: "getBoolPref",
+ set: "setBoolPref",
+ expectedOfflineValue: true,
+ expectedOtherValue: false,
+ },
+ {
+ name: "browser.urlbar.quicksuggest.shouldShowOnboardingDialog",
+ get: "getBoolPref",
+ set: "setBoolPref",
+ expectedOfflineValue: false,
+ expectedOtherValue: true,
+ },
+ {
+ name: "browser.urlbar.suggest.quicksuggest.nonsponsored",
+ get: "getBoolPref",
+ set: "setBoolPref",
+ expectedOfflineValue: true,
+ expectedOtherValue: false,
+ },
+ {
+ name: "browser.urlbar.suggest.quicksuggest.sponsored",
+ get: "getBoolPref",
+ set: "setBoolPref",
+ expectedOfflineValue: true,
+ expectedOtherValue: false,
+ },
+];
+
+add_task(async function init() {
+ await UrlbarTestUtils.initNimbusFeature();
+});
+
+add_task(async function test() {
+ let tests = [
+ { locale: "en-US", home: "US", expectedOfflineDefault: true },
+ { locale: "en-US", home: "CA", expectedOfflineDefault: false },
+ { locale: "en-CA", home: "US", expectedOfflineDefault: true },
+ { locale: "en-CA", home: "CA", expectedOfflineDefault: false },
+ { locale: "en-GB", home: "US", expectedOfflineDefault: true },
+ { locale: "en-GB", home: "GB", expectedOfflineDefault: false },
+ { locale: "de", home: "US", expectedOfflineDefault: false },
+ { locale: "de", home: "DE", expectedOfflineDefault: false },
+ ];
+ for (let { locale, home, expectedOfflineDefault } of tests) {
+ await doTest({ locale, home, expectedOfflineDefault });
+ }
+});
+
+/**
+ * Sets the app's locale and region, calls
+ * `UrlbarPrefs.updateFirefoxSuggestScenario`, and asserts that the pref values
+ * are correct.
+ *
+ * @param {object} options
+ * Options object.
+ * @param {string} options.locale
+ * The locale to simulate.
+ * @param {string} options.home
+ * The "home" region to simulate.
+ * @param {boolean} options.expectedOfflineDefault
+ * The expected value of whether offline should be enabled by default given
+ * the locale and region.
+ */
+async function doTest({ locale, home, expectedOfflineDefault }) {
+ // Setup: Clear any user values and save original default-branch values.
+ for (let pref of PREFS) {
+ Services.prefs.clearUserPref(pref.name);
+ pref.originalDefault = Services.prefs
+ .getDefaultBranch(pref.name)
+ [pref.get]("");
+ }
+
+ // Set the region and locale, call the function, check the pref values.
+ Region._setHomeRegion(home, false);
+ await QuickSuggestTestUtils.withLocales([locale], async () => {
+ await UrlbarPrefs.updateFirefoxSuggestScenario();
+ for (let { name, get, expectedOfflineValue, expectedOtherValue } of PREFS) {
+ let expectedValue = expectedOfflineDefault
+ ? expectedOfflineValue
+ : expectedOtherValue;
+
+ // Check the default-branch value.
+ Assert.strictEqual(
+ Services.prefs.getDefaultBranch(name)[get](""),
+ expectedValue,
+ `Default pref value for ${name}, locale ${locale}, home ${home}`
+ );
+
+ // For good measure, also check the return value of `UrlbarPrefs.get`
+ // since we use it everywhere. The value should be the same as the
+ // default-branch value.
+ UrlbarPrefs.get(
+ name.replace("browser.urlbar.", ""),
+ expectedValue,
+ `UrlbarPrefs.get() value for ${name}, locale ${locale}, home ${home}`
+ );
+ }
+ });
+
+ // Teardown: Restore original default-branch values for the next task.
+ for (let { name, originalDefault, set } of PREFS) {
+ if (originalDefault === undefined) {
+ Services.prefs.deleteBranch(name);
+ } else {
+ Services.prefs.getDefaultBranch(name)[set]("", originalDefault);
+ }
+ }
+}
diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_positionInSuggestions.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_positionInSuggestions.js
new file mode 100644
index 0000000000..2d1cf728c7
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_positionInSuggestions.js
@@ -0,0 +1,487 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests for quick suggest result position specified in suggestions.
+ */
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarProviderHeuristicFallback:
+ "resource:///modules/UrlbarProviderHeuristicFallback.sys.mjs",
+ UrlbarProviderPlaces: "resource:///modules/UrlbarProviderPlaces.sys.mjs",
+ UrlbarProviderTabToSearch:
+ "resource:///modules/UrlbarProviderTabToSearch.sys.mjs",
+});
+
+const SPONSORED_SECOND_POSITION_RESULT = {
+ id: 1,
+ url: "http://example.com/?q=sponsored-second",
+ title: "sponsored second",
+ keywords: ["s-s"],
+ click_url: "http://click.reporting.test.com/",
+ impression_url: "http://impression.reporting.test.com/",
+ advertiser: "TestAdvertiser",
+ iab_category: "22 - Shopping",
+ position: 1,
+};
+const SPONSORED_NORMAL_POSITION_RESULT = {
+ id: 2,
+ url: "http://example.com/?q=sponsored-normal",
+ title: "sponsored normal",
+ keywords: ["s-n"],
+ click_url: "http://click.reporting.test.com/",
+ impression_url: "http://impression.reporting.test.com/",
+ advertiser: "TestAdvertiser",
+ iab_category: "22 - Shopping",
+};
+const NONSPONSORED_SECOND_POSITION_RESULT = {
+ id: 3,
+ url: "http://example.com/?q=nonsponsored-second",
+ title: "nonsponsored second",
+ keywords: ["n-s"],
+ click_url: "http://click.reporting.test.com/nonsponsored",
+ impression_url: "http://impression.reporting.test.com/nonsponsored",
+ advertiser: "TestAdvertiserNonSponsored",
+ iab_category: "5 - Education",
+ position: 1,
+};
+const NONSPONSORED_NORMAL_POSITION_RESULT = {
+ id: 4,
+ url: "http://example.com/?q=nonsponsored-normal",
+ title: "nonsponsored normal",
+ keywords: ["n-n"],
+ click_url: "http://click.reporting.test.com/nonsponsored",
+ impression_url: "http://impression.reporting.test.com/nonsponsored",
+ advertiser: "TestAdvertiserNonSponsored",
+ iab_category: "5 - Education",
+};
+const FIRST_POSITION_RESULT = {
+ id: 5,
+ url: "http://example.com/?q=first-position",
+ title: "first position suggest",
+ keywords: ["first-position"],
+ click_url: "http://click.reporting.test.com/first-position",
+ impression_url: "http://impression.reporting.test.com/first-position",
+ advertiser: "TestAdvertiserFirstPositionQuickSuggest",
+ iab_category: "22 - Shopping",
+ position: 0,
+};
+const SECOND_POSITION_RESULT = {
+ id: 6,
+ url: "http://example.com/?q=second-position",
+ title: "second position suggest",
+ keywords: ["second-position"],
+ click_url: "http://click.reporting.test.com/second-position",
+ impression_url: "http://impression.reporting.test.com/second-position",
+ advertiser: "TestAdvertiserSecondPositionQuickSuggest",
+ iab_category: "22 - Shopping",
+ position: 1,
+};
+const THIRD_POSITION_RESULT = {
+ id: 7,
+ url: "http://example.com/?q=third-position",
+ title: "third position suggest",
+ keywords: ["third-position"],
+ click_url: "http://click.reporting.test.com/third-position",
+ impression_url: "http://impression.reporting.test.com/third-position",
+ advertiser: "TestAdvertiserThirdPositionQuickSuggest",
+ iab_category: "22 - Shopping",
+ position: 2,
+};
+
+const TABTOSEARCH_ENGINE_DOMAIN_FOR_FIRST_POSITION_TEST =
+ "first-position.example.com";
+const TABTOSEARCH_ENGINE_DOMAIN_FOR_SECOND_POSITION_TEST =
+ "second-position.example.com";
+
+const SECOND_POSITION_INTERVENTION_RESULT = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/a" }
+);
+SECOND_POSITION_INTERVENTION_RESULT.suggestedIndex = 1;
+const SECOND_POSITION_INTERVENTION_RESULT_PROVIDER =
+ new UrlbarTestUtils.TestProvider({
+ results: [SECOND_POSITION_INTERVENTION_RESULT],
+ priority: 0,
+ name: "second_position_intervention_provider",
+ });
+
+const EXPECTED_GENERAL_HEURISTIC_RESULT = {
+ providerName: UrlbarProviderHeuristicFallback.name,
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ heuristic: true,
+};
+
+const EXPECTED_GENERAL_PLACES_RESULT = {
+ providerName: UrlbarProviderPlaces.name,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ heuristic: false,
+};
+
+const EXPECTED_GENERAL_TABTOSEARCH_RESULT = {
+ providerName: UrlbarProviderTabToSearch.name,
+ type: UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ heuristic: false,
+};
+
+const EXPECTED_GENERAL_INTERVENTION_RESULT = {
+ providerName: SECOND_POSITION_INTERVENTION_RESULT_PROVIDER.name,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ heuristic: false,
+};
+
+function createExpectedQuickSuggestResult(suggest) {
+ let isSponsored = suggest.iab_category !== "5 - Education";
+ return {
+ providerName: UrlbarProviderQuickSuggest.name,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ heuristic: false,
+ payload: {
+ telemetryType: isSponsored ? "adm_sponsored" : "adm_nonsponsored",
+ qsSuggestion: suggest.keywords[0],
+ title: suggest.title,
+ url: suggest.url,
+ originalUrl: suggest.url,
+ icon: null,
+ sponsoredImpressionUrl: suggest.impression_url,
+ sponsoredClickUrl: suggest.click_url,
+ sponsoredBlockId: suggest.id,
+ sponsoredAdvertiser: suggest.advertiser,
+ sponsoredIabCategory: suggest.iab_category,
+ isSponsored,
+ helpUrl: QuickSuggest.HELP_URL,
+ helpL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-learn-more-about-firefox-suggest"
+ : "firefox-suggest-urlbar-learn-more",
+ },
+ isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"),
+ blockL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-dismiss-firefox-suggest"
+ : "firefox-suggest-urlbar-block",
+ },
+ displayUrl: suggest.url,
+ source: "remote-settings",
+ },
+ };
+}
+
+const TEST_CASES = [
+ {
+ description: "Test for second placable sponsored suggest",
+ input: SPONSORED_SECOND_POSITION_RESULT.keywords[0],
+ prefs: {
+ "quicksuggest.allowPositionInSuggestions": true,
+ },
+ providers: [
+ UrlbarProviderHeuristicFallback.name,
+ UrlbarProviderQuickSuggest.name,
+ UrlbarProviderPlaces.name,
+ ],
+ expected: [
+ EXPECTED_GENERAL_HEURISTIC_RESULT,
+ createExpectedQuickSuggestResult(SPONSORED_SECOND_POSITION_RESULT),
+ EXPECTED_GENERAL_PLACES_RESULT,
+ ],
+ },
+ {
+ description: "Test for normal sponsored suggest",
+ input: SPONSORED_NORMAL_POSITION_RESULT.keywords[0],
+ prefs: {
+ "quicksuggest.allowPositionInSuggestions": true,
+ },
+ providers: [
+ UrlbarProviderHeuristicFallback.name,
+ UrlbarProviderQuickSuggest.name,
+ UrlbarProviderPlaces.name,
+ ],
+ expected: [
+ EXPECTED_GENERAL_HEURISTIC_RESULT,
+ EXPECTED_GENERAL_PLACES_RESULT,
+ createExpectedQuickSuggestResult(SPONSORED_NORMAL_POSITION_RESULT),
+ ],
+ },
+ {
+ description: "Test for second placable nonsponsored suggest",
+ input: NONSPONSORED_SECOND_POSITION_RESULT.keywords[0],
+ prefs: {
+ "quicksuggest.allowPositionInSuggestions": true,
+ },
+ providers: [
+ UrlbarProviderHeuristicFallback.name,
+ UrlbarProviderQuickSuggest.name,
+ UrlbarProviderPlaces.name,
+ ],
+ expected: [
+ EXPECTED_GENERAL_HEURISTIC_RESULT,
+ createExpectedQuickSuggestResult(NONSPONSORED_SECOND_POSITION_RESULT),
+ EXPECTED_GENERAL_PLACES_RESULT,
+ ],
+ },
+ {
+ description: "Test for normal nonsponsored suggest",
+ input: NONSPONSORED_NORMAL_POSITION_RESULT.keywords[0],
+ prefs: {
+ "quicksuggest.allowPositionInSuggestions": true,
+ },
+ providers: [
+ UrlbarProviderHeuristicFallback.name,
+ UrlbarProviderQuickSuggest.name,
+ UrlbarProviderPlaces.name,
+ ],
+ expected: [
+ EXPECTED_GENERAL_HEURISTIC_RESULT,
+ EXPECTED_GENERAL_PLACES_RESULT,
+ createExpectedQuickSuggestResult(NONSPONSORED_NORMAL_POSITION_RESULT),
+ ],
+ },
+ {
+ description:
+ "Test for second placable sponsored suggest but secondPosition pref is disabled",
+ input: SPONSORED_SECOND_POSITION_RESULT.keywords[0],
+ prefs: {
+ "quicksuggest.allowPositionInSuggestions": false,
+ },
+ providers: [
+ UrlbarProviderHeuristicFallback.name,
+ UrlbarProviderQuickSuggest.name,
+ UrlbarProviderPlaces.name,
+ ],
+ expected: [
+ EXPECTED_GENERAL_HEURISTIC_RESULT,
+ EXPECTED_GENERAL_PLACES_RESULT,
+ createExpectedQuickSuggestResult(SPONSORED_SECOND_POSITION_RESULT),
+ ],
+ },
+ {
+ description: "Test the results with multi providers having same index",
+ input: SECOND_POSITION_RESULT.keywords[0],
+ prefs: {
+ "quicksuggest.allowPositionInSuggestions": true,
+ },
+ providers: [
+ UrlbarProviderQuickSuggest.name,
+ UrlbarProviderTabToSearch.name,
+ SECOND_POSITION_INTERVENTION_RESULT_PROVIDER.name,
+ ],
+ expected: [
+ EXPECTED_GENERAL_TABTOSEARCH_RESULT,
+ createExpectedQuickSuggestResult(SECOND_POSITION_RESULT),
+ EXPECTED_GENERAL_INTERVENTION_RESULT,
+ ],
+ },
+ {
+ description: "Test the results with tab-to-search",
+ input: SECOND_POSITION_RESULT.keywords[0],
+ prefs: {
+ "quicksuggest.allowPositionInSuggestions": true,
+ },
+ providers: [
+ UrlbarProviderTabToSearch.name,
+ UrlbarProviderQuickSuggest.name,
+ ],
+ expected: [
+ EXPECTED_GENERAL_TABTOSEARCH_RESULT,
+ createExpectedQuickSuggestResult(SECOND_POSITION_RESULT),
+ ],
+ },
+ {
+ description: "Test the results with another intervention",
+ input: SECOND_POSITION_RESULT.keywords[0],
+ prefs: {
+ "quicksuggest.allowPositionInSuggestions": true,
+ },
+ providers: [
+ UrlbarProviderQuickSuggest.name,
+ SECOND_POSITION_INTERVENTION_RESULT_PROVIDER.name,
+ ],
+ expected: [
+ createExpectedQuickSuggestResult(SECOND_POSITION_RESULT),
+ EXPECTED_GENERAL_INTERVENTION_RESULT,
+ ],
+ },
+ {
+ description: "Test the results with heuristic and tab-to-search",
+ input: SECOND_POSITION_RESULT.keywords[0],
+ prefs: {
+ "quicksuggest.allowPositionInSuggestions": true,
+ },
+ providers: [
+ UrlbarProviderHeuristicFallback.name,
+ UrlbarProviderTabToSearch.name,
+ UrlbarProviderQuickSuggest.name,
+ ],
+ expected: [
+ EXPECTED_GENERAL_HEURISTIC_RESULT,
+ EXPECTED_GENERAL_TABTOSEARCH_RESULT,
+ createExpectedQuickSuggestResult(SECOND_POSITION_RESULT),
+ ],
+ },
+ {
+ description: "Test the results with heuristic tab-to-search and places",
+ input: SECOND_POSITION_RESULT.keywords[0],
+ prefs: {
+ "quicksuggest.allowPositionInSuggestions": true,
+ },
+ providers: [
+ UrlbarProviderHeuristicFallback.name,
+ UrlbarProviderTabToSearch.name,
+ UrlbarProviderQuickSuggest.name,
+ UrlbarProviderPlaces.name,
+ ],
+ expected: [
+ EXPECTED_GENERAL_HEURISTIC_RESULT,
+ EXPECTED_GENERAL_TABTOSEARCH_RESULT,
+ createExpectedQuickSuggestResult(SECOND_POSITION_RESULT),
+ EXPECTED_GENERAL_PLACES_RESULT,
+ ],
+ },
+ {
+ description: "Test the results with heuristic and another intervention",
+ input: SECOND_POSITION_RESULT.keywords[0],
+ prefs: {
+ "quicksuggest.allowPositionInSuggestions": true,
+ },
+ providers: [
+ UrlbarProviderHeuristicFallback.name,
+ UrlbarProviderQuickSuggest.name,
+ SECOND_POSITION_INTERVENTION_RESULT_PROVIDER.name,
+ ],
+ expected: [
+ EXPECTED_GENERAL_HEURISTIC_RESULT,
+ createExpectedQuickSuggestResult(SECOND_POSITION_RESULT),
+ EXPECTED_GENERAL_INTERVENTION_RESULT,
+ ],
+ },
+ {
+ description:
+ "Test the results with heuristic, another intervention and places",
+ input: SECOND_POSITION_RESULT.keywords[0],
+ prefs: {
+ "quicksuggest.allowPositionInSuggestions": true,
+ },
+ providers: [
+ UrlbarProviderHeuristicFallback.name,
+ UrlbarProviderQuickSuggest.name,
+ SECOND_POSITION_INTERVENTION_RESULT_PROVIDER.name,
+ UrlbarProviderPlaces.name,
+ ],
+ expected: [
+ EXPECTED_GENERAL_HEURISTIC_RESULT,
+ createExpectedQuickSuggestResult(SECOND_POSITION_RESULT),
+ EXPECTED_GENERAL_INTERVENTION_RESULT,
+ EXPECTED_GENERAL_PLACES_RESULT,
+ ],
+ },
+ {
+ description: "Test for 0 indexed quick suggest",
+ input: FIRST_POSITION_RESULT.keywords[0],
+ prefs: {
+ "quicksuggest.allowPositionInSuggestions": true,
+ },
+ providers: [
+ UrlbarProviderTabToSearch.name,
+ UrlbarProviderQuickSuggest.name,
+ ],
+ expected: [
+ createExpectedQuickSuggestResult(FIRST_POSITION_RESULT),
+ EXPECTED_GENERAL_TABTOSEARCH_RESULT,
+ ],
+ },
+ {
+ description: "Test for 2 indexed quick suggest",
+ input: THIRD_POSITION_RESULT.keywords[0],
+ prefs: {
+ "quicksuggest.allowPositionInSuggestions": true,
+ },
+ providers: [
+ UrlbarProviderQuickSuggest.name,
+ SECOND_POSITION_INTERVENTION_RESULT_PROVIDER.name,
+ ],
+ expected: [
+ EXPECTED_GENERAL_INTERVENTION_RESULT,
+ createExpectedQuickSuggestResult(THIRD_POSITION_RESULT),
+ ],
+ },
+];
+
+add_task(async function setup() {
+ UrlbarPrefs.set("quicksuggest.enabled", true);
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
+
+ // Setup for quick suggest result.
+ await QuickSuggestTestUtils.ensureQuickSuggestInit({
+ remoteSettingsResults: [
+ {
+ type: "data",
+ attachment: [
+ SPONSORED_SECOND_POSITION_RESULT,
+ SPONSORED_NORMAL_POSITION_RESULT,
+ NONSPONSORED_SECOND_POSITION_RESULT,
+ NONSPONSORED_NORMAL_POSITION_RESULT,
+ FIRST_POSITION_RESULT,
+ SECOND_POSITION_RESULT,
+ THIRD_POSITION_RESULT,
+ ],
+ },
+ ],
+ });
+
+ // Setup for places result.
+ await PlacesUtils.history.clear();
+ await PlacesTestUtils.addVisits([
+ "http://example.com/" + SPONSORED_SECOND_POSITION_RESULT.keywords[0],
+ "http://example.com/" + SPONSORED_NORMAL_POSITION_RESULT.keywords[0],
+ "http://example.com/" + NONSPONSORED_SECOND_POSITION_RESULT.keywords[0],
+ "http://example.com/" + NONSPONSORED_NORMAL_POSITION_RESULT.keywords[0],
+ "http://example.com/" + SECOND_POSITION_RESULT.keywords[0],
+ ]);
+
+ // Setup for tab-to-search result.
+ await SearchTestUtils.installSearchExtension({
+ name: "first",
+ search_url: `https://${TABTOSEARCH_ENGINE_DOMAIN_FOR_FIRST_POSITION_TEST}/`,
+ });
+ await SearchTestUtils.installSearchExtension({
+ name: "second",
+ search_url: `https://${TABTOSEARCH_ENGINE_DOMAIN_FOR_SECOND_POSITION_TEST}/`,
+ });
+
+ /// Setup for another intervention result.
+ UrlbarProvidersManager.registerProvider(
+ SECOND_POSITION_INTERVENTION_RESULT_PROVIDER
+ );
+});
+
+add_task(async function basic() {
+ for (const { description, input, prefs, providers, expected } of TEST_CASES) {
+ info(description);
+
+ for (let name in prefs) {
+ UrlbarPrefs.set(name, prefs[name]);
+ }
+
+ const context = createContext(input, {
+ providers,
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: expected,
+ });
+
+ for (let name in prefs) {
+ UrlbarPrefs.clear(name);
+ }
+ }
+});
diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_topPicks.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_topPicks.js
new file mode 100644
index 0000000000..dd8b9dc575
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_topPicks.js
@@ -0,0 +1,284 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Tests top pick quick suggest results. "Top picks" refers to two different
+// concepts:
+//
+// (1) Any type of suggestion from Merino can have a boolean property called
+// `is_top_pick`. When true, Firefox should show the suggestion using the
+// "best match" UI treatment (labeled "top pick" in the UI) that makes a
+// result's row larger than usual and sets `suggestedIndex` to 1. However,
+// the treatment must be enabled on Firefox via the `bestMatch.enabled`
+// feature gate pref (Nimbus variable `bestMatchEnabled`) and the
+// `suggest.bestMatch` pref, which corresponds to a checkbox in
+// about:preferences. If the UI treatment is not enabled, Firefox should
+// show the suggestion as usual.
+// (2) There is a Merino provider called "top_picks" that returns a specific
+// type of suggestion called "navigational suggestions". These suggestions
+// also have `is_top_pick` set to true.
+//
+// This file tests aspects of both concepts.
+
+"use strict";
+
+const SUGGESTION_SEARCH_STRING = "example";
+const SUGGESTION_URL = "http://example.com/";
+const SUGGESTION_URL_WWW = "http://www.example.com/";
+const SUGGESTION_URL_DISPLAY = "http://example.com";
+
+const MERINO_SUGGESTIONS = [
+ {
+ is_top_pick: true,
+ provider: "top_picks",
+ url: SUGGESTION_URL,
+ title: "title",
+ icon: "icon",
+ is_sponsored: false,
+ score: 1,
+ },
+];
+
+add_setup(async function init() {
+ UrlbarPrefs.set("quicksuggest.enabled", true);
+ UrlbarPrefs.set("bestMatch.enabled", true);
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
+
+ // Disable search suggestions so we don't hit the network.
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", false);
+
+ await QuickSuggestTestUtils.ensureQuickSuggestInit({
+ merinoSuggestions: MERINO_SUGGESTIONS,
+ });
+});
+
+// When non-sponsored suggestions are disabled, navigational suggestions should
+// be disabled.
+add_task(async function nonsponsoredDisabled() {
+ // Disable sponsored suggestions. Navigational suggestions are non-sponsored,
+ // so doing this should not prevent them from being enabled.
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", false);
+
+ // First make sure the suggestion is added when non-sponsored suggestions are
+ // enabled.
+ await check_results({
+ context: createContext(SUGGESTION_SEARCH_STRING, {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ }),
+ matches: [
+ makeExpectedResult({
+ isBestMatch: true,
+ suggestedIndex: 1,
+ }),
+ ],
+ });
+
+ // Now disable them.
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false);
+ await check_results({
+ context: createContext(SUGGESTION_SEARCH_STRING, {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ }),
+ matches: [],
+ });
+
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
+ UrlbarPrefs.clear("suggest.quicksuggest.sponsored");
+});
+
+// Test that bestMatch navigational suggestion results are not shown when there
+// is a heuristic result for the same domain.
+add_task(async function heuristicDeduplication() {
+ let expectedNavSuggestResult = makeExpectedResult({
+ isBestMatch: true,
+ suggestedIndex: 1,
+ dupedHeuristic: false,
+ });
+
+ let scenarios = [
+ [SUGGESTION_URL, false],
+ [SUGGESTION_URL_WWW, false],
+ ["http://exampledomain.com/", true],
+ ];
+
+ // Stub `UrlbarProviderQuickSuggest.startQuery()` so we can collect the
+ // results it adds for each query.
+ let addedResults = [];
+ let sandbox = sinon.createSandbox();
+ let startQueryStub = sandbox.stub(UrlbarProviderQuickSuggest, "startQuery");
+ startQueryStub.callsFake((queryContext, add) => {
+ let fakeAdd = (provider, result) => {
+ addedResults.push(result);
+ add(provider, result);
+ };
+ return startQueryStub.wrappedMethod.call(
+ UrlbarProviderQuickSuggest,
+ queryContext,
+ fakeAdd
+ );
+ });
+
+ for (let [url, expectBestMatch] of scenarios) {
+ await PlacesTestUtils.addVisits(url);
+
+ // Do a search and check the results.
+ let context = createContext(SUGGESTION_SEARCH_STRING, {
+ providers: [UrlbarProviderQuickSuggest.name, UrlbarProviderAutofill.name],
+ isPrivate: false,
+ });
+ const EXPECTED_AUTOFILL_RESULT = makeVisitResult(context, {
+ uri: url,
+ title: `test visit for ${url}`,
+ heuristic: true,
+ });
+ await check_results({
+ context,
+ matches: expectBestMatch
+ ? [EXPECTED_AUTOFILL_RESULT, expectedNavSuggestResult]
+ : [EXPECTED_AUTOFILL_RESULT],
+ });
+
+ // Regardless of whether it was shown, one result should have been added and
+ // its `payload.dupedHeuristic` should be set properly.
+ Assert.equal(
+ addedResults.length,
+ 1,
+ "The provider should have added one result"
+ );
+ Assert.equal(
+ !addedResults[0].payload.dupedHeuristic,
+ expectBestMatch,
+ "dupedHeuristic should be the opposite of expectBestMatch"
+ );
+ addedResults = [];
+
+ await PlacesUtils.history.clear();
+ }
+
+ sandbox.restore();
+});
+
+add_task(async function prefs_0() {
+ await doPrefsTest({
+ bestMatchEnabled: false,
+ suggestBestMatch: false,
+ expected: {
+ isBestMatch: false,
+ suggestedIndex: -1,
+ },
+ });
+});
+
+add_task(async function prefs_1() {
+ await doPrefsTest({
+ bestMatchEnabled: false,
+ suggestBestMatch: true,
+ expected: {
+ isBestMatch: false,
+ suggestedIndex: -1,
+ },
+ });
+});
+
+add_task(async function prefs_2() {
+ await doPrefsTest({
+ bestMatchEnabled: true,
+ suggestBestMatch: false,
+ expected: {
+ isBestMatch: false,
+ suggestedIndex: -1,
+ },
+ });
+});
+
+add_task(async function prefs_3() {
+ await doPrefsTest({
+ bestMatchEnabled: true,
+ suggestBestMatch: true,
+ expected: {
+ isBestMatch: true,
+ suggestedIndex: 1,
+ },
+ });
+});
+
+async function doPrefsTest({
+ bestMatchEnabled,
+ suggestBestMatch,
+ expected: { isBestMatch, suggestedIndex },
+}) {
+ UrlbarPrefs.set("bestMatch.enabled", bestMatchEnabled);
+ UrlbarPrefs.set("suggest.bestmatch", suggestBestMatch);
+
+ // The mock suggestion has `provider` set to "top_picks", but Firefox should
+ // use only `is_top_pick` to determine whether it should be shown as best
+ // match, regardless of the provider. To make sure, change the provider to
+ // something else.
+ let originalProviders = [];
+ let provider = "some_unknown_provider";
+ for (let s of MerinoTestUtils.server.response.body.suggestions) {
+ originalProviders.push(s.provider);
+ s.provider = provider;
+ }
+
+ await check_results({
+ context: createContext("test", {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ }),
+ matches: [
+ makeExpectedResult({
+ isBestMatch,
+ suggestedIndex,
+ telemetryType: provider,
+ }),
+ ],
+ });
+
+ UrlbarPrefs.clear("bestMatch.enabled");
+ UrlbarPrefs.clear("suggest.bestmatch");
+
+ // Restore the original provider.
+ for (let s of MerinoTestUtils.server.response.body.suggestions) {
+ s.provider = originalProviders.shift();
+ }
+}
+
+function makeExpectedResult({
+ isBestMatch,
+ suggestedIndex,
+ dupedHeuristic,
+ telemetryType = "top_picks",
+}) {
+ let result = {
+ isBestMatch,
+ suggestedIndex,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ heuristic: false,
+ payload: {
+ dupedHeuristic,
+ telemetryType,
+ title: "title",
+ url: SUGGESTION_URL,
+ displayUrl: SUGGESTION_URL_DISPLAY,
+ icon: "icon",
+ isSponsored: false,
+ source: "merino",
+ helpUrl: QuickSuggest.HELP_URL,
+ helpL10n: {
+ id: "urlbar-result-menu-learn-more-about-firefox-suggest",
+ },
+ isBlockable: true,
+ blockL10n: {
+ id: "urlbar-result-menu-dismiss-firefox-suggest",
+ },
+ },
+ };
+ if (typeof dupedHeuristic == "boolean") {
+ result.payload.dupedHeuristic = dupedHeuristic;
+ }
+ return result;
+}
diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_suggestionsMap.js b/browser/components/urlbar/tests/quicksuggest/unit/test_suggestionsMap.js
new file mode 100644
index 0000000000..598f0d89a5
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/unit/test_suggestionsMap.js
@@ -0,0 +1,217 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Tests the chunking feature of `RemoteSettingsClient.#addResults()`.
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ SuggestionsMap:
+ "resource:///modules/urlbar/private/QuickSuggestRemoteSettings.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ ObjectUtils: "resource://gre/modules/ObjectUtils.jsm",
+});
+
+// This overrides `SuggestionsMap.chunkSize`. Testing the actual value can make
+// the test run too long. This is OK because the correctness of the chunking
+// behavior doesn't depend on the chunk size.
+const TEST_CHUNK_SIZE = 100;
+
+add_task(async function init() {
+ // Sanity check the actual `chunkSize` value.
+ Assert.equal(
+ typeof SuggestionsMap.chunkSize,
+ "number",
+ "Sanity check: chunkSize is a number"
+ );
+ Assert.greater(SuggestionsMap.chunkSize, 0, "Sanity check: chunkSize > 0");
+
+ // Set our test value.
+ SuggestionsMap.chunkSize = TEST_CHUNK_SIZE;
+});
+
+// Tests many suggestions with one keyword each.
+add_task(async function chunking_singleKeyword() {
+ let suggestionCounts = [
+ 1 * SuggestionsMap.chunkSize - 1,
+ 1 * SuggestionsMap.chunkSize,
+ 1 * SuggestionsMap.chunkSize + 1,
+ 2 * SuggestionsMap.chunkSize - 1,
+ 2 * SuggestionsMap.chunkSize,
+ 2 * SuggestionsMap.chunkSize + 1,
+ 3 * SuggestionsMap.chunkSize - 1,
+ 3 * SuggestionsMap.chunkSize,
+ 3 * SuggestionsMap.chunkSize + 1,
+ ];
+ for (let count of suggestionCounts) {
+ await doChunkingTest(count, 1);
+ }
+});
+
+// Tests a small number of suggestions with many keywords each.
+add_task(async function chunking_manyKeywords() {
+ let keywordCounts = [
+ 1 * SuggestionsMap.chunkSize - 1,
+ 1 * SuggestionsMap.chunkSize,
+ 1 * SuggestionsMap.chunkSize + 1,
+ 2 * SuggestionsMap.chunkSize - 1,
+ 2 * SuggestionsMap.chunkSize,
+ 2 * SuggestionsMap.chunkSize + 1,
+ 3 * SuggestionsMap.chunkSize - 1,
+ 3 * SuggestionsMap.chunkSize,
+ 3 * SuggestionsMap.chunkSize + 1,
+ ];
+ for (let suggestionCount = 1; suggestionCount <= 3; suggestionCount++) {
+ for (let keywordCount of keywordCounts) {
+ await doChunkingTest(suggestionCount, keywordCount);
+ }
+ }
+});
+
+async function doChunkingTest(suggestionCount, keywordCountPerSuggestion) {
+ info(
+ "Running chunking test: " +
+ JSON.stringify({ suggestionCount, keywordCountPerSuggestion })
+ );
+
+ // Create `suggestionCount` suggestions, each with `keywordCountPerSuggestion`
+ // keywords.
+ let suggestions = [];
+ for (let i = 0; i < suggestionCount; i++) {
+ let keywords = [];
+ for (let k = 0; k < keywordCountPerSuggestion; k++) {
+ keywords.push(`keyword-${i}-${k}`);
+ }
+ suggestions.push({
+ keywords,
+ id: i,
+ url: "http://example.com/" + i,
+ title: "Suggestion " + i,
+ click_url: "http://example.com/click",
+ impression_url: "http://example.com/impression",
+ advertiser: "TestAdvertiser",
+ iab_category: "22 - Shopping",
+ });
+ }
+
+ // Add the suggestions.
+ let map = new SuggestionsMap();
+ await map.add(suggestions);
+
+ // Make sure all keyword-suggestion pairs have been added.
+ for (let i = 0; i < suggestionCount; i++) {
+ for (let k = 0; k < keywordCountPerSuggestion; k++) {
+ let keyword = `keyword-${i}-${k}`;
+
+ // Check the map. Logging all assertions takes a ton of time and makes the
+ // test run much longer than it otherwise would, especially if `chunkSize`
+ // is large, so only log failing assertions.
+ let actualSuggestions = map.get(keyword);
+ if (!ObjectUtils.deepEqual(actualSuggestions, [suggestions[i]])) {
+ Assert.deepEqual(
+ actualSuggestions,
+ [suggestions[i]],
+ `Suggestion ${i} is present for keyword ${keyword}`
+ );
+ }
+ }
+ }
+}
+
+add_task(async function duplicateKeywords() {
+ let suggestions = [
+ {
+ title: "suggestion 0",
+ keywords: ["a", "a", "a", "b", "b", "c"],
+ },
+ {
+ title: "suggestion 1",
+ keywords: ["b", "c", "d"],
+ },
+ {
+ title: "suggestion 2",
+ keywords: ["c", "d", "e"],
+ },
+ {
+ title: "suggestion 3",
+ keywords: ["f", "f"],
+ },
+ ];
+
+ let expectedIndexesByKeyword = {
+ a: [0],
+ b: [0, 1],
+ c: [0, 1, 2],
+ d: [1, 2],
+ e: [2],
+ f: [3],
+ };
+
+ let map = new SuggestionsMap();
+ await map.add(suggestions);
+
+ for (let [keyword, indexes] of Object.entries(expectedIndexesByKeyword)) {
+ Assert.deepEqual(
+ map.get(keyword),
+ indexes.map(i => suggestions[i]),
+ "get() with keyword: " + keyword
+ );
+ }
+});
+
+add_task(async function mapKeywords() {
+ let suggestions = [
+ {
+ title: "suggestion 0",
+ keywords: ["a", "a", "a", "b", "b", "c"],
+ },
+ {
+ title: "suggestion 1",
+ keywords: ["b", "c", "d"],
+ },
+ {
+ title: "suggestion 2",
+ keywords: ["c", "d", "e"],
+ },
+ {
+ title: "suggestion 3",
+ keywords: ["f", "f"],
+ },
+ ];
+
+ let expectedIndexesByKeyword = {
+ a: [],
+ b: [],
+ c: [],
+ d: [],
+ e: [],
+ f: [],
+ ax: [0],
+ bx: [0, 1],
+ cx: [0, 1, 2],
+ dx: [1, 2],
+ ex: [2],
+ fx: [3],
+ fy: [3],
+ fz: [3],
+ };
+
+ let map = new SuggestionsMap();
+ await map.add(suggestions, keyword => {
+ if (keyword == "f") {
+ return [keyword + "x", keyword + "y", keyword + "z"];
+ }
+ return [keyword + "x"];
+ });
+
+ for (let [keyword, indexes] of Object.entries(expectedIndexesByKeyword)) {
+ Assert.deepEqual(
+ map.get(keyword),
+ indexes.map(i => suggestions[i]),
+ "get() with keyword: " + keyword
+ );
+ }
+});
diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_weather.js b/browser/components/urlbar/tests/quicksuggest/unit/test_weather.js
new file mode 100644
index 0000000000..04fbc0b9d8
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/unit/test_weather.js
@@ -0,0 +1,1394 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Tests the quick suggest weather feature.
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ MockRegistrar: "resource://testing-common/MockRegistrar.sys.mjs",
+ UrlbarProviderWeather: "resource:///modules/UrlbarProviderWeather.sys.mjs",
+});
+
+const HISTOGRAM_LATENCY = "FX_URLBAR_MERINO_LATENCY_WEATHER_MS";
+const HISTOGRAM_RESPONSE = "FX_URLBAR_MERINO_RESPONSE_WEATHER";
+
+const { WEATHER_RS_DATA, WEATHER_SUGGESTION } = MerinoTestUtils;
+
+add_task(async function init() {
+ await QuickSuggestTestUtils.ensureQuickSuggestInit({
+ remoteSettingsResults: [
+ {
+ type: "weather",
+ weather: WEATHER_RS_DATA,
+ },
+ ],
+ });
+ UrlbarPrefs.set("quicksuggest.enabled", true);
+
+ await MerinoTestUtils.initWeather();
+
+ // Give this a small value so it doesn't delay the test too long. Choose a
+ // value that's unlikely to be used anywhere else in the test so that when
+ // `lastFetchTimeMs` is expected to be `fetchDelayAfterComingOnlineMs`, we can
+ // be sure the value actually came from `fetchDelayAfterComingOnlineMs`.
+ QuickSuggest.weather._test_fetchDelayAfterComingOnlineMs = 53;
+});
+
+// The feature should be properly uninitialized when it's disabled and then
+// re-initialized when it's re-enabled. This task disables the feature using the
+// feature gate pref.
+add_task(async function disableAndEnable_featureGate() {
+ await doBasicDisableAndEnableTest("weather.featureGate");
+});
+
+// The feature should be properly uninitialized when it's disabled and then
+// re-initialized when it's re-enabled. This task disables the feature using the
+// suggest pref.
+add_task(async function disableAndEnable_suggestPref() {
+ await doBasicDisableAndEnableTest("suggest.weather");
+});
+
+async function doBasicDisableAndEnableTest(pref) {
+ // Sanity check initial state.
+ assertEnabled({
+ message: "Sanity check initial state",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+
+ // Disable the feature. It should be immediately uninitialized.
+ UrlbarPrefs.set(pref, false);
+ assertDisabled({
+ message: "After disabling",
+ pendingFetchCount: 0,
+ });
+
+ // No suggestion should be returned for a search.
+ let context = createContext(MerinoTestUtils.WEATHER_KEYWORD, {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [],
+ });
+
+ let histograms = MerinoTestUtils.getAndClearHistograms({
+ extraLatency: HISTOGRAM_LATENCY,
+ extraResponse: HISTOGRAM_RESPONSE,
+ });
+
+ // Re-enable the feature. It should be immediately initialized and a fetch
+ // should start.
+ info("Re-enable the feature");
+ let fetchPromise = QuickSuggest.weather.waitForFetches();
+ UrlbarPrefs.set(pref, true);
+ assertEnabled({
+ message: "Immediately after re-enabling",
+ hasSuggestion: false,
+ pendingFetchCount: 1,
+ });
+
+ await fetchPromise;
+ assertEnabled({
+ message: "After awaiting fetch",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+
+ Assert.equal(
+ QuickSuggest.weather._test_merino.lastFetchStatus,
+ "success",
+ "The request successfully finished"
+ );
+ MerinoTestUtils.checkAndClearHistograms({
+ histograms,
+ response: "success",
+ latencyRecorded: true,
+ client: QuickSuggest.weather._test_merino,
+ });
+
+ // The suggestion should be returned for a search.
+ context = createContext(MerinoTestUtils.WEATHER_KEYWORD, {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [makeExpectedResult()],
+ });
+}
+
+add_task(async function keywordsNotDefined() {
+ // Sanity check initial state.
+ assertEnabled({
+ message: "Sanity check initial state",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+
+ // Set RS data without any keywords. Fetching should immediately stop.
+ await QuickSuggestTestUtils.setRemoteSettingsResults([
+ {
+ type: "weather",
+ weather: {},
+ },
+ ]);
+ assertDisabled({
+ message: "After setting RS data without keywords",
+ pendingFetchCount: 0,
+ });
+
+ // No suggestion should be returned for a search.
+ let context = createContext(MerinoTestUtils.WEATHER_KEYWORD, {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [],
+ });
+
+ // Set keywords. Fetching should immediately start.
+ info("Setting keywords");
+ let fetchPromise = QuickSuggest.weather.waitForFetches();
+ await QuickSuggestTestUtils.setRemoteSettingsResults([
+ {
+ type: "weather",
+ weather: MerinoTestUtils.WEATHER_RS_DATA,
+ },
+ ]);
+ assertEnabled({
+ message: "Immediately after setting keywords",
+ hasSuggestion: false,
+ pendingFetchCount: 1,
+ });
+
+ await fetchPromise;
+ assertEnabled({
+ message: "After awaiting fetch",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+
+ Assert.equal(
+ QuickSuggest.weather._test_merino.lastFetchStatus,
+ "success",
+ "The request successfully finished"
+ );
+
+ // The suggestion should be returned for a search.
+ context = createContext(MerinoTestUtils.WEATHER_KEYWORD, {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [makeExpectedResult()],
+ });
+});
+
+// Disables and re-enables the feature without waiting for any intermediate
+// fetches to complete, using the following steps:
+//
+// 1. Disable
+// 2. Enable
+// 3. Disable again
+//
+// At this point, the fetch from step 2 will remain ongoing but once it finishes
+// it should be discarded since the feature is disabled.
+add_task(async function disableAndEnable_immediate1() {
+ // Sanity check initial state.
+ assertEnabled({
+ message: "Sanity check initial state",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+
+ // Disable the feature. It should be immediately uninitialized.
+ UrlbarPrefs.set("weather.featureGate", false);
+ assertDisabled({
+ message: "After disabling",
+ pendingFetchCount: 0,
+ });
+
+ // Re-enable the feature. It should be immediately initialized and a fetch
+ // should start.
+ let fetchPromise = QuickSuggest.weather.waitForFetches();
+ UrlbarPrefs.set("weather.featureGate", true);
+ assertEnabled({
+ message: "Immediately after re-enabling",
+ hasSuggestion: false,
+ pendingFetchCount: 1,
+ });
+
+ // Disable it again. The fetch will remain ongoing since pending fetches
+ // aren't stopped when the feature is disabled.
+ UrlbarPrefs.set("weather.featureGate", false);
+ assertDisabled({
+ message: "After disabling again",
+ pendingFetchCount: 1,
+ });
+
+ // Wait for the fetch to finish.
+ await fetchPromise;
+
+ // The fetched suggestion should be discarded and the feature should remain
+ // uninitialized.
+ assertDisabled({
+ message: "After awaiting fetch",
+ pendingFetchCount: 0,
+ });
+
+ // Clean up by re-enabling the feature for the remaining tasks.
+ fetchPromise = QuickSuggest.weather.waitForFetches();
+ UrlbarPrefs.set("weather.featureGate", true);
+ await fetchPromise;
+ assertEnabled({
+ message: "On cleanup",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+});
+
+// Disables and re-enables the feature without waiting for any intermediate
+// fetches to complete, using the following steps:
+//
+// 1. Disable
+// 2. Enable
+// 3. Disable again
+// 4. Enable again
+//
+// At this point, the fetches from steps 2 and 4 will remain ongoing. The fetch
+// from step 2 should be discarded.
+add_task(async function disableAndEnable_immediate2() {
+ // Sanity check initial state.
+ assertEnabled({
+ message: "Sanity check initial state",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+
+ // Disable the feature. It should be immediately uninitialized.
+ UrlbarPrefs.set("weather.featureGate", false);
+ assertDisabled({
+ message: "After disabling",
+ pendingFetchCount: 0,
+ });
+
+ // Re-enable the feature. It should be immediately initialized and a fetch
+ // should start.
+ UrlbarPrefs.set("weather.featureGate", true);
+ assertEnabled({
+ message: "Immediately after re-enabling",
+ hasSuggestion: false,
+ pendingFetchCount: 1,
+ });
+
+ // Disable it again. The fetch will remain ongoing since pending fetches
+ // aren't stopped when the feature is disabled.
+ UrlbarPrefs.set("weather.featureGate", false);
+ assertDisabled({
+ message: "After disabling again",
+ pendingFetchCount: 1,
+ });
+
+ // Re-enable it. A new fetch should start, so now there will be two pending
+ // fetches.
+ let fetchPromise = QuickSuggest.weather.waitForFetches();
+ UrlbarPrefs.set("weather.featureGate", true);
+ assertEnabled({
+ message: "Immediately after re-enabling again",
+ hasSuggestion: false,
+ pendingFetchCount: 2,
+ });
+
+ // Wait for both fetches to finish.
+ await fetchPromise;
+ assertEnabled({
+ message: "Immediately after re-enabling again",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+});
+
+// A fetch that doesn't return a suggestion should cause the last-fetched
+// suggestion to be discarded.
+add_task(async function noSuggestion() {
+ assertEnabled({
+ message: "Sanity check initial state",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+
+ let histograms = MerinoTestUtils.getAndClearHistograms({
+ extraLatency: HISTOGRAM_LATENCY,
+ extraResponse: HISTOGRAM_RESPONSE,
+ });
+
+ let { suggestions } = MerinoTestUtils.server.response.body;
+ MerinoTestUtils.server.response.body.suggestions = [];
+
+ await QuickSuggest.weather._test_fetch();
+
+ assertEnabled({
+ message: "After fetch",
+ hasSuggestion: false,
+ pendingFetchCount: 0,
+ });
+ Assert.equal(
+ QuickSuggest.weather._test_merino.lastFetchStatus,
+ "no_suggestion",
+ "The request successfully finished without suggestions"
+ );
+ MerinoTestUtils.checkAndClearHistograms({
+ histograms,
+ response: "no_suggestion",
+ latencyRecorded: true,
+ client: QuickSuggest.weather._test_merino,
+ });
+
+ let context = createContext(MerinoTestUtils.WEATHER_KEYWORD, {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [],
+ });
+
+ MerinoTestUtils.server.response.body.suggestions = suggestions;
+
+ // Clean up by forcing another fetch so the suggestion is non-null for the
+ // remaining tasks.
+ await QuickSuggest.weather._test_fetch();
+ assertEnabled({
+ message: "On cleanup",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+});
+
+// A network error should cause the last-fetched suggestion to be discarded.
+add_task(async function networkError() {
+ assertEnabled({
+ message: "Sanity check initial state",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+
+ let histograms = MerinoTestUtils.getAndClearHistograms({
+ extraLatency: HISTOGRAM_LATENCY,
+ extraResponse: HISTOGRAM_RESPONSE,
+ });
+
+ // Set the weather fetch timeout high enough that the network error exception
+ // will happen first. See `MerinoTestUtils.withNetworkError()`.
+ QuickSuggest.weather._test_setTimeoutMs(10000);
+
+ await MerinoTestUtils.server.withNetworkError(async () => {
+ await QuickSuggest.weather._test_fetch();
+ });
+
+ QuickSuggest.weather._test_setTimeoutMs(-1);
+
+ assertEnabled({
+ message: "After fetch",
+ hasSuggestion: false,
+ pendingFetchCount: 0,
+ });
+ Assert.equal(
+ QuickSuggest.weather._test_merino.lastFetchStatus,
+ "network_error",
+ "The request failed with a network error"
+ );
+ MerinoTestUtils.checkAndClearHistograms({
+ histograms,
+ response: "network_error",
+ latencyRecorded: false,
+ client: QuickSuggest.weather._test_merino,
+ });
+
+ let context = createContext(MerinoTestUtils.WEATHER_KEYWORD, {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [],
+ });
+
+ // Clean up by forcing another fetch so the suggestion is non-null for the
+ // remaining tasks.
+ await QuickSuggest.weather._test_fetch();
+ assertEnabled({
+ message: "On cleanup",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+});
+
+// An HTTP error should cause the last-fetched suggestion to be discarded.
+add_task(async function httpError() {
+ assertEnabled({
+ message: "Sanity check initial state",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+
+ let histograms = MerinoTestUtils.getAndClearHistograms({
+ extraLatency: HISTOGRAM_LATENCY,
+ extraResponse: HISTOGRAM_RESPONSE,
+ });
+
+ MerinoTestUtils.server.response = { status: 500 };
+ await QuickSuggest.weather._test_fetch();
+
+ assertEnabled({
+ message: "After fetch",
+ hasSuggestion: false,
+ pendingFetchCount: 0,
+ });
+ Assert.equal(
+ QuickSuggest.weather._test_merino.lastFetchStatus,
+ "http_error",
+ "The request failed with an HTTP error"
+ );
+ MerinoTestUtils.checkAndClearHistograms({
+ histograms,
+ response: "http_error",
+ latencyRecorded: true,
+ client: QuickSuggest.weather._test_merino,
+ });
+
+ let context = createContext(MerinoTestUtils.WEATHER_KEYWORD, {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [],
+ });
+
+ // Clean up by forcing another fetch so the suggestion is non-null for the
+ // remaining tasks.
+ MerinoTestUtils.server.reset();
+ MerinoTestUtils.server.response.body.suggestions = [WEATHER_SUGGESTION];
+ await QuickSuggest.weather._test_fetch();
+ assertEnabled({
+ message: "On cleanup",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+});
+
+// A fetch that doesn't return a suggestion due to a client timeout should cause
+// the last-fetched suggestion to be discarded.
+add_task(async function clientTimeout() {
+ assertEnabled({
+ message: "Sanity check initial state",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+
+ let histograms = MerinoTestUtils.getAndClearHistograms({
+ extraLatency: HISTOGRAM_LATENCY,
+ extraResponse: HISTOGRAM_RESPONSE,
+ });
+
+ // Make the server return a delayed response so the Merino client times out
+ // waiting for it.
+ MerinoTestUtils.server.response.delay = 400;
+
+ // Make the client time out immediately.
+ QuickSuggest.weather._test_setTimeoutMs(1);
+
+ // Set up a promise that will be resolved when the client finally receives the
+ // response.
+ let responsePromise = QuickSuggest.weather._test_merino.waitForNextResponse();
+
+ await QuickSuggest.weather._test_fetch();
+
+ assertEnabled({
+ message: "After fetch",
+ hasSuggestion: false,
+ pendingFetchCount: 0,
+ });
+ Assert.equal(
+ QuickSuggest.weather._test_merino.lastFetchStatus,
+ "timeout",
+ "The request timed out"
+ );
+ MerinoTestUtils.checkAndClearHistograms({
+ histograms,
+ response: "timeout",
+ latencyRecorded: false,
+ latencyStopwatchRunning: true,
+ client: QuickSuggest.weather._test_merino,
+ });
+
+ let context = createContext(MerinoTestUtils.WEATHER_KEYWORD, {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [],
+ });
+
+ // Await the response.
+ await responsePromise;
+
+ // The `checkAndClearHistograms()` call above cleared the histograms. After
+ // that, nothing else should have been recorded for the response.
+ MerinoTestUtils.checkAndClearHistograms({
+ histograms,
+ response: null,
+ latencyRecorded: true,
+ client: QuickSuggest.weather._test_merino,
+ });
+
+ QuickSuggest.weather._test_setTimeoutMs(-1);
+ delete MerinoTestUtils.server.response.delay;
+
+ // Clean up by forcing another fetch so the suggestion is non-null for the
+ // remaining tasks.
+ await QuickSuggest.weather._test_fetch();
+ assertEnabled({
+ message: "On cleanup",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+});
+
+// Locale task for when this test runs on an en-US OS.
+add_task(async function locale_enUS() {
+ await doLocaleTest({
+ shouldRunTask: osLocale => osLocale == "en-US",
+ osUnit: "f",
+ unitsByLocale: {
+ "en-US": "f",
+ // When the app's locale is set to any en-* locale, F will be used because
+ // `regionalPrefsLocales` will prefer the en-US OS locale.
+ "en-CA": "f",
+ "en-GB": "f",
+ de: "c",
+ },
+ });
+});
+
+// Locale task for when this test runs on a non-US English OS.
+add_task(async function locale_nonUSEnglish() {
+ await doLocaleTest({
+ shouldRunTask: osLocale => osLocale.startsWith("en") && osLocale != "en-US",
+ osUnit: "c",
+ unitsByLocale: {
+ // When the app's locale is set to en-US, C will be used because
+ // `regionalPrefsLocales` will prefer the non-US English OS locale.
+ "en-US": "c",
+ "en-CA": "c",
+ "en-GB": "c",
+ de: "c",
+ },
+ });
+});
+
+// Locale task for when this test runs on a non-English OS.
+add_task(async function locale_nonEnglish() {
+ await doLocaleTest({
+ shouldRunTask: osLocale => !osLocale.startsWith("en"),
+ osUnit: "c",
+ unitsByLocale: {
+ "en-US": "f",
+ "en-CA": "c",
+ "en-GB": "c",
+ de: "c",
+ },
+ });
+});
+
+/**
+ * Testing locales is tricky due to the weather feature's use of
+ * `Services.locale.regionalPrefsLocales`. By default `regionalPrefsLocales`
+ * prefers the OS locale if its language is the same as the app locale's
+ * language; otherwise it prefers the app locale. For example, assuming the OS
+ * locale is en-CA, then if the app locale is en-US it will prefer en-CA since
+ * both are English, but if the app locale is de it will prefer de. If the pref
+ * `intl.regional_prefs.use_os_locales` is set, then the OS locale is always
+ * preferred.
+ *
+ * This function tests a given set of locales with and without
+ * `intl.regional_prefs.use_os_locales` set.
+ *
+ * @param {object} options
+ * Options
+ * @param {Function} options.shouldRunTask
+ * Called with the OS locale. Should return true if the function should run.
+ * Use this to skip tasks that don't target a desired OS locale.
+ * @param {string} options.osUnit
+ * The expected "c" or "f" unit for the OS locale.
+ * @param {object} options.unitsByLocale
+ * The expected "c" or "f" unit when the app's locale is set to particular
+ * locales. This should be an object that maps locales to expected units. For
+ * each locale in the object, the app's locale is set to that locale and the
+ * actual unit is expected to be the unit in the object.
+ */
+async function doLocaleTest({ shouldRunTask, osUnit, unitsByLocale }) {
+ Services.prefs.setBoolPref("intl.regional_prefs.use_os_locales", true);
+ let osLocale = Services.locale.regionalPrefsLocales[0];
+ Services.prefs.clearUserPref("intl.regional_prefs.use_os_locales");
+
+ if (!shouldRunTask(osLocale)) {
+ info("Skipping task, should not run for this OS locale");
+ return;
+ }
+
+ assertEnabled({
+ message: "Sanity check initial state",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+
+ // Sanity check initial locale info.
+ Assert.equal(
+ Services.locale.appLocaleAsBCP47,
+ "en-US",
+ "Initial app locale should be en-US"
+ );
+ Assert.ok(
+ !Services.prefs.getBoolPref("intl.regional_prefs.use_os_locales"),
+ "intl.regional_prefs.use_os_locales should be false initially"
+ );
+
+ // Check locales.
+ for (let [locale, temperatureUnit] of Object.entries(unitsByLocale)) {
+ await QuickSuggestTestUtils.withLocales([locale], async () => {
+ info("Checking locale: " + locale);
+ await check_results({
+ context: createContext(MerinoTestUtils.WEATHER_KEYWORD, {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ }),
+ matches: [makeExpectedResult({ temperatureUnit })],
+ });
+
+ info(
+ "Checking locale with intl.regional_prefs.use_os_locales: " + locale
+ );
+ Services.prefs.setBoolPref("intl.regional_prefs.use_os_locales", true);
+ await check_results({
+ context: createContext(MerinoTestUtils.WEATHER_KEYWORD, {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ }),
+ matches: [makeExpectedResult({ temperatureUnit: osUnit })],
+ });
+ Services.prefs.clearUserPref("intl.regional_prefs.use_os_locales");
+ });
+ }
+}
+
+// Blocks a result and makes sure the weather pref is disabled.
+add_task(async function block() {
+ // Sanity check initial state.
+ assertEnabled({
+ message: "Sanity check initial state",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+ Assert.ok(
+ UrlbarPrefs.get("suggest.weather"),
+ "Sanity check: suggest.weather is true initially"
+ );
+
+ // Do a search so we can get an actual result.
+ let context = createContext(MerinoTestUtils.WEATHER_KEYWORD, {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [makeExpectedResult()],
+ });
+
+ // Block the result.
+ UrlbarProviderWeather.onEngagement(false, "engagement", context, {
+ result: context.results[0],
+ selType: "dismiss",
+ selIndex: context.results[0].rowIndex,
+ });
+ Assert.ok(
+ !UrlbarPrefs.get("suggest.weather"),
+ "suggest.weather is false after blocking the result"
+ );
+
+ // Do a second search. Nothing should be returned.
+ context = createContext(MerinoTestUtils.WEATHER_KEYWORD, {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [],
+ });
+
+ // Re-enable the pref and clean up.
+ let fetchPromise = QuickSuggest.weather.waitForFetches();
+ UrlbarPrefs.set("suggest.weather", true);
+ await fetchPromise;
+ assertEnabled({
+ message: "On cleanup",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+});
+
+// Simulates wake 100ms before the start of the next fetch period. A new fetch
+// should not start.
+add_task(async function wakeBeforeNextFetchPeriod() {
+ await doWakeTest({
+ sleepIntervalMs: QuickSuggest.weather._test_fetchIntervalMs - 100,
+ shouldFetchOnWake: false,
+ fetchTimerMsOnWake: 100,
+ });
+});
+
+// Simulates wake 100ms after the start of the next fetch period. A new fetch
+// should start.
+add_task(async function wakeAfterNextFetchPeriod() {
+ await doWakeTest({
+ sleepIntervalMs: QuickSuggest.weather._test_fetchIntervalMs + 100,
+ shouldFetchOnWake: true,
+ });
+});
+
+// Simulates wake after many fetch periods + 100ms. A new fetch should start.
+add_task(async function wakeAfterManyFetchPeriods() {
+ await doWakeTest({
+ sleepIntervalMs: 100 * QuickSuggest.weather._test_fetchIntervalMs + 100,
+ shouldFetchOnWake: true,
+ });
+});
+
+async function doWakeTest({
+ sleepIntervalMs,
+ shouldFetchOnWake,
+ fetchTimerMsOnWake,
+}) {
+ // Make `Date.now()` return a value under our control, doesn't matter what it
+ // is. This is the time the first fetch period will start.
+ let nowOnStart = 100;
+ let sandbox = sinon.createSandbox();
+ let dateNowStub = sandbox.stub(
+ Cu.getGlobalForObject(QuickSuggest.weather).Date,
+ "now"
+ );
+ dateNowStub.returns(nowOnStart);
+
+ // Start the first fetch period.
+ info("Starting first fetch period");
+ await QuickSuggest.weather._test_fetch();
+ Assert.equal(
+ QuickSuggest.weather._test_lastFetchTimeMs,
+ nowOnStart,
+ "Last fetch time should be updated after fetch"
+ );
+ Assert.equal(
+ QuickSuggest.weather._test_fetchTimerMs,
+ QuickSuggest.weather._test_fetchIntervalMs,
+ "Timer period should be full fetch interval"
+ );
+
+ let timer = QuickSuggest.weather._test_fetchTimer;
+
+ // Advance the clock and simulate wake.
+ info("Sending wake notification");
+ let nowOnWake = nowOnStart + sleepIntervalMs;
+ dateNowStub.returns(nowOnWake);
+ QuickSuggest.weather.observe(null, "wake_notification", "");
+
+ Assert.equal(
+ QuickSuggest.weather._test_pendingFetchCount,
+ 0,
+ "After wake, next fetch should not have immediately started"
+ );
+ Assert.equal(
+ QuickSuggest.weather._test_lastFetchTimeMs,
+ nowOnStart,
+ "After wake, last fetch time should be unchanged"
+ );
+ Assert.notEqual(
+ QuickSuggest.weather._test_fetchTimer,
+ 0,
+ "After wake, the timer should exist (be non-zero)"
+ );
+ Assert.notEqual(
+ QuickSuggest.weather._test_fetchTimer,
+ timer,
+ "After wake, a new timer should have been created"
+ );
+
+ if (shouldFetchOnWake) {
+ Assert.equal(
+ QuickSuggest.weather._test_fetchTimerMs,
+ QuickSuggest.weather._test_fetchDelayAfterComingOnlineMs,
+ "After wake, timer period should be fetchDelayAfterComingOnlineMs"
+ );
+ } else {
+ Assert.equal(
+ QuickSuggest.weather._test_fetchTimerMs,
+ fetchTimerMsOnWake,
+ "After wake, timer period should be the remaining interval"
+ );
+ }
+
+ // Wait for the fetch. If the wake didn't trigger it, then the caller should
+ // have passed in a `sleepIntervalMs` that will make it start soon.
+ info("Waiting for fetch after wake");
+ await QuickSuggest.weather.waitForFetches();
+
+ Assert.equal(
+ QuickSuggest.weather._test_fetchTimerMs,
+ QuickSuggest.weather._test_fetchIntervalMs,
+ "After post-wake fetch, timer period should remain full fetch interval"
+ );
+ Assert.equal(
+ QuickSuggest.weather._test_pendingFetchCount,
+ 0,
+ "After post-wake fetch, no more fetches should be pending"
+ );
+
+ dateNowStub.restore();
+}
+
+// When network:link-status-changed is observed and the suggestion is non-null,
+// a fetch should not start.
+add_task(async function networkLinkStatusChanged_nonNull() {
+ // See nsINetworkLinkService for possible data values.
+ await doOnlineTestWithSuggestion({
+ topic: "network:link-status-changed",
+ dataValues: [
+ "down",
+ "up",
+ "changed",
+ "unknown",
+ "this is not a valid data value",
+ ],
+ });
+});
+
+// When network:offline-status-changed is observed and the suggestion is
+// non-null, a fetch should not start.
+add_task(async function networkOfflineStatusChanged_nonNull() {
+ // See nsIIOService for possible data values.
+ await doOnlineTestWithSuggestion({
+ topic: "network:offline-status-changed",
+ dataValues: ["offline", "online", "this is not a valid data value"],
+ });
+});
+
+// When captive-portal-login-success is observed and the suggestion is non-null,
+// a fetch should not start.
+add_task(async function captivePortalLoginSuccess_nonNull() {
+ // See nsIIOService for possible data values.
+ await doOnlineTestWithSuggestion({
+ topic: "captive-portal-login-success",
+ dataValues: [""],
+ });
+});
+
+async function doOnlineTestWithSuggestion({ topic, dataValues }) {
+ info("Starting fetch period");
+ await QuickSuggest.weather._test_fetch();
+ Assert.ok(
+ QuickSuggest.weather.suggestion,
+ "Suggestion should have been fetched"
+ );
+
+ let timer = QuickSuggest.weather._test_fetchTimer;
+
+ for (let data of dataValues) {
+ info("Sending notification: " + JSON.stringify({ topic, data }));
+ QuickSuggest.weather.observe(null, topic, data);
+
+ Assert.equal(
+ QuickSuggest.weather._test_pendingFetchCount,
+ 0,
+ "Fetch should not have started"
+ );
+ Assert.equal(
+ QuickSuggest.weather._test_fetchTimer,
+ timer,
+ "Timer should not have been recreated"
+ );
+ Assert.equal(
+ QuickSuggest.weather._test_fetchTimerMs,
+ QuickSuggest.weather._test_fetchIntervalMs,
+ "Timer period should be the full fetch interval"
+ );
+ }
+}
+
+// When network:link-status-changed is observed and the suggestion is null, a
+// fetch should start unless the data indicates the status is offline.
+add_task(async function networkLinkStatusChanged_null() {
+ // See nsINetworkLinkService for possible data values.
+ await doOnlineTestWithNullSuggestion({
+ topic: "network:link-status-changed",
+ offlineData: "down",
+ otherDataValues: [
+ "up",
+ "changed",
+ "unknown",
+ "this is not a valid data value",
+ ],
+ });
+});
+
+// When network:offline-status-changed is observed and the suggestion is null, a
+// fetch should start unless the data indicates the status is offline.
+add_task(async function networkOfflineStatusChanged_null() {
+ // See nsIIOService for possible data values.
+ await doOnlineTestWithNullSuggestion({
+ topic: "network:offline-status-changed",
+ offlineData: "offline",
+ otherDataValues: ["online", "this is not a valid data value"],
+ });
+});
+
+// When captive-portal-login-success is observed and the suggestion is null, a
+// fetch should start.
+add_task(async function captivePortalLoginSuccess_null() {
+ // See nsIIOService for possible data values.
+ await doOnlineTestWithNullSuggestion({
+ topic: "captive-portal-login-success",
+ otherDataValues: [""],
+ });
+});
+
+async function doOnlineTestWithNullSuggestion({
+ topic,
+ otherDataValues,
+ offlineData = "",
+}) {
+ QuickSuggest.weather._test_setSuggestionToNull();
+ Assert.ok(!QuickSuggest.weather.suggestion, "Suggestion should be null");
+
+ let timer = QuickSuggest.weather._test_fetchTimer;
+
+ // First, send the notification with the offline data value. Nothing should
+ // happen.
+ if (offlineData) {
+ info("Sending notification: " + JSON.stringify({ topic, offlineData }));
+ QuickSuggest.weather.observe(null, topic, offlineData);
+
+ Assert.ok(
+ !QuickSuggest.weather.suggestion,
+ "Suggestion should remain null"
+ );
+ Assert.equal(
+ QuickSuggest.weather._test_pendingFetchCount,
+ 0,
+ "Fetch should not have started"
+ );
+ Assert.equal(
+ QuickSuggest.weather._test_fetchTimer,
+ timer,
+ "Timer should not have been recreated"
+ );
+ Assert.equal(
+ QuickSuggest.weather._test_fetchTimerMs,
+ QuickSuggest.weather._test_fetchIntervalMs,
+ "Timer period should be the full fetch interval"
+ );
+ }
+
+ // Now send it with all other data values. Fetches should be triggered.
+ for (let data of otherDataValues) {
+ QuickSuggest.weather._test_setSuggestionToNull();
+ Assert.ok(!QuickSuggest.weather.suggestion, "Suggestion should be null");
+
+ info("Sending notification: " + JSON.stringify({ topic, data }));
+ QuickSuggest.weather.observe(null, topic, data);
+
+ Assert.equal(
+ QuickSuggest.weather._test_pendingFetchCount,
+ 0,
+ "Fetch should not have started yet"
+ );
+ Assert.notEqual(
+ QuickSuggest.weather._test_fetchTimer,
+ 0,
+ "Timer should exist"
+ );
+ Assert.notEqual(
+ QuickSuggest.weather._test_fetchTimer,
+ timer,
+ "A new timer should have been created"
+ );
+ Assert.equal(
+ QuickSuggest.weather._test_fetchTimerMs,
+ QuickSuggest.weather._test_fetchDelayAfterComingOnlineMs,
+ "Timer ms should be fetchDelayAfterComingOnlineMs"
+ );
+
+ timer = QuickSuggest.weather._test_fetchTimer;
+
+ info("Waiting for fetch after notification");
+ await QuickSuggest.weather.waitForFetches();
+
+ Assert.equal(
+ QuickSuggest.weather._test_pendingFetchCount,
+ 0,
+ "Fetch should not be pending"
+ );
+ Assert.notEqual(
+ QuickSuggest.weather._test_fetchTimer,
+ 0,
+ "Timer should exist"
+ );
+ Assert.notEqual(
+ QuickSuggest.weather._test_fetchTimer,
+ timer,
+ "A new timer should have been created"
+ );
+ Assert.equal(
+ QuickSuggest.weather._test_fetchTimerMs,
+ QuickSuggest.weather._test_fetchIntervalMs,
+ "Timer period should be full fetch interval"
+ );
+
+ timer = QuickSuggest.weather._test_fetchTimer;
+ }
+}
+
+// When many online notifications are received at once, only one fetch should
+// start.
+add_task(async function manyOnlineNotifications() {
+ await doManyNotificationsTest([
+ ["network:link-status-changed", "changed"],
+ ["network:link-status-changed", "up"],
+ ["network:offline-status-changed", "online"],
+ ]);
+});
+
+// When wake and online notifications are received at once, only one fetch
+// should start.
+add_task(async function wakeAndOnlineNotifications() {
+ await doManyNotificationsTest([
+ ["wake_notification", ""],
+ ["network:link-status-changed", "changed"],
+ ["network:link-status-changed", "up"],
+ ["network:offline-status-changed", "online"],
+ ]);
+});
+
+async function doManyNotificationsTest(notifications) {
+ // Make `Date.now()` return a value under our control, doesn't matter what it
+ // is. This is the time the first fetch period will start.
+ let nowOnStart = 100;
+ let sandbox = sinon.createSandbox();
+ let dateNowStub = sandbox.stub(
+ Cu.getGlobalForObject(QuickSuggest.weather).Date,
+ "now"
+ );
+ dateNowStub.returns(nowOnStart);
+
+ // Start a first fetch period so that after we send the notifications below
+ // the last fetch time will be in the past.
+ info("Starting first fetch period");
+ await QuickSuggest.weather._test_fetch();
+ Assert.equal(
+ QuickSuggest.weather._test_lastFetchTimeMs,
+ nowOnStart,
+ "Last fetch time should be updated after fetch"
+ );
+
+ // Now advance the clock by many fetch intervals.
+ let nowOnWake = nowOnStart + 100 * QuickSuggest.weather._test_fetchIntervalMs;
+ dateNowStub.returns(nowOnWake);
+
+ // Set the suggestion to null so online notifications will trigger a fetch.
+ QuickSuggest.weather._test_setSuggestionToNull();
+ Assert.ok(!QuickSuggest.weather.suggestion, "Suggestion should be null");
+
+ // Clear the server's list of received requests.
+ MerinoTestUtils.server.reset();
+ MerinoTestUtils.server.response.body.suggestions = [
+ MerinoTestUtils.WEATHER_SUGGESTION,
+ ];
+
+ // Send the notifications.
+ for (let [topic, data] of notifications) {
+ info("Sending notification: " + JSON.stringify({ topic, data }));
+ QuickSuggest.weather.observe(null, topic, data);
+ }
+
+ info("Waiting for fetch after notifications");
+ await QuickSuggest.weather.waitForFetches();
+
+ Assert.equal(
+ QuickSuggest.weather._test_pendingFetchCount,
+ 0,
+ "Fetch should not be pending"
+ );
+ Assert.notEqual(
+ QuickSuggest.weather._test_fetchTimer,
+ 0,
+ "Timer should exist"
+ );
+ Assert.equal(
+ QuickSuggest.weather._test_fetchTimerMs,
+ QuickSuggest.weather._test_fetchIntervalMs,
+ "Timer period should be full fetch interval"
+ );
+
+ Assert.equal(
+ MerinoTestUtils.server.requests.length,
+ 1,
+ "Merino should have received only one request"
+ );
+
+ dateNowStub.restore();
+}
+
+// Fetching when a VPN is detected should set the suggestion to null, and
+// turning off the VPN should trigger a re-fetch.
+add_task(async function vpn() {
+ // Register a mock object that implements nsINetworkLinkService.
+ let mockLinkService = {
+ isLinkUp: true,
+ linkStatusKnown: true,
+ linkType: Ci.nsINetworkLinkService.LINK_TYPE_WIFI,
+ networkID: "abcd",
+ dnsSuffixList: [],
+ platformDNSIndications: Ci.nsINetworkLinkService.NONE_DETECTED,
+ QueryInterface: ChromeUtils.generateQI(["nsINetworkLinkService"]),
+ };
+ let networkLinkServiceCID = MockRegistrar.register(
+ "@mozilla.org/network/network-link-service;1",
+ mockLinkService
+ );
+ QuickSuggest.weather._test_linkService = mockLinkService;
+
+ // At this point no VPN is detected, so a fetch should complete successfully.
+ await QuickSuggest.weather._test_fetch();
+ Assert.ok(QuickSuggest.weather.suggestion, "Suggestion should exist");
+
+ // Modify the mock link service to indicate a VPN is detected.
+ mockLinkService.platformDNSIndications =
+ Ci.nsINetworkLinkService.VPN_DETECTED;
+
+ // Now a fetch should set the suggestion to null.
+ await QuickSuggest.weather._test_fetch();
+ Assert.ok(!QuickSuggest.weather.suggestion, "Suggestion should be null");
+
+ // Set `weather.ignoreVPN` and fetch again. It should complete successfully.
+ UrlbarPrefs.set("weather.ignoreVPN", true);
+ await QuickSuggest.weather._test_fetch();
+ Assert.ok(QuickSuggest.weather.suggestion, "Suggestion should be fetched");
+
+ // Clear the pref and fetch again. It should set the suggestion back to null.
+ UrlbarPrefs.clear("weather.ignoreVPN");
+ await QuickSuggest.weather._test_fetch();
+ Assert.ok(!QuickSuggest.weather.suggestion, "Suggestion should be null");
+
+ // Simulate the link status changing. Since the mock link service still
+ // indicates a VPN is detected, the suggestion should remain null.
+ let fetchPromise = QuickSuggest.weather.waitForFetches();
+ QuickSuggest.weather.observe(null, "network:link-status-changed", "changed");
+ await fetchPromise;
+ Assert.ok(!QuickSuggest.weather.suggestion, "Suggestion should remain null");
+
+ // Modify the mock link service to indicate a VPN is no longer detected.
+ mockLinkService.platformDNSIndications =
+ Ci.nsINetworkLinkService.NONE_DETECTED;
+
+ // Simulate the link status changing again. The suggestion should be fetched.
+ fetchPromise = QuickSuggest.weather.waitForFetches();
+ QuickSuggest.weather.observe(null, "network:link-status-changed", "changed");
+ await fetchPromise;
+ Assert.ok(QuickSuggest.weather.suggestion, "Suggestion should be fetched");
+
+ MockRegistrar.unregister(networkLinkServiceCID);
+ delete QuickSuggest.weather._test_linkService;
+});
+
+// When a Nimbus experiment is installed, it should override the remote settings
+// config.
+add_task(async function nimbusOverride() {
+ // Sanity check initial state.
+ assertEnabled({
+ message: "Sanity check initial state",
+ hasSuggestion: true,
+ pendingFetchCount: 0,
+ });
+
+ // Verify a search works as expected with the default remote settings config.
+ await check_results({
+ context: createContext(MerinoTestUtils.WEATHER_KEYWORD, {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ }),
+ matches: [makeExpectedResult()],
+ });
+
+ // Install an experiment with a different keyword and min length.
+ let nimbusCleanup = await UrlbarTestUtils.initNimbusFeature({
+ weatherKeywords: ["nimbusoverride"],
+ weatherKeywordsMinimumLength: "nimbus".length,
+ });
+
+ // The usual default keyword shouldn't match.
+ await check_results({
+ context: createContext(MerinoTestUtils.WEATHER_KEYWORD, {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ }),
+ matches: [],
+ });
+
+ // The new keyword from Nimbus should match.
+ await check_results({
+ context: createContext("nimbusoverride", {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ }),
+ matches: [makeExpectedResult()],
+ });
+ await check_results({
+ context: createContext("nimbus", {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ }),
+ matches: [makeExpectedResult()],
+ });
+
+ // Uninstall the experiment.
+ await nimbusCleanup();
+
+ // The usual default keyword should match again.
+ await check_results({
+ context: createContext(MerinoTestUtils.WEATHER_KEYWORD, {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ }),
+ matches: [makeExpectedResult()],
+ });
+
+ // The keywords from Nimbus shouldn't match anymore.
+ await check_results({
+ context: createContext("nimbusoverride", {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ }),
+ matches: [],
+ });
+ await check_results({
+ context: createContext("nimbus", {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ }),
+ matches: [],
+ });
+});
+
+function assertEnabled({ message, hasSuggestion, pendingFetchCount }) {
+ info("Asserting feature is enabled");
+ if (message) {
+ info(message);
+ }
+
+ Assert.equal(
+ !!QuickSuggest.weather.suggestion,
+ hasSuggestion,
+ "Suggestion is null or non-null as expected"
+ );
+ Assert.notEqual(
+ QuickSuggest.weather._test_fetchTimer,
+ 0,
+ "Fetch timer is non-zero"
+ );
+ Assert.ok(QuickSuggest.weather._test_merino, "Merino client is non-null");
+ Assert.equal(
+ QuickSuggest.weather._test_pendingFetchCount,
+ pendingFetchCount,
+ "Expected pending fetch count"
+ );
+}
+
+function assertDisabled({ message, pendingFetchCount }) {
+ info("Asserting feature is disabled");
+ if (message) {
+ info(message);
+ }
+
+ Assert.strictEqual(
+ QuickSuggest.weather.suggestion,
+ null,
+ "Suggestion is null"
+ );
+ Assert.strictEqual(
+ QuickSuggest.weather._test_fetchTimer,
+ 0,
+ "Fetch timer is zero"
+ );
+ Assert.strictEqual(
+ QuickSuggest.weather._test_merino,
+ null,
+ "Merino client is null"
+ );
+ Assert.equal(
+ QuickSuggest.weather._test_pendingFetchCount,
+ pendingFetchCount,
+ "Expected pending fetch count"
+ );
+}
+
+function makeExpectedResult({
+ suggestedIndex = 1,
+ temperatureUnit = undefined,
+} = {}) {
+ if (!temperatureUnit) {
+ temperatureUnit =
+ Services.locale.regionalPrefsLocales[0] == "en-US" ? "f" : "c";
+ }
+
+ return {
+ suggestedIndex,
+ type: UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ heuristic: false,
+ payload: {
+ temperatureUnit,
+ url: WEATHER_SUGGESTION.url,
+ iconId: "6",
+ helpUrl: QuickSuggest.HELP_URL,
+ helpL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-learn-more-about-firefox-suggest"
+ : "firefox-suggest-urlbar-learn-more",
+ },
+ isBlockable: true,
+ blockL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-dismiss-firefox-suggest"
+ : "firefox-suggest-urlbar-block",
+ },
+ requestId: MerinoTestUtils.server.response.body.request_id,
+ source: "merino",
+ merinoProvider: "accuweather",
+ dynamicType: "weather",
+ city: WEATHER_SUGGESTION.city_name,
+ temperature:
+ WEATHER_SUGGESTION.current_conditions.temperature[temperatureUnit],
+ currentConditions: WEATHER_SUGGESTION.current_conditions.summary,
+ forecast: WEATHER_SUGGESTION.forecast.summary,
+ high: WEATHER_SUGGESTION.forecast.high[temperatureUnit],
+ low: WEATHER_SUGGESTION.forecast.low[temperatureUnit],
+ shouldNavigate: true,
+ },
+ };
+}
diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_weather_keywords.js b/browser/components/urlbar/tests/quicksuggest/unit/test_weather_keywords.js
new file mode 100644
index 0000000000..b62a243fb0
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/unit/test_weather_keywords.js
@@ -0,0 +1,1395 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Tests the keywords and zero-prefix behavior of quick suggest weather.
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarProviderWeather: "resource:///modules/UrlbarProviderWeather.sys.mjs",
+});
+
+const { WEATHER_RS_DATA, WEATHER_SUGGESTION } = MerinoTestUtils;
+
+add_task(async function init() {
+ await QuickSuggestTestUtils.ensureQuickSuggestInit({
+ remoteSettingsResults: [
+ {
+ type: "weather",
+ weather: WEATHER_RS_DATA,
+ },
+ ],
+ });
+ UrlbarPrefs.set("quicksuggest.enabled", true);
+ await MerinoTestUtils.initWeather();
+});
+
+// * Settings data: none
+// * Nimbus values: none
+// * Min keyword length pref: none
+// * Expected: no suggestion
+add_task(async function () {
+ await doKeywordsTest({
+ desc: "No data",
+ tests: {
+ "": false,
+ w: false,
+ we: false,
+ wea: false,
+ weat: false,
+ weath: false,
+ weathe: false,
+ weather: false,
+ f: false,
+ fo: false,
+ for: false,
+ fore: false,
+ forec: false,
+ foreca: false,
+ forecas: false,
+ forecast: false,
+ },
+ });
+});
+
+// * Settings data: empty
+// * Nimbus values: none
+// * Min keyword length pref: none
+// * Expected: no suggestion
+add_task(async function () {
+ await doKeywordsTest({
+ desc: "Empty settings",
+ settingsData: {},
+ tests: {
+ "": false,
+ w: false,
+ we: false,
+ wea: false,
+ weat: false,
+ weath: false,
+ weathe: false,
+ weather: false,
+ f: false,
+ fo: false,
+ for: false,
+ fore: false,
+ forec: false,
+ foreca: false,
+ forecas: false,
+ forecast: false,
+ },
+ });
+});
+
+// * Settings data: keywords only
+// * Nimbus values: none
+// * Min keyword length pref: none
+// * Expected: full keywords only
+add_task(async function () {
+ await doKeywordsTest({
+ desc: "Settings only, keywords only",
+ settingsData: {
+ keywords: ["weather", "forecast"],
+ },
+ tests: {
+ "": false,
+ w: false,
+ we: false,
+ wea: false,
+ weat: false,
+ weath: false,
+ weathe: false,
+ weather: true,
+ f: false,
+ fo: false,
+ for: false,
+ fore: false,
+ forec: false,
+ foreca: false,
+ forecas: false,
+ forecast: true,
+ },
+ });
+});
+
+// * Settings data: keywords and min keyword length = 0
+// * Nimbus values: none
+// * Min keyword length pref: none
+// * Expected: full keywords only
+add_task(async function () {
+ await doKeywordsTest({
+ desc: "Settings only, min keyword length = 0",
+ settingsData: {
+ keywords: ["weather", "forecast"],
+ min_keyword_length: 0,
+ },
+ tests: {
+ "": false,
+ w: false,
+ we: false,
+ wea: false,
+ weat: false,
+ weath: false,
+ weathe: false,
+ weather: true,
+ f: false,
+ fo: false,
+ for: false,
+ fore: false,
+ forec: false,
+ foreca: false,
+ forecas: false,
+ forecast: true,
+ },
+ });
+});
+
+// * Settings data: keywords and min keyword length > 0
+// * Nimbus values: none
+// * Min keyword length pref: none
+// * Expected: use settings data
+add_task(async function () {
+ await doKeywordsTest({
+ desc: "Settings only, min keyword length > 0",
+ settingsData: {
+ keywords: ["weather", "forecast"],
+ min_keyword_length: 3,
+ },
+ tests: {
+ "": false,
+ w: false,
+ we: false,
+ wea: true,
+ weat: true,
+ weath: true,
+ weathe: true,
+ weather: true,
+ f: false,
+ fo: false,
+ for: true,
+ fore: true,
+ forec: true,
+ foreca: true,
+ forecas: true,
+ forecast: true,
+ },
+ });
+});
+
+// * Settings data: keywords and min keyword length = 0
+// * Nimbus values: none
+// * Min keyword length pref: 6
+// * Expected: use settings keywords and min keyword length pref
+add_task(async function () {
+ await doKeywordsTest({
+ desc: "Settings only, min keyword length = 0, pref exists",
+ settingsData: {
+ keywords: ["weather", "forecast"],
+ min_keyword_length: 0,
+ },
+ minKeywordLength: 6,
+ tests: {
+ "": false,
+ w: false,
+ we: false,
+ wea: false,
+ weat: false,
+ weath: false,
+ weathe: true,
+ weather: true,
+ f: false,
+ fo: false,
+ for: false,
+ fore: false,
+ forec: false,
+ foreca: true,
+ forecas: true,
+ forecast: true,
+ },
+ });
+});
+
+// * Settings data: keywords and min keyword length > 0
+// * Nimbus values: none
+// * Min keyword length pref: 6
+// * Expected: use settings keywords and min keyword length pref
+add_task(async function () {
+ await doKeywordsTest({
+ desc: "Settings only, min keyword length > 0, pref exists",
+ settingsData: {
+ keywords: ["weather", "forecast"],
+ min_keyword_length: 3,
+ },
+ minKeywordLength: 6,
+ tests: {
+ "": false,
+ w: false,
+ we: false,
+ wea: false,
+ weat: false,
+ weath: false,
+ weathe: true,
+ weather: true,
+ f: false,
+ fo: false,
+ for: false,
+ fore: false,
+ forec: false,
+ foreca: true,
+ forecas: true,
+ forecast: true,
+ },
+ });
+});
+
+// * Settings data: empty
+// * Nimbus values: empty
+// * Min keyword length pref: none
+// * Expected: no suggestion
+add_task(async function () {
+ await doKeywordsTest({
+ desc: "Settings: empty; Nimbus: empty",
+ settingsData: {},
+ nimbusValues: {},
+ tests: {
+ "": false,
+ w: false,
+ we: false,
+ wea: false,
+ weat: false,
+ weath: false,
+ weathe: false,
+ weather: false,
+ f: false,
+ fo: false,
+ for: false,
+ fore: false,
+ forec: false,
+ foreca: false,
+ forecas: false,
+ forecast: false,
+ },
+ });
+});
+
+// * Settings data: keywords only
+// * Nimbus values: keywords only
+// * Min keyword length pref: none
+// * Expected: full keywords in Nimbus
+add_task(async function () {
+ await doKeywordsTest({
+ desc: "Settings: keywords; Nimbus: keywords",
+ settingsData: {
+ keywords: ["weather"],
+ },
+ nimbusValues: {
+ weatherKeywords: ["forecast"],
+ },
+ tests: {
+ "": false,
+ w: false,
+ we: false,
+ wea: false,
+ weat: false,
+ weath: false,
+ weathe: false,
+ weather: false,
+ f: false,
+ fo: false,
+ for: false,
+ fore: false,
+ forec: false,
+ foreca: false,
+ forecas: false,
+ forecast: true,
+ },
+ });
+});
+
+// * Settings data: keywords and min keyword length = 0
+// * Nimbus values: keywords only
+// * Min keyword length pref: none
+// * Expected: full keywords in Nimbus
+add_task(async function () {
+ await doKeywordsTest({
+ desc: "Settings: keywords, min keyword length = 0; Nimbus: keywords",
+ settingsData: {
+ keywords: ["weather"],
+ min_keyword_length: 0,
+ },
+ nimbusValues: {
+ weatherKeywords: ["forecast"],
+ },
+ tests: {
+ "": false,
+ w: false,
+ we: false,
+ wea: false,
+ weat: false,
+ weath: false,
+ weathe: false,
+ weather: false,
+ f: false,
+ fo: false,
+ for: false,
+ fore: false,
+ forec: false,
+ foreca: false,
+ forecas: false,
+ forecast: true,
+ },
+ });
+});
+
+// * Settings data: keywords and min keyword length > 0
+// * Nimbus values: keywords only
+// * Min keyword length pref: none
+// * Expected: Nimbus keywords with settings min keyword length
+add_task(async function () {
+ await doKeywordsTest({
+ desc: "Settings: keywords, min keyword length > 0; Nimbus: keywords",
+ settingsData: {
+ keywords: ["weather"],
+ min_keyword_length: 3,
+ },
+ nimbusValues: {
+ weatherKeywords: ["forecast"],
+ },
+ tests: {
+ "": false,
+ w: false,
+ we: false,
+ wea: false,
+ weat: false,
+ weath: false,
+ weathe: false,
+ weather: false,
+ f: false,
+ fo: false,
+ for: true,
+ fore: true,
+ forec: true,
+ foreca: true,
+ forecas: true,
+ forecast: true,
+ },
+ });
+});
+
+// * Settings data: keywords and min keyword length > 0
+// * Nimbus values: keywords and min keyword length = 0
+// * Min keyword length pref: none
+// * Expected: Nimbus keywords with settings min keyword length
+add_task(async function () {
+ await doKeywordsTest({
+ desc: "Settings: keywords, min keyword length > 0; Nimbus: keywords, min keyword length = 0",
+ settingsData: {
+ keywords: ["weather"],
+ min_keyword_length: 3,
+ },
+ nimbusValues: {
+ weatherKeywords: ["forecast"],
+ weatherKeywordsMinimumLength: 0,
+ },
+ tests: {
+ "": false,
+ w: false,
+ we: false,
+ wea: false,
+ weat: false,
+ weath: false,
+ weathe: false,
+ weather: false,
+ f: false,
+ fo: false,
+ for: true,
+ fore: true,
+ forec: true,
+ foreca: true,
+ forecas: true,
+ forecast: true,
+ },
+ });
+});
+
+// * Settings data: keywords and min keyword length > 0
+// * Nimbus values: keywords and min keyword length > 0
+// * Min keyword length pref: none
+// * Expected: use Nimbus values
+add_task(async function () {
+ await doKeywordsTest({
+ desc: "Settings: keywords, min keyword length > 0; Nimbus: keywords, min keyword length > 0",
+ settingsData: {
+ keywords: ["weather"],
+ min_keyword_length: 3,
+ },
+ nimbusValues: {
+ weatherKeywords: ["forecast"],
+ weatherKeywordsMinimumLength: 4,
+ },
+ tests: {
+ "": false,
+ w: false,
+ we: false,
+ wea: false,
+ weat: false,
+ weath: false,
+ weathe: false,
+ weather: false,
+ f: false,
+ fo: false,
+ for: false,
+ fore: true,
+ forec: true,
+ foreca: true,
+ forecas: true,
+ forecast: true,
+ },
+ });
+});
+
+// * Settings data: keywords and min keyword length > 0
+// * Nimbus values: keywords and min keyword length = 0
+// * Min keyword length pref: exists
+// * Expected: Nimbus keywords with min keyword length pref
+add_task(async function () {
+ await doKeywordsTest({
+ desc: "Settings: keywords, min keyword length > 0; Nimbus: keywords, min keyword length = 0; pref exists",
+ settingsData: {
+ keywords: ["weather"],
+ min_keyword_length: 3,
+ },
+ nimbusValues: {
+ weatherKeywords: ["forecast"],
+ weatherKeywordsMinimumLength: 0,
+ },
+ minKeywordLength: 6,
+ tests: {
+ "": false,
+ w: false,
+ we: false,
+ wea: false,
+ weat: false,
+ weath: false,
+ weathe: false,
+ weather: false,
+ f: false,
+ fo: false,
+ for: false,
+ fore: false,
+ forec: false,
+ foreca: true,
+ forecas: true,
+ forecast: true,
+ },
+ });
+});
+
+// * Settings data: keywords and min keyword length > 0
+// * Nimbus values: keywords and min keyword length > 0
+// * Min keyword length pref: exists
+// * Expected: Nimbus keywords with min keyword length pref
+add_task(async function () {
+ await doKeywordsTest({
+ desc: "Settings: keywords, min keyword length > 0; Nimbus: keywords, min keyword length > 0; pref exists",
+ settingsData: {
+ keywords: ["weather", "forecast"],
+ min_keyword_length: 3,
+ },
+ nimbusValues: {
+ weatherKeywords: ["forecast"],
+ weatherKeywordsMinimumLength: 4,
+ },
+ minKeywordLength: 6,
+ tests: {
+ "": false,
+ w: false,
+ we: false,
+ wea: false,
+ weat: false,
+ weath: false,
+ weathe: false,
+ weather: false,
+ f: false,
+ fo: false,
+ for: false,
+ fore: false,
+ forec: false,
+ foreca: true,
+ forecas: true,
+ forecast: true,
+ },
+ });
+});
+
+// * Settings data: none
+// * Nimbus values: keywords only
+// * Min keyword length pref: none
+// * Expected: full keywords
+add_task(async function () {
+ await doKeywordsTest({
+ desc: "Settings: none; Nimbus: keywords",
+ nimbusValues: {
+ weatherKeywords: ["weather", "forecast"],
+ },
+ tests: {
+ "": false,
+ w: false,
+ we: false,
+ wea: false,
+ weat: false,
+ weath: false,
+ weathe: false,
+ weather: true,
+ f: false,
+ fo: false,
+ for: false,
+ fore: false,
+ forec: false,
+ foreca: false,
+ forecas: false,
+ forecast: true,
+ },
+ });
+});
+
+// * Settings data: none
+// * Nimbus values: keywords and min keyword length = 0
+// * Min keyword length pref: none
+// * Expected: full keywords
+add_task(async function () {
+ await doKeywordsTest({
+ desc: "Settings: none; Nimbus: keywords, min keyword length = 0",
+ nimbusValues: {
+ weatherKeywords: ["weather", "forecast"],
+ weatherKeywordsMinimumLength: 0,
+ },
+ tests: {
+ "": false,
+ w: false,
+ we: false,
+ wea: false,
+ weat: false,
+ weath: false,
+ weathe: false,
+ weather: true,
+ f: false,
+ fo: false,
+ for: false,
+ fore: false,
+ forec: false,
+ foreca: false,
+ forecas: false,
+ forecast: true,
+ },
+ });
+});
+
+// * Settings data: none
+// * Nimbus values: keywords and min keyword length > 0
+// * Min keyword length pref: none
+// * Expected: use Nimbus values
+add_task(async function () {
+ await doKeywordsTest({
+ desc: "Settings: none; Nimbus: keywords, min keyword length > 0",
+ nimbusValues: {
+ weatherKeywords: ["weather", "forecast"],
+ weatherKeywordsMinimumLength: 3,
+ },
+ tests: {
+ "": false,
+ w: false,
+ we: false,
+ wea: true,
+ weat: true,
+ weath: true,
+ weathe: true,
+ weather: true,
+ f: false,
+ fo: false,
+ for: true,
+ fore: true,
+ forec: true,
+ foreca: true,
+ forecas: true,
+ forecast: true,
+ },
+ });
+});
+
+// * Settings data: none
+// * Nimbus values: keywords and min keyword length > 0
+// * Min keyword length pref: exists
+// * Expected: use Nimbus keywords and min keyword length pref
+add_task(async function () {
+ await doKeywordsTest({
+ desc: "Settings: none; Nimbus: keywords, min keyword length > 0; pref exists",
+ nimbusValues: {
+ weatherKeywords: ["weather", "forecast"],
+ weatherKeywordsMinimumLength: 3,
+ },
+ minKeywordLength: 6,
+ tests: {
+ "": false,
+ w: false,
+ we: false,
+ wea: false,
+ weat: false,
+ weath: false,
+ weathe: true,
+ weather: true,
+ f: false,
+ fo: false,
+ for: false,
+ fore: false,
+ forec: false,
+ foreca: true,
+ forecas: true,
+ forecast: true,
+ },
+ });
+});
+
+// When `weatherKeywords` is non-null and `weatherKeywordsMinimumLength` is
+// larger than the length of all keywords, the suggestion should not be
+// triggered.
+add_task(async function minLength_large() {
+ await doKeywordsTest({
+ desc: "Large min length",
+ nimbusValues: {
+ weatherKeywords: ["weather", "forecast"],
+ weatherKeywordsMinimumLength: 999,
+ },
+ tests: {
+ "": false,
+ w: false,
+ we: false,
+ wea: false,
+ weat: false,
+ weath: false,
+ weathe: false,
+ weather: false,
+ f: false,
+ fo: false,
+ for: false,
+ fore: false,
+ forec: false,
+ foreca: false,
+ forecas: false,
+ forecast: false,
+ },
+ });
+});
+
+// Leading and trailing spaces should be ignored.
+add_task(async function leadingAndTrailingSpaces() {
+ await doKeywordsTest({
+ nimbusValues: {
+ weatherKeywords: ["weather"],
+ weatherKeywordsMinimumLength: 3,
+ },
+ tests: {
+ " wea": true,
+ " wea": true,
+ "wea ": true,
+ "wea ": true,
+ " wea ": true,
+ " weat": true,
+ " weat": true,
+ "weat ": true,
+ "weat ": true,
+ " weat ": true,
+ },
+ });
+});
+
+add_task(async function caseInsensitive() {
+ await doKeywordsTest({
+ desc: "Case insensitive",
+ settingsData: {
+ keywords: ["weather"],
+ min_keyword_length: 3,
+ },
+ tests: {
+ wea: true,
+ WEA: true,
+ Wea: true,
+ WeA: true,
+ WEATHER: true,
+ Weather: true,
+ WeAtHeR: true,
+ },
+ });
+});
+
+async function doKeywordsTest({
+ desc,
+ tests,
+ nimbusValues = null,
+ settingsData = null,
+ minKeywordLength = undefined,
+}) {
+ info("Doing keywords test: " + desc);
+ info(JSON.stringify({ nimbusValues, settingsData, minKeywordLength }));
+
+ // If a suggestion hasn't already been fetched and the data contains keywords,
+ // a fetch will start. Wait for it to finish below.
+ let fetchPromise;
+ if (
+ !QuickSuggest.weather.suggestion &&
+ (nimbusValues?.weatherKeywords || settingsData?.keywords)
+ ) {
+ fetchPromise = QuickSuggest.weather.waitForFetches();
+ }
+
+ let nimbusCleanup;
+ if (nimbusValues) {
+ nimbusCleanup = await UrlbarTestUtils.initNimbusFeature(nimbusValues);
+ }
+
+ await QuickSuggestTestUtils.setRemoteSettingsResults([
+ {
+ type: "weather",
+ weather: settingsData,
+ },
+ ]);
+
+ if (minKeywordLength) {
+ UrlbarPrefs.set("weather.minKeywordLength", minKeywordLength);
+ }
+
+ if (fetchPromise) {
+ info("Waiting for fetch");
+ assertFetchingStarted({ pendingFetchCount: 1 });
+ await fetchPromise;
+ info("Got fetch");
+ }
+
+ for (let [searchString, expected] of Object.entries(tests)) {
+ info(
+ "Doing search: " +
+ JSON.stringify({
+ searchString,
+ expected,
+ })
+ );
+
+ let suggestedIndex = searchString ? 1 : 0;
+ await check_results({
+ context: createContext(searchString, {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ }),
+ matches: expected ? [makeExpectedResult({ suggestedIndex })] : [],
+ });
+ }
+
+ await nimbusCleanup?.();
+
+ fetchPromise = null;
+ if (!QuickSuggest.weather.suggestion) {
+ fetchPromise = QuickSuggest.weather.waitForFetches();
+ }
+ await QuickSuggestTestUtils.setRemoteSettingsResults([
+ {
+ type: "weather",
+ weather: MerinoTestUtils.WEATHER_RS_DATA,
+ },
+ ]);
+
+ UrlbarPrefs.clear("weather.minKeywordLength");
+ await fetchPromise;
+}
+
+// When a sponsored quick suggest result matches the same keyword as the weather
+// result, the weather result should be shown and the quick suggest result
+// should not be shown.
+add_task(async function matchingQuickSuggest_sponsored() {
+ await doMatchingQuickSuggestTest("suggest.quicksuggest.sponsored", true);
+});
+
+// When a non-sponsored quick suggest result matches the same keyword as the
+// weather result, the weather result should be shown and the quick suggest
+// result should not be shown.
+add_task(async function matchingQuickSuggest_nonsponsored() {
+ await doMatchingQuickSuggestTest("suggest.quicksuggest.nonsponsored", false);
+});
+
+async function doMatchingQuickSuggestTest(pref, isSponsored) {
+ let keyword = "test";
+ let iab_category = isSponsored ? "22 - Shopping" : "5 - Education";
+
+ // Add a remote settings result to quick suggest.
+ UrlbarPrefs.set(pref, true);
+ await QuickSuggestTestUtils.setRemoteSettingsResults([
+ {
+ type: "data",
+ attachment: [
+ {
+ id: 1,
+ url: "http://example.com/",
+ title: "Suggestion",
+ keywords: [keyword],
+ click_url: "http://example.com/click",
+ impression_url: "http://example.com/impression",
+ advertiser: "TestAdvertiser",
+ iab_category,
+ },
+ ],
+ },
+ {
+ type: "weather",
+ weather: MerinoTestUtils.WEATHER_RS_DATA,
+ },
+ ]);
+
+ // First do a search to verify the quick suggest result matches the keyword.
+ info("Doing first search for quick suggest result");
+ await check_results({
+ context: createContext(keyword, {
+ providers: [UrlbarProviderQuickSuggest.name, UrlbarProviderWeather.name],
+ isPrivate: false,
+ }),
+ matches: [
+ {
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ heuristic: false,
+ payload: {
+ telemetryType: isSponsored ? "adm_sponsored" : "adm_nonsponsored",
+ qsSuggestion: keyword,
+ title: "Suggestion",
+ url: "http://example.com/",
+ displayUrl: "http://example.com",
+ originalUrl: "http://example.com/",
+ icon: null,
+ sponsoredImpressionUrl: "http://example.com/impression",
+ sponsoredClickUrl: "http://example.com/click",
+ sponsoredBlockId: 1,
+ sponsoredAdvertiser: "TestAdvertiser",
+ sponsoredIabCategory: iab_category,
+ isSponsored,
+ helpUrl: QuickSuggest.HELP_URL,
+ helpL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-learn-more-about-firefox-suggest"
+ : "firefox-suggest-urlbar-learn-more",
+ },
+ isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"),
+ blockL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-dismiss-firefox-suggest"
+ : "firefox-suggest-urlbar-block",
+ },
+ source: "remote-settings",
+ },
+ },
+ ],
+ });
+
+ // Set up the keyword for the weather suggestion and do a second search to
+ // verify only the weather result matches.
+ info("Doing second search for weather suggestion");
+ let cleanup = await UrlbarTestUtils.initNimbusFeature({
+ weatherKeywords: [keyword],
+ weatherKeywordsMinimumLength: 1,
+ });
+ await check_results({
+ context: createContext(keyword, {
+ providers: [UrlbarProviderQuickSuggest.name, UrlbarProviderWeather.name],
+ isPrivate: false,
+ }),
+ matches: [makeExpectedResult({ suggestedIndex: 1 })],
+ });
+ await cleanup();
+
+ UrlbarPrefs.clear(pref);
+}
+
+add_task(async function () {
+ await doIncrementTest({
+ desc: "Settings only",
+ setup: {
+ settingsData: {
+ keywords: ["forecast", "wind"],
+ min_keyword_length: 3,
+ },
+ },
+ tests: [
+ {
+ minKeywordLength: 3,
+ canIncrement: true,
+ searches: {
+ fo: false,
+ for: true,
+ fore: true,
+ forec: true,
+ wi: false,
+ win: true,
+ wind: true,
+ },
+ },
+ {
+ minKeywordLength: 4,
+ canIncrement: true,
+ searches: {
+ fo: false,
+ for: false,
+ fore: true,
+ forec: true,
+ wi: false,
+ win: false,
+ wind: true,
+ },
+ },
+ {
+ minKeywordLength: 5,
+ canIncrement: true,
+ searches: {
+ fo: false,
+ for: false,
+ fore: false,
+ forec: true,
+ wi: false,
+ win: false,
+ wind: false,
+ },
+ },
+ ],
+ });
+});
+
+add_task(async function () {
+ await doIncrementTest({
+ desc: "Settings only with cap",
+ setup: {
+ settingsData: {
+ keywords: ["forecast", "wind"],
+ min_keyword_length: 3,
+ min_keyword_length_cap: 6,
+ },
+ },
+ tests: [
+ {
+ minKeywordLength: 3,
+ canIncrement: true,
+ searches: {
+ fo: false,
+ for: true,
+ fore: true,
+ forec: true,
+ foreca: true,
+ forecas: true,
+ wi: false,
+ win: true,
+ wind: true,
+ },
+ },
+ {
+ minKeywordLength: 4,
+ canIncrement: true,
+ searches: {
+ fo: false,
+ for: false,
+ fore: true,
+ forec: true,
+ foreca: true,
+ forecas: true,
+ wi: false,
+ win: false,
+ wind: true,
+ },
+ },
+ {
+ minKeywordLength: 5,
+ canIncrement: true,
+ searches: {
+ fo: false,
+ for: false,
+ fore: false,
+ forec: true,
+ foreca: true,
+ forecas: true,
+ wi: false,
+ win: false,
+ wind: false,
+ windy: false,
+ },
+ },
+ {
+ minKeywordLength: 6,
+ canIncrement: false,
+ searches: {
+ fo: false,
+ for: false,
+ fore: false,
+ forec: false,
+ foreca: true,
+ forecas: true,
+ wi: false,
+ win: false,
+ wind: false,
+ windy: false,
+ },
+ },
+ {
+ minKeywordLength: 6,
+ canIncrement: false,
+ searches: {
+ fo: false,
+ for: false,
+ fore: false,
+ forec: false,
+ foreca: true,
+ forecas: true,
+ wi: false,
+ win: false,
+ wind: false,
+ windy: false,
+ },
+ },
+ ],
+ });
+});
+
+add_task(async function () {
+ await doIncrementTest({
+ desc: "Settings and Nimbus",
+ setup: {
+ settingsData: {
+ keywords: ["weather"],
+ min_keyword_length: 5,
+ },
+ nimbusValues: {
+ weatherKeywords: ["forecast", "wind"],
+ weatherKeywordsMinimumLength: 3,
+ },
+ },
+ tests: [
+ {
+ minKeywordLength: 3,
+ canIncrement: true,
+ searches: {
+ we: false,
+ wea: false,
+ weat: false,
+ weath: false,
+ fo: false,
+ for: true,
+ fore: true,
+ forec: true,
+ wi: false,
+ win: true,
+ wind: true,
+ },
+ },
+ {
+ minKeywordLength: 4,
+ canIncrement: true,
+ searches: {
+ we: false,
+ wea: false,
+ weat: false,
+ weath: false,
+ fo: false,
+ for: false,
+ fore: true,
+ forec: true,
+ wi: false,
+ win: false,
+ wind: true,
+ },
+ },
+ {
+ minKeywordLength: 5,
+ canIncrement: true,
+ searches: {
+ we: false,
+ wea: false,
+ weat: false,
+ weath: false,
+ fo: false,
+ for: false,
+ fore: false,
+ forec: true,
+ wi: false,
+ win: false,
+ wind: false,
+ windy: false,
+ },
+ },
+ ],
+ });
+});
+
+add_task(async function () {
+ await doIncrementTest({
+ desc: "Settings and Nimbus with cap in Nimbus",
+ setup: {
+ settingsData: {
+ keywords: ["weather"],
+ min_keyword_length: 5,
+ },
+ nimbusValues: {
+ weatherKeywords: ["forecast", "wind"],
+ weatherKeywordsMinimumLength: 3,
+ weatherKeywordsMinimumLengthCap: 6,
+ },
+ },
+ tests: [
+ {
+ minKeywordLength: 3,
+ canIncrement: true,
+ searches: {
+ we: false,
+ wea: false,
+ weat: false,
+ weath: false,
+ fo: false,
+ for: true,
+ fore: true,
+ forec: true,
+ foreca: true,
+ forecas: true,
+ wi: false,
+ win: true,
+ wind: true,
+ },
+ },
+ {
+ minKeywordLength: 4,
+ canIncrement: true,
+ searches: {
+ we: false,
+ wea: false,
+ weat: false,
+ weath: false,
+ fo: false,
+ for: false,
+ fore: true,
+ forec: true,
+ foreca: true,
+ forecas: true,
+ wi: false,
+ win: false,
+ wind: true,
+ },
+ },
+ {
+ minKeywordLength: 5,
+ canIncrement: true,
+ searches: {
+ we: false,
+ wea: false,
+ weat: false,
+ weath: false,
+ fo: false,
+ for: false,
+ fore: false,
+ forec: true,
+ foreca: true,
+ forecas: true,
+ wi: false,
+ win: false,
+ wind: false,
+ windy: false,
+ },
+ },
+ {
+ minKeywordLength: 6,
+ canIncrement: false,
+ searches: {
+ we: false,
+ wea: false,
+ weat: false,
+ weath: false,
+ fo: false,
+ for: false,
+ fore: false,
+ forec: false,
+ foreca: true,
+ forecas: true,
+ wi: false,
+ win: false,
+ wind: false,
+ windy: false,
+ },
+ },
+ {
+ minKeywordLength: 6,
+ canIncrement: false,
+ searches: {
+ we: false,
+ wea: false,
+ weat: false,
+ weath: false,
+ fo: false,
+ for: false,
+ fore: false,
+ forec: false,
+ foreca: true,
+ forecas: true,
+ wi: false,
+ win: false,
+ wind: false,
+ windy: false,
+ },
+ },
+ ],
+ });
+});
+
+async function doIncrementTest({ desc, setup, tests }) {
+ info("Doing increment test: " + desc);
+ info(JSON.stringify({ setup }));
+
+ let { nimbusValues, settingsData } = setup;
+
+ let fetchPromise;
+ if (
+ !QuickSuggest.weather.suggestion &&
+ (nimbusValues?.weatherKeywords || settingsData?.keywords)
+ ) {
+ fetchPromise = QuickSuggest.weather.waitForFetches();
+ }
+
+ let nimbusCleanup;
+ if (nimbusValues) {
+ nimbusCleanup = await UrlbarTestUtils.initNimbusFeature(nimbusValues);
+ }
+
+ await QuickSuggestTestUtils.setRemoteSettingsResults([
+ {
+ type: "weather",
+ weather: settingsData,
+ },
+ ]);
+
+ if (fetchPromise) {
+ info("Waiting for fetch");
+ assertFetchingStarted({ pendingFetchCount: 1 });
+ await fetchPromise;
+ info("Got fetch");
+ }
+
+ for (let { minKeywordLength, canIncrement, searches } of tests) {
+ info(
+ "Doing increment test case: " +
+ JSON.stringify({
+ minKeywordLength,
+ canIncrement,
+ })
+ );
+
+ Assert.equal(
+ QuickSuggest.weather.minKeywordLength,
+ minKeywordLength,
+ "minKeywordLength should be correct"
+ );
+ Assert.equal(
+ QuickSuggest.weather.canIncrementMinKeywordLength,
+ canIncrement,
+ "canIncrement should be correct"
+ );
+
+ for (let [searchString, expected] of Object.entries(searches)) {
+ Assert.equal(
+ QuickSuggest.weather.keywords.has(searchString),
+ expected,
+ "Keyword should be present/absent as expected: " + searchString
+ );
+
+ await check_results({
+ context: createContext(searchString, {
+ providers: [UrlbarProviderWeather.name],
+ isPrivate: false,
+ }),
+ matches: expected ? [makeExpectedResult({ suggestedIndex: 1 })] : [],
+ });
+ }
+
+ QuickSuggest.weather.incrementMinKeywordLength();
+ info(
+ "Incremented min keyword length, new value is: " +
+ QuickSuggest.weather.minKeywordLength
+ );
+ }
+
+ await nimbusCleanup?.();
+
+ fetchPromise = null;
+ if (!QuickSuggest.weather.suggestion) {
+ fetchPromise = QuickSuggest.weather.waitForFetches();
+ }
+ await QuickSuggestTestUtils.setRemoteSettingsResults([
+ {
+ type: "weather",
+ weather: MerinoTestUtils.WEATHER_RS_DATA,
+ },
+ ]);
+ UrlbarPrefs.clear("weather.minKeywordLength");
+ await fetchPromise;
+}
+
+function makeExpectedResult({
+ suggestedIndex = 0,
+ temperatureUnit = undefined,
+} = {}) {
+ if (!temperatureUnit) {
+ temperatureUnit =
+ Services.locale.regionalPrefsLocales[0] == "en-US" ? "f" : "c";
+ }
+
+ return {
+ suggestedIndex,
+ type: UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ heuristic: false,
+ payload: {
+ temperatureUnit,
+ url: WEATHER_SUGGESTION.url,
+ iconId: "6",
+ helpUrl: QuickSuggest.HELP_URL,
+ helpL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-learn-more-about-firefox-suggest"
+ : "firefox-suggest-urlbar-learn-more",
+ },
+ isBlockable: true,
+ blockL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-dismiss-firefox-suggest"
+ : "firefox-suggest-urlbar-block",
+ },
+ requestId: MerinoTestUtils.server.response.body.request_id,
+ source: "merino",
+ merinoProvider: "accuweather",
+ dynamicType: "weather",
+ city: WEATHER_SUGGESTION.city_name,
+ temperature:
+ WEATHER_SUGGESTION.current_conditions.temperature[temperatureUnit],
+ currentConditions: WEATHER_SUGGESTION.current_conditions.summary,
+ forecast: WEATHER_SUGGESTION.forecast.summary,
+ high: WEATHER_SUGGESTION.forecast.high[temperatureUnit],
+ low: WEATHER_SUGGESTION.forecast.low[temperatureUnit],
+ shouldNavigate: true,
+ },
+ };
+}
+
+function assertFetchingStarted() {
+ info("Asserting fetching has started");
+
+ Assert.notEqual(
+ QuickSuggest.weather._test_fetchTimer,
+ 0,
+ "Fetch timer is non-zero"
+ );
+ Assert.ok(QuickSuggest.weather._test_merino, "Merino client is non-null");
+ Assert.equal(
+ QuickSuggest.weather._test_pendingFetchCount,
+ 1,
+ "Expected pending fetch count"
+ );
+}
diff --git a/browser/components/urlbar/tests/quicksuggest/unit/xpcshell.ini b/browser/components/urlbar/tests/quicksuggest/unit/xpcshell.ini
new file mode 100644
index 0000000000..45a17687ae
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/unit/xpcshell.ini
@@ -0,0 +1,23 @@
+[DEFAULT]
+skip-if = toolkit == 'android' # bug 1730213
+head = ../../unit/head.js head.js
+firefox-appdir = browser
+
+[test_merinoClient.js]
+[test_merinoClient_sessions.js]
+[test_quicksuggest.js]
+[test_quicksuggest_addons.js]
+[test_quicksuggest_bestMatch.js]
+[test_quicksuggest_dynamicWikipedia.js]
+[test_quicksuggest_impressionCaps.js]
+[test_quicksuggest_merino.js]
+[test_quicksuggest_merinoSessions.js]
+[test_quicksuggest_migrate_v1.js]
+[test_quicksuggest_migrate_v2.js]
+[test_quicksuggest_nonUniqueKeywords.js]
+[test_quicksuggest_offlineDefault.js]
+[test_quicksuggest_positionInSuggestions.js]
+[test_quicksuggest_topPicks.js]
+[test_suggestionsMap.js]
+[test_weather.js]
+[test_weather_keywords.js]
diff --git a/browser/components/urlbar/tests/unit/data/engine.xml b/browser/components/urlbar/tests/unit/data/engine.xml
new file mode 100644
index 0000000000..61d776655f
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/data/engine.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>engine.xml</ShortName>
+<Description>A test search engine</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Url type="text/html" method="GET" template="http://www.example.com/">
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+<SearchForm>http://www.example.com/</SearchForm>
+</SearchPlugin>
diff --git a/browser/components/urlbar/tests/unit/head.js b/browser/components/urlbar/tests/unit/head.js
new file mode 100644
index 0000000000..8f15b0280e
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/head.js
@@ -0,0 +1,1127 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+var { UrlbarMuxer, UrlbarProvider, UrlbarQueryContext, UrlbarUtils } =
+ ChromeUtils.importESModule("resource:///modules/UrlbarUtils.sys.mjs");
+
+ChromeUtils.defineESModuleGetters(this, {
+ AddonTestUtils: "resource://testing-common/AddonTestUtils.sys.mjs",
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+ PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs",
+ SearchTestUtils: "resource://testing-common/SearchTestUtils.sys.mjs",
+ TestUtils: "resource://testing-common/TestUtils.sys.mjs",
+ UrlbarController: "resource:///modules/UrlbarController.sys.mjs",
+ UrlbarInput: "resource:///modules/UrlbarInput.sys.mjs",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
+ UrlbarProviderOpenTabs: "resource:///modules/UrlbarProviderOpenTabs.sys.mjs",
+ UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs",
+ UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
+ UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ HttpServer: "resource://testing-common/httpd.js",
+});
+
+XPCOMUtils.defineLazyGetter(this, "QuickSuggestTestUtils", () => {
+ const { QuickSuggestTestUtils: module } = ChromeUtils.importESModule(
+ "resource://testing-common/QuickSuggestTestUtils.sys.mjs"
+ );
+ module.init(this);
+ return module;
+});
+
+XPCOMUtils.defineLazyGetter(this, "MerinoTestUtils", () => {
+ const { MerinoTestUtils: module } = ChromeUtils.importESModule(
+ "resource://testing-common/MerinoTestUtils.sys.mjs"
+ );
+ module.init(this);
+ return module;
+});
+
+ChromeUtils.defineLazyGetter(this, "UrlbarTestUtils", () => {
+ const { UrlbarTestUtils: module } = ChromeUtils.importESModule(
+ "resource://testing-common/UrlbarTestUtils.sys.mjs"
+ );
+ module.init(this);
+ return module;
+});
+
+XPCOMUtils.defineLazyGetter(this, "PlacesFrecencyRecalculator", () => {
+ return Cc["@mozilla.org/places/frecency-recalculator;1"].getService(
+ Ci.nsIObserver
+ ).wrappedJSObject;
+});
+
+SearchTestUtils.init(this);
+AddonTestUtils.init(this, false);
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "42",
+ "42"
+);
+
+const SUGGESTIONS_ENGINE_NAME = "Suggestions";
+const TAIL_SUGGESTIONS_ENGINE_NAME = "Tail Suggestions";
+
+/**
+ * Gets the database connection. If the Places connection is invalid it will
+ * try to create a new connection.
+ *
+ * @param [optional] aForceNewConnection
+ * Forces creation of a new connection to the database. When a
+ * connection is asyncClosed it cannot anymore schedule async statements,
+ * though connectionReady will keep returning true (Bug 726990).
+ *
+ * @returns The database connection or null if unable to get one.
+ */
+var gDBConn;
+function DBConn(aForceNewConnection) {
+ if (!aForceNewConnection) {
+ let db = PlacesUtils.history.DBConnection;
+ if (db.connectionReady) {
+ return db;
+ }
+ }
+
+ // If the Places database connection has been closed, create a new connection.
+ if (!gDBConn || aForceNewConnection) {
+ let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ file.append("places.sqlite");
+ let dbConn = (gDBConn = Services.storage.openDatabase(file));
+
+ TestUtils.topicObserved("profile-before-change").then(() =>
+ dbConn.asyncClose()
+ );
+ }
+
+ return gDBConn.connectionReady ? gDBConn : null;
+}
+
+/**
+ * @param {string} searchString The search string to insert into the context.
+ * @param {object} properties Overrides for the default values.
+ * @returns {UrlbarQueryContext} Creates a dummy query context with pre-filled
+ * required options.
+ */
+function createContext(searchString = "foo", properties = {}) {
+ info(`Creating new queryContext with searchString: ${searchString}`);
+ let context = new UrlbarQueryContext(
+ Object.assign(
+ {
+ allowAutofill: UrlbarPrefs.get("autoFill"),
+ isPrivate: true,
+ maxResults: UrlbarPrefs.get("maxRichResults"),
+ searchString,
+ },
+ properties
+ )
+ );
+ context.view = {
+ get visibleResults() {
+ return context.results;
+ },
+ controller: {
+ removeResult() {},
+ },
+ acknowledgeDismissal() {},
+ };
+ UrlbarTokenizer.tokenize(context);
+ return context;
+}
+
+/**
+ * Waits for the given notification from the supplied controller.
+ *
+ * @param {UrlbarController} controller The controller to wait for a response from.
+ * @param {string} notification The name of the notification to wait for.
+ * @param {boolean} expected Wether the notification is expected.
+ * @returns {Promise} A promise that is resolved with the arguments supplied to
+ * the notification.
+ */
+function promiseControllerNotification(
+ controller,
+ notification,
+ expected = true
+) {
+ return new Promise((resolve, reject) => {
+ let proxifiedObserver = new Proxy(
+ {},
+ {
+ get: (target, name) => {
+ if (name == notification) {
+ return (...args) => {
+ controller.removeQueryListener(proxifiedObserver);
+ if (expected) {
+ resolve(args);
+ } else {
+ reject();
+ }
+ };
+ }
+ return () => false;
+ },
+ }
+ );
+ controller.addQueryListener(proxifiedObserver);
+ });
+}
+
+/**
+ * A basic test provider, returning all the provided matches.
+ */
+class TestProvider extends UrlbarTestUtils.TestProvider {
+ isActive(context) {
+ Assert.ok(context, "context is passed-in");
+ return true;
+ }
+ getPriority(context) {
+ Assert.ok(context, "context is passed-in");
+ return 0;
+ }
+ async startQuery(context, add) {
+ Assert.ok(context, "context is passed-in");
+ Assert.equal(typeof add, "function", "add is a callback");
+ this._context = context;
+ for (const result of this._results) {
+ add(this, result);
+ }
+ }
+ cancelQuery(context) {
+ // If the query was created but didn't run, this._context will be undefined.
+ if (this._context) {
+ Assert.equal(this._context, context, "cancelQuery: context is the same");
+ }
+ if (this._onCancel) {
+ this._onCancel();
+ }
+ }
+}
+
+function convertToUtf8(str) {
+ return String.fromCharCode(...new TextEncoder().encode(str));
+}
+
+/**
+ * Helper function to clear the existing providers and register a basic provider
+ * that returns only the results given.
+ *
+ * @param {Array} results The results for the provider to return.
+ * @param {Function} [onCancel] Optional, called when the query provider
+ * receives a cancel instruction.
+ * @param {UrlbarUtils.PROVIDER_TYPE} type The provider type.
+ * @param {string} [name] Optional, use as the provider name.
+ * If none, a default name is chosen.
+ * @returns {UrlbarProvider} The provider
+ */
+function registerBasicTestProvider(results = [], onCancel, type, name) {
+ let provider = new TestProvider({ results, onCancel, type, name });
+ UrlbarProvidersManager.registerProvider(provider);
+ registerCleanupFunction(() =>
+ UrlbarProvidersManager.unregisterProvider(provider)
+ );
+ return provider;
+}
+
+// Creates an HTTP server for the test.
+function makeTestServer(port = -1) {
+ let httpServer = new HttpServer();
+ httpServer.start(port);
+ registerCleanupFunction(() => httpServer.stop(() => {}));
+ return httpServer;
+}
+
+/**
+ * Sets up a search engine that provides some suggestions by appending strings
+ * onto the search query.
+ *
+ * @param {Function} suggestionsFn
+ * A function that returns an array of suggestion strings given a
+ * search string. If not given, a default function is used.
+ * @returns {nsISearchEngine} The new engine.
+ */
+async function addTestSuggestionsEngine(suggestionsFn = null) {
+ // This port number should match the number in engine-suggestions.xml.
+ let server = makeTestServer();
+ server.registerPathHandler("/suggest", (req, resp) => {
+ let params = new URLSearchParams(req.queryString);
+ let searchStr = params.get("q");
+ let suggestions = suggestionsFn
+ ? suggestionsFn(searchStr)
+ : [searchStr].concat(["foo", "bar"].map(s => searchStr + " " + s));
+ let data = [searchStr, suggestions];
+ resp.setHeader("Content-Type", "application/json", false);
+ resp.write(JSON.stringify(data));
+ });
+ await SearchTestUtils.installSearchExtension({
+ name: SUGGESTIONS_ENGINE_NAME,
+ search_url: `http://localhost:${server.identity.primaryPort}/search`,
+ suggest_url: `http://localhost:${server.identity.primaryPort}/suggest`,
+ suggest_url_get_params: "?q={searchTerms}",
+ // test_search_suggestions_aliases.js uses the search form.
+ search_form: `http://localhost:${server.identity.primaryPort}/search?q={searchTerms}`,
+ });
+ let engine = Services.search.getEngineByName("Suggestions");
+ return engine;
+}
+
+/**
+ * Sets up a search engine that provides some tail suggestions by creating an
+ * array that mimics Google's tail suggestion responses.
+ *
+ * @param {Function} suggestionsFn
+ * A function that returns an array that mimics Google's tail suggestion
+ * responses. See bug 1626897.
+ * NOTE: Consumers specifying suggestionsFn must include searchStr as a
+ * part of the array returned by suggestionsFn.
+ * @returns {nsISearchEngine} The new engine.
+ */
+async function addTestTailSuggestionsEngine(suggestionsFn = null) {
+ // This port number should match the number in engine-tail-suggestions.xml.
+ let server = makeTestServer();
+ server.registerPathHandler("/suggest", (req, resp) => {
+ let params = new URLSearchParams(req.queryString);
+ let searchStr = params.get("q");
+ let suggestions = suggestionsFn
+ ? suggestionsFn(searchStr)
+ : [
+ "what time is it in t",
+ ["what is the time today texas"].concat(
+ ["toronto", "tunisia"].map(s => searchStr + s.slice(1))
+ ),
+ [],
+ {
+ "google:irrelevantparameter": [],
+ "google:suggestdetail": [{}].concat(
+ ["toronto", "tunisia"].map(s => ({
+ mp: "… ",
+ t: s,
+ }))
+ ),
+ },
+ ];
+ let data = suggestions;
+ let jsonString = JSON.stringify(data);
+ // This script must be evaluated as UTF-8 for this to write out the bytes of
+ // the string in UTF-8. If it's evaluated as Latin-1, the written bytes
+ // will be the result of UTF-8-encoding the result-string *twice*, which
+ // will break the "… " match prefixes.
+ let stringOfUtf8Bytes = convertToUtf8(jsonString);
+ resp.setHeader("Content-Type", "application/json", false);
+ resp.write(stringOfUtf8Bytes);
+ });
+ await SearchTestUtils.installSearchExtension({
+ name: TAIL_SUGGESTIONS_ENGINE_NAME,
+ search_url: `http://localhost:${server.identity.primaryPort}/search`,
+ suggest_url: `http://localhost:${server.identity.primaryPort}/suggest`,
+ suggest_url_get_params: "?q={searchTerms}",
+ });
+ let engine = Services.search.getEngineByName("Tail Suggestions");
+ return engine;
+}
+
+async function addOpenPages(uri, count = 1, userContextId = 0) {
+ for (let i = 0; i < count; i++) {
+ await UrlbarProviderOpenTabs.registerOpenTab(
+ uri.spec,
+ userContextId,
+ false
+ );
+ }
+}
+
+async function removeOpenPages(aUri, aCount = 1, aUserContextId = 0) {
+ for (let i = 0; i < aCount; i++) {
+ await UrlbarProviderOpenTabs.unregisterOpenTab(
+ aUri.spec,
+ aUserContextId,
+ false
+ );
+ }
+}
+
+/**
+ * Helper for tests that generate search results but aren't interested in
+ * suggestions, such as autofill tests. Installs a test engine and disables
+ * suggestions.
+ */
+function testEngine_setup() {
+ add_task(async function setup() {
+ await cleanupPlaces();
+ let engine = await addTestSuggestionsEngine();
+ let oldDefaultEngine = await Services.search.getDefault();
+
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ Services.prefs.clearUserPref(
+ "browser.search.separatePrivateDefault.ui.enabled"
+ );
+ Services.search.setDefault(
+ oldDefaultEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ });
+
+ Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ Services.prefs.setBoolPref(
+ "browser.search.separatePrivateDefault.ui.enabled",
+ false
+ );
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ });
+}
+
+async function cleanupPlaces() {
+ Services.prefs.clearUserPref("browser.urlbar.autoFill");
+ Services.prefs.clearUserPref("browser.urlbar.autoFill.searchEngines");
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+}
+
+/**
+ * Creates a UrlbarResult for a bookmark result.
+ *
+ * @param {UrlbarQueryContext} queryContext
+ * The context that this result will be displayed in.
+ * @param {object} options
+ * Options for the result.
+ * @param {string} options.title
+ * The page title.
+ * @param {string} options.uri
+ * The page URI.
+ * @param {string} [options.iconUri]
+ * A URI for the page's icon.
+ * @param {Array} [options.tags]
+ * An array of string tags. Defaults to an empty array.
+ * @param {boolean} [options.heuristic]
+ * True if this is a heuristic result. Defaults to false.
+ * @param {number} [options.source]
+ * Where the results should be sourced from. See {@link UrlbarUtils.RESULT_SOURCE}.
+ * @returns {UrlbarResult}
+ */
+function makeBookmarkResult(
+ queryContext,
+ {
+ title,
+ uri,
+ iconUri,
+ tags = [],
+ heuristic = false,
+ source = UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ }
+) {
+ let result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ source,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ url: [uri, UrlbarUtils.HIGHLIGHT.TYPED],
+ // Check against undefined so consumers can pass in the empty string.
+ icon: [typeof iconUri != "undefined" ? iconUri : `page-icon:${uri}`],
+ title: [title, UrlbarUtils.HIGHLIGHT.TYPED],
+ tags: [tags, UrlbarUtils.HIGHLIGHT.TYPED],
+ })
+ );
+
+ result.heuristic = heuristic;
+ return result;
+}
+
+/**
+ * Creates a UrlbarResult for a form history result.
+ *
+ * @param {UrlbarQueryContext} queryContext
+ * The context that this result will be displayed in.
+ * @param {object} options
+ * Options for the result.
+ * @param {string} options.suggestion
+ * The form history suggestion.
+ * @param {string} options.engineName
+ * The name of the engine that will do the search when the result is picked.
+ * @returns {UrlbarResult}
+ */
+function makeFormHistoryResult(queryContext, { suggestion, engineName }) {
+ return new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ engine: engineName,
+ suggestion: [suggestion, UrlbarUtils.HIGHLIGHT.SUGGESTED],
+ lowerCaseSuggestion: suggestion.toLocaleLowerCase(),
+ })
+ );
+}
+
+/**
+ * Creates a UrlbarResult for an omnibox extension result. For more information,
+ * see the documentation for omnibox.SuggestResult:
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/omnibox/SuggestResult
+ *
+ * @param {UrlbarQueryContext} queryContext
+ * The context that this result will be displayed in.
+ * @param {object} options
+ * Options for the result.
+ * @param {string} options.content
+ * The string displayed when the result is highlighted.
+ * @param {string} options.description
+ * The string displayed in the address bar dropdown.
+ * @param {string} options.keyword
+ * The keyword associated with the extension returning the result.
+ * @param {boolean} [options.heuristic]
+ * True if this is a heuristic result. Defaults to false.
+ * @returns {UrlbarResult}
+ */
+function makeOmniboxResult(
+ queryContext,
+ { content, description, keyword, heuristic = false }
+) {
+ let payload = {
+ title: [description, UrlbarUtils.HIGHLIGHT.TYPED],
+ content: [content, UrlbarUtils.HIGHLIGHT.TYPED],
+ keyword: [keyword, UrlbarUtils.HIGHLIGHT.TYPED],
+ icon: [UrlbarUtils.ICON.EXTENSION],
+ };
+ if (!heuristic) {
+ payload.blockL10n = { id: "urlbar-result-menu-dismiss-firefox-suggest" };
+ }
+ let result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.OMNIBOX,
+ UrlbarUtils.RESULT_SOURCE.ADDON,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, payload)
+ );
+ result.heuristic = heuristic;
+
+ return result;
+}
+
+/**
+ * Creates a UrlbarResult for an switch-to-tab result.
+ *
+ * @param {UrlbarQueryContext} queryContext
+ * The context that this result will be displayed in.
+ * @param {object} options
+ * Options for the result.
+ * @param {string} options.uri
+ * The page URI.
+ * @param {string} [options.title]
+ * The page title.
+ * @param {string} [options.iconUri]
+ * A URI for the page icon.
+ * @returns {UrlbarResult}
+ */
+function makeTabSwitchResult(queryContext, { uri, title, iconUri }) {
+ return new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ url: [uri, UrlbarUtils.HIGHLIGHT.TYPED],
+ title: [title, UrlbarUtils.HIGHLIGHT.TYPED],
+ // Check against undefined so consumers can pass in the empty string.
+ icon: typeof iconUri != "undefined" ? iconUri : `page-icon:${uri}`,
+ })
+ );
+}
+
+/**
+ * Creates a UrlbarResult for a keyword search result.
+ *
+ * @param {UrlbarQueryContext} queryContext
+ * The context that this result will be displayed in.
+ * @param {object} options
+ * Options for the result.
+ * @param {string} options.uri
+ * The page URI.
+ * @param {string} options.keyword
+ * The page's search keyword.
+ * @param {string} [options.title]
+ * The title for the bookmarked keyword page.
+ * @param {string} [options.iconUri]
+ * A URI for the engine's icon.
+ * @param {string} [options.postData]
+ * The search POST data.
+ * @param {boolean} [options.heuristic]
+ * True if this is a heuristic result. Defaults to false.
+ * @returns {UrlbarResult}
+ */
+function makeKeywordSearchResult(
+ queryContext,
+ { uri, keyword, title, iconUri, postData, heuristic = false }
+) {
+ let result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.KEYWORD,
+ UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ title: [title ? title : uri, UrlbarUtils.HIGHLIGHT.TYPED],
+ url: [uri, UrlbarUtils.HIGHLIGHT.TYPED],
+ keyword: [keyword, UrlbarUtils.HIGHLIGHT.TYPED],
+ input: [queryContext.searchString, UrlbarUtils.HIGHLIGHT.TYPED],
+ postData: postData || null,
+ icon: typeof iconUri != "undefined" ? iconUri : `page-icon:${uri}`,
+ })
+ );
+
+ if (heuristic) {
+ result.heuristic = heuristic;
+ }
+ return result;
+}
+
+/**
+ * Creates a UrlbarResult for a priority search result.
+ *
+ * @param {UrlbarQueryContext} queryContext
+ * The context that this result will be displayed in.
+ * @param {object} options
+ * Options for the result.
+ * @param {string} [options.engineName]
+ * The name of the engine providing the suggestion. Leave blank if there
+ * is no suggestion.
+ * @param {string} [options.engineIconUri]
+ * A URI for the engine's icon.
+ * @param {boolean} [options.heuristic]
+ * True if this is a heuristic result. Defaults to false.
+ * @returns {UrlbarResult}
+ */
+function makePrioritySearchResult(
+ queryContext,
+ { engineName, engineIconUri, heuristic }
+) {
+ let result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ engine: [engineName, UrlbarUtils.HIGHLIGHT.TYPED],
+ icon: engineIconUri,
+ })
+ );
+
+ if (heuristic) {
+ result.heuristic = heuristic;
+ }
+ return result;
+}
+
+/**
+ * Creates a UrlbarResult for a remote tab result.
+ *
+ * @param {UrlbarQueryContext} queryContext
+ * The context that this result will be displayed in.
+ * @param {object} options
+ * Options for the result.
+ * @param {string} options.uri
+ * The page URI.
+ * @param {string} options.device
+ * The name of the device that the remote tab comes from.
+ * @param {string} [options.title]
+ * The page title.
+ * @param {number} [options.lastUsed]
+ * The last time the remote tab was visited, in epoch seconds. Defaults
+ * to 0.
+ * @param {string} [options.iconUri]
+ * A URI for the page's icon.
+ * @returns {UrlbarResult}
+ */
+function makeRemoteTabResult(
+ queryContext,
+ { uri, device, title, iconUri, lastUsed = 0 }
+) {
+ let payload = {
+ url: [uri, UrlbarUtils.HIGHLIGHT.TYPED],
+ device: [device, UrlbarUtils.HIGHLIGHT.TYPED],
+ // Check against undefined so consumers can pass in the empty string.
+ icon:
+ typeof iconUri != "undefined"
+ ? iconUri
+ : `moz-anno:favicon:page-icon:${uri}`,
+ lastUsed: lastUsed * 1000,
+ };
+
+ // Check against undefined so consumers can pass in the empty string.
+ if (typeof title != "undefined") {
+ payload.title = [title, UrlbarUtils.HIGHLIGHT.TYPED];
+ } else {
+ payload.title = [uri, UrlbarUtils.HIGHLIGHT.TYPED];
+ }
+
+ let result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.REMOTE_TAB,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, payload)
+ );
+
+ return result;
+}
+
+/**
+ * Creates a UrlbarResult for a search result.
+ *
+ * @param {UrlbarQueryContext} queryContext
+ * The context that this result will be displayed in.
+ * @param {object} options
+ * Options for the result.
+ * @param {string} [options.suggestion]
+ * The suggestion offered by the search engine.
+ * @param {string} [options.tailPrefix]
+ * The characters placed at the end of a Google "tail" suggestion. See
+ * {@link https://firefox-source-docs.mozilla.org/browser/urlbar/nontechnical-overview.html#search-suggestions}
+ * @param {*} [options.tail]
+ * The details of the URL bar tail
+ * @param {number} [options.tailOffsetIndex]
+ * The index of the first character in the tail suggestion that should be
+ * @param {string} [options.engineName]
+ * The name of the engine providing the suggestion. Leave blank if there
+ * is no suggestion.
+ * @param {string} [options.uri]
+ * The URI that the search result will navigate to.
+ * @param {string} [options.query]
+ * The query that started the search. This overrides
+ * `queryContext.searchString`. This is useful when the query that will show
+ * up in the result object will be different from what was typed. For example,
+ * if a leading restriction token will be used.
+ * @param {string} [options.alias]
+ * The alias for the search engine, if the search is an alias search.
+ * @param {string} [options.engineIconUri]
+ * A URI for the engine's icon.
+ * @param {boolean} [options.heuristic]
+ * True if this is a heuristic result. Defaults to false.
+ * @param {boolean} [options.providesSearchMode]
+ * Whether search mode is entered when this result is selected.
+ * @param {string} [options.providerName]
+ * The name of the provider offering this result. The test suite will not
+ * check which provider offered a result unless this option is specified.
+ * @param {boolean} [options.inPrivateWindow]
+ * If the window to test is a private window.
+ * @param {boolean} [options.isPrivateEngine]
+ * If the engine is a private engine.
+ * @param {number} [options.type]
+ * The type of the search result. Defaults to UrlbarUtils.RESULT_TYPE.SEARCH.
+ * @param {number} [options.source]
+ * The source of the search result. Defaults to UrlbarUtils.RESULT_SOURCE.SEARCH.
+ * @param {boolean} [options.satisfiesAutofillThreshold]
+ * If this search should appear in the autofill section of the box
+ * @param {boolean} [options.trending]
+ * If the search result is a trending result. `Defaults to false`.
+ * @param {boolean} [options.isRichSuggestion]
+ * If the search result is a rich result. `Defaults to false`.
+ * @returns {UrlbarResult}
+ */
+function makeSearchResult(
+ queryContext,
+ {
+ suggestion,
+ tailPrefix,
+ tail,
+ tailOffsetIndex,
+ engineName,
+ alias,
+ uri,
+ query,
+ engineIconUri,
+ providesSearchMode,
+ providerName,
+ inPrivateWindow,
+ isPrivateEngine,
+ heuristic = false,
+ trending = false,
+ isRichSuggestion = false,
+ type = UrlbarUtils.RESULT_TYPE.SEARCH,
+ source = UrlbarUtils.RESULT_SOURCE.SEARCH,
+ satisfiesAutofillThreshold = false,
+ }
+) {
+ // Tail suggestion common cases, handled here to reduce verbosity in tests.
+ if (tail) {
+ if (!tailPrefix) {
+ tailPrefix = "… ";
+ }
+ if (!tailOffsetIndex) {
+ tailOffsetIndex = suggestion.indexOf(tail);
+ }
+ }
+
+ let payload = {
+ engine: [engineName, UrlbarUtils.HIGHLIGHT.TYPED],
+ suggestion: [suggestion, UrlbarUtils.HIGHLIGHT.SUGGESTED],
+ tailPrefix,
+ tail: [tail, UrlbarUtils.HIGHLIGHT.SUGGESTED],
+ tailOffsetIndex,
+ keyword: [
+ alias,
+ providesSearchMode
+ ? UrlbarUtils.HIGHLIGHT.TYPED
+ : UrlbarUtils.HIGHLIGHT.NONE,
+ ],
+ // Check against undefined so consumers can pass in the empty string.
+ query: [
+ typeof query != "undefined" ? query : queryContext.trimmedSearchString,
+ UrlbarUtils.HIGHLIGHT.TYPED,
+ ],
+ icon: engineIconUri,
+ providesSearchMode,
+ inPrivateWindow,
+ isPrivateEngine,
+ };
+
+ // Passing even an undefined URL in the payload creates a potentially-unwanted
+ // displayUrl parameter, so we add it only if specified.
+ if (uri) {
+ payload.url = uri;
+ }
+ if (providerName == "TabToSearch") {
+ payload.satisfiesAutofillThreshold = satisfiesAutofillThreshold;
+ if (payload.url.startsWith("www.")) {
+ payload.url = payload.url.substring(4);
+ }
+ payload.isGeneralPurposeEngine = false;
+ }
+
+ let result = new UrlbarResult(
+ type,
+ source,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, payload)
+ );
+
+ if (typeof suggestion == "string") {
+ result.payload.lowerCaseSuggestion =
+ result.payload.suggestion.toLocaleLowerCase();
+ result.payload.trending = trending;
+ result.payload.isRichSuggestion = isRichSuggestion;
+ }
+
+ if (providerName) {
+ result.providerName = providerName;
+ }
+
+ result.heuristic = heuristic;
+ return result;
+}
+
+/**
+ * Creates a UrlbarResult for a history result.
+ *
+ * @param {UrlbarQueryContext} queryContext
+ * The context that this result will be displayed in.
+ * @param {object} options Options for the result.
+ * @param {string} options.title
+ * The page title.
+ * @param {string} [options.fallbackTitle]
+ * The provider has capability to use the actual page title though,
+ * when the provider can’t get the page title, use this value as the fallback.
+ * @param {string} options.uri
+ * The page URI.
+ * @param {Array} [options.tags]
+ * An array of string tags. Defaults to an empty array.
+ * @param {string} [options.iconUri]
+ * A URI for the page's icon.
+ * @param {boolean} [options.heuristic]
+ * True if this is a heuristic result. Defaults to false.
+ * @param {string} options.providerName
+ * The name of the provider offering this result. The test suite will not
+ * check which provider offered a result unless this option is specified.
+ * @param {number} [options.source]
+ * The source of the result
+ * @returns {UrlbarResult}
+ */
+function makeVisitResult(
+ queryContext,
+ {
+ title,
+ fallbackTitle,
+ uri,
+ iconUri,
+ providerName,
+ tags = [],
+ heuristic = false,
+ source = UrlbarUtils.RESULT_SOURCE.HISTORY,
+ }
+) {
+ let payload = {
+ url: [uri, UrlbarUtils.HIGHLIGHT.TYPED],
+ };
+
+ if (title) {
+ payload.title = [title, UrlbarUtils.HIGHLIGHT.TYPED];
+ }
+
+ if (fallbackTitle) {
+ payload.fallbackTitle = [fallbackTitle, UrlbarUtils.HIGHLIGHT.TYPED];
+ }
+
+ if (iconUri) {
+ payload.icon = iconUri;
+ } else if (
+ iconUri === undefined &&
+ source != UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL
+ ) {
+ payload.icon = `page-icon:${uri}`;
+ }
+
+ if (!heuristic && tags) {
+ payload.tags = [tags, UrlbarUtils.HIGHLIGHT.TYPED];
+ }
+
+ let result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ source,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, payload)
+ );
+
+ if (providerName) {
+ result.providerName = providerName;
+ }
+
+ result.heuristic = heuristic;
+ return result;
+}
+
+/**
+ * Checks that the results returned by a UrlbarController match those in
+ * the param `matches`.
+ *
+ * @param {object} options Options for the check.
+ * @param {UrlbarQueryContext} options.context
+ * The context for this query.
+ * @param {string} [options.incompleteSearch]
+ * A search will be fired for this string and then be immediately canceled by
+ * the query in `context`.
+ * @param {string} [options.autofilled]
+ * The autofilled value in the first result.
+ * @param {string} [options.completed]
+ * The value that would be filled if the autofill result was confirmed.
+ * Has no effect if `autofilled` is not specified.
+ * @param {Array} options.matches
+ * An array of UrlbarResults.
+ */
+async function check_results({
+ context,
+ incompleteSearch,
+ autofilled,
+ completed,
+ matches = [],
+} = {}) {
+ if (!context) {
+ return;
+ }
+
+ // At this point frecency could still be updating due to latest pages
+ // updates.
+ // This is not a problem in real life, but autocomplete tests should
+ // return reliable resultsets, thus we have to wait.
+ await PlacesTestUtils.promiseAsyncUpdates();
+
+ const controller = UrlbarTestUtils.newMockController({
+ input: {
+ isPrivate: context.isPrivate,
+ onFirstResult() {
+ return false;
+ },
+ getSearchSource() {
+ return "dummy-search-source";
+ },
+ window: {
+ location: {
+ href: AppConstants.BROWSER_CHROME_URL,
+ },
+ },
+ },
+ });
+
+ if (incompleteSearch) {
+ let incompleteContext = createContext(incompleteSearch, {
+ isPrivate: context.isPrivate,
+ });
+ controller.startQuery(incompleteContext);
+ }
+ await controller.startQuery(context);
+
+ if (autofilled) {
+ Assert.ok(context.results[0], "There is a first result.");
+ Assert.ok(
+ context.results[0].autofill,
+ "The first result is an autofill result"
+ );
+ Assert.equal(
+ context.results[0].autofill.value,
+ autofilled,
+ "The correct value was autofilled."
+ );
+ if (completed) {
+ Assert.equal(
+ context.results[0].payload.url,
+ completed,
+ "The completed autofill value is correct."
+ );
+ }
+ }
+ if (context.results.length != matches.length) {
+ info("Actual results: " + JSON.stringify(context.results));
+ }
+ Assert.equal(
+ context.results.length,
+ matches.length,
+ "Found the expected number of results."
+ );
+
+ function getPayload(result) {
+ let payload = {};
+ for (let [key, value] of Object.entries(result.payload)) {
+ if (value !== undefined) {
+ payload[key] = value;
+ }
+ }
+ return payload;
+ }
+
+ for (let i = 0; i < matches.length; i++) {
+ let actual = context.results[i];
+ let expected = matches[i];
+ info(
+ `Comparing results at index ${i}:` +
+ " actual=" +
+ JSON.stringify(actual) +
+ " expected=" +
+ JSON.stringify(expected)
+ );
+ Assert.equal(
+ actual.type,
+ expected.type,
+ `result.type at result index ${i}`
+ );
+ Assert.equal(
+ actual.source,
+ expected.source,
+ `result.source at result index ${i}`
+ );
+ Assert.equal(
+ actual.heuristic,
+ expected.heuristic,
+ `result.heuristic at result index ${i}`
+ );
+ Assert.equal(
+ !!actual.isBestMatch,
+ !!expected.isBestMatch,
+ `result.isBestMatch at result index ${i}`
+ );
+ if (expected.providerName) {
+ Assert.equal(
+ actual.providerName,
+ expected.providerName,
+ `result.providerName at result index ${i}`
+ );
+ }
+ if (expected.hasOwnProperty("suggestedIndex")) {
+ Assert.equal(
+ actual.suggestedIndex,
+ expected.suggestedIndex,
+ `result.suggestedIndex at result index ${i}`
+ );
+ }
+
+ if (expected.payload) {
+ Assert.deepEqual(
+ getPayload(actual),
+ getPayload(expected),
+ `result.payload at result index ${i}`
+ );
+ }
+ }
+}
+
+/**
+ * Returns the frecency of an origin.
+ *
+ * @param {string} prefix
+ * The origin's prefix, e.g., "http://".
+ * @param {string} aHost
+ * The origin's host.
+ * @returns {number} The origin's frecency.
+ */
+async function getOriginFrecency(prefix, aHost) {
+ let db = await PlacesUtils.promiseDBConnection();
+ let rows = await db.execute(
+ `
+ SELECT frecency
+ FROM moz_origins
+ WHERE prefix = :prefix AND host = :host
+ `,
+ { prefix, host: aHost }
+ );
+ Assert.equal(rows.length, 1);
+ return rows[0].getResultByIndex(0);
+}
+
+/**
+ * Returns the origin frecency stats.
+ *
+ * @returns {object}
+ * An object { count, sum, squares }.
+ */
+async function getOriginFrecencyStats() {
+ let db = await PlacesUtils.promiseDBConnection();
+ let rows = await db.execute(`
+ SELECT
+ IFNULL((SELECT value FROM moz_meta WHERE key = 'origin_frecency_count'), 0),
+ IFNULL((SELECT value FROM moz_meta WHERE key = 'origin_frecency_sum'), 0),
+ IFNULL((SELECT value FROM moz_meta WHERE key = 'origin_frecency_sum_of_squares'), 0)
+ `);
+ let count = rows[0].getResultByIndex(0);
+ let sum = rows[0].getResultByIndex(1);
+ let squares = rows[0].getResultByIndex(2);
+ return { count, sum, squares };
+}
+
+/**
+ * Returns the origin autofill frecency threshold.
+ *
+ * @returns {number}
+ * The threshold.
+ */
+async function getOriginAutofillThreshold() {
+ let { count, sum, squares } = await getOriginFrecencyStats();
+ if (!count) {
+ return 0;
+ }
+ if (count == 1) {
+ return sum;
+ }
+ let stddevMultiplier = UrlbarPrefs.get("autoFill.stddevMultiplier");
+ return (
+ sum / count +
+ stddevMultiplier * Math.sqrt((squares - (sum * sum) / count) / count)
+ );
+}
+
+/**
+ * Checks that origins appear in a given order in the database.
+ *
+ * @param {string} host The "fixed" host, without "www."
+ * @param {Array} prefixOrder The prefixes (scheme + www.) sorted appropriately.
+ */
+async function checkOriginsOrder(host, prefixOrder) {
+ await PlacesUtils.withConnectionWrapper("checkOriginsOrder", async db => {
+ let prefixes = (
+ await db.execute(
+ `SELECT prefix || iif(instr(host, "www.") = 1, "www.", "")
+ FROM moz_origins
+ WHERE host = :host OR host = "www." || :host
+ ORDER BY ROWID ASC
+ `,
+ { host }
+ )
+ ).map(r => r.getResultByIndex(0));
+ Assert.deepEqual(prefixes, prefixOrder);
+ });
+}
diff --git a/browser/components/urlbar/tests/unit/test_000_frecency.js b/browser/components/urlbar/tests/unit/test_000_frecency.js
new file mode 100644
index 0000000000..cef110963f
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_000_frecency.js
@@ -0,0 +1,245 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+
+Autocomplete Frecency Tests
+
+- add a visit for each score permutation
+- search
+- test number of matches
+- test each item's location in results
+
+*/
+
+testEngine_setup();
+
+try {
+ var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].getService(
+ Ci.nsINavHistoryService
+ );
+} catch (ex) {
+ do_throw("Could not get services\n");
+}
+
+var bucketPrefs = [
+ ["firstBucketCutoff", "firstBucketWeight"],
+ ["secondBucketCutoff", "secondBucketWeight"],
+ ["thirdBucketCutoff", "thirdBucketWeight"],
+ ["fourthBucketCutoff", "fourthBucketWeight"],
+ [null, "defaultBucketWeight"],
+];
+
+var bonusPrefs = {
+ embedVisitBonus: PlacesUtils.history.TRANSITION_EMBED,
+ framedLinkVisitBonus: PlacesUtils.history.TRANSITION_FRAMED_LINK,
+ linkVisitBonus: PlacesUtils.history.TRANSITION_LINK,
+ typedVisitBonus: PlacesUtils.history.TRANSITION_TYPED,
+ bookmarkVisitBonus: PlacesUtils.history.TRANSITION_BOOKMARK,
+ downloadVisitBonus: PlacesUtils.history.TRANSITION_DOWNLOAD,
+ permRedirectVisitBonus: PlacesUtils.history.TRANSITION_REDIRECT_PERMANENT,
+ tempRedirectVisitBonus: PlacesUtils.history.TRANSITION_REDIRECT_TEMPORARY,
+ reloadVisitBonus: PlacesUtils.history.TRANSITION_RELOAD,
+};
+
+// create test data
+var searchTerm = "frecency";
+var results = [];
+var now = Date.now();
+var prefPrefix = "places.frecency.";
+
+async function task_initializeBucket(bucket) {
+ let [cutoffName, weightName] = bucket;
+ // get pref values
+ let weight = Services.prefs.getIntPref(prefPrefix + weightName, 0);
+ let cutoff = Services.prefs.getIntPref(prefPrefix + cutoffName, 0);
+ if (cutoff < 1) {
+ return;
+ }
+
+ // generate a date within the cutoff period
+ let dateInPeriod = (now - (cutoff - 1) * 86400 * 1000) * 1000;
+
+ for (let [bonusName, visitType] of Object.entries(bonusPrefs)) {
+ let frecency = -1;
+ let calculatedURI = null;
+ let matchTitle = "";
+ let bonusValue = Services.prefs.getIntPref(prefPrefix + bonusName);
+ // unvisited (only for first cutoff date bucket)
+ if (
+ bonusName == "unvisitedBookmarkBonus" ||
+ bonusName == "unvisitedTypedBonus"
+ ) {
+ if (cutoffName == "firstBucketCutoff") {
+ let points = Math.ceil((bonusValue / parseFloat(100.0)) * weight);
+ let visitCount = 1; // bonusName == "unvisitedBookmarkBonus" ? 1 : 0;
+ frecency = Math.ceil(visitCount * points);
+ calculatedURI = Services.io.newURI(
+ "http://" +
+ searchTerm +
+ ".com/" +
+ bonusName +
+ ":" +
+ bonusValue +
+ "/cutoff:" +
+ cutoff +
+ "/weight:" +
+ weight +
+ "/frecency:" +
+ frecency
+ );
+ if (bonusName == "unvisitedBookmarkBonus") {
+ matchTitle = searchTerm + "UnvisitedBookmark";
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: calculatedURI,
+ title: matchTitle,
+ });
+ } else {
+ matchTitle = searchTerm + "UnvisitedTyped";
+ await PlacesTestUtils.addVisits({
+ uri: calculatedURI,
+ title: matchTitle,
+ transition: visitType,
+ visitDate: now,
+ });
+ histsvc.markPageAsTyped(calculatedURI);
+ }
+ }
+ } else {
+ // visited
+ // visited bookmarks get the visited bookmark bonus twice
+ if (visitType == Ci.nsINavHistoryService.TRANSITION_BOOKMARK) {
+ bonusValue = bonusValue * 2;
+ }
+
+ let points = Math.ceil(
+ (1 * ((bonusValue / parseFloat(100.0)).toFixed(6) * weight)) / 1
+ );
+ if (!points) {
+ if (
+ visitType == Ci.nsINavHistoryService.TRANSITION_EMBED ||
+ visitType == Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK ||
+ visitType == Ci.nsINavHistoryService.TRANSITION_DOWNLOAD ||
+ visitType == Ci.nsINavHistoryService.TRANSITION_RELOAD ||
+ bonusName == "defaultVisitBonus"
+ ) {
+ frecency = 0;
+ } else {
+ frecency = -1;
+ }
+ } else {
+ frecency = points;
+ }
+ calculatedURI = Services.io.newURI(
+ "http://" +
+ searchTerm +
+ ".com/" +
+ bonusName +
+ ":" +
+ bonusValue +
+ "/cutoff:" +
+ cutoff +
+ "/weight:" +
+ weight +
+ "/frecency:" +
+ frecency
+ );
+ if (visitType == Ci.nsINavHistoryService.TRANSITION_BOOKMARK) {
+ matchTitle = searchTerm + "Bookmarked";
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: calculatedURI,
+ title: matchTitle,
+ });
+ } else {
+ matchTitle = calculatedURI.spec.substr(
+ calculatedURI.spec.lastIndexOf("/") + 1
+ );
+ }
+ await PlacesTestUtils.addVisits({
+ uri: calculatedURI,
+ transition: visitType,
+ visitDate: dateInPeriod,
+ });
+ }
+
+ if (calculatedURI && frecency) {
+ results.push([calculatedURI, frecency, matchTitle]);
+ await PlacesTestUtils.addVisits({
+ uri: calculatedURI,
+ title: matchTitle,
+ transition: visitType,
+ visitDate: dateInPeriod,
+ });
+ }
+ }
+}
+
+add_task(async function test_frecency() {
+ // Disable autoFill for this test.
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ // always search in history + bookmarks, no matter what the default is
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmarks", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.history");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.bookmarks");
+ Services.prefs.clearUserPref("browser.urlbar.autoFill");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+ for (let bucket of bucketPrefs) {
+ await task_initializeBucket(bucket);
+ }
+
+ // Sort results by frecency. Break ties by alphabetical URL.
+ results.sort((a, b) => {
+ let frecencyDiff = b[1] - a[1];
+ if (frecencyDiff == 0) {
+ return a[0].spec.localeCompare(b[0].spec);
+ }
+ return frecencyDiff;
+ });
+
+ // Make sure there's enough results returned
+ Services.prefs.setIntPref(
+ "browser.urlbar.maxRichResults",
+ // +1 for the heuristic search result.
+ results.length + 1
+ );
+
+ await PlacesTestUtils.promiseAsyncUpdates();
+ let context = createContext(searchTerm, { isPrivate: false });
+ let urlbarResults = [];
+ for (let result of results) {
+ let url = result[0].spec;
+ if (url.toLowerCase().includes("bookmark")) {
+ urlbarResults.push(
+ makeBookmarkResult(context, {
+ uri: url,
+ title: result[2],
+ })
+ );
+ } else {
+ urlbarResults.push(
+ makeVisitResult(context, {
+ uri: url,
+ title: result[2],
+ })
+ );
+ }
+ }
+
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...urlbarResults,
+ ],
+ });
+});
diff --git a/browser/components/urlbar/tests/unit/test_UrlbarController_integration.js b/browser/components/urlbar/tests/unit/test_UrlbarController_integration.js
new file mode 100644
index 0000000000..d94de4f439
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_UrlbarController_integration.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * These tests test the UrlbarController in association with the model.
+ */
+
+"use strict";
+
+const { PromiseUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PromiseUtils.sys.mjs"
+);
+
+const TEST_URL = "http://example.com";
+const match = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: TEST_URL }
+);
+let controller;
+
+add_task(async function setup() {
+ controller = UrlbarTestUtils.newMockController();
+});
+
+add_task(async function test_basic_search() {
+ let provider = registerBasicTestProvider([match]);
+ const context = createContext(TEST_URL, { providers: [provider.name] });
+
+ let startedPromise = promiseControllerNotification(
+ controller,
+ "onQueryStarted"
+ );
+ let resultsPromise = promiseControllerNotification(
+ controller,
+ "onQueryResults"
+ );
+
+ controller.startQuery(context);
+
+ let params = await startedPromise;
+
+ Assert.equal(params[0], context);
+
+ params = await resultsPromise;
+
+ Assert.deepEqual(
+ params[0].results,
+ [match],
+ "Should have the expected match"
+ );
+});
+
+add_task(async function test_cancel_search() {
+ let providerCanceledDeferred = PromiseUtils.defer();
+ let provider = registerBasicTestProvider(
+ [match],
+ providerCanceledDeferred.resolve
+ );
+ const context = createContext(TEST_URL, { providers: [provider.name] });
+
+ let startedPromise = promiseControllerNotification(
+ controller,
+ "onQueryStarted"
+ );
+ let cancelPromise = promiseControllerNotification(
+ controller,
+ "onQueryCancelled"
+ );
+
+ controller.startQuery(context);
+
+ let params = await startedPromise;
+
+ controller.cancelQuery(context);
+
+ Assert.equal(params[0], context);
+
+ info("Should tell the provider the query is canceled");
+ await providerCanceledDeferred.promise;
+
+ params = await cancelPromise;
+});
diff --git a/browser/components/urlbar/tests/unit/test_UrlbarController_telemetry.js b/browser/components/urlbar/tests/unit/test_UrlbarController_telemetry.js
new file mode 100644
index 0000000000..bce6bb21ff
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_UrlbarController_telemetry.js
@@ -0,0 +1,256 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * These tests unit test the functionality of UrlbarController by stubbing out the
+ * model and providing stubs to be called.
+ */
+
+"use strict";
+
+const TEST_URL = "http://example.com";
+const MATCH = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: TEST_URL }
+);
+const TELEMETRY_1ST_RESULT = "PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS";
+const TELEMETRY_6_FIRST_RESULTS = "PLACES_AUTOCOMPLETE_6_FIRST_RESULTS_TIME_MS";
+
+let controller;
+let firstHistogram;
+let sixthHistogram;
+
+/**
+ * A delayed test provider, allowing the query to be delayed for an amount of time.
+ */
+class DelayedProvider extends TestProvider {
+ async startQuery(context, add) {
+ Assert.ok(context, "context is passed-in");
+ Assert.equal(typeof add, "function", "add is a callback");
+ this._add = add;
+ await new Promise(resolve => {
+ this._resultsAdded = resolve;
+ });
+ }
+ async addResults(matches, finish = true) {
+ // startQuery may have not been invoked yet, so wait for it
+ await TestUtils.waitForCondition(
+ () => !!this._add,
+ "Waiting for the _add callback"
+ );
+ for (const match of matches) {
+ this._add(this, match);
+ }
+ if (finish) {
+ this._add = null;
+ this._resultsAdded();
+ }
+ }
+}
+
+/**
+ * Returns the number of reports sent recorded within the histogram results.
+ *
+ * @param {object} results a snapshot of histogram results to check.
+ * @returns {number} The count of reports recorded in the histogram.
+ */
+function getHistogramReportsCount(results) {
+ let sum = 0;
+ for (let [, value] of Object.entries(results.values)) {
+ sum += value;
+ }
+ return sum;
+}
+
+add_task(function setup() {
+ controller = UrlbarTestUtils.newMockController();
+
+ firstHistogram = Services.telemetry.getHistogramById(TELEMETRY_1ST_RESULT);
+ sixthHistogram = Services.telemetry.getHistogramById(
+ TELEMETRY_6_FIRST_RESULTS
+ );
+});
+
+add_task(async function test_n_autocomplete_cancel() {
+ firstHistogram.clear();
+ sixthHistogram.clear();
+
+ let providerCanceledDeferred = PromiseUtils.defer();
+ let provider = new TestProvider({
+ results: [],
+ onCancel: providerCanceledDeferred.resolve,
+ });
+ UrlbarProvidersManager.registerProvider(provider);
+ const context = createContext(TEST_URL, { providers: [provider.name] });
+
+ Assert.ok(
+ !TelemetryStopwatch.running(TELEMETRY_1ST_RESULT, context),
+ "Should not have started first result stopwatch"
+ );
+ Assert.ok(
+ !TelemetryStopwatch.running(TELEMETRY_6_FIRST_RESULTS, context),
+ "Should not have started first 6 results stopwatch"
+ );
+
+ controller.startQuery(context);
+
+ Assert.ok(
+ TelemetryStopwatch.running(TELEMETRY_1ST_RESULT, context),
+ "Should have started first result stopwatch"
+ );
+ Assert.ok(
+ TelemetryStopwatch.running(TELEMETRY_6_FIRST_RESULTS, context),
+ "Should have started first 6 results stopwatch"
+ );
+
+ controller.cancelQuery(context);
+
+ await providerCanceledDeferred.promise;
+
+ Assert.ok(
+ !TelemetryStopwatch.running(TELEMETRY_1ST_RESULT, context),
+ "Should have canceled first result stopwatch"
+ );
+ Assert.ok(
+ !TelemetryStopwatch.running(TELEMETRY_6_FIRST_RESULTS, context),
+ "Should have canceled first 6 results stopwatch"
+ );
+
+ let results = firstHistogram.snapshot();
+ Assert.equal(
+ results.sum,
+ 0,
+ "Should not have recorded any times (first result)"
+ );
+ results = sixthHistogram.snapshot();
+ Assert.equal(
+ results.sum,
+ 0,
+ "Should not have recorded any times (first 6 results)"
+ );
+});
+
+add_task(async function test_n_autocomplete_results() {
+ firstHistogram.clear();
+ sixthHistogram.clear();
+
+ let provider = new DelayedProvider();
+ UrlbarProvidersManager.registerProvider(provider);
+ const context = createContext(TEST_URL, { providers: [provider.name] });
+
+ let resultsPromise = promiseControllerNotification(
+ controller,
+ "onQueryResults"
+ );
+
+ Assert.ok(
+ !TelemetryStopwatch.running(TELEMETRY_1ST_RESULT, context),
+ "Should not have started first result stopwatch"
+ );
+ Assert.ok(
+ !TelemetryStopwatch.running(TELEMETRY_6_FIRST_RESULTS, context),
+ "Should not have started first 6 results stopwatch"
+ );
+
+ controller.startQuery(context);
+
+ Assert.ok(
+ TelemetryStopwatch.running(TELEMETRY_1ST_RESULT, context),
+ "Should have started first result stopwatch"
+ );
+ Assert.ok(
+ TelemetryStopwatch.running(TELEMETRY_6_FIRST_RESULTS, context),
+ "Should have started first 6 results stopwatch"
+ );
+
+ await provider.addResults([MATCH], false);
+ await resultsPromise;
+
+ Assert.ok(
+ !TelemetryStopwatch.running(TELEMETRY_1ST_RESULT, context),
+ "Should have stopped the first stopwatch"
+ );
+ Assert.ok(
+ TelemetryStopwatch.running(TELEMETRY_6_FIRST_RESULTS, context),
+ "Should have kept the first 6 results stopwatch running"
+ );
+
+ let firstResults = firstHistogram.snapshot();
+ let first6Results = sixthHistogram.snapshot();
+ Assert.equal(
+ getHistogramReportsCount(firstResults),
+ 1,
+ "Should have recorded one time for the first result"
+ );
+ Assert.equal(
+ getHistogramReportsCount(first6Results),
+ 0,
+ "Should not have recorded any times (first 6 results)"
+ );
+
+ // Now add 5 more results, so that the first 6 results is triggered.
+ for (let i = 0; i < 5; i++) {
+ resultsPromise = promiseControllerNotification(
+ controller,
+ "onQueryResults"
+ );
+ await provider.addResults(
+ [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: TEST_URL + "/" + i }
+ ),
+ ],
+ false
+ );
+ await resultsPromise;
+ }
+
+ Assert.ok(
+ !TelemetryStopwatch.running(TELEMETRY_1ST_RESULT, context),
+ "Should have stopped the first stopwatch"
+ );
+ Assert.ok(
+ !TelemetryStopwatch.running(TELEMETRY_6_FIRST_RESULTS, context),
+ "Should have stopped the first 6 results stopwatch"
+ );
+
+ let updatedResults = firstHistogram.snapshot();
+ let updated6Results = sixthHistogram.snapshot();
+ Assert.deepEqual(
+ updatedResults,
+ firstResults,
+ "Should not have changed the histogram for the first result"
+ );
+ Assert.equal(
+ getHistogramReportsCount(updated6Results),
+ 1,
+ "Should have recorded one time for the first 6 results"
+ );
+
+ // Add one more, to check neither are updated.
+ resultsPromise = promiseControllerNotification(controller, "onQueryResults");
+ await provider.addResults([
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: TEST_URL + "/6" }
+ ),
+ ]);
+ await resultsPromise;
+
+ let secondUpdateResults = firstHistogram.snapshot();
+ let secondUpdate6Results = sixthHistogram.snapshot();
+ Assert.deepEqual(
+ secondUpdateResults,
+ firstResults,
+ "Should not have changed the histogram for the first result"
+ );
+ Assert.equal(
+ getHistogramReportsCount(secondUpdate6Results),
+ 1,
+ "Should not have changed the histogram for the first 6 results"
+ );
+});
diff --git a/browser/components/urlbar/tests/unit/test_UrlbarController_unit.js b/browser/components/urlbar/tests/unit/test_UrlbarController_unit.js
new file mode 100644
index 0000000000..6d77e6a1ac
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_UrlbarController_unit.js
@@ -0,0 +1,389 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * These tests unit test the functionality of UrlbarController by stubbing out the
+ * model and providing stubs to be called.
+ */
+
+"use strict";
+
+// A fake ProvidersManager.
+let fPM;
+let sandbox;
+let generalListener;
+let controller;
+
+/**
+ * Asserts that the query context has the expected values.
+ *
+ * @param {UrlbarQueryContext} context The query context.
+ * @param {object} expectedValues The expected values for the UrlbarQueryContext.
+ */
+function assertContextMatches(context, expectedValues) {
+ Assert.ok(
+ context instanceof UrlbarQueryContext,
+ "Should be a UrlbarQueryContext"
+ );
+
+ for (let [key, value] of Object.entries(expectedValues)) {
+ Assert.equal(
+ context[key],
+ value,
+ `Should have the expected value for ${key} in the UrlbarQueryContext`
+ );
+ }
+}
+
+add_task(function setup() {
+ sandbox = sinon.createSandbox();
+
+ fPM = {
+ startQuery: sandbox.stub(),
+ cancelQuery: sandbox.stub(),
+ };
+
+ generalListener = {
+ onQueryStarted: sandbox.stub(),
+ onQueryResults: sandbox.stub(),
+ onQueryCancelled: sandbox.stub(),
+ };
+
+ controller = UrlbarTestUtils.newMockController({
+ manager: fPM,
+ });
+ controller.addQueryListener(generalListener);
+});
+
+add_task(function test_constructor_throws() {
+ Assert.throws(
+ () => new UrlbarController(),
+ /Missing options: input/,
+ "Should throw if the input was not supplied"
+ );
+ Assert.throws(
+ () => new UrlbarController({ input: {} }),
+ /input is missing 'window' property/,
+ "Should throw if the input is not a UrlbarInput"
+ );
+ Assert.throws(
+ () => new UrlbarController({ input: { window: {} } }),
+ /input.window should be an actual browser window/,
+ "Should throw if the input.window is not a window"
+ );
+ Assert.throws(
+ () =>
+ new UrlbarController({
+ input: {
+ window: {
+ location: "about:fake",
+ },
+ },
+ }),
+ /input.window should be an actual browser window/,
+ "Should throw if the input.window is not an object"
+ );
+ Assert.throws(
+ () =>
+ new UrlbarController({
+ input: {
+ window: {
+ location: {
+ href: "about:fake",
+ },
+ },
+ },
+ }),
+ /input.window should be an actual browser window/,
+ "Should throw if the input.window does not have the correct location"
+ );
+ Assert.throws(
+ () =>
+ new UrlbarController({
+ input: {
+ window: {
+ location: {
+ href: AppConstants.BROWSER_CHROME_URL,
+ },
+ },
+ },
+ }),
+ /input.isPrivate must be set/,
+ "Should throw if input.isPrivate is not set"
+ );
+
+ new UrlbarController({
+ input: {
+ isPrivate: false,
+ window: {
+ location: {
+ href: AppConstants.BROWSER_CHROME_URL,
+ },
+ },
+ },
+ });
+ Assert.ok(true, "Correct call should not throw");
+});
+
+add_task(function test_add_and_remove_listeners() {
+ Assert.throws(
+ () => controller.addQueryListener(null),
+ /Expected listener to be an object/,
+ "Should throw for a null listener"
+ );
+ Assert.throws(
+ () => controller.addQueryListener(123),
+ /Expected listener to be an object/,
+ "Should throw for a non-object listener"
+ );
+
+ const listener = {};
+
+ controller.addQueryListener(listener);
+
+ Assert.ok(
+ controller._listeners.has(listener),
+ "Should have added the listener to the list."
+ );
+
+ // Adding a non-existent listener shouldn't throw.
+ controller.removeQueryListener(123);
+
+ controller.removeQueryListener(listener);
+
+ Assert.ok(
+ !controller._listeners.has(listener),
+ "Should have removed the listener from the list"
+ );
+
+ sandbox.resetHistory();
+});
+
+add_task(function test__notify() {
+ const listener1 = {
+ onFake: sandbox.stub().callsFake(() => {
+ throw new Error("fake error");
+ }),
+ };
+ const listener2 = {
+ onFake: sandbox.stub(),
+ };
+
+ controller.addQueryListener(listener1);
+ controller.addQueryListener(listener2);
+
+ const param = "1234";
+
+ controller.notify("onFake", param);
+
+ Assert.equal(
+ listener1.onFake.callCount,
+ 1,
+ "Should have called the first listener method."
+ );
+ Assert.deepEqual(
+ listener1.onFake.args[0],
+ [param],
+ "Should have called the first listener with the correct argument"
+ );
+ Assert.equal(
+ listener2.onFake.callCount,
+ 1,
+ "Should have called the second listener method."
+ );
+ Assert.deepEqual(
+ listener2.onFake.args[0],
+ [param],
+ "Should have called the first listener with the correct argument"
+ );
+
+ controller.removeQueryListener(listener2);
+ controller.removeQueryListener(listener1);
+
+ // This should succeed without errors.
+ controller.notify("onNewFake");
+
+ sandbox.resetHistory();
+});
+
+add_task(function test_handle_query_starts_search() {
+ const context = createContext();
+ controller.startQuery(context);
+
+ Assert.equal(
+ fPM.startQuery.callCount,
+ 1,
+ "Should have called startQuery once"
+ );
+ Assert.equal(
+ fPM.startQuery.args[0].length,
+ 2,
+ "Should have called startQuery with two arguments"
+ );
+
+ assertContextMatches(fPM.startQuery.args[0][0], {});
+ Assert.equal(
+ fPM.startQuery.args[0][1],
+ controller,
+ "Should have passed the controller as the second argument"
+ );
+
+ Assert.equal(
+ generalListener.onQueryStarted.callCount,
+ 1,
+ "Should have called onQueryStarted for the listener"
+ );
+ Assert.deepEqual(
+ generalListener.onQueryStarted.args[0],
+ [context],
+ "Should have called onQueryStarted with the context"
+ );
+
+ sandbox.resetHistory();
+});
+
+add_task(async function test_handle_query_starts_search_sets_allowAutofill() {
+ let originalValue = Services.prefs.getBoolPref("browser.urlbar.autoFill");
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", !originalValue);
+
+ await controller.startQuery(createContext());
+
+ Assert.equal(
+ fPM.startQuery.callCount,
+ 1,
+ "Should have called startQuery once"
+ );
+ Assert.equal(
+ fPM.startQuery.args[0].length,
+ 2,
+ "Should have called startQuery with two arguments"
+ );
+
+ assertContextMatches(fPM.startQuery.args[0][0], {
+ allowAutofill: !originalValue,
+ });
+ Assert.equal(
+ fPM.startQuery.args[0][1],
+ controller,
+ "Should have passed the controller as the second argument"
+ );
+
+ sandbox.resetHistory();
+
+ Services.prefs.clearUserPref("browser.urlbar.autoFill");
+});
+
+add_task(function test_cancel_query() {
+ const context = createContext();
+ controller.startQuery(context);
+
+ controller.cancelQuery();
+
+ Assert.equal(
+ fPM.cancelQuery.callCount,
+ 1,
+ "Should have called cancelQuery once"
+ );
+ Assert.equal(
+ fPM.cancelQuery.args[0].length,
+ 1,
+ "Should have called cancelQuery with one argument"
+ );
+
+ Assert.equal(
+ generalListener.onQueryCancelled.callCount,
+ 1,
+ "Should have called onQueryCancelled for the listener"
+ );
+ Assert.deepEqual(
+ generalListener.onQueryCancelled.args[0],
+ [context],
+ "Should have called onQueryCancelled with the context"
+ );
+
+ sandbox.resetHistory();
+});
+
+add_task(function test_receiveResults() {
+ const context = createContext();
+ context.results = [];
+ controller.receiveResults(context);
+
+ Assert.equal(
+ generalListener.onQueryResults.callCount,
+ 1,
+ "Should have called onQueryResults for the listener"
+ );
+ Assert.deepEqual(
+ generalListener.onQueryResults.args[0],
+ [context],
+ "Should have called onQueryResults with the context"
+ );
+
+ sandbox.resetHistory();
+});
+
+add_task(async function test_notifications_order() {
+ // Clear any pending notifications.
+ const context = createContext();
+ await controller.startQuery(context);
+
+ // Check that when multiple queries are executed, the notifications arrive
+ // in the proper order.
+ let collectingListener = new Proxy(
+ {},
+ {
+ _notifications: [],
+ get(target, name) {
+ if (name == "notifications") {
+ return this._notifications;
+ }
+ return () => {
+ this._notifications.push(name);
+ };
+ },
+ }
+ );
+ controller.addQueryListener(collectingListener);
+ controller.startQuery(context);
+ Assert.deepEqual(
+ ["onQueryStarted"],
+ collectingListener.notifications,
+ "Check onQueryStarted is fired synchronously"
+ );
+ controller.startQuery(context);
+ Assert.deepEqual(
+ ["onQueryStarted", "onQueryCancelled", "onQueryFinished", "onQueryStarted"],
+ collectingListener.notifications,
+ "Check order of notifications"
+ );
+ controller.cancelQuery();
+ Assert.deepEqual(
+ [
+ "onQueryStarted",
+ "onQueryCancelled",
+ "onQueryFinished",
+ "onQueryStarted",
+ "onQueryCancelled",
+ "onQueryFinished",
+ ],
+ collectingListener.notifications,
+ "Check order of notifications"
+ );
+ await controller.startQuery(context);
+ controller.cancelQuery();
+ Assert.deepEqual(
+ [
+ "onQueryStarted",
+ "onQueryCancelled",
+ "onQueryFinished",
+ "onQueryStarted",
+ "onQueryCancelled",
+ "onQueryFinished",
+ "onQueryStarted",
+ "onQueryFinished",
+ ],
+ collectingListener.notifications,
+ "Check order of notifications"
+ );
+});
diff --git a/browser/components/urlbar/tests/unit/test_UrlbarPrefs.js b/browser/components/urlbar/tests/unit/test_UrlbarPrefs.js
new file mode 100644
index 0000000000..c681aca387
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_UrlbarPrefs.js
@@ -0,0 +1,449 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(function test() {
+ Assert.throws(
+ () => UrlbarPrefs.get("browser.migration.version"),
+ /Trying to access an unknown pref/,
+ "Should throw when passing an untracked pref"
+ );
+
+ Assert.throws(
+ () => UrlbarPrefs.set("browser.migration.version", 100),
+ /Trying to access an unknown pref/,
+ "Should throw when passing an untracked pref"
+ );
+ Assert.throws(
+ () => UrlbarPrefs.set("maxRichResults", "10"),
+ /Invalid value/,
+ "Should throw when passing an invalid value type"
+ );
+
+ Assert.deepEqual(UrlbarPrefs.get("formatting.enabled"), true);
+ UrlbarPrefs.set("formatting.enabled", false);
+ Assert.deepEqual(UrlbarPrefs.get("formatting.enabled"), false);
+
+ Assert.deepEqual(UrlbarPrefs.get("maxRichResults"), 10);
+ UrlbarPrefs.set("maxRichResults", 6);
+ Assert.deepEqual(UrlbarPrefs.get("maxRichResults"), 6);
+
+ Assert.deepEqual(UrlbarPrefs.get("autoFill.stddevMultiplier"), 0.0);
+ UrlbarPrefs.set("autoFill.stddevMultiplier", 0.01);
+ // Due to rounding errors, floats are slightly imprecise, so we can't
+ // directly compare what we set to what we retrieve.
+ Assert.deepEqual(
+ parseFloat(UrlbarPrefs.get("autoFill.stddevMultiplier").toFixed(2)),
+ 0.01
+ );
+});
+
+// Tests UrlbarPrefs.makeResultGroups({ showSearchSuggestionsFirst: true }).
+add_task(function makeResultGroups_true() {
+ Assert.deepEqual(
+ UrlbarPrefs.makeResultGroups({ showSearchSuggestionsFirst: true }),
+ {
+ children: [
+ // heuristic
+ {
+ maxResultCount: 1,
+ children: [
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_EXTENSION },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_SEARCH_TIP },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_OMNIBOX },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_ENGINE_ALIAS },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_BOOKMARK_KEYWORD },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_AUTOFILL },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_PRELOADED },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TOKEN_ALIAS_ENGINE },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_HISTORY_URL },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK },
+ ],
+ },
+ // extensions using the omnibox API
+ {
+ group: UrlbarUtils.RESULT_GROUP.OMNIBOX,
+ },
+ // main group
+ {
+ flexChildren: true,
+ children: [
+ // suggestions
+ {
+ flex: 2,
+ children: [
+ {
+ flexChildren: true,
+ children: [
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ },
+ {
+ flex: 4,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.TAIL_SUGGESTION,
+ },
+ ],
+ },
+ // general
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT,
+ flex: 1,
+ children: [
+ {
+ availableSpan: 3,
+ group: UrlbarUtils.RESULT_GROUP.INPUT_HISTORY,
+ },
+ {
+ flexChildren: true,
+ children: [
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_TAB,
+ },
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.ABOUT_PAGES,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.PRELOADED,
+ },
+ ],
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.INPUT_HISTORY,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ }
+ );
+});
+
+// Tests UrlbarPrefs.makeResultGroups({ showSearchSuggestionsFirst: false }).
+add_task(function makeResultGroups_false() {
+ Assert.deepEqual(
+ UrlbarPrefs.makeResultGroups({ showSearchSuggestionsFirst: false }),
+
+ {
+ children: [
+ // heuristic
+ {
+ maxResultCount: 1,
+ children: [
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_EXTENSION },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_SEARCH_TIP },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_OMNIBOX },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_ENGINE_ALIAS },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_BOOKMARK_KEYWORD },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_AUTOFILL },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_PRELOADED },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TOKEN_ALIAS_ENGINE },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_HISTORY_URL },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK },
+ ],
+ },
+ // extensions using the omnibox API
+ {
+ group: UrlbarUtils.RESULT_GROUP.OMNIBOX,
+ },
+ // main group
+ {
+ flexChildren: true,
+ children: [
+ // general
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT,
+ flex: 2,
+ children: [
+ {
+ availableSpan: 3,
+ group: UrlbarUtils.RESULT_GROUP.INPUT_HISTORY,
+ },
+ {
+ flexChildren: true,
+ children: [
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_TAB,
+ },
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.ABOUT_PAGES,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.PRELOADED,
+ },
+ ],
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.INPUT_HISTORY,
+ },
+ ],
+ },
+ // suggestions
+ {
+ flex: 1,
+ children: [
+ {
+ flexChildren: true,
+ children: [
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ },
+ {
+ flex: 4,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.TAIL_SUGGESTION,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ }
+ );
+});
+
+// Tests interaction between showSearchSuggestionsFirst and resultGroups.
+add_task(function showSearchSuggestionsFirst_resultGroups() {
+ // Check initial values.
+ Assert.equal(
+ UrlbarPrefs.get("showSearchSuggestionsFirst"),
+ true,
+ "showSearchSuggestionsFirst is true initially"
+ );
+ Assert.deepEqual(
+ UrlbarPrefs.resultGroups,
+ UrlbarPrefs.makeResultGroups({ showSearchSuggestionsFirst: true }),
+ "resultGroups is the same as the groups for which howSearchSuggestionsFirst is true"
+ );
+
+ // Set showSearchSuggestionsFirst = false.
+ UrlbarPrefs.set("showSearchSuggestionsFirst", false);
+ Assert.deepEqual(
+ UrlbarPrefs.resultGroups,
+ UrlbarPrefs.makeResultGroups({ showSearchSuggestionsFirst: false }),
+ "resultGroups is updated after setting showSearchSuggestionsFirst = false"
+ );
+
+ // Set showSearchSuggestionsFirst = true.
+ UrlbarPrefs.set("showSearchSuggestionsFirst", true);
+ Assert.deepEqual(
+ UrlbarPrefs.resultGroups,
+ UrlbarPrefs.makeResultGroups({ showSearchSuggestionsFirst: true }),
+ "resultGroups is updated after setting showSearchSuggestionsFirst = true"
+ );
+
+ // Set showSearchSuggestionsFirst = false again so we can clear it next.
+ UrlbarPrefs.set("showSearchSuggestionsFirst", false);
+ Assert.deepEqual(
+ UrlbarPrefs.resultGroups,
+ UrlbarPrefs.makeResultGroups({ showSearchSuggestionsFirst: false }),
+ "resultGroups is updated after setting showSearchSuggestionsFirst = false"
+ );
+
+ // Clear showSearchSuggestionsFirst.
+ Services.prefs.clearUserPref("browser.urlbar.showSearchSuggestionsFirst");
+ Assert.deepEqual(
+ UrlbarPrefs.resultGroups,
+ UrlbarPrefs.makeResultGroups({ showSearchSuggestionsFirst: true }),
+ "resultGroups is updated immediately after clearing showSearchSuggestionsFirst"
+ );
+ Assert.equal(
+ UrlbarPrefs.get("showSearchSuggestionsFirst"),
+ true,
+ "showSearchSuggestionsFirst defaults to true after clearing it"
+ );
+ Assert.deepEqual(
+ UrlbarPrefs.resultGroups,
+ UrlbarPrefs.makeResultGroups({ showSearchSuggestionsFirst: true }),
+ "resultGroups remains correct after getting showSearchSuggestionsFirst"
+ );
+});
+
+// Tests UrlbarPrefs.initializeShowSearchSuggestionsFirstPref() and the
+// interaction between matchGroups, showSearchSuggestionsFirst, and
+// resultGroups. It's a little complex, but the flow is:
+//
+// 1. The old matchGroups pref has some value
+// 2. UrlbarPrefs.initializeShowSearchSuggestionsFirstPref() is called to
+// translate matchGroups into the newer showSearchSuggestionsFirst pref
+// 3. The update to showSearchSuggestionsFirst causes the new resultGroups
+// pref to be set
+add_task(function initializeShowSearchSuggestionsFirstPref() {
+ // Each value in `tests`: [matchGroups, expectedShowSearchSuggestionsFirst]
+ let tests = [
+ ["suggestion:4,general:Infinity", true],
+ ["suggestion:4,general:5", true],
+ ["suggestion:1,general:5,suggestion:Infinity", true],
+ ["suggestion:Infinity", true],
+ ["suggestion:4", true],
+
+ ["foo:1,suggestion:4,general:Infinity", true],
+ ["foo:2,suggestion:4,general:5", true],
+ ["foo:3,suggestion:1,general:5,suggestion:Infinity", true],
+ ["foo:4,suggestion:Infinity", true],
+ ["foo:5,suggestion:4", true],
+
+ ["general:5,suggestion:Infinity", false],
+ ["general:5,suggestion:4", false],
+ ["general:1,suggestion:4,general:Infinity", false],
+ ["general:Infinity", false],
+ ["general:5", false],
+
+ ["foo:1,general:5,suggestion:Infinity", false],
+ ["foo:2,general:5,suggestion:4", false],
+ ["foo:3,general:1,suggestion:4,general:Infinity", false],
+ ["foo:4,general:Infinity", false],
+ ["foo:5,general:5", false],
+
+ ["", true],
+ ["bogus groups", true],
+ ];
+
+ for (let [matchGroups, expectedValue] of tests) {
+ info("Running test: " + JSON.stringify({ matchGroups, expectedValue }));
+ Services.prefs.clearUserPref("browser.urlbar.showSearchSuggestionsFirst");
+
+ // Set matchGroups.
+ Services.prefs.setCharPref("browser.urlbar.matchGroups", matchGroups);
+
+ // Call initializeShowSearchSuggestionsFirstPref.
+ UrlbarPrefs.initializeShowSearchSuggestionsFirstPref();
+
+ // Both showSearchSuggestionsFirst and resultGroups should be updated.
+ Assert.equal(
+ Services.prefs.getBoolPref("browser.urlbar.showSearchSuggestionsFirst"),
+ expectedValue,
+ "showSearchSuggestionsFirst has the expected value"
+ );
+ Assert.deepEqual(
+ UrlbarPrefs.resultGroups,
+ UrlbarPrefs.makeResultGroups({
+ showSearchSuggestionsFirst: expectedValue,
+ }),
+ "resultGroups should be updated with the appropriate default"
+ );
+ }
+
+ Services.prefs.clearUserPref("browser.urlbar.matchGroups");
+});
+
+// Tests whether observer.onNimbusChanged works.
+add_task(async function onNimbusChanged() {
+ Services.prefs.setBoolPref(
+ "browser.urlbar.autoFill.adaptiveHistory.enabled",
+ false
+ );
+
+ // Add an observer that throws an Error and an observer that does not define
+ // anything to check whether the other observers can get notifications.
+ UrlbarPrefs.addObserver({
+ onPrefChanged(pref) {
+ throw new Error("From onPrefChanged");
+ },
+ onNimbusChanged(pref) {
+ throw new Error("From onNimbusChanged");
+ },
+ });
+ UrlbarPrefs.addObserver({});
+
+ const observer = {
+ onPrefChanged(pref) {
+ this.prefChangedList.push(pref);
+ },
+ onNimbusChanged(pref) {
+ this.nimbusChangedList.push(pref);
+ },
+ };
+ observer.prefChangedList = [];
+ observer.nimbusChangedList = [];
+ UrlbarPrefs.addObserver(observer);
+
+ const doCleanup = await UrlbarTestUtils.initNimbusFeature({
+ autoFillAdaptiveHistoryEnabled: true,
+ });
+ Assert.equal(observer.prefChangedList.length, 0);
+ Assert.ok(
+ observer.nimbusChangedList.includes("autoFillAdaptiveHistoryEnabled")
+ );
+ doCleanup();
+});
+
+// Tests whether observer.onPrefChanged works.
+add_task(async function onPrefChanged() {
+ const doCleanup = await UrlbarTestUtils.initNimbusFeature({
+ autoFillAdaptiveHistoryEnabled: false,
+ });
+ Services.prefs.setBoolPref(
+ "browser.urlbar.autoFill.adaptiveHistory.enabled",
+ false
+ );
+
+ // Add an observer that throws an Error and an observer that does not define
+ // anything to check whether the other observers can get notifications.
+ UrlbarPrefs.addObserver({
+ onPrefChanged(pref) {
+ throw new Error("From onPrefChanged");
+ },
+ onNimbusChanged(pref) {
+ throw new Error("From onNimbusChanged");
+ },
+ });
+ UrlbarPrefs.addObserver({});
+
+ const deferred = PromiseUtils.defer();
+ const observer = {
+ onPrefChanged(pref) {
+ this.prefChangedList.push(pref);
+ deferred.resolve();
+ },
+ onNimbusChanged(pref) {
+ this.nimbusChangedList.push(pref);
+ deferred.resolve();
+ },
+ };
+ observer.prefChangedList = [];
+ observer.nimbusChangedList = [];
+ UrlbarPrefs.addObserver(observer);
+
+ Services.prefs.setBoolPref(
+ "browser.urlbar.autoFill.adaptiveHistory.enabled",
+ true
+ );
+ await deferred.promise;
+ Assert.equal(observer.prefChangedList.length, 1);
+ Assert.equal(observer.prefChangedList[0], "autoFill.adaptiveHistory.enabled");
+ Assert.equal(observer.nimbusChangedList.length, 0);
+
+ Services.prefs.clearUserPref(
+ "browser.urlbar.autoFill.adaptiveHistory.enabled"
+ );
+ doCleanup();
+});
diff --git a/browser/components/urlbar/tests/unit/test_UrlbarQueryContext.js b/browser/components/urlbar/tests/unit/test_UrlbarQueryContext.js
new file mode 100644
index 0000000000..e30e2fa0eb
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_UrlbarQueryContext.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(function test_constructor() {
+ Assert.throws(
+ () => new UrlbarQueryContext(),
+ /Missing or empty allowAutofill provided to UrlbarQueryContext/,
+ "Should throw with no arguments"
+ );
+
+ Assert.throws(
+ () =>
+ new UrlbarQueryContext({
+ allowAutofill: true,
+ isPrivate: false,
+ searchString: "foo",
+ }),
+ /Missing or empty maxResults provided to UrlbarQueryContext/,
+ "Should throw with a missing maxResults parameter"
+ );
+
+ Assert.throws(
+ () =>
+ new UrlbarQueryContext({
+ allowAutofill: true,
+ maxResults: 1,
+ searchString: "foo",
+ }),
+ /Missing or empty isPrivate provided to UrlbarQueryContext/,
+ "Should throw with a missing isPrivate parameter"
+ );
+
+ Assert.throws(
+ () =>
+ new UrlbarQueryContext({
+ isPrivate: false,
+ maxResults: 1,
+ searchString: "foo",
+ }),
+ /Missing or empty allowAutofill provided to UrlbarQueryContext/,
+ "Should throw with a missing allowAutofill parameter"
+ );
+
+ let qc = new UrlbarQueryContext({
+ allowAutofill: false,
+ isPrivate: true,
+ maxResults: 1,
+ searchString: "foo",
+ });
+
+ Assert.strictEqual(
+ qc.allowAutofill,
+ false,
+ "Should have saved the correct value for allowAutofill"
+ );
+ Assert.strictEqual(
+ qc.isPrivate,
+ true,
+ "Should have saved the correct value for isPrivate"
+ );
+ Assert.equal(
+ qc.maxResults,
+ 1,
+ "Should have saved the correct value for maxResults"
+ );
+ Assert.equal(
+ qc.searchString,
+ "foo",
+ "Should have saved the correct value for searchString"
+ );
+});
diff --git a/browser/components/urlbar/tests/unit/test_UrlbarQueryContext_restrictSource.js b/browser/components/urlbar/tests/unit/test_UrlbarQueryContext_restrictSource.js
new file mode 100644
index 0000000000..0aadb8aef6
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_UrlbarQueryContext_restrictSource.js
@@ -0,0 +1,132 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test for restrictions set through UrlbarQueryContext.sources.
+ */
+
+testEngine_setup();
+
+add_task(async function test_restrictions() {
+ await PlacesTestUtils.addVisits([
+ { uri: "http://history.com/", title: "match" },
+ ]);
+ await PlacesUtils.bookmarks.insert({
+ url: "http://bookmark.com/",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "match",
+ });
+ await UrlbarProviderOpenTabs.registerOpenTab(
+ "http://openpagematch.com/",
+ 0,
+ false
+ );
+
+ info("Bookmark restrict");
+ let results = await get_results({
+ sources: [UrlbarUtils.RESULT_SOURCE.BOOKMARKS],
+ searchString: "match",
+ });
+ // Skip the heuristic result.
+ Assert.deepEqual(
+ results.filter(r => !r.heuristic).map(r => r.payload.url),
+ ["http://bookmark.com/"]
+ );
+
+ info("History restrict");
+ results = await get_results({
+ sources: [UrlbarUtils.RESULT_SOURCE.HISTORY],
+ searchString: "match",
+ });
+ // Skip the heuristic result.
+ Assert.deepEqual(
+ results.filter(r => !r.heuristic).map(r => r.payload.url),
+ ["http://history.com/"]
+ );
+
+ info("tabs restrict");
+ results = await get_results({
+ sources: [UrlbarUtils.RESULT_SOURCE.TABS],
+ searchString: "match",
+ });
+ // Skip the heuristic result.
+ Assert.deepEqual(
+ results.filter(r => !r.heuristic).map(r => r.payload.url),
+ ["http://openpagematch.com/"]
+ );
+
+ info("search restrict");
+ results = await get_results({
+ sources: [UrlbarUtils.RESULT_SOURCE.SEARCH],
+ searchString: "match",
+ });
+ Assert.ok(
+ !results.some(r => r.payload.engine != SUGGESTIONS_ENGINE_NAME),
+ "All the results should be search results"
+ );
+
+ info("search restrict should ignore restriction token");
+ results = await get_results({
+ sources: [UrlbarUtils.RESULT_SOURCE.SEARCH],
+ searchString: `${UrlbarTokenizer.RESTRICT.BOOKMARKS} match`,
+ });
+ Assert.ok(
+ !results.some(r => r.payload.engine != SUGGESTIONS_ENGINE_NAME),
+ "All the results should be search results"
+ );
+ Assert.equal(
+ results[0].payload.query,
+ `${UrlbarTokenizer.RESTRICT.BOOKMARKS} match`,
+ "The restriction token should be ignored and not stripped"
+ );
+
+ info("search restrict with alias");
+ await SearchTestUtils.installSearchExtension({
+ name: "Test",
+ keyword: "match",
+ });
+ results = await get_results({
+ sources: [UrlbarUtils.RESULT_SOURCE.SEARCH],
+ searchString: "match this",
+ });
+ Assert.ok(
+ !results.some(r => r.payload.engine != SUGGESTIONS_ENGINE_NAME),
+ "All the results should be search results and the alias should be ignored"
+ );
+ Assert.equal(
+ results[0].payload.query,
+ `match this`,
+ "The restriction token should be ignored and not stripped"
+ );
+
+ info("search restrict with other engine");
+ results = await get_results({
+ sources: [UrlbarUtils.RESULT_SOURCE.SEARCH],
+ searchString: "match",
+ engineName: "Test",
+ });
+ Assert.ok(
+ !results.some(r => r.payload.engine != "Test"),
+ "All the results should be search results from the Test engine"
+ );
+});
+
+async function get_results(test) {
+ let controller = UrlbarTestUtils.newMockController();
+ let options = {
+ allowAutofill: false,
+ isPrivate: false,
+ maxResults: 10,
+ sources: test.sources,
+ };
+ if (test.engineName) {
+ options.searchMode = {
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ engineName: test.engineName,
+ };
+ }
+ let queryContext = createContext(test.searchString, options);
+ await controller.startQuery(queryContext);
+ return queryContext.results;
+}
diff --git a/browser/components/urlbar/tests/unit/test_UrlbarSearchUtils.js b/browser/components/urlbar/tests/unit/test_UrlbarSearchUtils.js
new file mode 100644
index 0000000000..a39815937e
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_UrlbarSearchUtils.js
@@ -0,0 +1,462 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { UrlbarSearchUtils } = ChromeUtils.importESModule(
+ "resource:///modules/UrlbarSearchUtils.sys.mjs"
+);
+
+let baconEngineExtension;
+
+add_task(async function () {
+ await UrlbarSearchUtils.init();
+ // Tell the search service we are running in the US. This also has the
+ // desired side-effect of preventing our geoip lookup.
+ Services.prefs.setCharPref("browser.search.region", "US");
+
+ Services.search.restoreDefaultEngines();
+ Services.search.resetToAppDefaultEngine();
+});
+
+add_task(async function search_engine_match() {
+ let engine = await Services.search.getDefault();
+ let domain = engine.searchUrlDomain;
+ let token = domain.substr(0, 1);
+ let matchedEngine = (
+ await UrlbarSearchUtils.enginesForDomainPrefix(token)
+ )[0];
+ Assert.equal(matchedEngine, engine);
+});
+
+add_task(async function no_match() {
+ Assert.equal(
+ 0,
+ (await UrlbarSearchUtils.enginesForDomainPrefix("test")).length
+ );
+});
+
+add_task(async function hide_search_engine_nomatch() {
+ let engine = await Services.search.getDefault();
+ let domain = engine.searchUrlDomain;
+ let token = domain.substr(0, 1);
+ let promiseTopic = promiseSearchTopic("engine-changed");
+ await Promise.all([Services.search.removeEngine(engine), promiseTopic]);
+ Assert.ok(engine.hidden);
+ let matchedEngines = await UrlbarSearchUtils.enginesForDomainPrefix(token);
+ Assert.ok(
+ !matchedEngines.length || matchedEngines[0].searchUrlDomain != domain
+ );
+ engine.hidden = false;
+ await TestUtils.waitForCondition(
+ async () => (await UrlbarSearchUtils.enginesForDomainPrefix(token)).length
+ );
+ let matchedEngine2 = (
+ await UrlbarSearchUtils.enginesForDomainPrefix(token)
+ )[0];
+ Assert.ok(matchedEngine2);
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+});
+
+add_task(async function onlyEnabled_option_nomatch() {
+ let engine = await Services.search.getDefault();
+ let domain = engine.searchUrlDomain;
+ let token = domain.substr(0, 1);
+ Services.prefs.setCharPref("browser.search.hiddenOneOffs", engine.name);
+ let matchedEngines = await UrlbarSearchUtils.enginesForDomainPrefix(token, {
+ onlyEnabled: true,
+ });
+ Assert.notEqual(matchedEngines[0].searchUrlDomain, domain);
+ Services.prefs.clearUserPref("browser.search.hiddenOneOffs");
+ matchedEngines = await UrlbarSearchUtils.enginesForDomainPrefix(token, {
+ onlyEnabled: true,
+ });
+ Assert.equal(matchedEngines[0].searchUrlDomain, domain);
+});
+
+add_task(async function add_search_engine_match() {
+ Assert.equal(
+ 0,
+ (await UrlbarSearchUtils.enginesForDomainPrefix("bacon")).length
+ );
+ baconEngineExtension = await SearchTestUtils.installSearchExtension(
+ {
+ name: "bacon",
+ keyword: "pork",
+ search_url: "https://www.bacon.moz/",
+ },
+ { skipUnload: true }
+ );
+ let matchedEngine = (
+ await UrlbarSearchUtils.enginesForDomainPrefix("bacon")
+ )[0];
+ Assert.ok(matchedEngine);
+ Assert.equal(matchedEngine.searchForm, "https://www.bacon.moz");
+ Assert.equal(matchedEngine.name, "bacon");
+ Assert.equal(matchedEngine.iconURI, null);
+ info("also type part of the public suffix");
+ matchedEngine = (
+ await UrlbarSearchUtils.enginesForDomainPrefix("bacon.m")
+ )[0];
+ Assert.ok(matchedEngine);
+ Assert.equal(matchedEngine.searchForm, "https://www.bacon.moz");
+ Assert.equal(matchedEngine.name, "bacon");
+ Assert.equal(matchedEngine.iconURI, null);
+});
+
+add_task(async function match_multiple_search_engines() {
+ Assert.equal(
+ 0,
+ (await UrlbarSearchUtils.enginesForDomainPrefix("baseball")).length
+ );
+ await SearchTestUtils.installSearchExtension({
+ name: "baseball",
+ search_url: "https://www.baseball.moz/",
+ });
+ let matchedEngines = await UrlbarSearchUtils.enginesForDomainPrefix("ba");
+ Assert.equal(
+ matchedEngines.length,
+ 2,
+ "enginesForDomainPrefix returned two engines."
+ );
+ Assert.equal(matchedEngines[0].searchForm, "https://www.bacon.moz");
+ Assert.equal(matchedEngines[0].name, "bacon");
+ Assert.equal(matchedEngines[1].searchForm, "https://www.baseball.moz");
+ Assert.equal(matchedEngines[1].name, "baseball");
+});
+
+add_task(async function test_aliased_search_engine_match() {
+ Assert.equal(null, await UrlbarSearchUtils.engineForAlias("sober"));
+ // Lower case
+ let matchedEngine = await UrlbarSearchUtils.engineForAlias("pork");
+ Assert.ok(matchedEngine);
+ Assert.equal(matchedEngine.name, "bacon");
+ Assert.ok(matchedEngine.aliases.includes("pork"));
+ Assert.equal(matchedEngine.iconURI, null);
+ // Upper case
+ matchedEngine = await UrlbarSearchUtils.engineForAlias("PORK");
+ Assert.ok(matchedEngine);
+ Assert.equal(matchedEngine.name, "bacon");
+ Assert.ok(matchedEngine.aliases.includes("pork"));
+ Assert.equal(matchedEngine.iconURI, null);
+ // Cap case
+ matchedEngine = await UrlbarSearchUtils.engineForAlias("Pork");
+ Assert.ok(matchedEngine);
+ Assert.equal(matchedEngine.name, "bacon");
+ Assert.ok(matchedEngine.aliases.includes("pork"));
+ Assert.equal(matchedEngine.iconURI, null);
+});
+
+add_task(async function test_aliased_search_engine_match_upper_case_alias() {
+ Assert.equal(
+ 0,
+ (await UrlbarSearchUtils.enginesForDomainPrefix("patch")).length
+ );
+ await SearchTestUtils.installSearchExtension({
+ name: "patch",
+ keyword: "PR",
+ search_url: "https://www.patch.moz/",
+ });
+ // lower case
+ let matchedEngine = await UrlbarSearchUtils.engineForAlias("pr");
+ Assert.ok(matchedEngine);
+ Assert.equal(matchedEngine.name, "patch");
+ Assert.ok(matchedEngine.aliases.includes("PR"));
+ Assert.equal(matchedEngine.iconURI, null);
+ // Upper case
+ matchedEngine = await UrlbarSearchUtils.engineForAlias("PR");
+ Assert.ok(matchedEngine);
+ Assert.equal(matchedEngine.name, "patch");
+ Assert.ok(matchedEngine.aliases.includes("PR"));
+ Assert.equal(matchedEngine.iconURI, null);
+ // Cap case
+ matchedEngine = await UrlbarSearchUtils.engineForAlias("Pr");
+ Assert.ok(matchedEngine);
+ Assert.equal(matchedEngine.name, "patch");
+ Assert.ok(matchedEngine.aliases.includes("PR"));
+ Assert.equal(matchedEngine.iconURI, null);
+});
+
+add_task(async function remove_search_engine_nomatch() {
+ let promiseTopic = promiseSearchTopic("engine-removed");
+ await Promise.all([baconEngineExtension.unload(), promiseTopic]);
+ Assert.equal(
+ 0,
+ (await UrlbarSearchUtils.enginesForDomainPrefix("bacon")).length
+ );
+});
+
+add_task(async function test_builtin_aliased_search_engine_match() {
+ let engine = await UrlbarSearchUtils.engineForAlias("@google");
+ Assert.ok(engine);
+ Assert.equal(engine.name, "Google");
+ let promiseTopic = promiseSearchTopic("engine-changed");
+ await Promise.all([Services.search.removeEngine(engine), promiseTopic]);
+ let matchedEngine = await UrlbarSearchUtils.engineForAlias("@google");
+ Assert.ok(!matchedEngine);
+ engine.hidden = false;
+ await TestUtils.waitForCondition(() =>
+ UrlbarSearchUtils.engineForAlias("@google")
+ );
+ engine = await UrlbarSearchUtils.engineForAlias("@google");
+ Assert.ok(engine);
+});
+
+add_task(async function test_serps_are_equivalent() {
+ info("Subset URL has extraneous parameters.");
+ let url1 = "https://example.com/search?q=test&type=images";
+ let url2 = "https://example.com/search?q=test";
+ Assert.ok(!UrlbarSearchUtils.serpsAreEquivalent(url1, url2));
+ info("Superset URL has extraneous parameters.");
+ Assert.ok(UrlbarSearchUtils.serpsAreEquivalent(url2, url1));
+
+ info("Same keys, different values.");
+ url1 = "https://example.com/search?q=test&type=images";
+ url2 = "https://example.com/search?q=test123&type=maps";
+ Assert.ok(!UrlbarSearchUtils.serpsAreEquivalent(url1, url2));
+ Assert.ok(!UrlbarSearchUtils.serpsAreEquivalent(url2, url1));
+
+ info("Subset matching isn't strict (URL is subset of itself).");
+ Assert.ok(UrlbarSearchUtils.serpsAreEquivalent(url1, url1));
+
+ info("Origin and pathname are ignored.");
+ url1 = "https://example.com/search?q=test";
+ url2 = "https://example-1.com/maps?q=test";
+ Assert.ok(UrlbarSearchUtils.serpsAreEquivalent(url1, url2));
+ Assert.ok(UrlbarSearchUtils.serpsAreEquivalent(url2, url1));
+
+ info("Params can be optionally ignored");
+ url1 = "https://example.com/search?q=test&abc=123&foo=bar";
+ url2 = "https://example.com/search?q=test";
+ Assert.ok(!UrlbarSearchUtils.serpsAreEquivalent(url1, url2));
+ Assert.ok(UrlbarSearchUtils.serpsAreEquivalent(url1, url2, ["abc", "foo"]));
+});
+
+add_task(async function test_get_root_domain_from_engine() {
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: "TestEngine2",
+ search_url: "https://example.com/",
+ },
+ { skipUnload: true }
+ );
+ let engine = Services.search.getEngineByName("TestEngine2");
+ Assert.equal(UrlbarSearchUtils.getRootDomainFromEngine(engine), "example");
+ await extension.unload();
+
+ extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: "TestEngine",
+ search_url: "https://www.subdomain.othersubdomain.example.com",
+ },
+ { skipUnload: true }
+ );
+ engine = Services.search.getEngineByName("TestEngine");
+ Assert.equal(UrlbarSearchUtils.getRootDomainFromEngine(engine), "example");
+ await extension.unload();
+
+ // We let engines with URL ending in .test through even though its not a valid
+ // TLD.
+ extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: "TestMalformed",
+ search_url: "https://mochi.test/",
+ search_url_get_params: "search={searchTerms}",
+ },
+ { skipUnload: true }
+ );
+ engine = Services.search.getEngineByName("TestMalformed");
+ Assert.equal(UrlbarSearchUtils.getRootDomainFromEngine(engine), "mochi");
+ await extension.unload();
+
+ // We return the domain for engines with a malformed URL.
+ extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: "TestMalformed",
+ search_url: "https://subdomain.foobar/",
+ search_url_get_params: "search={searchTerms}",
+ },
+ { skipUnload: true }
+ );
+ engine = Services.search.getEngineByName("TestMalformed");
+ Assert.equal(
+ UrlbarSearchUtils.getRootDomainFromEngine(engine),
+ "subdomain.foobar"
+ );
+ await extension.unload();
+});
+
+// Tests getSearchTermIfDefaultSerpUri() by using a variety of
+// input strings and nsIURI's.
+// Should not throw an error if the consumer passes an input
+// that when accessed, could cause an error.
+add_task(async function get_search_term_if_default_serp_uri() {
+ let testCases = [
+ {
+ url: null,
+ skipUriTest: true,
+ },
+ {
+ url: "",
+ skipUriTest: true,
+ },
+ {
+ url: "about:blank",
+ },
+ {
+ url: "about:home",
+ },
+ {
+ url: "about:newtab",
+ },
+ {
+ url: "not://a/supported/protocol",
+ },
+ {
+ url: "view-source:http://www.example.com/",
+ },
+ {
+ // Not a default engine.
+ url: "http://mochi.test:8888/?q=chocolate&pc=sample_code",
+ },
+ {
+ // Not the correct protocol.
+ url: "http://example.com/?q=chocolate&pc=sample_code",
+ },
+ {
+ // Not the same query param values.
+ url: "https://example.com/?q=chocolate&pc=sample_code2",
+ },
+ {
+ // Not the same query param values.
+ url: "https://example.com/?q=chocolate&pc=sample_code&pc2=sample_code_2",
+ },
+ {
+ url: "https://example.com/?q=chocolate&pc=sample_code",
+ expectedString: "chocolate",
+ },
+ {
+ url: "https://example.com/?q=chocolate+cakes&pc=sample_code",
+ expectedString: "chocolate cakes",
+ },
+ ];
+
+ // Create a specific engine so that the tests are matched
+ // exactly against the query params used.
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: "TestEngine",
+ search_url: "https://example.com/",
+ search_url_get_params: "?q={searchTerms}&pc=sample_code",
+ },
+ { skipUnload: true }
+ );
+ let engine = Services.search.getEngineByName("TestEngine");
+ let originalDefaultEngine = Services.search.defaultEngine;
+ Services.search.defaultEngine = engine;
+
+ for (let testCase of testCases) {
+ let expectedString = testCase.expectedString ?? "";
+ Assert.equal(
+ UrlbarSearchUtils.getSearchTermIfDefaultSerpUri(testCase.url),
+ expectedString,
+ `Should return ${
+ expectedString == "" ? "an empty string" : "a matching search string"
+ }`
+ );
+ // Convert the string into a nsIURI and then
+ // try the test case with it.
+ if (!testCase.skipUriTest) {
+ Assert.equal(
+ UrlbarSearchUtils.getSearchTermIfDefaultSerpUri(
+ Services.io.newURI(testCase.url)
+ ),
+ expectedString,
+ `Should return ${
+ expectedString == "" ? "an empty string" : "a matching search string"
+ }`
+ );
+ }
+ }
+
+ Services.search.defaultEngine = originalDefaultEngine;
+ await extension.unload();
+});
+
+add_task(async function matchAllDomainLevels() {
+ let baseHostname = "matchalldomainlevels";
+ Assert.equal(
+ (await UrlbarSearchUtils.enginesForDomainPrefix(baseHostname)).length,
+ 0,
+ `Sanity check: No engines initially match ${baseHostname}`
+ );
+
+ // Install engines with the following domains. When we match engines below,
+ // perfectly matching domains should come before partially matching domains.
+ let baseDomain = `${baseHostname}.com`;
+ let perfectDomains = [baseDomain, `www.${baseDomain}`];
+ let partialDomains = [`foo.${baseDomain}`, `foo.bar.${baseDomain}`];
+
+ // Install engines with partially matching domains first so that the test
+ // isn't incidentally passing because engines are installed in the order it
+ // ultimately expects them in. Wait for each engine to finish installing
+ // before starting the next one to avoid intermittent out-of-order failures.
+ let extensions = [];
+ for (let list of [partialDomains, perfectDomains]) {
+ for (let domain of list) {
+ let ext = await SearchTestUtils.installSearchExtension(
+ {
+ name: domain,
+ search_url: `https://${domain}/`,
+ },
+ { skipUnload: true }
+ );
+ extensions.push(ext);
+ }
+ }
+
+ // Perfect matches come before partial matches.
+ let expectedDomains = [...perfectDomains, ...partialDomains];
+
+ // Do searches for the following strings. Each should match all the engines
+ // installed above.
+ let searchStrings = [baseHostname, baseHostname + "."];
+ for (let searchString of searchStrings) {
+ info(`Searching for "${searchString}"`);
+ let engines = await UrlbarSearchUtils.enginesForDomainPrefix(searchString, {
+ matchAllDomainLevels: true,
+ });
+ let engineData = engines.map(e => ({
+ name: e.name,
+ searchForm: e.searchForm,
+ }));
+ info("Matching engines: " + JSON.stringify(engineData));
+
+ Assert.equal(
+ engines.length,
+ expectedDomains.length,
+ "Expected number of matching engines"
+ );
+ Assert.deepEqual(
+ engineData.map(d => d.name),
+ expectedDomains,
+ "Expected matching engine names/domains in the expected order"
+ );
+ }
+
+ await Promise.all(extensions.map(e => e.unload()));
+});
+
+function promiseSearchTopic(expectedVerb) {
+ return new Promise(resolve => {
+ Services.obs.addObserver(function observe(subject, topic, verb) {
+ info("browser-search-engine-modified: " + verb);
+ if (verb == expectedVerb) {
+ Services.obs.removeObserver(observe, "browser-search-engine-modified");
+ resolve();
+ }
+ }, "browser-search-engine-modified");
+ });
+}
diff --git a/browser/components/urlbar/tests/unit/test_UrlbarUtils_addToUrlbarHistory.js b/browser/components/urlbar/tests/unit/test_UrlbarUtils_addToUrlbarHistory.js
new file mode 100644
index 0000000000..6eabc3bb52
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_UrlbarUtils_addToUrlbarHistory.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * These tests unit test the functionality of the functions in UrlbarUtils.
+ * Some functions are bigger, and split out into sepearate test_UrlbarUtils_* files.
+ */
+
+"use strict";
+
+const { PrivateBrowsingUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PrivateBrowsingUtils.sys.mjs"
+);
+const { PlacesUIUtils } = ChromeUtils.importESModule(
+ "resource:///modules/PlacesUIUtils.sys.mjs"
+);
+
+let sandbox;
+
+add_task(function setup() {
+ sandbox = sinon.createSandbox();
+});
+
+add_task(function test_addToUrlbarHistory() {
+ sandbox.stub(PlacesUIUtils, "markPageAsTyped");
+ sandbox.stub(PrivateBrowsingUtils, "isWindowPrivate").returns(false);
+
+ UrlbarUtils.addToUrlbarHistory("http://example.com");
+ Assert.ok(
+ PlacesUIUtils.markPageAsTyped.calledOnce,
+ "Should have marked a simple URL as typed."
+ );
+ PlacesUIUtils.markPageAsTyped.resetHistory();
+
+ UrlbarUtils.addToUrlbarHistory();
+ Assert.ok(
+ PlacesUIUtils.markPageAsTyped.notCalled,
+ "Should not have attempted to mark a null URL as typed."
+ );
+ PlacesUIUtils.markPageAsTyped.resetHistory();
+
+ UrlbarUtils.addToUrlbarHistory("http://exam ple.com");
+ Assert.ok(
+ PlacesUIUtils.markPageAsTyped.notCalled,
+ "Should not have marked a URL containing a space as typed."
+ );
+ PlacesUIUtils.markPageAsTyped.resetHistory();
+
+ UrlbarUtils.addToUrlbarHistory("http://exam\x01ple.com");
+ Assert.ok(
+ PlacesUIUtils.markPageAsTyped.notCalled,
+ "Should not have marked a URL containing a control character as typed."
+ );
+ PlacesUIUtils.markPageAsTyped.resetHistory();
+
+ PrivateBrowsingUtils.isWindowPrivate.returns(true);
+ UrlbarUtils.addToUrlbarHistory("http://example.com");
+ Assert.ok(
+ PlacesUIUtils.markPageAsTyped.notCalled,
+ "Should not have marked a URL provided by a private browsing page as typed."
+ );
+ PlacesUIUtils.markPageAsTyped.resetHistory();
+});
diff --git a/browser/components/urlbar/tests/unit/test_UrlbarUtils_getShortcutOrURIAndPostData.js b/browser/components/urlbar/tests/unit/test_UrlbarUtils_getShortcutOrURIAndPostData.js
new file mode 100644
index 0000000000..034005b0fa
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_UrlbarUtils_getShortcutOrURIAndPostData.js
@@ -0,0 +1,249 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * These tests unit test the functionality of UrlbarController by stubbing out the
+ * model and providing stubs to be called.
+ */
+
+"use strict";
+
+function getPostDataString(aIS) {
+ if (!aIS) {
+ return null;
+ }
+
+ let sis = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ sis.init(aIS);
+ let dataLines = sis.read(aIS.available()).split("\n");
+
+ // only want the last line
+ return dataLines[dataLines.length - 1];
+}
+
+function keywordResult(aURL, aPostData, aIsUnsafe) {
+ this.url = aURL;
+ this.postData = aPostData;
+ this.isUnsafe = aIsUnsafe;
+}
+
+function keyWordData() {}
+keyWordData.prototype = {
+ init(aKeyWord, aURL, aPostData, aSearchWord) {
+ this.keyword = aKeyWord;
+ this.uri = Services.io.newURI(aURL);
+ this.postData = aPostData;
+ this.searchWord = aSearchWord;
+
+ this.method = this.postData ? "POST" : "GET";
+ },
+};
+
+function bmKeywordData(aKeyWord, aURL, aPostData, aSearchWord) {
+ this.init(aKeyWord, aURL, aPostData, aSearchWord);
+}
+bmKeywordData.prototype = new keyWordData();
+
+function searchKeywordData(aKeyWord, aURL, aPostData, aSearchWord) {
+ this.init(aKeyWord, aURL, aPostData, aSearchWord);
+}
+searchKeywordData.prototype = new keyWordData();
+
+var testData = [
+ [
+ new bmKeywordData("bmget", "https://bmget/search=%s", null, "foo"),
+ new keywordResult("https://bmget/search=foo", null),
+ ],
+
+ [
+ new bmKeywordData("bmpost", "https://bmpost/", "search=%s", "foo2"),
+ new keywordResult("https://bmpost/", "search=foo2"),
+ ],
+
+ [
+ new bmKeywordData(
+ "bmpostget",
+ "https://bmpostget/search1=%s",
+ "search2=%s",
+ "foo3"
+ ),
+ new keywordResult("https://bmpostget/search1=foo3", "search2=foo3"),
+ ],
+
+ [
+ new bmKeywordData("bmget-nosearch", "https://bmget-nosearch/", null, ""),
+ new keywordResult("https://bmget-nosearch/", null),
+ ],
+
+ [
+ new searchKeywordData(
+ "searchget",
+ "https://searchget/?search={searchTerms}",
+ null,
+ "foo4"
+ ),
+ new keywordResult("https://searchget/?search=foo4", null, true),
+ ],
+
+ [
+ new searchKeywordData(
+ "searchpost",
+ "https://searchpost/",
+ "search={searchTerms}",
+ "foo5"
+ ),
+ new keywordResult("https://searchpost/", "search=foo5", true),
+ ],
+
+ [
+ new searchKeywordData(
+ "searchpostget",
+ "https://searchpostget/?search1={searchTerms}",
+ "search2={searchTerms}",
+ "foo6"
+ ),
+ new keywordResult(
+ "https://searchpostget/?search1=foo6",
+ "search2=foo6",
+ true
+ ),
+ ],
+
+ // Bookmark keywords that don't take parameters should not be activated if a
+ // parameter is passed (bug 420328).
+ [
+ new bmKeywordData("bmget-noparam", "https://bmget-noparam/", null, "foo7"),
+ new keywordResult(null, null, true),
+ ],
+ [
+ new bmKeywordData(
+ "bmpost-noparam",
+ "https://bmpost-noparam/",
+ "not_a=param",
+ "foo8"
+ ),
+ new keywordResult(null, null, true),
+ ],
+
+ // Test escaping (%s = escaped, %S = raw)
+ // UTF-8 default
+ [
+ new bmKeywordData(
+ "bmget-escaping",
+ "https://bmget/?esc=%s&raw=%S",
+ null,
+ "fo\xE9"
+ ),
+ new keywordResult("https://bmget/?esc=fo%C3%A9&raw=fo\xE9", null),
+ ],
+ // Explicitly-defined ISO-8859-1
+ [
+ new bmKeywordData(
+ "bmget-escaping2",
+ "https://bmget/?esc=%s&raw=%S&mozcharset=ISO-8859-1",
+ null,
+ "fo\xE9"
+ ),
+ new keywordResult("https://bmget/?esc=fo%E9&raw=fo\xE9", null),
+ ],
+
+ // Bug 359809: Test escaping +, /, and @
+ // UTF-8 default
+ [
+ new bmKeywordData(
+ "bmget-escaping",
+ "https://bmget/?esc=%s&raw=%S",
+ null,
+ "+/@"
+ ),
+ new keywordResult("https://bmget/?esc=%2B%2F%40&raw=+/@", null),
+ ],
+ // Explicitly-defined ISO-8859-1
+ [
+ new bmKeywordData(
+ "bmget-escaping2",
+ "https://bmget/?esc=%s&raw=%S&mozcharset=ISO-8859-1",
+ null,
+ "+/@"
+ ),
+ new keywordResult("https://bmget/?esc=%2B%2F%40&raw=+/@", null),
+ ],
+
+ // Test using a non-bmKeywordData object, to test the behavior of
+ // getShortcutOrURIAndPostData for non-keywords (setupKeywords only adds keywords for
+ // bmKeywordData objects)
+ [{ keyword: "https://gavinsharp.com" }, new keywordResult(null, null, true)],
+];
+
+add_task(async function test_getshortcutoruri() {
+ await setupKeywords();
+
+ for (let item of testData) {
+ let [data, result] = item;
+
+ let query = data.keyword;
+ if (data.searchWord) {
+ query += " " + data.searchWord;
+ }
+ let returnedData = await UrlbarUtils.getShortcutOrURIAndPostData(query);
+ // null result.url means we should expect the same query we sent in
+ let expected = result.url || query;
+ Assert.equal(
+ returnedData.url,
+ expected,
+ "got correct URL for " + data.keyword
+ );
+ Assert.equal(
+ getPostDataString(returnedData.postData),
+ result.postData,
+ "got correct postData for " + data.keyword
+ );
+ Assert.equal(
+ returnedData.mayInheritPrincipal,
+ !result.isUnsafe,
+ "got correct mayInheritPrincipal for " + data.keyword
+ );
+ }
+
+ await cleanupKeywords();
+});
+
+var folder = null;
+
+async function setupKeywords() {
+ folder = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "keyword-test",
+ });
+ for (let item of testData) {
+ let data = item[0];
+ if (data instanceof bmKeywordData) {
+ await PlacesUtils.bookmarks.insert({
+ url: data.uri,
+ parentGuid: folder.guid,
+ });
+ await PlacesUtils.keywords.insert({
+ keyword: data.keyword,
+ url: data.uri.spec,
+ postData: data.postData,
+ });
+ }
+
+ if (data instanceof searchKeywordData) {
+ await SearchTestUtils.installSearchExtension({
+ name: data.keyword,
+ keyword: data.keyword,
+ search_url: data.uri.spec,
+ search_url_get_params: "",
+ search_url_post_params: data.postData,
+ });
+ }
+ }
+}
+
+async function cleanupKeywords() {
+ await PlacesUtils.bookmarks.remove(folder);
+}
diff --git a/browser/components/urlbar/tests/unit/test_UrlbarUtils_getTokenMatches.js b/browser/components/urlbar/tests/unit/test_UrlbarUtils_getTokenMatches.js
new file mode 100644
index 0000000000..bae6ffc879
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_UrlbarUtils_getTokenMatches.js
@@ -0,0 +1,294 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests UrlbarUtils.getTokenMatches.
+ */
+
+"use strict";
+
+add_task(function test() {
+ const tests = [
+ {
+ tokens: ["mozilla", "is", "i"],
+ phrase: "mozilla is for the Open Web",
+ expected: [
+ [0, 7],
+ [8, 2],
+ ],
+ },
+ {
+ tokens: ["mozilla", "is", "i"],
+ phrase: "MOZILLA IS for the Open Web",
+ expected: [
+ [0, 7],
+ [8, 2],
+ ],
+ },
+ {
+ tokens: ["mozilla", "is", "i"],
+ phrase: "MoZiLlA Is for the Open Web",
+ expected: [
+ [0, 7],
+ [8, 2],
+ ],
+ },
+ {
+ tokens: ["MOZILLA", "IS", "I"],
+ phrase: "mozilla is for the Open Web",
+ expected: [
+ [0, 7],
+ [8, 2],
+ ],
+ },
+ {
+ tokens: ["MoZiLlA", "Is", "I"],
+ phrase: "mozilla is for the Open Web",
+ expected: [
+ [0, 7],
+ [8, 2],
+ ],
+ },
+ {
+ tokens: ["mo", "b"],
+ phrase: "mozilla is for the Open Web",
+ expected: [
+ [0, 2],
+ [26, 1],
+ ],
+ },
+ {
+ tokens: ["mo", "b"],
+ phrase: "MOZILLA is for the OPEN WEB",
+ expected: [
+ [0, 2],
+ [26, 1],
+ ],
+ },
+ {
+ tokens: ["MO", "B"],
+ phrase: "mozilla is for the Open Web",
+ expected: [
+ [0, 2],
+ [26, 1],
+ ],
+ },
+ {
+ tokens: ["mo", ""],
+ phrase: "mozilla is for the Open Web",
+ expected: [[0, 2]],
+ },
+ {
+ tokens: ["mozilla"],
+ phrase: "mozilla",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["mozilla"],
+ phrase: "MOZILLA",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["mozilla"],
+ phrase: "MoZiLlA",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["mozilla"],
+ phrase: "mOzIlLa",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["MOZILLA"],
+ phrase: "mozilla",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["MoZiLlA"],
+ phrase: "mozilla",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["mOzIlLa"],
+ phrase: "mozilla",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["\u9996"],
+ phrase: "Test \u9996\u9875 Test",
+ expected: [[5, 1]],
+ },
+ {
+ tokens: ["mo", "zilla"],
+ phrase: "mozilla",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["mo", "zilla"],
+ phrase: "MOZILLA",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["mo", "zilla"],
+ phrase: "MoZiLlA",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["mo", "zilla"],
+ phrase: "mOzIlLa",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["MO", "ZILLA"],
+ phrase: "mozilla",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["Mo", "Zilla"],
+ phrase: "mozilla",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["moz", "zilla"],
+ phrase: "mozilla",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: [""], // Should never happen in practice.
+ phrase: "mozilla",
+ expected: [],
+ },
+ {
+ tokens: ["mo", "om"],
+ phrase: "mozilla mozzarella momo",
+ expected: [
+ [0, 2],
+ [8, 2],
+ [19, 4],
+ ],
+ },
+ {
+ tokens: ["mo", "om"],
+ phrase: "MOZILLA MOZZARELLA MOMO",
+ expected: [
+ [0, 2],
+ [8, 2],
+ [19, 4],
+ ],
+ },
+ {
+ tokens: ["MO", "OM"],
+ phrase: "mozilla mozzarella momo",
+ expected: [
+ [0, 2],
+ [8, 2],
+ [19, 4],
+ ],
+ },
+ {
+ tokens: ["resume"],
+ phrase: "résumé",
+ expected: [[0, 6]],
+ },
+ {
+ // This test should succeed even in a Spanish locale where N and Ñ are
+ // considered distinct letters.
+ tokens: ["jalapeno"],
+ phrase: "jalapeño",
+ expected: [[0, 8]],
+ },
+ ];
+ for (let { tokens, phrase, expected } of tests) {
+ tokens = tokens.map(t => ({
+ value: t,
+ lowerCaseValue: t.toLocaleLowerCase(),
+ }));
+ Assert.deepEqual(
+ UrlbarUtils.getTokenMatches(tokens, phrase, UrlbarUtils.HIGHLIGHT.TYPED),
+ expected,
+ `Match "${tokens.map(t => t.value).join(", ")}" on "${phrase}"`
+ );
+ }
+});
+
+/**
+ * Tests suggestion highlighting. Note that suggestions are only highlighted if
+ * the matching token is at the beginning of a word in the matched string.
+ */
+add_task(function testSuggestions() {
+ const tests = [
+ {
+ tokens: ["mozilla", "is", "i"],
+ phrase: "mozilla is for the Open Web",
+ expected: [
+ [7, 1],
+ [10, 17],
+ ],
+ },
+ {
+ tokens: ["\u9996"],
+ phrase: "Test \u9996\u9875 Test",
+ expected: [
+ [0, 5],
+ [6, 6],
+ ],
+ },
+ {
+ tokens: ["mo", "zilla"],
+ phrase: "mOzIlLa",
+ expected: [[2, 5]],
+ },
+ {
+ tokens: ["MO", "ZILLA"],
+ phrase: "mozilla",
+ expected: [[2, 5]],
+ },
+ {
+ tokens: [""], // Should never happen in practice.
+ phrase: "mozilla",
+ expected: [[0, 7]],
+ },
+ {
+ tokens: ["mo", "om", "la"],
+ phrase: "mozilla mozzarella momo",
+ expected: [
+ [2, 6],
+ [10, 9],
+ [21, 2],
+ ],
+ },
+ {
+ tokens: ["mo", "om", "la"],
+ phrase: "MOZILLA MOZZARELLA MOMO",
+ expected: [
+ [2, 6],
+ [10, 9],
+ [21, 2],
+ ],
+ },
+ {
+ tokens: ["MO", "OM", "LA"],
+ phrase: "mozilla mozzarella momo",
+ expected: [
+ [2, 6],
+ [10, 9],
+ [21, 2],
+ ],
+ },
+ ];
+ for (let { tokens, phrase, expected } of tests) {
+ tokens = tokens.map(t => ({
+ value: t,
+ lowerCaseValue: t.toLocaleLowerCase(),
+ }));
+ Assert.deepEqual(
+ UrlbarUtils.getTokenMatches(
+ tokens,
+ phrase,
+ UrlbarUtils.HIGHLIGHT.SUGGESTED
+ ),
+ expected,
+ `Match "${tokens.map(t => t.value).join(", ")}" on "${phrase}"`
+ );
+ }
+});
diff --git a/browser/components/urlbar/tests/unit/test_UrlbarUtils_unEscapeURIForUI.js b/browser/components/urlbar/tests/unit/test_UrlbarUtils_unEscapeURIForUI.js
new file mode 100644
index 0000000000..6efc6711c6
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_UrlbarUtils_unEscapeURIForUI.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test for unEscapeURIForUI function in UrlbarUtils.
+ */
+
+"use strict";
+
+const TEST_DATA = [
+ {
+ description: "Test for characters including percent encoded chars",
+ input: "A%E3%81%82%F0%A0%AE%B7%21",
+ expected: "Aあ𠮷!",
+ testMessage: "Unescape given characters correctly",
+ },
+ {
+ description: "Test for characters over the limit",
+ input: "A%E3%81%82%F0%A0%AE%B7%21".repeat(
+ Math.ceil(UrlbarUtils.MAX_TEXT_LENGTH / 25)
+ ),
+ expected: "A%E3%81%82%F0%A0%AE%B7%21".repeat(
+ Math.ceil(UrlbarUtils.MAX_TEXT_LENGTH / 25)
+ ),
+ testMessage: "Return given characters as it is because of over the limit",
+ },
+];
+
+add_task(function () {
+ for (const { description, input, expected, testMessage } of TEST_DATA) {
+ info(description);
+
+ const result = UrlbarUtils.unEscapeURIForUI(input);
+ Assert.equal(result, expected, testMessage);
+ }
+});
diff --git a/browser/components/urlbar/tests/unit/test_about_urls.js b/browser/components/urlbar/tests/unit/test_about_urls.js
new file mode 100644
index 0000000000..277ddb8ee1
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_about_urls.js
@@ -0,0 +1,176 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { AboutPagesUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/AboutPagesUtils.sys.mjs"
+);
+
+testEngine_setup();
+
+// "about:ab" should match "about:about"
+add_task(async function aboutAb() {
+ let context = createContext("about:ab", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "about:about",
+ completed: "about:about",
+ matches: [
+ makeVisitResult(context, {
+ uri: "about:about",
+ title: "about:about",
+ heuristic: true,
+ }),
+ ],
+ });
+});
+
+// "about:Ab" should match "about:about"
+add_task(async function aboutAb() {
+ let context = createContext("about:Ab", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "about:About",
+ completed: "about:about",
+ matches: [
+ makeVisitResult(context, {
+ uri: "about:about",
+ title: "about:about",
+ heuristic: true,
+ }),
+ ],
+ });
+});
+
+// "about:about" should match "about:about"
+add_task(async function aboutAbout() {
+ let context = createContext("about:about", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "about:about",
+ completed: "about:about",
+ matches: [
+ makeVisitResult(context, {
+ uri: "about:about",
+ title: "about:about",
+ heuristic: true,
+ }),
+ ],
+ });
+});
+
+// "about:a" should complete to "about:about" and also match "about:addons"
+add_task(async function aboutAboutAndAboutAddons() {
+ let context = createContext("about:a", { isPrivate: false });
+ await check_results({
+ context,
+ search: "about:a",
+ autofilled: "about:about",
+ completed: "about:about",
+ matches: [
+ makeVisitResult(context, {
+ uri: "about:about",
+ title: "about:about",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "about:addons",
+ title: "about:addons",
+ tags: null,
+ providerName: "AboutPages",
+ }),
+ ],
+ });
+});
+
+// "about:" by itself matches a list of about: pages and nothing else
+add_task(async function aboutColonMatchesOnlyAboutPages() {
+ // We generate 9 about: page results because there are 10 results total,
+ // and the first result is the heuristic result.
+ function getFirst9AboutPages() {
+ const aboutPageNames = AboutPagesUtils.visibleAboutUrls.slice(0, 9);
+ const aboutPageResults = aboutPageNames.map(aboutPageName => {
+ return makeVisitResult(context, {
+ uri: aboutPageName,
+ title: aboutPageName,
+ tags: null,
+ providerName: "AboutPages",
+ });
+ });
+ return aboutPageResults;
+ }
+
+ let context = createContext("about:", { isPrivate: false });
+ await check_results({
+ context,
+ search: "about:",
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ providerName: "HeuristicFallback",
+ heuristic: true,
+ }),
+ ...getFirst9AboutPages(),
+ ],
+ });
+});
+
+// Results for about: pages do not match webpage titles from the user's history
+add_task(async function aboutResultsDoNotMatchTitlesInHistory() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: Services.io.newURI("http://example.com/guide/config/"),
+ title: "Guide to config in Firefox",
+ },
+ ]);
+
+ let context = createContext("about:config", { isPrivate: false });
+ await check_results({
+ context,
+ search: "about:config",
+ matches: [
+ makeVisitResult(context, {
+ uri: "about:config",
+ title: "about:config",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ ],
+ });
+});
+
+// Tests that about: pages are shown after general results.
+add_task(async function after_general() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: Services.io.newURI("http://example.com/guide/aboutaddons/"),
+ title: "Guide to about:addons in Firefox",
+ },
+ ]);
+
+ let context = createContext("about:a", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: "about:about",
+ title: "about:about",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/guide/aboutaddons/",
+ title: "Guide to about:addons in Firefox",
+ }),
+ makeVisitResult(context, {
+ uri: "about:addons",
+ title: "about:addons",
+ tags: null,
+ providerName: "AboutPages",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_autofill_adaptiveHistory.js b/browser/components/urlbar/tests/unit/test_autofill_adaptiveHistory.js
new file mode 100644
index 0000000000..94b29b913a
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_adaptiveHistory.js
@@ -0,0 +1,1441 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Test for adaptive history autofill.
+
+testEngine_setup();
+
+registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions");
+});
+Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false);
+
+const TEST_DATA = [
+ {
+ description: "Basic behavior for adaptive history autofill",
+ pref: true,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/test",
+ completed: "http://example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description: "URL that has www",
+ pref: true,
+ visitHistory: ["http://www.example.com/test"],
+ inputHistory: [{ uri: "http://www.example.com/test", input: "exa" }],
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/test",
+ completed: "http://www.example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://www.example.com/test",
+ title: "test visit for http://www.example.com/test",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description: "User's input starts with www",
+ pref: true,
+ visitHistory: ["http://www.example.com/test"],
+ inputHistory: [{ uri: "http://www.example.com/test", input: "www.exa" }],
+ userInput: "www.exa",
+ expected: {
+ autofilled: "www.example.com/test",
+ completed: "http://www.example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://www.example.com/test",
+ title: "test visit for http://www.example.com/test",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description: "Case differences for user's input are ignored",
+ pref: true,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "EXA" }],
+ userInput: "eXA",
+ expected: {
+ autofilled: "eXAmple.com/test",
+ completed: "http://example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description:
+ "Case differences for user's input that starts with www are ignored",
+ pref: true,
+ visitHistory: ["http://www.example.com/test"],
+ inputHistory: [{ uri: "http://www.example.com/test", input: "www.exa" }],
+ userInput: "WWW.exa",
+ expected: {
+ autofilled: "WWW.example.com/test",
+ completed: "http://www.example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://www.example.com/test",
+ title: "test visit for http://www.example.com/test",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description: "Mutiple case difference input history",
+ pref: true,
+ visitHistory: ["http://example.com/yes", "http://example.com/no"],
+ inputHistory: [
+ { uri: "http://example.com/yes", input: "exa" },
+ { uri: "http://example.com/yes", input: "EXA" },
+ { uri: "http://example.com/yes", input: "EXa" },
+ { uri: "http://example.com/yes", input: "eXa" },
+ { uri: "http://example.com/yes", input: "eXA" },
+ { uri: "http://example.com/no", input: "exa" },
+ { uri: "http://example.com/no", input: "exa" },
+ { uri: "http://example.com/no", input: "exa" },
+ ],
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/yes",
+ completed: "http://example.com/yes",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/yes",
+ title: "test visit for http://example.com/yes",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/no",
+ title: "test visit for http://example.com/no",
+ }),
+ ],
+ },
+ },
+ {
+ description: "Multiple input history count",
+ pref: true,
+ visitHistory: ["http://example.com/few", "http://example.com/many"],
+ inputHistory: [
+ { uri: "http://example.com/many", input: "exa" },
+ { uri: "http://example.com/few", input: "exa" },
+ { uri: "http://example.com/many", input: "examp" },
+ ],
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/many",
+ completed: "http://example.com/many",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/many",
+ title: "test visit for http://example.com/many",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/few",
+ title: "test visit for http://example.com/few",
+ }),
+ ],
+ },
+ },
+ {
+ description: "Multiple input history count with same input",
+ pref: true,
+ visitHistory: ["http://example.com/few", "http://example.com/many"],
+ inputHistory: [
+ { uri: "http://example.com/many", input: "exa" },
+ { uri: "http://example.com/few", input: "exa" },
+ { uri: "http://example.com/many", input: "exa" },
+ ],
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/many",
+ completed: "http://example.com/many",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/many",
+ title: "test visit for http://example.com/many",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/few",
+ title: "test visit for http://example.com/few",
+ }),
+ ],
+ },
+ },
+ {
+ description:
+ "Multiple input history count with same input but different frecency",
+ pref: true,
+ visitHistory: [
+ "http://example.com/few",
+ "http://example.com/many",
+ "http://example.com/many",
+ ],
+ inputHistory: [
+ { uri: "http://example.com/many", input: "exa" },
+ { uri: "http://example.com/few", input: "exa" },
+ ],
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/many",
+ completed: "http://example.com/many",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/many",
+ title: "test visit for http://example.com/many",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/few",
+ title: "test visit for http://example.com/few",
+ }),
+ ],
+ },
+ },
+ {
+ description: "User input is shorter than the input history",
+ pref: true,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ userInput: "e",
+ expected: {
+ autofilled: "example.com/",
+ completed: "http://example.com/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ fallbackTitle: "example.com",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ }),
+ ],
+ },
+ },
+ {
+ description: "User input is longer than the input history",
+ pref: true,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ userInput: "example",
+ expected: {
+ autofilled: "example.com/test",
+ completed: "http://example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description:
+ "User input starts with input history and includes path of the url",
+ pref: true,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ userInput: "example.com/te",
+ expected: {
+ autofilled: "example.com/test",
+ completed: "http://example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description: "User input starts with input history and but another url",
+ pref: true,
+ visitHistory: ["http://example.com/test", "http://example.org/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ userInput: "example.o",
+ expected: {
+ autofilled: "example.org/",
+ completed: "http://example.org/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.org/",
+ fallbackTitle: "example.org",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.org/test",
+ title: "test visit for http://example.org/test",
+ }),
+ ],
+ },
+ },
+ {
+ description: "User input does not start with input history",
+ pref: true,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "notmatch" }],
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/",
+ completed: "http://example.com/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ fallbackTitle: "example.com",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ }),
+ ],
+ },
+ },
+ {
+ description:
+ "User input does not start with input history, but it includes as part of URL",
+ pref: true,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ userInput: "test",
+ expected: {
+ results: [
+ context =>
+ makeSearchResult(context, {
+ engineName: "Suggestions",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ }),
+ ],
+ },
+ },
+ {
+ description: "User input does not start with visited URL",
+ pref: true,
+ visitHistory: ["http://mozilla.com/test"],
+ inputHistory: [{ uri: "http://mozilla.com/test", input: "exa" }],
+ userInput: "exa",
+ expected: {
+ results: [
+ context =>
+ makeSearchResult(context, {
+ engineName: "Suggestions",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://mozilla.com/test",
+ title: "test visit for http://mozilla.com/test",
+ }),
+ ],
+ },
+ },
+ {
+ description: "Visited page is bookmarked",
+ pref: true,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ bookmarks: [{ uri: "http://example.com/test", title: "test bookmark" }],
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/test",
+ completed: "http://example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test bookmark",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description: "Visit history and no bookamrk with HISTORY source",
+ pref: true,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/test",
+ completed: "http://example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description: "Visit history and no bookamrk with BOOKMARK source",
+ pref: true,
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ userInput: "exa",
+ expected: {
+ results: [
+ context =>
+ makeSearchResult(context, {
+ engineName: "Suggestions",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description: "Bookmarked visit history with HISTORY source",
+ pref: true,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ visitHistory: ["http://example.com/test", "http://example.com/bookmarked"],
+ bookmarks: [
+ { uri: "http://example.com/bookmarked", title: "test bookmark" },
+ ],
+ inputHistory: [
+ {
+ uri: "http://example.com/test",
+ input: "exa",
+ },
+ {
+ uri: "http://example.com/bookmarked",
+ input: "exa",
+ },
+ ],
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/bookmarked",
+ completed: "http://example.com/bookmarked",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/bookmarked",
+ title: "test bookmark",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ }),
+ ],
+ },
+ },
+ {
+ description: "Bookmarked visit history with BOOKMARK source",
+ pref: true,
+ source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ visitHistory: ["http://example.com/test", "http://example.com/bookmarked"],
+ bookmarks: [
+ { uri: "http://example.com/bookmarked", title: "test bookmark" },
+ ],
+ inputHistory: [
+ {
+ uri: "http://example.com/test",
+ input: "exa",
+ },
+ {
+ uri: "http://example.com/bookmarked",
+ input: "exa",
+ },
+ ],
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/bookmarked",
+ completed: "http://example.com/bookmarked",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/bookmarked",
+ title: "test bookmark",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description: "No visit history with HISTORY source",
+ pref: true,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ userInput: "exa",
+ expected: {
+ results: [
+ context =>
+ makeSearchResult(context, {
+ engineName: "Suggestions",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description: "No visit history with BOOKMARK source",
+ pref: true,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ bookmarks: [{ uri: "http://example.com/bookmarked", title: "test" }],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ userInput: "exa",
+ expected: {
+ results: [
+ context =>
+ makeSearchResult(context, {
+ engineName: "Suggestions",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description: "Match with path expression",
+ pref: true,
+ visitHistory: [
+ "http://example.com/testMany",
+ "http://example.com/testMany",
+ "http://example.com/test",
+ ],
+ inputHistory: [{ uri: "http://example.com/test", input: "example.com/te" }],
+ userInput: "example.com/te",
+ expected: {
+ autofilled: "example.com/test",
+ completed: "http://example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/testMany",
+ title: "test visit for http://example.com/testMany",
+ }),
+ ],
+ },
+ },
+ {
+ description:
+ "Prefixed URL for input history and the same string for user input",
+ pref: true,
+ visitHistory: [
+ "http://example.com/testMany",
+ "http://example.com/testMany",
+ "http://example.com/test",
+ ],
+ inputHistory: [
+ { uri: "http://example.com/test", input: "http://example.com/test" },
+ ],
+ userInput: "http://example.com/test",
+ expected: {
+ autofilled: "http://example.com/test",
+ completed: "http://example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/testMany",
+ title: "test visit for http://example.com/testMany",
+ }),
+ ],
+ },
+ },
+ {
+ description:
+ "Prefixed URL for input history and URL expression for user input",
+ pref: true,
+ visitHistory: [
+ "http://example.com/testMany",
+ "http://example.com/testMany",
+ "http://example.com/test",
+ ],
+ inputHistory: [
+ { uri: "http://example.com/test", input: "http://example.com/te" },
+ ],
+ userInput: "http://example.com/te",
+ expected: {
+ autofilled: "http://example.com/test",
+ completed: "http://example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/testMany",
+ title: "test visit for http://example.com/testMany",
+ }),
+ ],
+ },
+ },
+ {
+ description:
+ "Prefixed URL for input history and path expression for user input",
+ pref: true,
+ visitHistory: [
+ "http://example.com/testMany",
+ "http://example.com/testMany",
+ "http://example.com/test",
+ ],
+ inputHistory: [
+ { uri: "http://example.com/test", input: "http://example.com/te" },
+ ],
+ userInput: "example.com/te",
+ expected: {
+ autofilled: "example.com/testMany",
+ completed: "http://example.com/testMany",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/testMany",
+ title: "test visit for http://example.com/testMany",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ }),
+ ],
+ },
+ },
+ {
+ description: "Prefixed URL for input history and 'http' for user input",
+ pref: true,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "http" }],
+ userInput: "http",
+ expected: {
+ results: [
+ context =>
+ makeSearchResult(context, {
+ engineName: "Suggestions",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ }),
+ ],
+ },
+ },
+ {
+ description: "Prefixed URL for input history and 'http:' for user input",
+ pref: true,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "http:" }],
+ userInput: "http:",
+ expected: {
+ results: [
+ context =>
+ makeSearchResult(context, {
+ engineName: "Suggestions",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ }),
+ ],
+ },
+ },
+ {
+ description: "Prefixed URL for input history and 'http:/' for user input",
+ pref: true,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "http:/" }],
+ userInput: "http:/",
+ expected: {
+ results: [
+ context =>
+ makeSearchResult(context, {
+ engineName: "Suggestions",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ }),
+ ],
+ },
+ },
+ {
+ description: "Prefixed URL for input history and 'http://' for user input",
+ pref: true,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "http://" }],
+ userInput: "http://",
+ expected: {
+ autofilled: "http://example.com/test",
+ completed: "http://example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description: "Prefixed URL for input history and 'http://e' for user input",
+ pref: true,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "http://e" }],
+ userInput: "http://e",
+ expected: {
+ autofilled: "http://example.com/test",
+ completed: "http://example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description:
+ "Prefixed URL with www omitted for input history and 'http://e' for user input",
+ pref: true,
+ visitHistory: ["http://www.example.com/test"],
+ inputHistory: [{ uri: "http://www.example.com/test", input: "http://e" }],
+ userInput: "http://e",
+ expected: {
+ autofilled: "http://example.com/test",
+ completed: "http://www.example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://www.example.com/test",
+ title: "test visit for http://www.example.com/test",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description:
+ "Those that match with fixed URL take precedence over those that match prefixed URL",
+ pref: true,
+ visitHistory: ["http://http.example.com/test", "http://example.com/test"],
+ inputHistory: [
+ { uri: "http://http.example.com/test", input: "http" },
+ { uri: "http://example.com/test", input: "http://example.com/test" },
+ ],
+ userInput: "http",
+ expected: {
+ autofilled: "http.example.com/test",
+ completed: "http://http.example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://http.example.com/test",
+ title: "test visit for http://http.example.com/test",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ }),
+ ],
+ },
+ },
+ {
+ description: "Input history is totally different string from the URL",
+ pref: true,
+ visitHistory: [
+ "http://example.com/testMany",
+ "http://example.com/testMany",
+ "http://example.com/test",
+ ],
+ inputHistory: [
+ { uri: "http://example.com/test", input: "totally-different-string" },
+ ],
+ userInput: "totally",
+ expected: {
+ results: [
+ context =>
+ makeSearchResult(context, {
+ engineName: "Suggestions",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ }),
+ ],
+ },
+ },
+ {
+ description:
+ "Input history is totally different string from the URL and there is a visit history whose URL starts with the input",
+ pref: true,
+ visitHistory: ["http://example.com/test", "http://totally.example.com"],
+ inputHistory: [
+ { uri: "http://example.com/test", input: "totally-different-string" },
+ ],
+ userInput: "totally",
+ expected: {
+ autofilled: "totally.example.com/",
+ completed: "http://totally.example.com/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://totally.example.com/",
+ title: "test visit for http://totally.example.com/",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ }),
+ ],
+ },
+ },
+ {
+ description: "Use count threshold is as same as use count of input history",
+ pref: true,
+ useCountThreshold: 1 * 0.9 + 1,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [
+ { uri: "http://example.com/test", input: "exa" },
+ { uri: "http://example.com/test", input: "exa" },
+ ],
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/test",
+ completed: "http://example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description: "Use count threshold is less than use count of input history",
+ pref: true,
+ useCountThreshold: 3,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [
+ { uri: "http://example.com/test", input: "exa" },
+ { uri: "http://example.com/test", input: "exa" },
+ { uri: "http://example.com/test", input: "exa" },
+ { uri: "http://example.com/test", input: "exa" },
+ ],
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/test",
+ completed: "http://example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description: "Use count threshold is more than use count of input history",
+ pref: true,
+ useCountThreshold: 10,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [
+ { uri: "http://example.com/test", input: "exa" },
+ { uri: "http://example.com/test", input: "exa" },
+ { uri: "http://example.com/test", input: "exa" },
+ { uri: "http://example.com/test", input: "exa" },
+ { uri: "http://example.com/test", input: "exa" },
+ ],
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/",
+ completed: "http://example.com/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ fallbackTitle: "example.com",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ }),
+ ],
+ },
+ },
+ {
+ description: "minCharsThreshold pref equals to the user input length",
+ pref: true,
+ minCharsThreshold: 3,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/test",
+ completed: "http://example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description: "minCharsThreshold pref is smaller than the user input length",
+ pref: true,
+ minCharsThreshold: 2,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/test",
+ completed: "http://example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description: "minCharsThreshold pref is larger than the user input length",
+ pref: true,
+ minCharsThreshold: 4,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/",
+ completed: "http://example.com/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ fallbackTitle: "example.com",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ }),
+ ],
+ },
+ },
+ {
+ description:
+ "Prioritize path component with case-sensitive and that is visited",
+ pref: true,
+ visitHistory: [
+ "http://example.com/TEST",
+ "http://example.com/TEST",
+ "http://example.com/test",
+ ],
+ inputHistory: [
+ { uri: "http://example.com/TEST", input: "example.com/test" },
+ { uri: "http://example.com/test", input: "example.com/test" },
+ ],
+ userInput: "example.com/test",
+ expected: {
+ autofilled: "example.com/test",
+ completed: "http://example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/TEST",
+ title: "test visit for http://example.com/TEST",
+ }),
+ ],
+ },
+ },
+ {
+ description:
+ "Prioritize path component with case-sensitive but no visited data",
+ pref: true,
+ visitHistory: ["http://example.com/TEST"],
+ inputHistory: [
+ { uri: "http://example.com/TEST", input: "example.com/test" },
+ ],
+ userInput: "example.com/test",
+ expected: {
+ autofilled: "example.com/test",
+ completed: "http://example.com/test",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ fallbackTitle: "example.com/test",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/TEST",
+ title: "test visit for http://example.com/TEST",
+ }),
+ ],
+ },
+ },
+ {
+ description:
+ "With history and bookmarks sources, foreign_count == 0, frecency <= 0: No adaptive history autofill",
+ pref: true,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ frecency: 0,
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/",
+ completed: "http://example.com/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ fallbackTitle: "example.com",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ }),
+ ],
+ },
+ },
+ {
+ description:
+ "With history source, visit_count == 0, foreign_count != 0: No adaptive history autofill",
+ pref: true,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ bookmarks: [{ uri: "http://example.com/test", title: "test bookmark" }],
+ userInput: "exa",
+ expected: {
+ results: [
+ context =>
+ makeSearchResult(context, {
+ engineName: "Suggestions",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description:
+ "With history source, visit_count > 0, foreign_count != 0, frecency <= 20: No adaptive history autofill",
+ pref: true,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ bookmarks: [{ uri: "http://example.com/test", title: "test bookmark" }],
+ frecency: 0,
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/",
+ completed: "http://example.com/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ fallbackTitle: "example.com",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ description:
+ "With history source, visit_count > 0, foreign_count == 0, frecency <= 20: No adaptive history autofill",
+ pref: true,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ frecency: 0,
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/",
+ completed: "http://example.com/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ fallbackTitle: "example.com",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ }),
+ ],
+ },
+ },
+ {
+ description: "Empty input string",
+ pref: true,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "" }],
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/",
+ completed: "http://example.com/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ fallbackTitle: "example.com",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ }),
+ ],
+ },
+ },
+ {
+ description: "Turn the pref off",
+ pref: false,
+ visitHistory: ["http://example.com/test"],
+ inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
+ userInput: "exa",
+ expected: {
+ autofilled: "example.com/",
+ completed: "http://example.com/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ fallbackTitle: "example.com",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ }),
+ ],
+ },
+ },
+];
+
+add_task(async function inputTest() {
+ for (const {
+ description,
+ pref,
+ minCharsThreshold,
+ useCountThreshold,
+ source,
+ visitHistory,
+ inputHistory,
+ bookmarks,
+ frecency,
+ userInput,
+ expected,
+ } of TEST_DATA) {
+ info(description);
+
+ UrlbarPrefs.set("autoFill.adaptiveHistory.enabled", pref);
+
+ if (!isNaN(minCharsThreshold)) {
+ UrlbarPrefs.set(
+ "autoFill.adaptiveHistory.minCharsThreshold",
+ minCharsThreshold
+ );
+ }
+
+ if (!isNaN(useCountThreshold)) {
+ UrlbarPrefs.set(
+ "autoFill.adaptiveHistory.useCountThreshold",
+ useCountThreshold
+ );
+ }
+
+ if (visitHistory && visitHistory.length) {
+ await PlacesTestUtils.addVisits(visitHistory);
+ }
+ for (const { uri, input } of inputHistory) {
+ await UrlbarUtils.addToInputHistory(uri, input);
+ }
+ for (const bookmark of bookmarks || []) {
+ await PlacesTestUtils.addBookmarkWithDetails(bookmark);
+ }
+
+ if (typeof frecency == "number") {
+ await PlacesUtils.withConnectionWrapper("test::setFrecency", db =>
+ db.execute(
+ `UPDATE moz_places SET frecency = :frecency WHERE url = :url`,
+ {
+ frecency,
+ url: visitHistory[0],
+ }
+ )
+ );
+ }
+
+ const sources = source
+ ? [source]
+ : [
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ ];
+
+ const context = createContext(userInput, {
+ sources,
+ isPrivate: false,
+ });
+
+ await check_results({
+ context,
+ autofilled: expected.autofilled,
+ completed: expected.completed,
+ hasAutofillTitle: expected.hasAutofillTitle,
+ matches: expected.results.map(f => f(context)),
+ });
+
+ await cleanupPlaces();
+ UrlbarPrefs.clear("autoFill.adaptiveHistory.enabled");
+ UrlbarPrefs.clear("autoFill.adaptiveHistory.minCharsThreshold");
+ UrlbarPrefs.clear("autoFill.adaptiveHistory.useCountThreshold");
+ }
+});
+
+add_task(async function urlCase() {
+ UrlbarPrefs.set("autoFill.adaptiveHistory.enabled", true);
+
+ const testVisitFixed = "example.com/ABC/DEF";
+ const testVisitURL = `http://${testVisitFixed}`;
+ const testInput = "example";
+ await PlacesTestUtils.addVisits([testVisitURL]);
+ await UrlbarUtils.addToInputHistory(testVisitURL, testInput);
+
+ const userInput = "example.COM/abc/def";
+ for (let i = 1; i <= userInput.length; i++) {
+ const currentUserInput = userInput.substring(0, i);
+ const context = createContext(currentUserInput, { isPrivate: false });
+
+ if (currentUserInput.length < testInput.length) {
+ // Autofill with host.
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ completed: "http://example.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ fallbackTitle: "example.com",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/DEF",
+ title: "test visit for http://example.com/ABC/DEF",
+ }),
+ ],
+ });
+ } else if (currentUserInput.length !== testVisitFixed.length) {
+ // Autofill using input history.
+ const autofilled = currentUserInput + testVisitFixed.substring(i);
+ await check_results({
+ context,
+ autofilled,
+ completed: "http://example.com/ABC/DEF",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/DEF",
+ title: "test visit for http://example.com/ABC/DEF",
+ heuristic: true,
+ }),
+ ],
+ });
+ } else {
+ // Autofill using user's input.
+ await check_results({
+ context,
+ autofilled: "example.COM/abc/def",
+ completed: "http://example.com/abc/def",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://example.com/abc/def",
+ fallbackTitle: "example.com/abc/def",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/DEF",
+ title: "test visit for http://example.com/ABC/DEF",
+ }),
+ ],
+ });
+ }
+ }
+
+ await cleanupPlaces();
+ UrlbarPrefs.clear("autoFill.adaptiveHistory.enabled");
+});
+
+add_task(async function decayTest() {
+ UrlbarPrefs.set("autoFill.adaptiveHistory.enabled", true);
+
+ await PlacesTestUtils.addVisits(["http://example.com/test"]);
+ await UrlbarUtils.addToInputHistory("http://example.com/test", "exa");
+
+ const initContext = createContext("exa", { isPrivate: false });
+ await check_results({
+ context: initContext,
+ autofilled: "example.com/test",
+ completed: "http://example.com/test",
+ matches: [
+ makeVisitResult(initContext, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // The decay rate for a day is 0.975, as defined in PlacesFrecencyRecalculator
+ // Therefore, after 30 days, as use_count will be 0.975^30 = 0.468, we set the
+ // useCountThreshold 0.47 to not take the input history passed 30 days.
+ UrlbarPrefs.set("autoFill.adaptiveHistory.useCountThreshold", 0.47);
+
+ // Make 29 days later.
+ for (let i = 0; i < 29; i++) {
+ await Cc["@mozilla.org/places/frecency-recalculator;1"]
+ .getService(Ci.nsIObserver)
+ .wrappedJSObject.decay();
+ }
+ const midContext = createContext("exa", { isPrivate: false });
+ await check_results({
+ context: midContext,
+ autofilled: "example.com/test",
+ completed: "http://example.com/test",
+ matches: [
+ makeVisitResult(midContext, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // Total 30 days later.
+ await Cc["@mozilla.org/places/frecency-recalculator;1"]
+ .getService(Ci.nsIObserver)
+ .wrappedJSObject.decay();
+ const context = createContext("exa", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ completed: "http://example.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ fallbackTitle: "example.com",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/test",
+ title: "test visit for http://example.com/test",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+ UrlbarPrefs.clear("autoFill.adaptiveHistory.enabled");
+ UrlbarPrefs.clear("autoFill.adaptiveHistory.useCountThreshold");
+});
diff --git a/browser/components/urlbar/tests/unit/test_autofill_bookmarked.js b/browser/components/urlbar/tests/unit/test_autofill_bookmarked.js
new file mode 100644
index 0000000000..9b8b77e82c
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_bookmarked.js
@@ -0,0 +1,150 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This is a specific autofill test to ensure we pick the correct bookmarked
+// state of an origin. Regardless of the order of origins, we should always pick
+// the correct bookmarked status.
+
+add_task(async function () {
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions");
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false);
+
+ let host = "example.com";
+ // Add a bookmark to the http version, but ensure the https version has an
+ // higher frecency.
+ let bookmark = await PlacesUtils.bookmarks.insert({
+ url: `http://${host}`,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits(`https://${host}`);
+ }
+ // ensure both fall below the threshold.
+ for (let i = 0; i < 15; i++) {
+ await PlacesTestUtils.addVisits(`https://not-${host}`);
+ }
+
+ async function check_autofill() {
+ let threshold = await getOriginAutofillThreshold();
+ let httpOriginFrecency = await getOriginFrecency("http://", host);
+ Assert.less(
+ httpOriginFrecency,
+ threshold,
+ "Http origin frecency should be below the threshold"
+ );
+ let httpsOriginFrecency = await getOriginFrecency("https://", host);
+ Assert.less(
+ httpsOriginFrecency,
+ threshold,
+ "Https origin frecency should be below the threshold"
+ );
+ Assert.less(
+ httpOriginFrecency,
+ httpsOriginFrecency,
+ "Http origin frecency should be below the https origin frecency"
+ );
+
+ // The http version should be filled because it's bookmarked, but with the
+ // https prefix that is more frecent.
+ let context = createContext("ex", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: `${host}/`,
+ completed: `https://${host}/`,
+ matches: [
+ makeVisitResult(context, {
+ uri: `https://${host}/`,
+ title: `test visit for https://${host}/`,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: `https://not-${host}/`,
+ title: `test visit for https://not-${host}/`,
+ }),
+ ],
+ });
+ }
+
+ await check_autofill();
+
+ // Now remove the bookmark, ensure to remove the orphans, then reinsert the
+ // bookmark; thus we physically invert the order of the rows in the table.
+ await checkOriginsOrder(host, ["http://", "https://"]);
+ await PlacesUtils.bookmarks.remove(bookmark);
+ await PlacesUtils.withConnectionWrapper("removeOrphans", async db => {
+ db.execute(`DELETE FROM moz_places WHERE url = :url`, {
+ url: `http://${host}/`,
+ });
+ db.execute(
+ `DELETE FROM moz_origins WHERE prefix = "http://" AND host = :host`,
+ { host }
+ );
+ });
+ bookmark = await PlacesUtils.bookmarks.insert({
+ url: `http://${host}`,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+
+ await checkOriginsOrder(host, ["https://", "http://"]);
+
+ await check_autofill();
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.remove(bookmark);
+});
+
+add_task(async function test_www() {
+ // Add a bookmark to the www version
+ let host = "example.com";
+ await PlacesUtils.bookmarks.insert({
+ url: `http://www.${host}`,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+
+ info("search for start of www.");
+ let context = createContext("w", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: `www.${host}/`,
+ completed: `http://www.${host}/`,
+ matches: [
+ makeVisitResult(context, {
+ uri: `http://www.${host}/`,
+ fallbackTitle: `www.${host}`,
+ heuristic: true,
+ }),
+ ],
+ });
+ info("search for full www.");
+ context = createContext("www.", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: `www.${host}/`,
+ completed: `http://www.${host}/`,
+ matches: [
+ makeVisitResult(context, {
+ uri: `http://www.${host}/`,
+ fallbackTitle: `www.${host}`,
+ heuristic: true,
+ }),
+ ],
+ });
+ info("search for host without www.");
+ context = createContext("ex", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: `${host}/`,
+ completed: `http://www.${host}/`,
+ matches: [
+ makeVisitResult(context, {
+ uri: `http://www.${host}/`,
+ fallbackTitle: `www.${host}`,
+ heuristic: true,
+ }),
+ ],
+ });
+});
diff --git a/browser/components/urlbar/tests/unit/test_autofill_do_not_trim.js b/browser/components/urlbar/tests/unit/test_autofill_do_not_trim.js
new file mode 100644
index 0000000000..5782bca210
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_do_not_trim.js
@@ -0,0 +1,140 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// We should not autofill when the search string contains spaces.
+
+testEngine_setup();
+
+add_task(async function setup() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/link/"),
+ });
+
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ await cleanupPlaces();
+ });
+});
+
+add_task(async function test_not_autofill_ws_1() {
+ info("Do not autofill whitespaced entry 1");
+ let context = createContext("mozilla.org ", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/",
+ fallbackTitle: "http://mozilla.org/",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/link/",
+ title: "test visit for http://mozilla.org/link/",
+ }),
+ ],
+ });
+});
+
+add_task(async function test_not_autofill_ws_2() {
+ info("Do not autofill whitespaced entry 2");
+ let context = createContext("mozilla.org/ ", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/",
+ fallbackTitle: "http://mozilla.org/",
+ iconUri: "page-icon:http://mozilla.org/",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/link/",
+ title: "test visit for http://mozilla.org/link/",
+ }),
+ ],
+ });
+});
+
+add_task(async function test_not_autofill_ws_3() {
+ info("Do not autofill whitespaced entry 3");
+ let context = createContext("mozilla.org/link ", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/link",
+ fallbackTitle: "http://mozilla.org/link",
+ iconUri: "page-icon:http://mozilla.org/",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/link/",
+ title: "test visit for http://mozilla.org/link/",
+ }),
+ ],
+ });
+});
+
+add_task(async function test_not_autofill_ws_4() {
+ info(
+ "Do not autofill whitespaced entry 4, but UrlbarProviderPlaces provides heuristic result"
+ );
+ let context = createContext("mozilla.org/link/ ", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/link/",
+ title: "test visit for http://mozilla.org/link/",
+ iconUri: "page-icon:http://mozilla.org/link/",
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ heuristic: true,
+ }),
+ ],
+ });
+});
+
+add_task(async function test_not_autofill_ws_5() {
+ info("Do not autofill whitespaced entry 5");
+ let context = createContext("moz illa ", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ // query is made explict so makeSearchResult doesn't trim it.
+ query: "moz illa ",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/link/",
+ title: "test visit for http://mozilla.org/link/",
+ }),
+ ],
+ });
+});
+
+add_task(async function test_not_autofill_ws_6() {
+ info("Do not autofill whitespaced entry 6");
+ let context = createContext(" mozilla", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ // query is made explict so makeSearchResult doesn't trim it.
+ query: " mozilla",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/link/",
+ title: "test visit for http://mozilla.org/link/",
+ }),
+ ],
+ });
+});
diff --git a/browser/components/urlbar/tests/unit/test_autofill_functional.js b/browser/components/urlbar/tests/unit/test_autofill_functional.js
new file mode 100644
index 0000000000..632a0d580d
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_functional.js
@@ -0,0 +1,115 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Functional tests for inline autocomplete
+
+add_task(async function setup() {
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions");
+ });
+
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false);
+});
+
+add_task(async function test_urls_order() {
+ info("Add urls, check for correct order");
+ let places = [
+ { uri: Services.io.newURI("http://visit1.mozilla.org") },
+ { uri: Services.io.newURI("http://visit2.mozilla.org") },
+ ];
+ await PlacesTestUtils.addVisits(places);
+ let context = createContext("vis", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "visit2.mozilla.org/",
+ completed: "http://visit2.mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://visit2.mozilla.org/",
+ title: "test visit for http://visit2.mozilla.org/",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://visit1.mozilla.org/",
+ title: "test visit for http://visit1.mozilla.org/",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_bookmark_first() {
+ info("With a bookmark and history, the query result should be the bookmark");
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: Services.io.newURI("http://bookmark1.mozilla.org/"),
+ });
+ await PlacesTestUtils.addVisits(
+ Services.io.newURI("http://bookmark1.mozilla.org/foo")
+ );
+ let context = createContext("bookmark", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "bookmark1.mozilla.org/",
+ completed: "http://bookmark1.mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://bookmark1.mozilla.org/",
+ title: "A bookmark",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://bookmark1.mozilla.org/foo",
+ title: "test visit for http://bookmark1.mozilla.org/foo",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_complete_querystring() {
+ info("Check to make sure we autocomplete after ?");
+ await PlacesTestUtils.addVisits(
+ Services.io.newURI("http://smokey.mozilla.org/foo?bacon=delicious")
+ );
+ let context = createContext("smokey.mozilla.org/foo?", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "smokey.mozilla.org/foo?bacon=delicious",
+ completed: "http://smokey.mozilla.org/foo?bacon=delicious",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://smokey.mozilla.org/foo?bacon=delicious",
+ title: "test visit for http://smokey.mozilla.org/foo?bacon=delicious",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_complete_fragment() {
+ info("Check to make sure we autocomplete after #");
+ await PlacesTestUtils.addVisits(
+ Services.io.newURI("http://smokey.mozilla.org/foo?bacon=delicious#bar")
+ );
+ let context = createContext("smokey.mozilla.org/foo?bacon=delicious#bar", {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ autofilled: "smokey.mozilla.org/foo?bacon=delicious#bar",
+ completed: "http://smokey.mozilla.org/foo?bacon=delicious#bar",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://smokey.mozilla.org/foo?bacon=delicious#bar",
+ title:
+ "test visit for http://smokey.mozilla.org/foo?bacon=delicious#bar",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_autofill_origins.js b/browser/components/urlbar/tests/unit/test_autofill_origins.js
new file mode 100644
index 0000000000..6913b1a3b9
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_origins.js
@@ -0,0 +1,1041 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const HEURISTIC_FALLBACK_PROVIDERNAME = "HeuristicFallback";
+
+const origin = "example.com";
+
+async function cleanup() {
+ let suggestPrefs = ["history", "bookmark", "openpage"];
+ for (let type of suggestPrefs) {
+ Services.prefs.clearUserPref("browser.urlbar.suggest." + type);
+ }
+ await cleanupPlaces();
+}
+
+testEngine_setup();
+
+registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions");
+});
+Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false);
+
+// "example.com/" should match http://example.com/. i.e., the search string
+// should be treated as if it didn't have the trailing slash.
+add_task(async function trailingSlash() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com/",
+ },
+ ]);
+
+ let context = createContext(`${origin}/`, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: `${origin}/`,
+ completed: `http://${origin}/`,
+ matches: [
+ makeVisitResult(context, {
+ uri: `http://${origin}/`,
+ title: `test visit for http://${origin}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "example.com/" should match http://www.example.com/. i.e., the search string
+// should be treated as if it didn't have the trailing slash.
+add_task(async function trailingSlashWWW() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://www.example.com/",
+ },
+ ]);
+ let context = createContext(`${origin}/`, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ completed: "http://www.example.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: `http://www.${origin}/`,
+ title: `test visit for http://www.${origin}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "ex" should match http://example.com:8888/, and the port should be completed.
+add_task(async function port() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com:8888/",
+ },
+ ]);
+ let context = createContext("ex", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com:8888/",
+ completed: "http://example.com:8888/",
+ matches: [
+ makeVisitResult(context, {
+ uri: `http://${origin}:8888/`,
+ title: `test visit for http://${origin}:8888/`,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "example.com:8" should match http://example.com:8888/, and the port should
+// be completed.
+add_task(async function portPartial() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com:8888/",
+ },
+ ]);
+ let context = createContext(`${origin}:8`, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com:8888/",
+ completed: "http://example.com:8888/",
+ matches: [
+ makeVisitResult(context, {
+ uri: `http://${origin}:8888/`,
+ title: `test visit for http://${origin}:8888/`,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "EXaM" should match http://example.com/ and the case of the search string
+// should be preserved in the autofilled value.
+add_task(async function preserveCase() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com/",
+ },
+ ]);
+ let context = createContext("EXaM", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "EXaMple.com/",
+ completed: "http://example.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: `http://${origin}/`,
+ title: `test visit for http://${origin}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "EXaM" should match http://example.com:8888/, the port should be completed,
+// and the case of the search string should be preserved in the autofilled
+// value.
+add_task(async function preserveCasePort() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com:8888/",
+ },
+ ]);
+ let context = createContext("EXaM", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "EXaMple.com:8888/",
+ completed: "http://example.com:8888/",
+ matches: [
+ makeVisitResult(context, {
+ uri: `http://${origin}:8888/`,
+ title: `test visit for http://${origin}:8888/`,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "example.com:89" should *not* match http://example.com:8888/.
+add_task(async function portNoMatch1() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com:8888/",
+ },
+ ]);
+ let context = createContext(`${origin}:89`, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${origin}:89/`,
+ fallbackTitle: `http://${origin}:89/`,
+ iconUri: "",
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "example.com:9" should *not* match http://example.com:8888/.
+add_task(async function portNoMatch2() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com:8888/",
+ },
+ ]);
+ let context = createContext(`${origin}:9`, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${origin}:9/`,
+ fallbackTitle: `http://${origin}:9/`,
+ iconUri: "",
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "example/" should *not* match http://example.com/.
+add_task(async function trailingSlash_2() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com/",
+ },
+ ]);
+ let context = createContext("example/", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://example/",
+ fallbackTitle: "http://example/",
+ iconUri: "page-icon:http://example/",
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// multi.dotted.domain, search up to dot.
+add_task(async function multidotted() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://www.example.co.jp:8888/",
+ },
+ ]);
+ let context = createContext("www.example.co.", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "www.example.co.jp:8888/",
+ completed: "http://www.example.co.jp:8888/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://www.example.co.jp:8888/",
+ title: "test visit for http://www.example.co.jp:8888/",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+add_task(async function test_ip() {
+ // IP addresses have complicated rules around whether they show
+ // HeuristicFallback's backup search result. Flip this pref to disable that
+ // backup search and simplify ths subtest.
+ Services.prefs.setBoolPref("keyword.enabled", false);
+ for (let str of [
+ "192.168.1.1/",
+ "255.255.255.255:8080/",
+ "[2001:db8::1428:57ab]/",
+ "[::c0a8:5909]/",
+ "[::1]/",
+ ]) {
+ info("testing " + str);
+ await PlacesTestUtils.addVisits("http://" + str);
+ for (let i = 1; i < str.length; ++i) {
+ let context = createContext(str.substring(0, i), { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: str,
+ completed: "http://" + str,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + str,
+ title: `test visit for http://${str}`,
+ heuristic: true,
+ }),
+ ],
+ });
+ }
+ await cleanup();
+ }
+ Services.prefs.clearUserPref("keyword.enabled");
+});
+
+// host starting with large number.
+add_task(async function large_number_host() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://12345example.it:8888/",
+ },
+ ]);
+ let context = createContext("1234", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "12345example.it:8888/",
+ completed: "http://12345example.it:8888/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://12345example.it:8888/",
+ title: "test visit for http://12345example.it:8888/",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// When determining which origins should be autofilled, all the origins sharing
+// a host should be added together to get their combined frecency -- i.e.,
+// prefixes should be collapsed. And then from that list, the origin with the
+// highest frecency should be chosen.
+add_task(async function groupByHost() {
+ // Add some visits to the same host, example.com. Add one http and two https
+ // so that https has a higher frecency and is therefore the origin that should
+ // be autofilled. Also add another origin that has a higher frecency than
+ // both so that alone, neither http nor https would be autofilled, but added
+ // together they should be.
+ await PlacesTestUtils.addVisits([
+ { uri: "http://example.com/" },
+
+ { uri: "https://example.com/" },
+ { uri: "https://example.com/" },
+
+ { uri: "https://mozilla.org/" },
+ { uri: "https://mozilla.org/" },
+ { uri: "https://mozilla.org/" },
+ { uri: "https://mozilla.org/" },
+ ]);
+
+ let httpFrec = await PlacesTestUtils.getDatabaseValue(
+ "moz_places",
+ "frecency",
+ { url: "http://example.com/" }
+ );
+ let httpsFrec = await PlacesTestUtils.getDatabaseValue(
+ "moz_places",
+ "frecency",
+ { url: "https://example.com/" }
+ );
+ let otherFrec = await PlacesTestUtils.getDatabaseValue(
+ "moz_places",
+ "frecency",
+ { url: "https://mozilla.org/" }
+ );
+ Assert.less(httpFrec, httpsFrec, "Sanity check");
+ Assert.less(httpsFrec, otherFrec, "Sanity check");
+
+ // Make sure the frecencies of the three origins are as expected in relation
+ // to the threshold.
+ let threshold = await getOriginAutofillThreshold();
+ Assert.less(httpFrec, threshold, "http origin should be < threshold");
+ Assert.less(httpsFrec, threshold, "https origin should be < threshold");
+ Assert.ok(threshold <= otherFrec, "Other origin should cross threshold");
+
+ Assert.ok(
+ threshold <= httpFrec + httpsFrec,
+ "http and https origin added together should cross threshold"
+ );
+
+ // The https origin should be autofilled.
+ let context = createContext("ex", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ completed: "https://example.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://example.com/",
+ title: "test visit for https://example.com/",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanup();
+});
+
+// This is the same as the previous (groupByHost), but it changes the standard
+// deviation multiplier by setting the corresponding pref. This makes sure that
+// the pref is respected.
+add_task(async function groupByHostNonDefaultStddevMultiplier() {
+ let stddevMultiplier = 1.5;
+ Services.prefs.setCharPref(
+ "browser.urlbar.autoFill.stddevMultiplier",
+ Number(stddevMultiplier).toFixed(1)
+ );
+
+ await PlacesTestUtils.addVisits([
+ { uri: "http://example.com/" },
+ { uri: "http://example.com/" },
+
+ { uri: "https://example.com/" },
+ { uri: "https://example.com/" },
+ { uri: "https://example.com/" },
+
+ { uri: "https://foo.com/" },
+ { uri: "https://foo.com/" },
+ { uri: "https://foo.com/" },
+
+ { uri: "https://mozilla.org/" },
+ { uri: "https://mozilla.org/" },
+ { uri: "https://mozilla.org/" },
+ { uri: "https://mozilla.org/" },
+ { uri: "https://mozilla.org/" },
+ ]);
+
+ let httpFrec = await PlacesTestUtils.getDatabaseValue(
+ "moz_places",
+ "frecency",
+ {
+ url: "http://example.com/",
+ }
+ );
+ let httpsFrec = await PlacesTestUtils.getDatabaseValue(
+ "moz_places",
+ "frecency",
+ {
+ url: "https://example.com/",
+ }
+ );
+ let otherFrec = await PlacesTestUtils.getDatabaseValue(
+ "moz_places",
+ "frecency",
+ {
+ url: "https://mozilla.org/",
+ }
+ );
+ Assert.less(httpFrec, httpsFrec, "Sanity check");
+ Assert.less(httpsFrec, otherFrec, "Sanity check");
+
+ // Make sure the frecencies of the three origins are as expected in relation
+ // to the threshold.
+ let threshold = await getOriginAutofillThreshold();
+ Assert.less(httpFrec, threshold, "http origin should be < threshold");
+ Assert.less(httpsFrec, threshold, "https origin should be < threshold");
+ Assert.ok(threshold <= otherFrec, "Other origin should cross threshold");
+
+ Assert.ok(
+ threshold <= httpFrec + httpsFrec,
+ "http and https origin added together should cross threshold"
+ );
+
+ // The https origin should be autofilled.
+ let context = createContext("ex", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ completed: "https://example.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://example.com/",
+ title: "test visit for https://example.com/",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ Services.prefs.clearUserPref("browser.urlbar.autoFill.stddevMultiplier");
+
+ await cleanup();
+});
+
+// This is similar to suggestHistoryFalse_bookmark_0 in test_autofill_tasks.js,
+// but it adds unbookmarked visits for multiple URLs with the same origin.
+add_task(async function suggestHistoryFalse_bookmark_multiple() {
+ // Force only bookmarked pages to be suggested and therefore only bookmarked
+ // pages to be completed.
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+
+ let search = "ex";
+ let baseURL = "http://example.com/";
+ let bookmarkedURL = baseURL + "bookmarked";
+
+ // Add visits for three different URLs all sharing the same origin, and then
+ // bookmark the second one. After that, the origin should be autofilled. The
+ // reason for adding unbookmarked visits before and after adding the
+ // bookmarked visit is to make sure our aggregate SQL query for determining
+ // whether an origin is bookmarked is correct.
+
+ await PlacesTestUtils.addVisits([
+ {
+ uri: baseURL + "other1",
+ },
+ ]);
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await PlacesTestUtils.addVisits([
+ {
+ uri: bookmarkedURL,
+ },
+ ]);
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await PlacesTestUtils.addVisits([
+ {
+ uri: baseURL + "other2",
+ },
+ ]);
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // Now bookmark the second URL. It should be suggested and completed.
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: bookmarkedURL,
+ });
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ completed: baseURL,
+ matches: [
+ makeVisitResult(context, {
+ uri: baseURL,
+ fallbackTitle: "example.com",
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: bookmarkedURL,
+ title: "A bookmark",
+ }),
+ ],
+ });
+
+ await cleanup();
+});
+
+// This is similar to suggestHistoryFalse_bookmark_prefix_0 in
+// autofill_test_autofill_originsAndQueries.js, but it adds unbookmarked visits
+// for multiple URLs with the same origin.
+add_task(async function suggestHistoryFalse_bookmark_prefix_multiple() {
+ // Force only bookmarked pages to be suggested and therefore only bookmarked
+ // pages to be completed.
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+
+ let search = "http://ex";
+ let baseURL = "http://example.com/";
+ let bookmarkedURL = baseURL + "bookmarked";
+
+ // Add visits for three different URLs all sharing the same origin, and then
+ // bookmark the second one. After that, the origin should be autofilled. The
+ // reason for adding unbookmarked visits before and after adding the
+ // bookmarked visit is to make sure our aggregate SQL query for determining
+ // whether an origin is bookmarked is correct.
+
+ await PlacesTestUtils.addVisits([
+ {
+ uri: baseURL + "other1",
+ },
+ ]);
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `${search}/`,
+ fallbackTitle: `${search}/`,
+ iconUri: "",
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+
+ await PlacesTestUtils.addVisits([
+ {
+ uri: bookmarkedURL,
+ },
+ ]);
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `${search}/`,
+ fallbackTitle: `${search}/`,
+ iconUri: "",
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+
+ await PlacesTestUtils.addVisits([
+ {
+ uri: baseURL + "other2",
+ },
+ ]);
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `${search}/`,
+ fallbackTitle: `${search}/`,
+ iconUri: "",
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+
+ // Now bookmark the second URL. It should be suggested and completed.
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: bookmarkedURL,
+ });
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://example.com/",
+ completed: baseURL,
+ matches: [
+ makeVisitResult(context, {
+ uri: baseURL,
+ fallbackTitle: "example.com",
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: bookmarkedURL,
+ title: "A bookmark",
+ }),
+ ],
+ });
+
+ await cleanup();
+});
+
+// When the autofilled URL is `example.com/`, a visit for `example.com/?` should
+// not be included in the results since it dupes the autofill result.
+add_task(async function searchParams() {
+ await PlacesTestUtils.addVisits([
+ "http://example.com/",
+ "http://example.com/?",
+ "http://example.com/?foo",
+ ]);
+
+ // First, do a search with autofill disabled to make sure the visits were
+ // properly added. `example.com/?foo` has the highest frecency because it was
+ // added last; `example.com/?` has the next highest. `example.com/` dupes
+ // `example.com/?`, so it should not appear.
+ UrlbarPrefs.set("autoFill", false);
+ let context = createContext("ex", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/?foo",
+ title: "test visit for http://example.com/?foo",
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/?",
+ title: "test visit for http://example.com/?",
+ }),
+ ],
+ });
+
+ // Now do a search with autofill enabled. This time `example.com/` will be
+ // autofilled, and since `example.com/?` dupes it, `example.com/?` should not
+ // appear.
+ UrlbarPrefs.clear("autoFill");
+ context = createContext("ex", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ completed: "http://example.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ title: "test visit for http://example.com/",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/?foo",
+ title: "test visit for http://example.com/?foo",
+ }),
+ ],
+ });
+
+ await cleanup();
+});
+
+// When the autofilled URL is `example.com/`, a visit for `example.com/?` should
+// not be included in the results since it dupes the autofill result. (Same as
+// the previous task but with https URLs instead of http. There shouldn't be any
+// substantive difference.)
+add_task(async function searchParams_https() {
+ await PlacesTestUtils.addVisits([
+ "https://example.com/",
+ "https://example.com/?",
+ "https://example.com/?foo",
+ ]);
+
+ // First, do a search with autofill disabled to make sure the visits were
+ // properly added. `example.com/?foo` has the highest frecency because it was
+ // added last; `example.com/?` has the next highest. `example.com/` dupes
+ // `example.com/?`, so it should not appear.
+ UrlbarPrefs.set("autoFill", false);
+ let context = createContext("ex", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://example.com/?foo",
+ title: "test visit for https://example.com/?foo",
+ }),
+ makeVisitResult(context, {
+ uri: "https://example.com/?",
+ title: "test visit for https://example.com/?",
+ }),
+ ],
+ });
+
+ // Now do a search with autofill enabled. This time `example.com/` will be
+ // autofilled, and since `example.com/?` dupes it, `example.com/?` should not
+ // appear.
+ UrlbarPrefs.clear("autoFill");
+ context = createContext("ex", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ completed: "https://example.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://example.com/",
+ title: "test visit for https://example.com/",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://example.com/?foo",
+ title: "test visit for https://example.com/?foo",
+ }),
+ ],
+ });
+
+ await cleanup();
+});
+
+// Checks an origin that looks like a prefix: a scheme with no dots + a port.
+add_task(async function originLooksLikePrefix() {
+ let hostAndPort = "localhost:8888";
+ let address = `http://${hostAndPort}/`;
+ await PlacesTestUtils.addVisits([{ uri: address }]);
+
+ // addTestSuggestionsEngine adds a search engine
+ // with localhost as a server, so we have to disable the
+ // TTS result or else it will show up as a second result
+ // when searching l to localhost
+ UrlbarPrefs.set("suggest.engines", false);
+
+ for (let search of ["lo", "localhost", "localhost:", "localhost:8888"]) {
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: hostAndPort + "/",
+ completed: address,
+ matches: [
+ makeVisitResult(context, {
+ uri: address,
+ title: `test visit for http://${hostAndPort}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+ }
+ await cleanup();
+});
+
+// Checks an origin whose prefix is "about:".
+add_task(async function about() {
+ const testData = [
+ {
+ uri: "about:config",
+ input: "conf",
+ results: [
+ context =>
+ makeSearchResult(context, {
+ engineName: "Suggestions",
+ heuristic: true,
+ }),
+ context =>
+ makeBookmarkResult(context, {
+ uri: "about:config",
+ title: "A bookmark",
+ }),
+ ],
+ },
+ {
+ uri: "about:blank",
+ input: "about:blan",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "about:blan",
+ fallbackTitle: "about:blan",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ context =>
+ makeBookmarkResult(context, {
+ uri: "about:blank",
+ title: "A bookmark",
+ }),
+ ],
+ },
+ ];
+
+ for (const { uri, input, results } of testData) {
+ await PlacesTestUtils.addBookmarkWithDetails({ uri });
+
+ const context = createContext(input, { isPrivate: false });
+ await check_results({
+ context,
+ matches: results.map(f => f(context)),
+ });
+ await cleanup();
+ }
+});
+
+// Checks an origin whose prefix is "place:".
+add_task(async function place() {
+ const testData = [
+ {
+ uri: "place:transition=7&sort=4",
+ input: "tran",
+ },
+ {
+ uri: "place:transition=7&sort=4",
+ input: "place:tran",
+ },
+ ];
+
+ for (const { uri, input } of testData) {
+ await PlacesTestUtils.addBookmarkWithDetails({ uri });
+
+ const context = createContext(input, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: "Suggestions",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+ }
+});
+
+add_task(async function nullTitle() {
+ await doTitleTest({
+ visits: [
+ {
+ uri: "http://example.com/",
+ // Set title of visits data to an empty string causes
+ // the title to be null in the database.
+ title: "",
+ frecency: 100,
+ },
+ {
+ uri: "https://www.example.com/",
+ title: "high frecency",
+ frecency: 50,
+ },
+ {
+ uri: "http://www.example.com/",
+ title: "low frecency",
+ frecency: 1,
+ },
+ ],
+ input: "example.com",
+ expected: {
+ autofilled: "example.com/",
+ completed: "http://example.com/",
+ matches: context => [
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ title: "high frecency",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://www.example.com/",
+ title: "high frecency",
+ }),
+ ],
+ },
+ });
+});
+
+add_task(async function domainTitle() {
+ await doTitleTest({
+ visits: [
+ {
+ uri: "http://example.com/",
+ title: "example.com",
+ frecency: 100,
+ },
+ {
+ uri: "https://www.example.com/",
+ title: "",
+ frecency: 50,
+ },
+ {
+ uri: "http://www.example.com/",
+ title: "lowest frecency but has title",
+ frecency: 1,
+ },
+ ],
+ input: "example.com",
+ expected: {
+ autofilled: "example.com/",
+ completed: "http://example.com/",
+ matches: context => [
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ title: "lowest frecency but has title",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://www.example.com/",
+ title: "www.example.com",
+ }),
+ ],
+ },
+ });
+});
+
+add_task(async function exactMatchedTitle() {
+ await doTitleTest({
+ visits: [
+ {
+ uri: "http://example.com/",
+ title: "exact match",
+ frecency: 50,
+ },
+ {
+ uri: "https://www.example.com/",
+ title: "high frecency uri",
+ frecency: 100,
+ },
+ ],
+ input: "http://example.com/",
+ expected: {
+ autofilled: "http://example.com/",
+ completed: "http://example.com/",
+ matches: context => [
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ title: "exact match",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://www.example.com/",
+ title: "high frecency uri",
+ }),
+ ],
+ },
+ });
+});
+
+async function doTitleTest({ visits, input, expected }) {
+ await PlacesTestUtils.addVisits(visits);
+ for (const { uri, frecency } of visits) {
+ // Prepare data.
+ await PlacesUtils.withConnectionWrapper("test::doTitleTest", async db => {
+ await db.execute(
+ `UPDATE moz_places SET frecency = :frecency, recalc_frecency=0 WHERE url = :url`,
+ {
+ frecency,
+ url: uri,
+ }
+ );
+ await db.executeCached("DELETE FROM moz_updateoriginsupdate_temp");
+ });
+ }
+
+ const context = createContext(input, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: expected.autofilled,
+ completed: expected.completed,
+ matches: expected.matches(context),
+ });
+
+ await cleanup();
+}
diff --git a/browser/components/urlbar/tests/unit/test_autofill_originsAndQueries.js b/browser/components/urlbar/tests/unit/test_autofill_originsAndQueries.js
new file mode 100644
index 0000000000..9b305717c0
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_originsAndQueries.js
@@ -0,0 +1,2471 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const HEURISTIC_FALLBACK_PROVIDERNAME = "HeuristicFallback";
+const PLACES_PROVIDERNAME = "Places";
+
+/**
+ * Helpful reminder of the `autofilled` and `completed` properties in the
+ * object passed to check_results:
+ * autofilled: expected input.value after autofill
+ * completed: expected input.value after autofill and enter is pressed
+ *
+ * `completed` is the URL that the controller sets to input.value, and the URL
+ * that will ultimately be loaded when you press enter.
+ */
+
+async function cleanup() {
+ let suggestPrefs = ["history", "bookmark", "openpage"];
+ for (let type of suggestPrefs) {
+ Services.prefs.clearUserPref("browser.urlbar.suggest." + type);
+ }
+ await cleanupPlaces();
+}
+
+testEngine_setup();
+
+registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions");
+});
+Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false);
+
+let path;
+let search;
+let searchCase;
+let visitTitle;
+let url;
+const host = "example.com";
+let origins;
+
+function add_autofill_task(callback) {
+ let func = async () => {
+ info(`Running subtest with origins disabled: ${callback.name}`);
+ origins = false;
+ path = "/foo";
+ search = "example.com/f";
+ searchCase = "EXAMPLE.COM/f";
+ visitTitle = (protocol, sub) =>
+ `test visit for ${protocol}://${sub}example.com/foo`;
+ url = host + path;
+ await callback();
+
+ info(`Running subtest with origins enabled: ${callback.name}`);
+ origins = true;
+ path = "/";
+ search = "ex";
+ searchCase = "EX";
+ visitTitle = (protocol, sub) =>
+ `test visit for ${protocol}://${sub}example.com/`;
+ url = host + path;
+ await callback();
+ };
+ Object.defineProperty(func, "name", { value: callback.name });
+ add_task(func);
+}
+
+// "ex" should match http://example.com/.
+add_autofill_task(async function basic() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://" + url,
+ },
+ ]);
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: visitTitle("http", ""),
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "EX" should match http://example.com/.
+add_autofill_task(async function basicCase() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://" + url,
+ },
+ ]);
+ let context = createContext(searchCase, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: searchCase + url.substr(searchCase.length),
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: visitTitle("http", ""),
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "ex" should match http://www.example.com/.
+add_autofill_task(async function noWWWShouldMatchWWW() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://www." + url,
+ },
+ ]);
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://www." + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://www." + url,
+ title: visitTitle("http", "www."),
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "EX" should match http://www.example.com/.
+add_autofill_task(async function noWWWShouldMatchWWWCase() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://www." + url,
+ },
+ ]);
+ let context = createContext(searchCase, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: searchCase + url.substr(searchCase.length),
+ completed: "http://www." + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://www." + url,
+ title: visitTitle("http", "www."),
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "www.ex" should *not* match http://example.com/.
+add_autofill_task(async function wwwShouldNotMatchNoWWW() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://" + url,
+ },
+ ]);
+ let context = createContext("www." + search, { isPrivate: false });
+ if (origins) {
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://www." + search + "/",
+ fallbackTitle: "http://www." + search + "/",
+ displayUrl: "http://www." + search,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ } else {
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://www." + search,
+ fallbackTitle: "http://www." + search,
+ iconUri: `page-icon:http://www.${host}/`,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ }
+ await cleanup();
+});
+
+// "http://ex" should match http://example.com/.
+add_autofill_task(async function prefix() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://" + url,
+ },
+ ]);
+ let context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://" + url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: visitTitle("http", ""),
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "HTTP://EX" should match http://example.com/.
+add_autofill_task(async function prefixCase() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://" + url,
+ },
+ ]);
+ let context = createContext("HTTP://" + searchCase, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "HTTP://" + searchCase + url.substr(searchCase.length),
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: visitTitle("http", ""),
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "http://ex" should match http://www.example.com/.
+add_autofill_task(async function prefixNoWWWShouldMatchWWW() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://www." + url,
+ },
+ ]);
+ let context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://" + url,
+ completed: "http://www." + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://www." + url,
+ title: visitTitle("http", "www."),
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "HTTP://EX" should match http://www.example.com/.
+add_autofill_task(async function prefixNoWWWShouldMatchWWWCase() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://www." + url,
+ },
+ ]);
+ let context = createContext("HTTP://" + searchCase, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "HTTP://" + searchCase + url.substr(searchCase.length),
+ completed: "http://www." + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://www." + url,
+ title: visitTitle("http", "www."),
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "http://www.ex" should *not* match http://example.com/.
+add_autofill_task(async function prefixWWWShouldNotMatchNoWWW() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://" + url,
+ },
+ ]);
+ let context = createContext("http://www." + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://www.${search}/` : `http://www.${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ fallbackTitle: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://www.${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "http://ex" should *not* match https://example.com/.
+add_autofill_task(async function httpPrefixShouldNotMatchHTTPS() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "https://" + url,
+ },
+ ]);
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ fallbackTitle: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: "test visit for https://" + url,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "ex" should match https://example.com/.
+add_autofill_task(async function httpsBasic() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "https://" + url,
+ },
+ ]);
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "https://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: visitTitle("https", ""),
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "ex" should match https://www.example.com/.
+add_autofill_task(async function httpsNoWWWShouldMatchWWW() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "https://www." + url,
+ },
+ ]);
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "https://www." + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www." + url,
+ title: visitTitle("https", "www."),
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "www.ex" should *not* match https://example.com/.
+add_autofill_task(async function httpsWWWShouldNotMatchNoWWW() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "https://" + url,
+ },
+ ]);
+ let context = createContext("www." + search, { isPrivate: false });
+ if (origins) {
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://www." + search + "/",
+ fallbackTitle: "http://www." + search + "/",
+ displayUrl: "http://www." + search,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ } else {
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://www." + search,
+ fallbackTitle: "http://www." + search,
+ iconUri: `page-icon:http://www.${host}/`,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ }
+ await cleanup();
+});
+
+// "https://ex" should match https://example.com/.
+add_autofill_task(async function httpsPrefix() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "https://" + url,
+ },
+ ]);
+ let context = createContext("https://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "https://" + url,
+ completed: "https://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: visitTitle("https", ""),
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "https://ex" should match https://www.example.com/.
+add_autofill_task(async function httpsPrefixNoWWWShouldMatchWWW() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "https://www." + url,
+ },
+ ]);
+ let context = createContext("https://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "https://" + url,
+ completed: "https://www." + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www." + url,
+ title: visitTitle("https", "www."),
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "https://www.ex" should *not* match https://example.com/.
+add_autofill_task(async function httpsPrefixWWWShouldNotMatchNoWWW() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "https://" + url,
+ },
+ ]);
+ let context = createContext("https://www." + search, { isPrivate: false });
+ let prefixedUrl = origins
+ ? `https://www.${search}/`
+ : `https://www.${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ fallbackTitle: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:https://www.${host}/`,
+ providerame: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "https://ex" should *not* match http://example.com/.
+add_autofill_task(async function httpsPrefixShouldNotMatchHTTP() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://" + url,
+ },
+ ]);
+ let context = createContext("https://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `https://${search}/` : `https://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ fallbackTitle: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:https://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: "test visit for http://" + url,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// "https://ex" should *not* match http://example.com/, even if the latter is
+// more frecent and both could be autofilled.
+add_autofill_task(async function httpsPrefixShouldNotMatchMoreFrecentHTTP() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://" + url,
+ transition: PlacesUtils.history.TRANSITIONS.TYPED,
+ },
+ {
+ uri: "http://" + url,
+ },
+ {
+ uri: "https://" + url,
+ transition: PlacesUtils.history.TRANSITIONS.TYPED,
+ },
+ {
+ uri: "http://otherpage",
+ },
+ ]);
+ let context = createContext("https://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "https://" + url,
+ completed: "https://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: visitTitle("https", ""),
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Autofill should respond to frecency changes.
+add_autofill_task(async function frecency() {
+ // Start with an http visit. It should be completed.
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://" + url,
+ },
+ ]);
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: visitTitle("http", ""),
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // Add two https visits. https should now be completed.
+ for (let i = 0; i < 2; i++) {
+ await PlacesTestUtils.addVisits([{ uri: "https://" + url }]);
+ }
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "https://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: visitTitle("https", ""),
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // Add two more http visits, three total. http should now be completed
+ // again.
+ for (let i = 0; i < 2; i++) {
+ await PlacesTestUtils.addVisits([{ uri: "http://" + url }]);
+ }
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: visitTitle("http", ""),
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: "test visit for https://" + url,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ ],
+ });
+
+ // Add four www https visits. www https should now be completed.
+ for (let i = 0; i < 4; i++) {
+ await PlacesTestUtils.addVisits([{ uri: "https://www." + url }]);
+ }
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "https://www." + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www." + url,
+ title: visitTitle("https", "www."),
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: "test visit for https://" + url,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ ],
+ });
+
+ // Remove the www https page.
+ await PlacesUtils.history.remove(["https://www." + url]);
+
+ // http should now be completed again.
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: visitTitle("http", ""),
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: "test visit for https://" + url,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ ],
+ });
+
+ // Remove the http page.
+ await PlacesUtils.history.remove(["http://" + url]);
+
+ // https should now be completed again.
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "https://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: visitTitle("https", ""),
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // Add a visit with a different host so that "ex" doesn't autofill it.
+ // https://example.com/ should still have a higher frecency though, so it
+ // should still be autofilled.
+ await PlacesTestUtils.addVisits([{ uri: "https://not-" + url }]);
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "https://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: visitTitle("https", ""),
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://not-" + url,
+ title: "test visit for https://not-" + url,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ ],
+ });
+
+ // Now add 10 more visits to the different host so that the frecency of
+ // https://example.com/ falls below the autofill threshold. It should not
+ // be autofilled now.
+ for (let i = 0; i < 10; i++) {
+ await PlacesTestUtils.addVisits([{ uri: "https://not-" + url }]);
+ }
+
+ // In the `origins` case, the failure to make an autofill match means
+ // HeuristicFallback should not create a heuristic result. In the
+ // `!origins` case, autofill should still happen since there's no threshold
+ // comparison.
+ context = createContext(search, { isPrivate: false });
+ if (origins) {
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "https://not-" + url,
+ title: "test visit for https://not-" + url,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: "test visit for https://" + url,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ ],
+ });
+ } else {
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "https://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: visitTitle("https", ""),
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://not-" + url,
+ title: "test visit for https://not-" + url,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ ],
+ });
+ }
+
+ // Remove the visits to the different host.
+ await PlacesUtils.history.remove(["https://not-" + url]);
+
+ // https should be completed again.
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "https://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://" + url,
+ title: visitTitle("https", ""),
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // Remove the https visits.
+ await PlacesUtils.history.remove(["https://" + url]);
+
+ // Now nothing should be completed.
+ context = createContext(search, { isPrivate: false });
+ if (origins) {
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ } else {
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://" + search,
+ fallbackTitle: "http://" + search,
+ iconUri: `page-icon:http://${host}/`,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ }
+
+ await cleanup();
+});
+
+// Bookmarked places should always be autofilled, even when they don't meet
+// the threshold.
+add_autofill_task(async function bookmarkBelowThreshold() {
+ // Add some visits to a URL so that the origin autofill threshold is large.
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://not-" + url,
+ },
+ ]);
+ }
+
+ // Now bookmark another URL.
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://" + url,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ // Make sure the bookmarked origin and place frecencies are below the
+ // threshold so that the origin/URL otherwise would not be autofilled.
+ let placeFrecency = await PlacesTestUtils.getDatabaseValue(
+ "moz_places",
+ "frecency",
+ { url: "http://" + url }
+ );
+ let originFrecency = await getOriginFrecency("http://", host);
+ let threshold = await getOriginAutofillThreshold();
+ Assert.ok(
+ placeFrecency < threshold,
+ `Place frecency should be below the threshold: ` +
+ `placeFrecency=${placeFrecency} threshold=${threshold}`
+ );
+ Assert.ok(
+ originFrecency < threshold,
+ `Origin frecency should be below the threshold: ` +
+ `originFrecency=${originFrecency} threshold=${threshold}`
+ );
+
+ // The bookmark should be autofilled.
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: "A bookmark",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://not-" + url,
+ title: "test visit for http://not-" + url,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ ],
+ });
+
+ await cleanup();
+});
+
+// Bookmarked places should be autofilled when they *do* meet the threshold.
+add_autofill_task(async function bookmarkAboveThreshold() {
+ // Bookmark a URL.
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://" + url,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ // The frecencies of the place and origin should be >= the threshold. In
+ // fact they should be the same as the threshold since the place is the only
+ // place in the database.
+ let placeFrecency = await PlacesTestUtils.getDatabaseValue(
+ "moz_places",
+ "frecency",
+ { url: "http://" + url }
+ );
+ let originFrecency = await getOriginFrecency("http://", host);
+ let threshold = await getOriginAutofillThreshold();
+ Assert.equal(placeFrecency, threshold);
+ Assert.equal(originFrecency, threshold);
+
+ // The bookmark should be autofilled.
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: "A bookmark",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanup();
+});
+
+// Bookmark a page and then clear history.
+// The bookmarked origin/URL should still be autofilled.
+add_autofill_task(async function zeroThreshold() {
+ const pageUrl = "http://" + url;
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: pageUrl,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ await PlacesUtils.history.clear();
+ await PlacesUtils.withConnectionWrapper("zeroThreshold", async db => {
+ await db.execute("UPDATE moz_places SET frecency = -1 WHERE url = :url", {
+ url: pageUrl,
+ });
+ await db.executeCached("DELETE FROM moz_updateoriginsupdate_temp");
+ });
+
+ // Make sure the place's frecency is -1.
+ let placeFrecency = await PlacesTestUtils.getDatabaseValue(
+ "moz_places",
+ "frecency",
+ { url: pageUrl }
+ );
+ Assert.equal(placeFrecency, -1);
+
+ // Make sure the origin's frecency is 0.
+ let originFrecency = await getOriginFrecency("http://", host);
+ Assert.equal(originFrecency, 0);
+
+ // Make sure the autofill threshold is 0.
+ let threshold = await getOriginAutofillThreshold();
+ Assert.equal(threshold, 0);
+
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: "A bookmark",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = false
+// suggest.bookmark = true
+// search for: visit
+// prefix search: no
+// prefix matches search: n/a
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(async function suggestHistoryFalse_visit() {
+ await PlacesTestUtils.addVisits("http://" + url);
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: visitTitle("http", ""),
+ heuristic: true,
+ }),
+ ],
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ context = createContext(search, { isPrivate: false });
+ if (origins) {
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ } else {
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://" + search,
+ fallbackTitle: "http://" + search,
+ iconUri: `page-icon:http://${host}/`,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ }
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = false
+// suggest.bookmark = true
+// search for: visit
+// prefix search: yes
+// prefix matches search: yes
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(async function suggestHistoryFalse_visit_prefix() {
+ await PlacesTestUtils.addVisits("http://" + url);
+ let context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://" + url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: visitTitle("http", ""),
+ heuristic: true,
+ }),
+ ],
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ context = createContext(search, { isPrivate: false });
+ if (origins) {
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ } else {
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://" + search,
+ fallbackTitle: "http://" + search,
+ iconUri: `page-icon:http://${host}/`,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ }
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = false
+// suggest.bookmark = true
+// search for: bookmark
+// prefix search: no
+// prefix matches search: n/a
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: yes
+add_autofill_task(async function suggestHistoryFalse_bookmark_0() {
+ // Add the bookmark.
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://" + url,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ // Make the bookmark fall below the autofill frecency threshold so we ensure
+ // the bookmark is always autofilled in this case, even if it doesn't meet
+ // the threshold.
+ let meetsThreshold = true;
+ while (meetsThreshold) {
+ // Add a visit to another origin to boost the threshold.
+ await PlacesTestUtils.addVisits("http://foo-" + url);
+ let originFrecency = await getOriginFrecency("http://", host);
+ let threshold = await getOriginAutofillThreshold();
+ meetsThreshold = threshold <= originFrecency;
+ }
+
+ // At this point, the bookmark doesn't meet the threshold, but it should
+ // still be autofilled.
+ let originFrecency = await getOriginFrecency("http://", host);
+ let threshold = await getOriginAutofillThreshold();
+ Assert.ok(originFrecency < threshold);
+
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: "A bookmark",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = false
+// suggest.bookmark = true
+// search for: bookmark
+// prefix search: no
+// prefix matches search: n/a
+// origin matches search: no
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(async function suggestHistoryFalse_bookmark_1() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://non-matching-" + url,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ let context = createContext(search, { isPrivate: false });
+ let matches = [
+ makeBookmarkResult(context, {
+ uri: "http://non-matching-" + url,
+ title: "A bookmark",
+ }),
+ ];
+ if (origins) {
+ matches.unshift(
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ })
+ );
+ } else {
+ matches.unshift(
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://" + search,
+ fallbackTitle: "http://" + search,
+ iconUri: `page-icon:http://${host}/`,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ })
+ );
+ }
+ await check_results({
+ context,
+ matches,
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = false
+// suggest.bookmark = true
+// search for: bookmark
+// prefix search: yes
+// prefix matches search: yes
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: yes
+add_autofill_task(async function suggestHistoryFalse_bookmark_prefix_0() {
+ // Add the bookmark.
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://" + url,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ // Make the bookmark fall below the autofill frecency threshold so we ensure
+ // the bookmark is always autofilled in this case, even if it doesn't meet
+ // the threshold.
+ let meetsThreshold = true;
+ while (meetsThreshold) {
+ // Add a visit to another origin to boost the threshold.
+ await PlacesTestUtils.addVisits("http://foo-" + url);
+ let originFrecency = await getOriginFrecency("http://", host);
+ let threshold = await getOriginAutofillThreshold();
+ meetsThreshold = threshold <= originFrecency;
+ }
+
+ // At this point, the bookmark doesn't meet the threshold, but it should
+ // still be autofilled.
+ let originFrecency = await getOriginFrecency("http://", host);
+ let threshold = await getOriginAutofillThreshold();
+ Assert.ok(originFrecency < threshold);
+
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://" + url,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ let context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://" + url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: "A bookmark",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = false
+// suggest.bookmark = true
+// search for: bookmark
+// prefix search: yes
+// prefix matches search: no
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(async function suggestHistoryFalse_bookmark_prefix_1() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "ftp://" + url,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ fallbackTitle: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeBookmarkResult(context, {
+ uri: "ftp://" + url,
+ title: "A bookmark",
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = false
+// suggest.bookmark = true
+// search for: bookmark
+// prefix search: yes
+// prefix matches search: yes
+// origin matches search: no
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(async function suggestHistoryFalse_bookmark_prefix_2() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://non-matching-" + url,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ fallbackTitle: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://non-matching-" + url,
+ title: "A bookmark",
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = false
+// suggest.bookmark = true
+// search for: bookmark
+// prefix search: yes
+// prefix matches search: no
+// origin matches search: no
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(async function suggestHistoryFalse_bookmark_prefix_3() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "ftp://non-matching-" + url,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ fallbackTitle: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeBookmarkResult(context, {
+ uri: "ftp://non-matching-" + url,
+ title: "A bookmark",
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visit
+// prefix search: no
+// prefix matches search: n/a
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: yes
+add_autofill_task(async function suggestBookmarkFalse_visit_0() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ await PlacesTestUtils.addVisits("http://" + url);
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: visitTitle("http", ""),
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visit
+// prefix search: no
+// prefix matches search: n/a
+// origin matches search: no
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(async function suggestBookmarkFalse_visit_1() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ await PlacesTestUtils.addVisits("http://non-matching-" + url);
+ let context = createContext(search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ let matches = [
+ makeVisitResult(context, {
+ uri: "http://non-matching-" + url,
+ title: "test visit for http://non-matching-" + url,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ ];
+ if (origins) {
+ matches.unshift(
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ })
+ );
+ } else {
+ matches.unshift(
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ fallbackTitle: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ })
+ );
+ }
+ await check_results({
+ context,
+ matches,
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visit
+// prefix search: yes
+// prefix matches search: yes
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: yes
+add_autofill_task(async function suggestBookmarkFalse_visit_prefix_0() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ await PlacesTestUtils.addVisits("http://" + url);
+ let context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://" + url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: visitTitle("http", ""),
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visit
+// prefix search: yes
+// prefix matches search: no
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(async function suggestBookmarkFalse_visit_prefix_1() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ await PlacesTestUtils.addVisits("ftp://" + url);
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ fallbackTitle: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "ftp://" + url,
+ title: "test visit for ftp://" + url,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visit
+// prefix search: yes
+// prefix matches search: yes
+// origin matches search: no
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(async function suggestBookmarkFalse_visit_prefix_2() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ await PlacesTestUtils.addVisits("http://non-matching-" + url);
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ fallbackTitle: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://non-matching-" + url,
+ title: "test visit for http://non-matching-" + url,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visit
+// prefix search: yes
+// prefix matches search: no
+// origin matches search: no
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(async function suggestBookmarkFalse_visit_prefix_3() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ await PlacesTestUtils.addVisits("ftp://non-matching-" + url);
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ fallbackTitle: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "ftp://non-matching-" + url,
+ title: "test visit for ftp://non-matching-" + url,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: unvisited bookmark
+// prefix search: no
+// prefix matches search: n/a
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(async function suggestBookmarkFalse_unvisitedBookmark() {
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://" + url,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: "A bookmark",
+ heuristic: true,
+ }),
+ ],
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ context = createContext(search, { isPrivate: false });
+ if (origins) {
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ } else {
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://" + search,
+ fallbackTitle: "http://" + search,
+ iconUri: `page-icon:http://${host}/`,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ }
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: unvisited bookmark
+// prefix search: yes
+// prefix matches search: yes
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(
+ async function suggestBookmarkFalse_unvisitedBookmark_prefix_0() {
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://" + url,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ let context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://" + url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: "A bookmark",
+ heuristic: true,
+ }),
+ ],
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ fallbackTitle: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+ }
+);
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: unvisited bookmark
+// prefix search: yes
+// prefix matches search: no
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(
+ async function suggestBookmarkFalse_unvisitedBookmark_prefix_1() {
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "ftp://" + url,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ fallbackTitle: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+ }
+);
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: unvisited bookmark
+// prefix search: yes
+// prefix matches search: yes
+// origin matches search: no
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(
+ async function suggestBookmarkFalse_unvisitedBookmark_prefix_2() {
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://non-matching-" + url,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ fallbackTitle: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+ }
+);
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: unvisited bookmark
+// prefix search: yes
+// prefix matches search: no
+// origin matches search: no
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(
+ async function suggestBookmarkFalse_unvisitedBookmark_prefix_3() {
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "ftp://non-matching-" + url,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ fallbackTitle: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+ }
+);
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visited bookmark above autofill threshold
+// prefix search: no
+// prefix matches search: n/a
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: yes
+add_autofill_task(async function suggestBookmarkFalse_visitedBookmark_above() {
+ await PlacesTestUtils.addVisits("http://" + url);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://" + url,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: visitTitle("http", ""),
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visited bookmark above autofill threshold
+// prefix search: yes
+// prefix matches search: yes
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: yes
+add_autofill_task(
+ async function suggestBookmarkFalse_visitedBookmarkAbove_prefix_0() {
+ await PlacesTestUtils.addVisits("http://" + url);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://" + url,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ let context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://" + url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: visitTitle("http", ""),
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanup();
+ }
+);
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visited bookmark above autofill threshold
+// prefix search: yes
+// prefix matches search: no
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(
+ async function suggestBookmarkFalse_visitedBookmarkAbove_prefix_1() {
+ await PlacesTestUtils.addVisits("ftp://" + url);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "ftp://" + url,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ fallbackTitle: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeBookmarkResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ uri: "ftp://" + url,
+ title: "A bookmark",
+ }),
+ ],
+ });
+ await cleanup();
+ }
+);
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visited bookmark above autofill threshold
+// prefix search: yes
+// prefix matches search: yes
+// origin matches search: no
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(
+ async function suggestBookmarkFalse_visitedBookmarkAbove_prefix_2() {
+ await PlacesTestUtils.addVisits("http://non-matching-" + url);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://non-matching-" + url,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ fallbackTitle: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeBookmarkResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ uri: "http://non-matching-" + url,
+ title: "A bookmark",
+ }),
+ ],
+ });
+ await cleanup();
+ }
+);
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visited bookmark above autofill threshold
+// prefix search: yes
+// prefix matches search: no
+// origin matches search: no
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(
+ async function suggestBookmarkFalse_visitedBookmarkAbove_prefix_3() {
+ await PlacesTestUtils.addVisits("ftp://non-matching-" + url);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "ftp://non-matching-" + url,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ let context = createContext("http://" + search, { isPrivate: false });
+ let prefixedUrl = origins ? `http://${search}/` : `http://${search}`;
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: prefixedUrl,
+ fallbackTitle: prefixedUrl,
+ heuristic: true,
+ iconUri: origins ? "" : `page-icon:http://${host}/`,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeBookmarkResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ uri: "ftp://non-matching-" + url,
+ title: "A bookmark",
+ }),
+ ],
+ });
+ await cleanup();
+ }
+);
+
+// The following suggestBookmarkFalse_visitedBookmarkBelow* tests are similar
+// to the suggestBookmarkFalse_visitedBookmarkAbove* tests, but instead of
+// checking visited bookmarks above the autofill threshold, they check visited
+// bookmarks below the threshold. These tests don't make sense for URL
+// queries (as opposed to origin queries) because URL queries don't use the
+// same autofill threshold, so we skip them when !origins.
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visited bookmark below autofill threshold
+// prefix search: no
+// prefix matches search: n/a
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(async function suggestBookmarkFalse_visitedBookmarkBelow() {
+ if (!origins) {
+ // See comment above suggestBookmarkFalse_visitedBookmarkBelow.
+ return;
+ }
+ // First, make sure that `url` is below the autofill threshold.
+ await PlacesTestUtils.addVisits("http://" + url);
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits("http://some-other-" + url);
+ }
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://some-other-" + url,
+ title: "test visit for http://some-other-" + url,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: "test visit for http://" + url,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ ],
+ });
+ // Now bookmark it and set suggest.bookmark to false.
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://" + url,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://some-other-" + url,
+ title: "test visit for http://some-other-" + url,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: "A bookmark",
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+});
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visited bookmark below autofill threshold
+// prefix search: yes
+// prefix matches search: yes
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(
+ async function suggestBookmarkFalse_visitedBookmarkBelow_prefix_0() {
+ if (!origins) {
+ // See comment above suggestBookmarkFalse_visitedBookmarkBelow.
+ return;
+ }
+ // First, make sure that `url` is below the autofill threshold.
+ await PlacesTestUtils.addVisits("http://" + url);
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits("http://some-other-" + url);
+ }
+ let context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${search}/`,
+ fallbackTitle: `http://${search}/`,
+ heuristic: true,
+ iconUri: "",
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://some-other-" + url,
+ title: "test visit for http://some-other-" + url,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: "test visit for http://" + url,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ ],
+ });
+ // Now bookmark it and set suggest.bookmark to false.
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://" + url,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${search}/`,
+ fallbackTitle: `http://${search}/`,
+ heuristic: true,
+ iconUri: "",
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://some-other-" + url,
+ title: "test visit for http://some-other-" + url,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: "A bookmark",
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+ }
+);
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visited bookmark below autofill threshold
+// prefix search: yes
+// prefix matches search: no
+// origin matches search: yes
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(
+ async function suggestBookmarkFalse_visitedBookmarkBelow_prefix_1() {
+ if (!origins) {
+ // See comment above suggestBookmarkFalse_visitedBookmarkBelow.
+ return;
+ }
+ // First, make sure that `url` is below the autofill threshold.
+ await PlacesTestUtils.addVisits("ftp://" + url);
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits("ftp://some-other-" + url);
+ }
+ let context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${search}/`,
+ fallbackTitle: `http://${search}/`,
+ heuristic: true,
+ iconUri: "",
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "ftp://some-other-" + url,
+ title: "test visit for ftp://some-other-" + url,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "ftp://" + url,
+ title: "test visit for ftp://" + url,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ ],
+ });
+ // Now bookmark it and set suggest.bookmark to false.
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "ftp://" + url,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${search}/`,
+ fallbackTitle: `http://${search}/`,
+ heuristic: true,
+ iconUri: "",
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "ftp://some-other-" + url,
+ title: "test visit for ftp://some-other-" + url,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "ftp://" + url,
+ title: "A bookmark",
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+ }
+);
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visited bookmark below autofill threshold
+// prefix search: yes
+// prefix matches search: yes
+// origin matches search: no
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(
+ async function suggestBookmarkFalse_visitedBookmarkBelow_prefix_2() {
+ if (!origins) {
+ // See comment above suggestBookmarkFalse_visitedBookmarkBelow.
+ return;
+ }
+ // First, make sure that `url` is below the autofill threshold.
+ await PlacesTestUtils.addVisits("http://non-matching-" + url);
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits("http://some-other-" + url);
+ }
+ let context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${search}/`,
+ fallbackTitle: `http://${search}/`,
+ heuristic: true,
+ iconUri: "",
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://some-other-" + url,
+ title: "test visit for http://some-other-" + url,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://non-matching-" + url,
+ title: "test visit for http://non-matching-" + url,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ ],
+ });
+ // Now bookmark it and set suggest.bookmark to false.
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://non-matching-" + url,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${search}/`,
+ fallbackTitle: `http://${search}/`,
+ heuristic: true,
+ iconUri: "",
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://some-other-" + url,
+ title: "test visit for http://some-other-" + url,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://non-matching-" + url,
+ title: "A bookmark",
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+ }
+);
+
+// Tests interaction between the suggest.history and suggest.bookmark prefs.
+//
+// Config:
+// suggest.history = true
+// suggest.bookmark = false
+// search for: visited bookmark below autofill threshold
+// prefix search: yes
+// prefix matches search: no
+// origin matches search: no
+//
+// Expected result:
+// should autofill: no
+add_autofill_task(
+ async function suggestBookmarkFalse_visitedBookmarkBelow_prefix_3() {
+ if (!origins) {
+ // See comment above suggestBookmarkFalse_visitedBookmarkBelow.
+ return;
+ }
+ // First, make sure that `url` is below the autofill threshold.
+ await PlacesTestUtils.addVisits("ftp://non-matching-" + url);
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits("ftp://some-other-" + url);
+ }
+ let context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${search}/`,
+ fallbackTitle: `http://${search}/`,
+ heuristic: true,
+ iconUri: "",
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "ftp://some-other-" + url,
+ title: "test visit for ftp://some-other-" + url,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "ftp://non-matching-" + url,
+ title: "test visit for ftp://non-matching-" + url,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ ],
+ });
+ // Now bookmark it and set suggest.bookmark to false.
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "ftp://non-matching-" + url,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ context = createContext("http://" + search, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${search}/`,
+ fallbackTitle: `http://${search}/`,
+ heuristic: true,
+ iconUri: "",
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "ftp://some-other-" + url,
+ title: "test visit for ftp://some-other-" + url,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "ftp://non-matching-" + url,
+ title: "A bookmark",
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanup();
+ }
+);
+
+// When the heuristic is hidden, "ex" should autofill http://example.com/, and
+// there should be an additional http://example.com/ non-autofill result.
+add_autofill_task(async function hideHeuristic() {
+ UrlbarPrefs.set("experimental.hideHeuristic", true);
+ await PlacesTestUtils.addVisits("http://" + url);
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: "http://" + url,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: visitTitle("http", ""),
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://" + url,
+ title: "test visit for http://" + url,
+ }),
+ ],
+ });
+ await cleanup();
+ UrlbarPrefs.set("experimental.hideHeuristic", false);
+});
diff --git a/browser/components/urlbar/tests/unit/test_autofill_origins_alt_frecency.js b/browser/components/urlbar/tests/unit/test_autofill_origins_alt_frecency.js
new file mode 100644
index 0000000000..872f891f63
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_origins_alt_frecency.js
@@ -0,0 +1,243 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This is a basic autofill test to ensure enabling the alternative frecency
+// algorithm doesn't break autofill or tab-to-search. A more comprehensive
+// testing of the algorithm itself is not included since it's something that
+// may change frequently according to experimentation results.
+// Other existing autofill tests will, of course, need to be adapted once an
+// algorithm is promoted to be the default.
+
+XPCOMUtils.defineLazyGetter(this, "PlacesFrecencyRecalculator", () => {
+ return Cc["@mozilla.org/places/frecency-recalculator;1"].getService(
+ Ci.nsIObserver
+ ).wrappedJSObject;
+});
+
+testEngine_setup();
+
+add_task(
+ {
+ pref_set: [["browser.urlbar.suggest.quickactions", false]],
+ },
+ async function test_autofill() {
+ const origin = "example.com";
+ let context = createContext(origin.substring(0, 2), { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: "Suggestions",
+ heuristic: true,
+ }),
+ ],
+ });
+ // Add many visits.
+ const url = `https://${origin}/`;
+ await PlacesTestUtils.addVisits(new Array(10).fill(url));
+ Assert.equal(
+ await PlacesUtils.metadata.get("origin_alt_frecency_threshold", 0),
+ 0,
+ "Check there's no threshold initially"
+ );
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ Assert.greater(
+ await PlacesUtils.metadata.get("origin_alt_frecency_threshold", 0),
+ 0,
+ "Check a threshold has been calculated"
+ );
+ await check_results({
+ context,
+ autofilled: `${origin}/`,
+ completed: url,
+ matches: [
+ makeVisitResult(context, {
+ uri: url,
+ title: `test visit for ${url}`,
+ heuristic: true,
+ }),
+ ],
+ });
+ await PlacesUtils.history.clear();
+ }
+);
+
+add_task(
+ {
+ pref_set: [["browser.urlbar.suggest.quickactions", false]],
+ },
+ async function test_autofill_www() {
+ const origin = "example.com";
+ // Add many visits.
+ const url = `https://www.${origin}/`;
+ await PlacesTestUtils.addVisits(new Array(10).fill(url));
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ let context = createContext(origin.substring(0, 2), { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: `${origin}/`,
+ completed: url,
+ matches: [
+ makeVisitResult(context, {
+ uri: url,
+ title: `test visit for ${url}`,
+ heuristic: true,
+ }),
+ ],
+ });
+ await PlacesUtils.history.clear();
+ }
+);
+
+add_task(
+ {
+ pref_set: [
+ ["browser.urlbar.suggest.quickactions", false],
+ ["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0],
+ ],
+ },
+ async function test_autofill_prefix_priority() {
+ const origin = "localhost";
+ const url = `https://${origin}/`;
+ await PlacesTestUtils.addVisits([url, `http://${origin}/`]);
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ let engine = Services.search.defaultEngine;
+ let context = createContext(origin.substring(0, 2), { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: `${origin}/`,
+ completed: url,
+ matches: [
+ makeVisitResult(context, {
+ uri: url,
+ title: `test visit for ${url}`,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: engine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(engine.searchUrlDomain),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ ],
+ });
+ await PlacesUtils.history.clear();
+ }
+);
+
+add_task(
+ {
+ pref_set: [["browser.urlbar.suggest.quickactions", false]],
+ },
+ async function test_autofill_threshold() {
+ async function getOriginAltFrecency(origin) {
+ let db = await PlacesUtils.promiseDBConnection();
+ let rows = await db.execute(
+ "SELECT alt_frecency FROM moz_origins WHERE host = :origin",
+ { origin }
+ );
+ return rows?.[0].getResultByName("alt_frecency");
+ }
+ async function getThreshold() {
+ let db = await PlacesUtils.promiseDBConnection();
+ let rows = await db.execute("SELECT avg(alt_frecency) FROM moz_origins");
+ return rows[0].getResultByIndex(0);
+ }
+
+ await PlacesTestUtils.addVisits(new Array(10).fill("https://example.com/"));
+ // Add more visits to the same origins to differenciate the frecency scores.
+ await PlacesTestUtils.addVisits([
+ "https://example.com/2",
+ "https://example.com/3",
+ ]);
+ await PlacesTestUtils.addVisits("https://somethingelse.org/");
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ let threshold = await PlacesUtils.metadata.get(
+ "origin_alt_frecency_threshold",
+ 0
+ );
+ Assert.greater(
+ threshold,
+ await getOriginAltFrecency("somethingelse.org"),
+ "Check mozilla.org has a lower frecency than the threshold"
+ );
+ Assert.equal(
+ threshold,
+ await getThreshold(),
+ "Check the threshold has been calculared correctly"
+ );
+
+ let engine = Services.search.defaultEngine;
+ let context = createContext("so", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ heuristic: true,
+ query: "so",
+ engineName: engine.name,
+ }),
+ makeVisitResult(context, {
+ uri: "https://somethingelse.org/",
+ title: "test visit for https://somethingelse.org/",
+ }),
+ ],
+ });
+ await PlacesUtils.history.clear();
+ }
+);
+
+add_task(
+ {
+ pref_set: [["browser.urlbar.suggest.quickactions", false]],
+ },
+ async function test_autofill_cutoff() {
+ async function getOriginAltFrecency(origin) {
+ let db = await PlacesUtils.promiseDBConnection();
+ let rows = await db.execute(
+ "SELECT alt_frecency FROM moz_origins WHERE host = :origin",
+ { origin }
+ );
+ return rows?.[0].getResultByName("alt_frecency");
+ }
+
+ // Add many visits older than the default 90 days cutoff.
+ const visitDate = new Date(Date.now() - 120 * 86400000);
+ await PlacesTestUtils.addVisits(
+ new Array(10)
+ .fill("https://example.com/")
+ .map(url => ({ url, visitDate }))
+ );
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ Assert.strictEqual(
+ await getOriginAltFrecency("example.com"),
+ null,
+ "Check example.com has a NULL frecency"
+ );
+
+ let engine = Services.search.defaultEngine;
+ let context = createContext("ex", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ heuristic: true,
+ query: "ex",
+ engineName: engine.name,
+ }),
+ makeVisitResult(context, {
+ uri: "https://example.com/",
+ title: "test visit for https://example.com/",
+ }),
+ ],
+ });
+ await PlacesUtils.history.clear();
+ }
+);
diff --git a/browser/components/urlbar/tests/unit/test_autofill_prefix_fallback.js b/browser/components/urlbar/tests/unit/test_autofill_prefix_fallback.js
new file mode 100644
index 0000000000..dfb5de2149
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_prefix_fallback.js
@@ -0,0 +1,76 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This tests autofill prefix fallback in case multiple origins have the same
+// exact frecency.
+// We should prefer https, or in case of other prefixes just sort by descending
+// id.
+
+add_task(async function () {
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions");
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false);
+
+ let host = "example.com";
+ let prefixes = ["https://", "https://www.", "http://", "http://www."];
+ for (let prefix of prefixes) {
+ await PlacesUtils.bookmarks.insert({
+ url: `${prefix}${host}`,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ }
+ await checkOriginsOrder(host, prefixes);
+
+ // The https://www version should be filled because it's https and the www
+ // version has been added later so it has an higher id.
+ let context = createContext("ex", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: `${host}/`,
+ completed: `https://www.${host}/`,
+ matches: [
+ makeVisitResult(context, {
+ uri: `https://www.${host}/`,
+ fallbackTitle: `https://www.${host}`,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: `https://${host}/`,
+ title: `${host}`,
+ }),
+ ],
+ });
+
+ // Remove and reinsert bookmarks in another order.
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+ prefixes = ["https://www.", "http://", "https://", "http://www."];
+ for (let prefix of prefixes) {
+ await PlacesUtils.bookmarks.insert({
+ url: `${prefix}${host}`,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ }
+ await checkOriginsOrder(host, prefixes);
+
+ await check_results({
+ context,
+ autofilled: `${host}/`,
+ completed: `https://${host}/`,
+ matches: [
+ makeVisitResult(context, {
+ uri: `https://${host}/`,
+ fallbackTitle: `https://${host}`,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: `https://www.${host}/`,
+ title: `www.${host}`,
+ }),
+ ],
+ });
+});
diff --git a/browser/components/urlbar/tests/unit/test_autofill_search_engine_aliases.js b/browser/components/urlbar/tests/unit/test_autofill_search_engine_aliases.js
new file mode 100644
index 0000000000..018a2e1681
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_search_engine_aliases.js
@@ -0,0 +1,85 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Tests autofilling search engine token ("@") aliases.
+
+"use strict";
+
+const TEST_ENGINE_NAME = "test autofill aliases";
+const TEST_ENGINE_ALIAS = "@autofilltest";
+
+add_task(async function init() {
+ // Add an engine with an "@" alias.
+ await SearchTestUtils.installSearchExtension({
+ name: TEST_ENGINE_NAME,
+ keyword: TEST_ENGINE_ALIAS,
+ });
+});
+
+// Searching for @autofi should autofill to @autofilltest.
+add_task(async function basic() {
+ // Add a history visit that should normally match but for the fact that the
+ // search uses an @ alias. When an @ alias is autofilled, there should be no
+ // other matches except the autofill heuristic match.
+ await PlacesTestUtils.addVisits({
+ uri: "http://example.com/",
+ title: TEST_ENGINE_ALIAS,
+ });
+
+ let search = TEST_ENGINE_ALIAS.substr(
+ 0,
+ Math.round(TEST_ENGINE_ALIAS.length / 2)
+ );
+ let autofilledValue = TEST_ENGINE_ALIAS + " ";
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: autofilledValue,
+ matches: [
+ makeSearchResult(context, {
+ engineName: TEST_ENGINE_NAME,
+ alias: TEST_ENGINE_ALIAS,
+ query: "",
+ providesSearchMode: true,
+ heuristic: false,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+// Searching for @AUTOFI should autofill to @AUTOFIlltest, preserving the case
+// in the search string.
+add_task(async function preserveCase() {
+ // Add a history visit that should normally match but for the fact that the
+ // search uses an @ alias. When an @ alias is autofilled, there should be no
+ // other matches except the autofill heuristic match.
+ await PlacesTestUtils.addVisits({
+ uri: "http://example.com/",
+ title: TEST_ENGINE_ALIAS,
+ });
+
+ let search = TEST_ENGINE_ALIAS.toUpperCase().substr(
+ 0,
+ Math.round(TEST_ENGINE_ALIAS.length / 2)
+ );
+ let alias = search + TEST_ENGINE_ALIAS.substr(search.length);
+
+ let autofilledValue = alias + " ";
+ let context = createContext(search, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: autofilledValue,
+ matches: [
+ makeSearchResult(context, {
+ engineName: TEST_ENGINE_NAME,
+ alias,
+ query: "",
+ providesSearchMode: true,
+ heuristic: false,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_autofill_search_engines.js b/browser/components/urlbar/tests/unit/test_autofill_search_engines.js
new file mode 100644
index 0000000000..7b836a7c4d
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_search_engines.js
@@ -0,0 +1,246 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// The autoFill.searchEngines pref autofills the domains of engines registered
+// with the search service. That's what this test checks. It's a different
+// path in ProviderAutofill from normal moz_places autofill, which is tested
+// in test_autofill_origins.js and test_autofill_urls.js.
+
+"use strict";
+
+const ENGINE_NAME = "engine.xml";
+
+add_task(async function searchEngines() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ Services.prefs.setBoolPref(
+ "browser.search.separatePrivateDefault.ui.enabled",
+ false
+ );
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.autoFill.searchEngines");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ Services.prefs.clearUserPref(
+ "browser.search.separatePrivateDefault.ui.enabled"
+ );
+ });
+
+ // Bug 1149672: Once we drop support for http with OpenSearch engines,
+ // we should be able to drop the http part of this.
+ for (let scheme of ["https", "http"]) {
+ let extension;
+ if (scheme == "https") {
+ extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: ENGINE_NAME,
+ search_url: "https://www.example.com/",
+ },
+ { skipUnload: true }
+ );
+ } else {
+ let httpServer = makeTestServer();
+ httpServer.registerDirectory("/", do_get_cwd());
+ await Services.search.addOpenSearchEngine(
+ `http://localhost:${httpServer.identity.primaryPort}/data/engine.xml`,
+ null
+ );
+ }
+
+ let context = createContext("ex", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("example.com", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("example.com/", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("www.ex", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "www.example.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("www.example.com", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "www.example.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("www.example.com/", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "www.example.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext(scheme + "://ex", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: scheme + "://example.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext(scheme + "://example.com", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: scheme + "://example.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext(scheme + "://example.com/", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: scheme + "://example.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext(scheme + "://www.ex", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: scheme + "://www.example.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext(scheme + "://www.example.com", {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ autofilled: scheme + "://www.example.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext(scheme + "://www.example.com/", {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ search: scheme + "://www.example.com/",
+ autofilled: scheme + "://www.example.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // We should just get a normal heuristic result from HeuristicFallback for
+ // these queries.
+ let otherScheme = scheme == "http" ? "https" : "http";
+ context = createContext(otherScheme + "://ex", { isPrivate: false });
+ await check_results({
+ context,
+ search: otherScheme + "://ex",
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: otherScheme + "://ex/",
+ fallbackTitle: otherScheme + "://ex/",
+ heuristic: true,
+ }),
+ ],
+ });
+ context = createContext(otherScheme + "://www.ex", { isPrivate: false });
+ await check_results({
+ context,
+ search: otherScheme + "://www.ex",
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: otherScheme + "://www.ex/",
+ fallbackTitle: otherScheme + "://www.ex/",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("example/", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://example/",
+ fallbackTitle: "http://example/",
+ iconUri: "page-icon:http://example/",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await extension?.unload();
+ }
+});
diff --git a/browser/components/urlbar/tests/unit/test_autofill_urls.js b/browser/components/urlbar/tests/unit/test_autofill_urls.js
new file mode 100644
index 0000000000..c816736531
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_autofill_urls.js
@@ -0,0 +1,881 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const HEURISTIC_FALLBACK_PROVIDERNAME = "HeuristicFallback";
+const PLACES_PROVIDERNAME = "Places";
+
+// "example.com/foo/" should match http://example.com/foo/.
+testEngine_setup();
+
+registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions");
+});
+Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false);
+
+add_task(async function multipleSlashes() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com/foo/",
+ },
+ ]);
+ let context = createContext("example.com/foo/", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/foo/",
+ completed: "http://example.com/foo/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://example.com/foo/",
+ title: "test visit for http://example.com/foo/",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+// "example.com:8888/f" should match http://example.com:8888/foo.
+add_task(async function port() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com:8888/foo",
+ },
+ ]);
+ let context = createContext("example.com:8888/f", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com:8888/foo",
+ completed: "http://example.com:8888/foo",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://example.com:8888/foo",
+ title: "test visit for http://example.com:8888/foo",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+// "example.com:8999/f" should *not* autofill http://example.com:8888/foo.
+add_task(async function portNoMatch() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com:8888/foo",
+ },
+ ]);
+ let context = createContext("example.com:8999/f", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://example.com:8999/f",
+ fallbackTitle: "http://example.com:8999/f",
+ iconUri: "page-icon:http://example.com:8999/",
+ heuristic: true,
+ providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+// autofill to the next slash
+add_task(async function port() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com:8888/foo/bar/baz",
+ },
+ ]);
+ let context = createContext("example.com:8888/foo/b", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com:8888/foo/bar/",
+ completed: "http://example.com:8888/foo/bar/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://example.com:8888/foo/bar/",
+ fallbackTitle: "example.com:8888/foo/bar/",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com:8888/foo/bar/baz",
+ title: "test visit for http://example.com:8888/foo/bar/baz",
+ tags: [],
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+// autofill to the next slash, end of url
+add_task(async function port() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com:8888/foo/bar/baz",
+ },
+ ]);
+ let context = createContext("example.com:8888/foo/bar/b", {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ autofilled: "example.com:8888/foo/bar/baz",
+ completed: "http://example.com:8888/foo/bar/baz",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://example.com:8888/foo/bar/baz",
+ title: "test visit for http://example.com:8888/foo/bar/baz",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+// autofill with case insensitive from history and bookmark.
+add_task(async function caseInsensitiveFromHistoryAndBookmark() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", true);
+
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com/foo",
+ },
+ ]);
+
+ await testCaseInsensitive();
+
+ Services.prefs.clearUserPref("browser.urlbar.suggest.bookmark");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.history");
+ await cleanupPlaces();
+});
+
+// autofill with case insensitive from history.
+add_task(async function caseInsensitiveFromHistory() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", true);
+
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com/foo",
+ },
+ ]);
+
+ await testCaseInsensitive();
+
+ Services.prefs.clearUserPref("browser.urlbar.suggest.bookmark");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.history");
+ await cleanupPlaces();
+});
+
+// autofill with case insensitive from bookmark.
+add_task(async function caseInsensitiveFromBookmark() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://example.com/foo",
+ });
+
+ await testCaseInsensitive(true);
+
+ Services.prefs.clearUserPref("browser.urlbar.suggest.bookmark");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.history");
+ await cleanupPlaces();
+});
+
+// should *not* autofill if the URI fragment does not match with case-sensitive.
+add_task(async function uriFragmentCaseSensitive() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com/#TEST",
+ },
+ ]);
+ const context = createContext("http://example.com/#t", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://example.com/#t",
+ fallbackTitle: "http://example.com/#t",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ uri: "http://example.com/#TEST",
+ title: "test visit for http://example.com/#TEST",
+ tags: [],
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+// should autofill if the URI fragment matches with case-sensitive.
+add_task(async function uriFragmentCaseSensitive() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com/#TEST",
+ },
+ ]);
+ const context = createContext("http://example.com/#T", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://example.com/#TEST",
+ completed: "http://example.com/#TEST",
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ uri: "http://example.com/#TEST",
+ title: "test visit for http://example.com/#TEST",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function uriCase() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com/ABC/DEF",
+ },
+ ]);
+
+ const testData = [
+ {
+ input: "example.COM",
+ expected: {
+ autofilled: "example.COM/",
+ completed: "http://example.com/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ fallbackTitle: "example.com",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/DEF",
+ title: "test visit for http://example.com/ABC/DEF",
+ }),
+ ],
+ },
+ },
+ {
+ input: "example.COM/",
+ expected: {
+ autofilled: "example.COM/",
+ completed: "http://example.com/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ fallbackTitle: "example.com/",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/DEF",
+ title: "test visit for http://example.com/ABC/DEF",
+ }),
+ ],
+ },
+ },
+ {
+ input: "example.COM/a",
+ expected: {
+ autofilled: "example.COM/aBC/",
+ completed: "http://example.com/ABC/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/",
+ fallbackTitle: "example.com/ABC/",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/DEF",
+ title: "test visit for http://example.com/ABC/DEF",
+ }),
+ ],
+ },
+ },
+ {
+ input: "example.com/ab",
+ expected: {
+ autofilled: "example.com/abC/",
+ completed: "http://example.com/ABC/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/",
+ fallbackTitle: "example.com/ABC/",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/DEF",
+ title: "test visit for http://example.com/ABC/DEF",
+ }),
+ ],
+ },
+ },
+ {
+ input: "example.com/abc",
+ expected: {
+ autofilled: "example.com/abc/",
+ completed: "http://example.com/ABC/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/",
+ fallbackTitle: "example.com/ABC/",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/DEF",
+ title: "test visit for http://example.com/ABC/DEF",
+ }),
+ ],
+ },
+ },
+ {
+ input: "example.com/abc/",
+ expected: {
+ autofilled: "example.com/abc/",
+ completed: "http://example.com/abc/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/abc/",
+ fallbackTitle: "example.com/abc/",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/DEF",
+ title: "test visit for http://example.com/ABC/DEF",
+ }),
+ ],
+ },
+ },
+ {
+ input: "example.com/abc/d",
+ expected: {
+ autofilled: "example.com/abc/dEF",
+ completed: "http://example.com/ABC/DEF",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/DEF",
+ title: "test visit for http://example.com/ABC/DEF",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ input: "example.com/abc/de",
+ expected: {
+ autofilled: "example.com/abc/deF",
+ completed: "http://example.com/ABC/DEF",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/DEF",
+ title: "test visit for http://example.com/ABC/DEF",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ input: "example.com/abc/def",
+ expected: {
+ autofilled: "example.com/abc/def",
+ completed: "http://example.com/abc/def",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/abc/def",
+ fallbackTitle: "example.com/abc/def",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/DEF",
+ title: "test visit for http://example.com/ABC/DEF",
+ }),
+ ],
+ },
+ },
+ {
+ input: "http://example.com/a",
+ expected: {
+ autofilled: "http://example.com/aBC/",
+ completed: "http://example.com/ABC/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/",
+ fallbackTitle: "example.com/ABC/",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/DEF",
+ title: "test visit for http://example.com/ABC/DEF",
+ }),
+ ],
+ },
+ },
+ {
+ input: "http://example.com/abc/",
+ expected: {
+ autofilled: "http://example.com/abc/",
+ completed: "http://example.com/abc/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/abc/",
+ fallbackTitle: "example.com/abc/",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/DEF",
+ title: "test visit for http://example.com/ABC/DEF",
+ }),
+ ],
+ },
+ },
+ {
+ input: "http://example.com/abc/d",
+ expected: {
+ autofilled: "http://example.com/abc/dEF",
+ completed: "http://example.com/ABC/DEF",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/DEF",
+ title: "test visit for http://example.com/ABC/DEF",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ input: "http://example.com/abc/def",
+ expected: {
+ autofilled: "http://example.com/abc/def",
+ completed: "http://example.com/abc/def",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/abc/def",
+ fallbackTitle: "example.com/abc/def",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/DEF",
+ title: "test visit for http://example.com/ABC/DEF",
+ }),
+ ],
+ },
+ },
+ {
+ input: "http://eXAMple.com/ABC/DEF",
+ expected: {
+ autofilled: "http://eXAMple.com/ABC/DEF",
+ completed: "http://example.com/ABC/DEF",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/DEF",
+ title: "test visit for http://example.com/ABC/DEF",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ input: "http://eXAMple.com/abc/def",
+ expected: {
+ autofilled: "http://eXAMple.com/abc/def",
+ completed: "http://example.com/abc/def",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/abc/def",
+ fallbackTitle: "example.com/abc/def",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "http://example.com/ABC/DEF",
+ title: "test visit for http://example.com/ABC/DEF",
+ }),
+ ],
+ },
+ },
+ ];
+
+ for (const { input, expected } of testData) {
+ const context = createContext(input, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ autofilled: expected.autofilled,
+ completed: expected.completed,
+ matches: expected.results.map(f => f(context)),
+ });
+ }
+
+ await cleanupPlaces();
+});
+
+async function testCaseInsensitive(isBookmark = false) {
+ const testData = [
+ {
+ input: "example.com/F",
+ expectedAutofill: "example.com/Foo",
+ },
+ {
+ // Test with prefix.
+ input: "http://example.com/F",
+ expectedAutofill: "http://example.com/Foo",
+ },
+ ];
+
+ for (const { input, expectedAutofill } of testData) {
+ const context = createContext(input, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ autofilled: expectedAutofill,
+ completed: "http://example.com/foo",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://example.com/foo",
+ title: isBookmark
+ ? "A bookmark"
+ : "test visit for http://example.com/foo",
+ heuristic: true,
+ }),
+ ],
+ });
+ }
+
+ await cleanupPlaces();
+}
+
+// Checks a URL with an origin that looks like a prefix: a scheme with no dots +
+// a port.
+add_task(async function originLooksLikePrefix1() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://localhost:8888/foo",
+ },
+ ]);
+ const context = createContext("localhost:8888/f", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "localhost:8888/foo",
+ completed: "http://localhost:8888/foo",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://localhost:8888/foo",
+ title: "test visit for http://localhost:8888/foo",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+// Same as previous (originLooksLikePrefix1) but uses a URL whose path has two
+// slashes, not one.
+add_task(async function originLooksLikePrefix2() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://localhost:8888/foo/bar",
+ },
+ ]);
+
+ let context = createContext("localhost:8888/f", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "localhost:8888/foo/",
+ completed: "http://localhost:8888/foo/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://localhost:8888/foo/",
+ fallbackTitle: "localhost:8888/foo/",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://localhost:8888/foo/bar",
+ title: "test visit for http://localhost:8888/foo/bar",
+ providerName: PLACES_PROVIDERNAME,
+ tags: [],
+ }),
+ ],
+ });
+
+ context = createContext("localhost:8888/foo/b", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "localhost:8888/foo/bar",
+ completed: "http://localhost:8888/foo/bar",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://localhost:8888/foo/bar",
+ title: "test visit for http://localhost:8888/foo/bar",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+// Checks view-source pages as a prefix
+// Uses bookmark because addVisits does not allow non-http uri's
+add_task(async function viewSourceAsPrefix() {
+ let address = "view-source:https://www.example.com/";
+ let title = "A view source bookmark";
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: address,
+ title,
+ });
+
+ let testData = [
+ {
+ input: "view-source:h",
+ completed: "view-source:https:/",
+ autofilled: "view-source:https:/",
+ },
+ {
+ input: "view-source:http",
+ completed: "view-source:https:/",
+ autofilled: "view-source:https:/",
+ },
+ {
+ input: "VIEW-SOURCE:http",
+ completed: "view-source:https:/",
+ autofilled: "VIEW-SOURCE:https:/",
+ },
+ ];
+
+ // Only autofills from view-source:h to view-source:https:/
+ for (let { input, completed, autofilled } of testData) {
+ let context = createContext(input, { isPrivate: false });
+ await check_results({
+ context,
+ completed,
+ autofilled,
+ matches: [
+ {
+ heuristic: true,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ },
+ makeBookmarkResult(context, {
+ uri: address,
+ iconUri: "chrome://global/skin/icons/defaultFavicon.svg",
+ title,
+ }),
+ ],
+ });
+ }
+
+ await cleanupPlaces();
+});
+
+// Checks data url prefixes
+// Uses bookmark because addVisits does not allow non-http uri's
+add_task(async function dataAsPrefix() {
+ let address = "data:text/html,%3Ch1%3EHello%2C World!%3C%2Fh1%3E";
+ let title = "A data url bookmark";
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: address,
+ title,
+ });
+
+ let testData = [
+ {
+ input: "data:t",
+ completed: "data:text/",
+ autofilled: "data:text/",
+ },
+ {
+ input: "data:text",
+ completed: "data:text/",
+ autofilled: "data:text/",
+ },
+ {
+ input: "DATA:text",
+ completed: "data:text/",
+ autofilled: "DATA:text/",
+ },
+ ];
+
+ for (let { input, completed, autofilled } of testData) {
+ let context = createContext(input, { isPrivate: false });
+ await check_results({
+ context,
+ completed,
+ autofilled,
+ matches: [
+ {
+ heuristic: true,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ },
+ makeBookmarkResult(context, {
+ uri: address,
+ iconUri: "chrome://global/skin/icons/defaultFavicon.svg",
+ title,
+ }),
+ ],
+ });
+ }
+
+ await cleanupPlaces();
+});
+
+// Checks about prefixes
+add_task(async function aboutAsPrefix() {
+ let testData = [
+ {
+ input: "about:abou",
+ completed: "about:about",
+ autofilled: "about:about",
+ },
+ {
+ input: "ABOUT:abou",
+ completed: "about:about",
+ autofilled: "ABOUT:about",
+ },
+ ];
+
+ for (let { input, completed, autofilled } of testData) {
+ let context = createContext(input, { isPrivate: false });
+ await check_results({
+ context,
+ completed,
+ autofilled,
+ matches: [
+ {
+ heuristic: true,
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ },
+ ],
+ });
+ }
+
+ await cleanupPlaces();
+});
+
+// Checks a URL that has www name in history.
+add_task(async function wwwHistory() {
+ const testData = [
+ {
+ input: "example.com/",
+ visitHistory: [{ uri: "http://www.example.com/", title: "Example" }],
+ expected: {
+ autofilled: "example.com/",
+ completed: "http://www.example.com/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "http://www.example.com/",
+ title: "Example",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ input: "https://example.com/",
+ visitHistory: [{ uri: "https://www.example.com/", title: "Example" }],
+ expected: {
+ autofilled: "https://example.com/",
+ completed: "https://www.example.com/",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "https://www.example.com/",
+ title: "Example",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ input: "https://example.com/abc",
+ visitHistory: [{ uri: "https://www.example.com/abc", title: "Example" }],
+ expected: {
+ autofilled: "https://example.com/abc",
+ completed: "https://www.example.com/abc",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "https://www.example.com/abc",
+ title: "Example",
+ heuristic: true,
+ }),
+ ],
+ },
+ },
+ {
+ input: "https://example.com/ABC",
+ visitHistory: [{ uri: "https://www.example.com/abc", title: "Example" }],
+ expected: {
+ autofilled: "https://example.com/ABC",
+ completed: "https://www.example.com/ABC",
+ results: [
+ context =>
+ makeVisitResult(context, {
+ uri: "https://www.example.com/ABC",
+ fallbackTitle: "https://www.example.com/ABC",
+ heuristic: true,
+ }),
+ context =>
+ makeVisitResult(context, {
+ uri: "https://www.example.com/abc",
+ title: "Example",
+ }),
+ ],
+ },
+ },
+ ];
+
+ for (const { input, visitHistory, expected } of testData) {
+ await PlacesTestUtils.addVisits(visitHistory);
+ const context = createContext(input, { isPrivate: false });
+ await check_results({
+ context,
+ completed: expected.completed,
+ autofilled: expected.autofilled,
+ matches: expected.results.map(f => f(context)),
+ });
+ await cleanupPlaces();
+ }
+});
diff --git a/browser/components/urlbar/tests/unit/test_avoid_middle_complete.js b/browser/components/urlbar/tests/unit/test_avoid_middle_complete.js
new file mode 100644
index 0000000000..5e2dba3446
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_avoid_middle_complete.js
@@ -0,0 +1,270 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+testEngine_setup();
+
+add_task(async function test_prefix_space_noautofill() {
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://moz.org/test/"),
+ });
+
+ info("Should not try to autoFill if search string contains a space");
+ let context = createContext(" mo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ query: " mo",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://moz.org/test/",
+ title: "test visit for http://moz.org/test/",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_trailing_space_noautofill() {
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://moz.org/test/"),
+ });
+
+ info("Should not try to autoFill if search string contains a space");
+ let context = createContext("mo ", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ query: "mo ",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://moz.org/test/",
+ title: "test visit for http://moz.org/test/",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_searchEngine_autofill() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
+ await SearchTestUtils.installSearchExtension({
+ name: "CakeSearch",
+ search_url: "https://cake.search",
+ });
+
+ info(
+ "Should autoFill search engine if search string does not contains a space"
+ );
+ let context = createContext("ca", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: "CakeSearch",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_searchEngine_prefix_space_noautofill() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
+ await SearchTestUtils.installSearchExtension({
+ name: "CupcakeSearch",
+ search_url: "https://cupcake.search",
+ });
+
+ info(
+ "Should not try to autoFill search engine if search string contains a space"
+ );
+ let context = createContext(" cu", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ query: " cu",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_searchEngine_trailing_space_noautofill() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
+ await SearchTestUtils.installSearchExtension({
+ name: "BaconSearch",
+ search_url: "https://bacon.search",
+ });
+
+ info(
+ "Should not try to autoFill search engine if search string contains a space"
+ );
+ let context = createContext("ba ", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ query: "ba ",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_searchEngine_www_noautofill() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
+ await SearchTestUtils.installSearchExtension({
+ name: "HamSearch",
+ search_url: "https://ham.search",
+ });
+
+ info(
+ "Should not autoFill search engine if search string contains www. but engine doesn't"
+ );
+ let context = createContext("www.ham", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://www.ham/",
+ fallbackTitle: "http://www.ham/",
+ displayUrl: "http://www.ham",
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ query: "www.ham",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_searchEngine_different_scheme_noautofill() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
+ await SearchTestUtils.installSearchExtension({
+ name: "PieSearch",
+ search_url: "https://pie.search",
+ });
+
+ info(
+ "Should not autoFill search engine if search string has a different scheme."
+ );
+ let context = createContext("http://pie", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://pie/",
+ fallbackTitle: "http://pie/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_searchEngine_matching_prefix_autofill() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
+ await SearchTestUtils.installSearchExtension({
+ name: "BeanSearch",
+ search_url: "https://www.bean.search",
+ });
+
+ info("Should autoFill search engine if search string has matching prefix.");
+ let context = createContext("https://www.be", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "https://www.bean.search/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: "BeanSearch",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Should autoFill search engine if search string has www prefix.");
+ context = createContext("www.be", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "www.bean.search/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: "BeanSearch",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Should autoFill search engine if search string has matching scheme.");
+ context = createContext("https://be", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "https://bean.search/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: "BeanSearch",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_prefix_autofill() {
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ });
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://moz.org/test/"),
+ });
+
+ info(
+ "Should not try to autoFill in-the-middle if a search is canceled immediately"
+ );
+ let context = createContext("mozi", { isPrivate: false });
+ await check_results({
+ context,
+ incompleteSearch: "moz",
+ autofilled: "mozilla.org/",
+ completed: "http://mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/",
+ fallbackTitle: "mozilla.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/test/",
+ title: "test visit for http://mozilla.org/test/",
+ providerName: "Places",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_avoid_stripping_to_empty_tokens.js b/browser/components/urlbar/tests/unit/test_avoid_stripping_to_empty_tokens.js
new file mode 100644
index 0000000000..b4aa7e50bd
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_avoid_stripping_to_empty_tokens.js
@@ -0,0 +1,119 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+testEngine_setup();
+
+add_task(async function test_protocol_trimming() {
+ for (let prot of ["http", "https"]) {
+ let visit = {
+ // Include the protocol in the query string to ensure we get matches (see bug 1059395)
+ uri: Services.io.newURI(
+ prot +
+ "://www.mozilla.org/test/?q=" +
+ prot +
+ encodeURIComponent("://") +
+ "www.foo"
+ ),
+ title: "Test title",
+ };
+ await PlacesTestUtils.addVisits(visit);
+
+ let input = prot + "://www.";
+ info("Searching for: " + input);
+ let context = createContext(input, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: prot + "://www.mozilla.org/",
+ completed: prot + "://www.mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: prot + "://www.mozilla.org/",
+ fallbackTitle:
+ prot == "http" ? "www.mozilla.org" : prot + "://www.mozilla.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: visit.uri.spec,
+ title: visit.title,
+ }),
+ ],
+ });
+
+ input = "www.";
+ info("Searching for: " + input);
+ context = createContext(input, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "www.mozilla.org/",
+ completed: prot + "://www.mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: prot + "://www.mozilla.org/",
+ fallbackTitle:
+ prot == "http" ? "www.mozilla.org" : prot + "://www.mozilla.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: visit.uri.spec,
+ title: visit.title,
+ }),
+ ],
+ });
+
+ input = prot + "://www. ";
+ info("Searching for: " + input);
+ context = createContext(input, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `${input.trim()}/`,
+ fallbackTitle: `${input.trim()}/`,
+ iconUri: "",
+ heuristic: true,
+ providerName: "HeuristicFallback",
+ }),
+ makeVisitResult(context, {
+ uri: visit.uri.spec,
+ title: visit.title,
+ providerName: "Places",
+ }),
+ ],
+ });
+
+ let inputs = [
+ prot + "://",
+ prot + ":// ",
+ prot + ":// mo",
+ prot + "://mo te",
+ prot + "://www. mo",
+ prot + "://www.mo te",
+ "www. ",
+ "www. mo",
+ "www.mo te",
+ ];
+ for (input of inputs) {
+ info("Searching for: " + input);
+ context = createContext(input, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ query: input,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: visit.uri.spec,
+ title: visit.title,
+ providerName: "Places",
+ }),
+ ],
+ });
+ }
+
+ await cleanupPlaces();
+ }
+});
diff --git a/browser/components/urlbar/tests/unit/test_calculator.js b/browser/components/urlbar/tests/unit/test_calculator.js
new file mode 100644
index 0000000000..7fa899f320
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_calculator.js
@@ -0,0 +1,46 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ Calculator: "resource:///modules/UrlbarProviderCalculator.sys.mjs",
+});
+
+const FORMULAS = [
+ ["1+1", 2],
+ ["3+4*2/(1-5)", 1],
+ ["39+4*2/(1-5)", 37],
+ ["(39+4)*2/(1-5)", -21.5],
+ ["4+-5", -1],
+ ["-5*6", -30],
+ ["-5.5*6", -33],
+ ["-5.5*-6.4", 35.2],
+ ["-6-6-6", -18],
+ ["6-6-6", -6],
+ [".001 /2", 0.0005],
+ ["(0-.001)/2", -0.0005],
+ ["-.001/(0-2)", 0.0005],
+ ["1000000000000000000000000+1", 1e24],
+ ["1000000000000000000000000-1", 1e24],
+ ["1e+30+10", 1e30],
+ ["1e+30*10", 1e31],
+ ["1e+30/100", 1e28],
+ ["10/1000000000000000000000000", 1e-23],
+ ["10/-1000000000000000000000000", -1e-23],
+ ["1,500.5+2.5", 1503], // Ignore commas when using decimal seperators
+ ["1,5+2,5", 4], // Support comma seperators
+ ["1.500,5+2,5", 1503], // Ignore periods when using comma decimal seperators
+];
+
+add_task(function test() {
+ for (let [formula, result] of FORMULAS) {
+ let postfix = Calculator.infix2postfix(formula);
+ Assert.equal(
+ Calculator.evaluatePostfix(postfix),
+ result,
+ `${formula} should equal ${result}`
+ );
+ }
+});
diff --git a/browser/components/urlbar/tests/unit/test_casing.js b/browser/components/urlbar/tests/unit/test_casing.js
new file mode 100644
index 0000000000..91d8207c05
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_casing.js
@@ -0,0 +1,370 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const AUTOFILL_PROVIDERNAME = "Autofill";
+const PLACES_PROVIDERNAME = "Places";
+
+testEngine_setup();
+
+add_task(async function test_casing_1() {
+ info("Searching for cased entry 1");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ });
+ let context = createContext("MOZ", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "MOZilla.org/",
+ completed: "http://mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/",
+ fallbackTitle: "mozilla.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/test/",
+ title: "test visit for http://mozilla.org/test/",
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_casing_2() {
+ info("Searching for cased entry 2");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ });
+ let context = createContext("mozilla.org/T", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.org/Test/",
+ completed: "http://mozilla.org/test/",
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ uri: "http://mozilla.org/test/",
+ title: "test visit for http://mozilla.org/test/",
+ iconUri: "page-icon:http://mozilla.org/test/",
+ heuristic: true,
+ providerName: AUTOFILL_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_casing_3() {
+ info("Searching for cased entry 3");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/Test/"),
+ });
+ let context = createContext("mozilla.org/T", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.org/Test/",
+ completed: "http://mozilla.org/Test/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/Test/",
+ title: "test visit for http://mozilla.org/Test/",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_casing_4() {
+ info("Searching for cased entry 4");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/Test/"),
+ });
+ let context = createContext("mOzilla.org/t", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mOzilla.org/test/",
+ completed: "http://mozilla.org/Test/",
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ uri: "http://mozilla.org/Test/",
+ title: "test visit for http://mozilla.org/Test/",
+ iconUri: "page-icon:http://mozilla.org/Test/",
+ heuristic: true,
+ providerName: AUTOFILL_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_casing_5() {
+ info("Searching for cased entry 5");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/Test/"),
+ });
+ let context = createContext("mOzilla.org/T", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mOzilla.org/Test/",
+ completed: "http://mozilla.org/Test/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/Test/",
+ title: "test visit for http://mozilla.org/Test/",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_untrimmed_casing() {
+ info("Searching for untrimmed cased entry");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/Test/"),
+ });
+ let context = createContext("http://mOz", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://mOzilla.org/",
+ completed: "http://mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/",
+ fallbackTitle: "mozilla.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/Test/",
+ title: "test visit for http://mozilla.org/Test/",
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_untrimmed_www_casing() {
+ info("Searching for untrimmed cased entry with www");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://www.mozilla.org/Test/"),
+ });
+ let context = createContext("http://www.mOz", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://www.mOzilla.org/",
+ completed: "http://www.mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://www.mozilla.org/",
+ fallbackTitle: "www.mozilla.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://www.mozilla.org/Test/",
+ title: "test visit for http://www.mozilla.org/Test/",
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_untrimmed_path_casing() {
+ info("Searching for untrimmed cased entry with path");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/Test/"),
+ });
+ let context = createContext("http://mOzilla.org/t", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://mOzilla.org/test/",
+ completed: "http://mozilla.org/Test/",
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ uri: "http://mozilla.org/Test/",
+ title: "test visit for http://mozilla.org/Test/",
+ iconUri: "page-icon:http://mozilla.org/Test/",
+ heuristic: true,
+ providerName: AUTOFILL_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_untrimmed_path_casing_2() {
+ info("Searching for untrimmed cased entry with path 2");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/Test/"),
+ });
+ let context = createContext("http://mOzilla.org/T", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://mOzilla.org/Test/",
+ completed: "http://mozilla.org/Test/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/Test/",
+ title: "test visit for http://mozilla.org/Test/",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_untrimmed_path_www_casing() {
+ info("Searching for untrimmed cased entry with www and path");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://www.mozilla.org/Test/"),
+ });
+ let context = createContext("http://www.mOzilla.org/t", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://www.mOzilla.org/test/",
+ completed: "http://www.mozilla.org/Test/",
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ uri: "http://www.mozilla.org/Test/",
+ title: "test visit for http://www.mozilla.org/Test/",
+ iconUri: "page-icon:http://www.mozilla.org/Test/",
+ heuristic: true,
+ providerName: AUTOFILL_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_untrimmed_path_www_casing_2() {
+ info("Searching for untrimmed cased entry with www and path 2");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://www.mozilla.org/Test/"),
+ });
+ let context = createContext("http://www.mOzilla.org/T", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://www.mOzilla.org/Test/",
+ completed: "http://www.mozilla.org/Test/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://www.mozilla.org/Test/",
+ title: "test visit for http://www.mozilla.org/Test/",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_searching() {
+ let uri1 = Services.io.newURI("http://dummy/1/");
+ let uri2 = Services.io.newURI("http://dummy/2/");
+ let uri3 = Services.io.newURI("http://dummy/3/");
+ let uri4 = Services.io.newURI("http://dummy/4/");
+ let uri5 = Services.io.newURI("http://dummy/5/");
+
+ await PlacesTestUtils.addVisits([
+ { uri: uri1, title: "uppercase lambda \u039B" },
+ { uri: uri2, title: "lowercase lambda \u03BB" },
+ { uri: uri3, title: "symbol \u212A" }, // kelvin
+ { uri: uri4, title: "uppercase K" },
+ { uri: uri5, title: "lowercase k" },
+ ]);
+
+ info("Search for lowercase lambda");
+ let context = createContext("\u03BB", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: uri2.spec,
+ title: "lowercase lambda \u03BB",
+ }),
+ makeVisitResult(context, {
+ uri: uri1.spec,
+ title: "uppercase lambda \u039B",
+ }),
+ ],
+ });
+
+ info("Search for uppercase lambda");
+ context = createContext("\u039B", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: uri2.spec,
+ title: "lowercase lambda \u03BB",
+ }),
+ makeVisitResult(context, {
+ uri: uri1.spec,
+ title: "uppercase lambda \u039B",
+ }),
+ ],
+ });
+
+ info("Search for kelvin sign");
+ context = createContext("\u212A", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri5.spec, title: "lowercase k" }),
+ makeVisitResult(context, { uri: uri4.spec, title: "uppercase K" }),
+ makeVisitResult(context, { uri: uri3.spec, title: "symbol \u212A" }),
+ ],
+ });
+
+ info("Search for lowercase k");
+ context = createContext("k", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri5.spec, title: "lowercase k" }),
+ makeVisitResult(context, { uri: uri4.spec, title: "uppercase K" }),
+ makeVisitResult(context, { uri: uri3.spec, title: "symbol \u212A" }),
+ ],
+ });
+
+ info("Search for uppercase k");
+
+ context = createContext("K", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri5.spec, title: "lowercase k" }),
+ makeVisitResult(context, { uri: uri4.spec, title: "uppercase K" }),
+ makeVisitResult(context, { uri: uri3.spec, title: "symbol \u212A" }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_dedupe_prefix.js b/browser/components/urlbar/tests/unit/test_dedupe_prefix.js
new file mode 100644
index 0000000000..47a673d064
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_dedupe_prefix.js
@@ -0,0 +1,277 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Testing that we dedupe results that have the same URL and title as another
+// except for their prefix (e.g. http://www.).
+add_task(async function dedupe_prefix() {
+ // We need to set the title or else we won't dedupe. We only dedupe when
+ // titles match up to mitigate deduping when the www. version of a site is
+ // completely different from it's www-less counterpart and thus presumably
+ // has a different title.
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com/foo/",
+ title: "Example Page",
+ },
+ {
+ uri: "http://www.example.com/foo/",
+ title: "Example Page",
+ },
+ {
+ uri: "https://example.com/foo/",
+ title: "Example Page",
+ },
+ // Note that we add https://www.example.com/foo/ twice here.
+ {
+ uri: "https://www.example.com/foo/",
+ title: "Example Page",
+ },
+ {
+ uri: "https://www.example.com/foo/",
+ title: "Example Page",
+ },
+ ]);
+
+ // Expected results:
+ //
+ // Autofill result:
+ // https://www.example.com has the highest origin frecency since we added 2
+ // visits to https://www.example.com/foo/ and only one visit to the other
+ // URLs.
+ // Other results:
+ // https://example.com/foo/ has the highest possible prefix rank, and it
+ // does not dupe the autofill result, so only it should be included.
+ let context = createContext("example.com/foo/", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/foo/",
+ completed: "https://www.example.com/foo/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www.example.com/foo/",
+ title: "Example Page",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://example.com/foo/",
+ title: "Example Page",
+ }),
+ ],
+ });
+
+ // Add more visits to the lowest-priority prefix. It should be the heuristic
+ // result but we should still show our highest-priority result. https://www.
+ // should not appear at all.
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://www.example.com/foo/",
+ title: "Example Page",
+ },
+ ]);
+ }
+
+ // Expected results:
+ //
+ // Autofill result:
+ // http://www.example.com now has the highest origin frecency since we added
+ // 4 visits to http://www.example.com/foo/
+ // Other results:
+ // Same as before
+ context = createContext("example.com/foo/", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/foo/",
+ completed: "http://www.example.com/foo/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://www.example.com/foo/",
+ title: "Example Page",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://example.com/foo/",
+ title: "Example Page",
+ }),
+ ],
+ });
+
+ // Add enough https:// vists for it to have the highest frecency. It should
+ // be the heuristic result. We should still get the https://www. result
+ // because we still show results with the same key and protocol if they differ
+ // from the heuristic result in having www.
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "https://example.com/foo/",
+ title: "Example Page",
+ },
+ ]);
+ }
+
+ // Expected results:
+ //
+ // Autofill result:
+ // https://example.com now has the highest origin frecency since we added
+ // 6 visits to https://example.com/foo/
+ // Other results:
+ // https://example.com/foo/ has the highest possible prefix rank, but it
+ // dupes the heuristic so it should not be included.
+ // https://www.example.com/foo/ has the next highest prefix rank, and it
+ // does not dupe the heuristic, so only it should be included.
+ context = createContext("example.com/foo/", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/foo/",
+ completed: "https://example.com/foo/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://example.com/foo/",
+ title: "Example Page",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+// This is the same as the previous task but with `experimental.hideHeuristic`
+// enabled.
+add_task(async function hideHeuristic() {
+ UrlbarPrefs.set("experimental.hideHeuristic", true);
+
+ // We need to set the title or else we won't dedupe. We only dedupe when
+ // titles match up to mitigate deduping when the www. version of a site is
+ // completely different from it's www-less counterpart and thus presumably
+ // has a different title.
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com/foo/",
+ title: "Example Page",
+ },
+ {
+ uri: "http://www.example.com/foo/",
+ title: "Example Page",
+ },
+ {
+ uri: "https://example.com/foo/",
+ title: "Example Page",
+ },
+ // Note that we add https://www.example.com/foo/ twice here.
+ {
+ uri: "https://www.example.com/foo/",
+ title: "Example Page",
+ },
+ {
+ uri: "https://www.example.com/foo/",
+ title: "Example Page",
+ },
+ ]);
+
+ // Expected results:
+ //
+ // Autofill result:
+ // https://www.example.com has the highest origin frecency since we added 2
+ // visits to https://www.example.com/foo/ and only one visit to the other
+ // URLs.
+ // Other results:
+ // https://example.com/foo/ has the highest possible prefix rank, and it
+ // does not dupe the autofill result, so only it should be included.
+ let context = createContext("example.com/foo/", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/foo/",
+ completed: "https://www.example.com/foo/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www.example.com/foo/",
+ title: "Example Page",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://example.com/foo/",
+ title: "Example Page",
+ }),
+ ],
+ });
+
+ // Add more visits to the lowest-priority prefix. It should be the heuristic
+ // result but we should still show our highest-priority result. https://www.
+ // should not appear at all.
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://www.example.com/foo/",
+ title: "Example Page",
+ },
+ ]);
+ }
+
+ // Expected results:
+ //
+ // Autofill result:
+ // http://www.example.com now has the highest origin frecency since we added
+ // 4 visits to http://www.example.com/foo/
+ // Other results:
+ // Same as before
+ context = createContext("example.com/foo/", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/foo/",
+ completed: "http://www.example.com/foo/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://www.example.com/foo/",
+ title: "Example Page",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://example.com/foo/",
+ title: "Example Page",
+ }),
+ ],
+ });
+
+ // Add enough https:// vists for it to have the highest frecency.
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "https://example.com/foo/",
+ title: "Example Page",
+ },
+ ]);
+ }
+
+ // Expected results:
+ //
+ // Autofill result:
+ // https://example.com now has the highest origin frecency since we added
+ // 6 visits to https://example.com/foo/
+ // Other results:
+ // https://example.com/foo/ has the highest possible prefix rank. It dupes
+ // the heuristic so ordinarily it should not be included, but because the
+ // heuristic is hidden, only it should appear.
+ context = createContext("example.com/foo/", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/foo/",
+ completed: "https://example.com/foo/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://example.com/foo/",
+ title: "Example Page",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://example.com/foo/",
+ title: "Example Page",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+ UrlbarPrefs.clear("experimental.hideHeuristic");
+});
diff --git a/browser/components/urlbar/tests/unit/test_dedupe_switchTab.js b/browser/components/urlbar/tests/unit/test_dedupe_switchTab.js
new file mode 100644
index 0000000000..3b49866b1e
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_dedupe_switchTab.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+testEngine_setup();
+
+add_task(async function test_deduplication_for_switch_tab() {
+ // Set up Places to think the tab is open locally.
+ let uri = Services.io.newURI("http://example.com/");
+
+ await PlacesTestUtils.addVisits({ uri, title: "An Example" });
+ await addOpenPages(uri, 1);
+ await UrlbarUtils.addToInputHistory("http://example.com/", "An");
+
+ let query = "An";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeTabSwitchResult(context, {
+ uri: "http://example.com/",
+ title: "An Example",
+ }),
+ ],
+ });
+
+ await removeOpenPages(uri, 1);
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_download_embed_bookmarks.js b/browser/components/urlbar/tests/unit/test_download_embed_bookmarks.js
new file mode 100644
index 0000000000..29ce557748
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_download_embed_bookmarks.js
@@ -0,0 +1,137 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim:set ts=2 sw=2 sts=2 et:
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests bug 449406 to ensure that TRANSITION_DOWNLOAD, TRANSITION_EMBED and
+ * TRANSITION_FRAMED_LINK bookmarked uri's show up in the location bar.
+ */
+
+testEngine_setup();
+
+const TRANSITION_EMBED = PlacesUtils.history.TRANSITION_EMBED;
+const TRANSITION_FRAMED_LINK = PlacesUtils.history.TRANSITION_FRAMED_LINK;
+const TRANSITION_DOWNLOAD = PlacesUtils.history.TRANSITION_DOWNLOAD;
+
+add_task(async function test_download_embed_bookmarks() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+
+ let uri1 = Services.io.newURI("http://download/bookmarked");
+ let uri2 = Services.io.newURI("http://embed/bookmarked");
+ let uri3 = Services.io.newURI("http://framed/bookmarked");
+ let uri4 = Services.io.newURI("http://download");
+ let uri5 = Services.io.newURI("http://embed");
+ let uri6 = Services.io.newURI("http://framed");
+ await PlacesTestUtils.addVisits([
+ { uri: uri1, title: "download-bookmark", transition: TRANSITION_DOWNLOAD },
+ { uri: uri2, title: "embed-bookmark", transition: TRANSITION_EMBED },
+ { uri: uri3, title: "framed-bookmark", transition: TRANSITION_FRAMED_LINK },
+ { uri: uri4, title: "download2", transition: TRANSITION_DOWNLOAD },
+ { uri: uri5, title: "embed2", transition: TRANSITION_EMBED },
+ { uri: uri6, title: "framed2", transition: TRANSITION_FRAMED_LINK },
+ ]);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri1,
+ title: "download-bookmark",
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri2,
+ title: "embed-bookmark",
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri3,
+ title: "framed-bookmark",
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ info("Searching for bookmarked download uri matches");
+ let context = createContext("download-bookmark", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri1.spec,
+ title: "download-bookmark",
+ }),
+ ],
+ });
+
+ info("Searching for bookmarked embed uri matches");
+ context = createContext("embed-bookmark", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri2.spec,
+ title: "embed-bookmark",
+ }),
+ ],
+ });
+
+ info("Searching for bookmarked framed uri matches");
+ context = createContext("framed-bookmark", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri3.spec,
+ title: "framed-bookmark",
+ }),
+ ],
+ });
+
+ info("Searching for download uri does not match");
+ context = createContext("download2", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Searching for embed uri does not match");
+ context = createContext("embed2", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Searching for framed uri does not match");
+ context = createContext("framed2", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_empty_search.js b/browser/components/urlbar/tests/unit/test_empty_search.js
new file mode 100644
index 0000000000..2c6dffe8e6
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_empty_search.js
@@ -0,0 +1,181 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test for bug 426864 that makes sure searching a space only shows typed pages
+ * from history.
+ */
+
+testEngine_setup();
+
+add_task(async function test_empty_search() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions");
+ });
+
+ let uri1 = Services.io.newURI("http://t.foo/1");
+ let uri2 = Services.io.newURI("http://t.foo/2");
+ let uri3 = Services.io.newURI("http://t.foo/3");
+ let uri4 = Services.io.newURI("http://t.foo/4");
+ let uri5 = Services.io.newURI("http://t.foo/5");
+ let uri6 = Services.io.newURI("http://t.foo/6");
+ let uri7 = Services.io.newURI("http://t.foo/7");
+
+ await PlacesTestUtils.addVisits([
+ { uri: uri7, title: "title" },
+ { uri: uri6, title: "title" },
+ { uri: uri4, title: "title" },
+ { uri: uri3, title: "title" },
+ { uri: uri2, title: "title" },
+ { uri: uri1, title: "title" },
+ ]);
+
+ await PlacesTestUtils.addBookmarkWithDetails({ uri: uri6, title: "title" });
+ await PlacesTestUtils.addBookmarkWithDetails({ uri: uri5, title: "title" });
+ await PlacesTestUtils.addBookmarkWithDetails({ uri: uri4, title: "title" });
+ await PlacesTestUtils.addBookmarkWithDetails({ uri: uri2, title: "title" });
+
+ await addOpenPages(uri7, 1);
+
+ // Now remove page 6 from history, so it is an unvisited bookmark.
+ await PlacesUtils.history.remove(uri6);
+
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ // With the changes above, the sites in descending order of frecency are:
+ // uri2
+ // uri4
+ // uri5
+ // uri6
+ // uri1
+ // uri3
+ // uri7
+
+ info("Match everything");
+ let context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri2.spec,
+ title: "title",
+ }),
+ makeBookmarkResult(context, {
+ uri: uri4.spec,
+ title: "title",
+ }),
+ makeBookmarkResult(context, {
+ uri: uri5.spec,
+ title: "title",
+ }),
+ makeBookmarkResult(context, {
+ uri: uri6.spec,
+ title: "title",
+ }),
+ makeVisitResult(context, { uri: uri1.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri3.spec, title: "title" }),
+ makeTabSwitchResult(context, { uri: uri7.spec, title: "title" }),
+ ],
+ });
+
+ info("Match only history");
+ context = createContext(`foo ${UrlbarTokenizer.RESTRICT.HISTORY}`, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri2.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri4.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri1.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri3.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri7.spec, title: "title" }),
+ ],
+ });
+
+ info("Drop-down empty search matches history sorted by frecency desc");
+ context = createContext(" ", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ // query is made explict so makeSearchResult doesn't trim it.
+ query: " ",
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri2.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri4.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri1.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri3.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri7.spec, title: "title" }),
+ ],
+ });
+
+ info("Empty search matches only bookmarks when history is disabled");
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true);
+ context = createContext(" ", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ // query is made explict so makeSearchResult doesn't trim it.
+ query: " ",
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri2.spec,
+ title: "title",
+ }),
+ makeBookmarkResult(context, {
+ uri: uri4.spec,
+ title: "title",
+ }),
+ makeBookmarkResult(context, {
+ uri: uri5.spec,
+ title: "title",
+ }),
+ makeBookmarkResult(context, {
+ uri: uri6.spec,
+ title: "title",
+ }),
+ ],
+ });
+
+ info(
+ "Empty search matches only open tabs when bookmarks and history are disabled"
+ );
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ context = createContext(" ", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ // query is made explict so makeSearchResult doesn't trim it.
+ query: " ",
+ heuristic: true,
+ }),
+ makeTabSwitchResult(context, { uri: uri7.spec, title: "title" }),
+ ],
+ });
+
+ Services.prefs.clearUserPref("browser.urlbar.suggest.history");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.bookmark");
+
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_encoded_urls.js b/browser/components/urlbar/tests/unit/test_encoded_urls.js
new file mode 100644
index 0000000000..87a6015e86
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_encoded_urls.js
@@ -0,0 +1,97 @@
+add_task(async function test_encoded() {
+ info("Searching for over encoded url should not break it");
+ let url = "https://www.mozilla.com/search/top/?q=%25%32%35";
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI(url),
+ title: url,
+ });
+ let context = createContext(url, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: url,
+ matches: [
+ makeVisitResult(context, {
+ uri: url,
+ title: url,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_encoded_trimmed() {
+ info("Searching for over encoded url should not break it");
+ let url = "https://www.mozilla.com/search/top/?q=%25%32%35";
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI(url),
+ title: url,
+ });
+ let context = createContext("mozilla.com/search/top/?q=%25%32%35", {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ autofilled: "mozilla.com/search/top/?q=%25%32%35",
+ completed: url,
+ matches: [
+ makeVisitResult(context, {
+ uri: url,
+ title: url,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_encoded_partial() {
+ info("Searching for over encoded url should not break it");
+ let url = "https://www.mozilla.com/search/top/?q=%25%32%35";
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI(url),
+ title: url,
+ });
+ let context = createContext("https://www.mozilla.com/search/top/?q=%25", {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: url,
+ matches: [
+ makeVisitResult(context, {
+ uri: url,
+ title: url,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_encoded_path() {
+ info("Searching for over encoded url should not break it");
+ let url = "https://www.mozilla.com/%25%32%35/top/";
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI(url),
+ title: url,
+ });
+ let context = createContext("https://www.mozilla.com/%25%32%35/t", {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ autofilled: url,
+ completed: url,
+ matches: [
+ makeVisitResult(context, {
+ uri: url,
+ title: url,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_escaping_badEscapedURI.js b/browser/components/urlbar/tests/unit/test_escaping_badEscapedURI.js
new file mode 100644
index 0000000000..d330625bbb
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_escaping_badEscapedURI.js
@@ -0,0 +1,37 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test bug 422277 to make sure bad escaped uris don't get escaped. This makes
+ * sure we don't hit an assertion for "not a UTF8 string".
+ */
+
+testEngine_setup();
+
+add_task(async function test() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+
+ info("Bad escaped uri stays escaped");
+ let uri1 = Services.io.newURI("http://site/%EAid");
+ await PlacesTestUtils.addVisits([{ uri: uri1, title: "title" }]);
+ let context = createContext("site", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: uri1.spec,
+ title: "title",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_escaping_escapeSelf.js b/browser/components/urlbar/tests/unit/test_escaping_escapeSelf.js
new file mode 100644
index 0000000000..73fe3940c3
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_escaping_escapeSelf.js
@@ -0,0 +1,62 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test bug 422698 to make sure searches with urls from the location bar
+ * correctly match itself when it contains escaped characters.
+ */
+
+testEngine_setup();
+
+add_task(async function test_escape() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+
+ let uri1 = Services.io.newURI("http://unescapeduri/");
+ let uri2 = Services.io.newURI("http://escapeduri/%40/");
+ await PlacesTestUtils.addVisits([
+ { uri: uri1, title: "title" },
+ { uri: uri2, title: "title" },
+ ]);
+
+ info("Unescaped location matches itself");
+ let context = createContext("http://unescapeduri/", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: uri1.spec,
+ title: "title",
+ iconUri: `page-icon:${uri1.spec}`,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ heuristic: true,
+ }),
+ // Note that uri2 does not appear in results.
+ ],
+ });
+
+ info("Escaped location matches itself");
+ context = createContext("http://escapeduri/%40", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://escapeduri/%40",
+ fallbackTitle: "http://escapeduri/%40",
+ iconUri: "page-icon:http://escapeduri/",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: uri2.spec,
+ title: "title",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_exposure.js b/browser/components/urlbar/tests/unit/test_exposure.js
new file mode 100644
index 0000000000..837673a868
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_exposure.js
@@ -0,0 +1,195 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+ChromeUtils.defineESModuleGetters(this, {
+ QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs",
+ UrlbarProviderQuickSuggest:
+ "resource:///modules/UrlbarProviderQuickSuggest.sys.mjs",
+});
+
+// Tests that registering an exposureResults pref and triggering a match causes
+// the exposure event to be recorded on the UrlbarResults.
+const REMOTE_SETTINGS_RESULTS = [
+ {
+ id: 1,
+ url: "http://test.com/q=frabbits",
+ title: "frabbits",
+ keywords: ["test"],
+ click_url: "http://click.reporting.test.com/",
+ impression_url: "http://impression.reporting.test.com/",
+ advertiser: "TestAdvertiser",
+ },
+ {
+ id: 2,
+ url: "http://test.com/q=frabbits",
+ title: "frabbits",
+ keywords: ["non_sponsored"],
+ click_url: "http://click.reporting.test.com/",
+ impression_url: "http://impression.reporting.test.com/",
+ advertiser: "wikipedia",
+ iab_category: "5 - Education",
+ },
+];
+
+const EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT = {
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ heuristic: false,
+ payload: {
+ telemetryType: "adm_sponsored",
+ qsSuggestion: "test",
+ title: "frabbits",
+ url: "http://test.com/q=frabbits",
+ originalUrl: "http://test.com/q=frabbits",
+ icon: null,
+ sponsoredImpressionUrl: "http://impression.reporting.test.com/",
+ sponsoredClickUrl: "http://click.reporting.test.com/",
+ sponsoredBlockId: 1,
+ sponsoredAdvertiser: "TestAdvertiser",
+ isSponsored: true,
+ helpUrl: QuickSuggest.HELP_URL,
+ helpL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-learn-more-about-firefox-suggest"
+ : "firefox-suggest-urlbar-learn-more",
+ },
+ isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"),
+ blockL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-dismiss-firefox-suggest"
+ : "firefox-suggest-urlbar-block",
+ },
+ displayUrl: "http://test.com/q=frabbits",
+ source: "remote-settings",
+ },
+};
+
+const EXPECTED_NON_SPONSORED_REMOTE_SETTINGS_RESULT = {
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ heuristic: false,
+ payload: {
+ telemetryType: "adm_nonsponsored",
+ qsSuggestion: "non_sponsored",
+ title: "frabbits",
+ url: "http://test.com/q=frabbits",
+ originalUrl: "http://test.com/q=frabbits",
+ icon: null,
+ sponsoredImpressionUrl: "http://impression.reporting.test.com/",
+ sponsoredClickUrl: "http://click.reporting.test.com/",
+ sponsoredBlockId: 2,
+ sponsoredAdvertiser: "wikipedia",
+ sponsoredIabCategory: "5 - Education",
+ isSponsored: false,
+ helpUrl: QuickSuggest.HELP_URL,
+ helpL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-learn-more-about-firefox-suggest"
+ : "firefox-suggest-urlbar-learn-more",
+ },
+ isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"),
+ blockL10n: {
+ id: UrlbarPrefs.get("resultMenu")
+ ? "urlbar-result-menu-dismiss-firefox-suggest"
+ : "firefox-suggest-urlbar-block",
+ },
+ displayUrl: "http://test.com/q=frabbits",
+ source: "remote-settings",
+ },
+};
+
+add_setup(async function test_setup() {
+ // FOG needs a profile directory to put its data in.
+ do_get_profile();
+
+ // FOG needs to be initialized in order for data to flow.
+ Services.fog.initializeFOG();
+
+ UrlbarPrefs.set("quicksuggest.enabled", true);
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
+ UrlbarPrefs.set("quicksuggest.shouldShowOnboardingDialog", false);
+
+ await MerinoTestUtils.server.start();
+
+ // Set up the remote settings client with the test data.
+ await QuickSuggestTestUtils.ensureQuickSuggestInit({
+ remoteSettingsResults: [
+ {
+ type: "data",
+ attachment: REMOTE_SETTINGS_RESULTS,
+ },
+ ],
+ });
+});
+
+add_task(async function testExposureCheck() {
+ UrlbarPrefs.set("quicksuggest.remoteSettings.enabled", true);
+ UrlbarPrefs.set("exposureResults", "rs_adm_sponsored");
+ UrlbarPrefs.set("showExposureResults", true);
+
+ let context = createContext("test", {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+
+ await check_results({
+ context,
+ matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT],
+ });
+
+ Assert.equal(context.results[0].exposureResultType, "rs_adm_sponsored");
+ Assert.equal(context.results[0].exposureResultHidden, false);
+});
+
+add_task(async function testExposureCheckMultiple() {
+ UrlbarPrefs.set("quicksuggest.remoteSettings.enabled", true);
+ UrlbarPrefs.set("exposureResults", "rs_adm_sponsored,rs_adm_nonsponsored");
+ UrlbarPrefs.set("showExposureResults", true);
+
+ let context = createContext("test", {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+
+ await check_results({
+ context,
+ matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT],
+ });
+
+ Assert.equal(context.results[0].exposureResultType, "rs_adm_sponsored");
+ Assert.equal(context.results[0].exposureResultHidden, false);
+
+ context = createContext("non_sponsored", {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+
+ await check_results({
+ context,
+ matches: [EXPECTED_NON_SPONSORED_REMOTE_SETTINGS_RESULT],
+ });
+
+ Assert.equal(context.results[0].exposureResultType, "rs_adm_nonsponsored");
+ Assert.equal(context.results[0].exposureResultHidden, false);
+});
+
+add_task(async function exposureDisplayFiltering() {
+ UrlbarPrefs.set("quicksuggest.remoteSettings.enabled", true);
+ UrlbarPrefs.set("exposureResults", "rs_adm_sponsored");
+ UrlbarPrefs.set("showExposureResults", false);
+
+ let context = createContext("test", {
+ providers: [UrlbarProviderQuickSuggest.name],
+ isPrivate: false,
+ });
+
+ await check_results({
+ context,
+ matches: [EXPECTED_REMOTE_SETTINGS_URLBAR_RESULT],
+ });
+
+ Assert.equal(context.results[0].exposureResultType, "rs_adm_sponsored");
+ Assert.equal(context.results[0].exposureResultHidden, true);
+});
diff --git a/browser/components/urlbar/tests/unit/test_frecency.js b/browser/components/urlbar/tests/unit/test_frecency.js
new file mode 100644
index 0000000000..0d7a007e0d
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_frecency.js
@@ -0,0 +1,403 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test for bug 406358 to make sure frecency works for empty input/search, but
+ * this also tests for non-empty inputs as well. Because the interactions among
+ * DIFFERENT* visit counts and visit dates is not well defined, this test
+ * holds one of the two values constant when modifying the other.
+ *
+ * Also test bug 419068 to make sure tagged pages don't necessarily have to be
+ * first in the results.
+ *
+ * Also test bug 426166 to make sure that the results of autocomplete searches
+ * are stable. Note that failures of this test will be intermittent by nature
+ * since we are testing to make sure that the unstable sort algorithm used
+ * by SQLite is not changing the order of the results on us.
+ */
+
+testEngine_setup();
+
+async function task_setCountDate(uri, count, date) {
+ // We need visits so that frecency can be computed over multiple visits
+ let visits = [];
+ for (let i = 0; i < count; i++) {
+ visits.push({
+ uri,
+ visitDate: date,
+ transition: PlacesUtils.history.TRANSITION_TYPED,
+ });
+ }
+ await PlacesTestUtils.addVisits(visits);
+}
+
+async function setBookmark(uri) {
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ url: uri,
+ title: "bleh",
+ });
+}
+
+async function tagURI(uri, tags) {
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: uri,
+ title: "bleh",
+ });
+ PlacesUtils.tagging.tagURI(uri, tags);
+}
+
+var uri1 = Services.io.newURI("http://site.tld/1");
+var uri2 = Services.io.newURI("http://site.tld/2");
+var uri3 = Services.io.newURI("http://aaaaaaaaaa/1");
+var uri4 = Services.io.newURI("http://aaaaaaaaaa/2");
+
+// d1 is younger (should show up higher) than d2 (PRTime is in usecs not msec)
+// Make sure the dates fall into different frecency groups
+var d1 = new Date(Date.now() - 1000 * 60 * 60) * 1000;
+var d2 = new Date(Date.now() - 1000 * 60 * 60 * 24 * 10) * 1000;
+// c1 is larger (should show up higher) than c2
+var c1 = 10;
+var c2 = 1;
+
+var tests = [
+ // test things without a search term
+ async function () {
+ info("Test 0: same count, different date");
+ await task_setCountDate(uri1, c1, d1);
+ await task_setCountDate(uri2, c1, d2);
+ await tagURI(uri1, ["site"]);
+ let context = createContext(" ", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ query: " ",
+ }),
+ // uri1 is a visit result despite being a tagged bookmark because we
+ // are searching for the empty string. By default, the empty string
+ // filters to history. uri1 will be displayed as a bookmark later in the
+ // test when we are searching with a non-empty string.
+ makeVisitResult(context, {
+ uri: uri1.spec,
+ title: "bleh",
+ }),
+ makeVisitResult(context, {
+ uri: uri2.spec,
+ title: `test visit for ${uri2.spec}`,
+ }),
+ ],
+ });
+ },
+ async function () {
+ info("Test 1: same count, different date");
+ await task_setCountDate(uri1, c1, d2);
+ await task_setCountDate(uri2, c1, d1);
+ await tagURI(uri1, ["site"]);
+ let context = createContext(" ", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ query: " ",
+ }),
+ makeVisitResult(context, {
+ uri: uri2.spec,
+ title: `test visit for ${uri2.spec}`,
+ }),
+ makeVisitResult(context, {
+ uri: uri1.spec,
+ title: "bleh",
+ }),
+ ],
+ });
+ },
+ async function () {
+ info("Test 2: different count, same date");
+ await task_setCountDate(uri1, c1, d1);
+ await task_setCountDate(uri2, c2, d1);
+ await tagURI(uri1, ["site"]);
+ let context = createContext(" ", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ query: " ",
+ }),
+ makeVisitResult(context, {
+ uri: uri1.spec,
+ title: "bleh",
+ }),
+ makeVisitResult(context, {
+ uri: uri2.spec,
+ title: `test visit for ${uri2.spec}`,
+ }),
+ ],
+ });
+ },
+ async function () {
+ info("Test 3: different count, same date");
+ await task_setCountDate(uri1, c2, d1);
+ await task_setCountDate(uri2, c1, d1);
+ await tagURI(uri1, ["site"]);
+ let context = createContext(" ", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ query: " ",
+ }),
+ makeVisitResult(context, {
+ uri: uri2.spec,
+ title: `test visit for ${uri2.spec}`,
+ }),
+ makeVisitResult(context, {
+ uri: uri1.spec,
+ title: "bleh",
+ }),
+ ],
+ });
+ },
+
+ // test things with a search term
+ async function () {
+ info("Test 4: same count, different date");
+ await task_setCountDate(uri1, c1, d1);
+ await task_setCountDate(uri2, c1, d2);
+ await tagURI(uri1, ["site"]);
+ let context = createContext("site", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri1.spec,
+ title: "bleh",
+ tags: ["site"],
+ }),
+ makeVisitResult(context, {
+ uri: uri2.spec,
+ title: `test visit for ${uri2.spec}`,
+ }),
+ ],
+ });
+ },
+ async function () {
+ info("Test 5: same count, different date");
+ await task_setCountDate(uri1, c1, d2);
+ await task_setCountDate(uri2, c1, d1);
+ await tagURI(uri1, ["site"]);
+ let context = createContext("site", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: uri2.spec,
+ title: `test visit for ${uri2.spec}`,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri1.spec,
+ title: "bleh",
+ tags: ["site"],
+ }),
+ ],
+ });
+ },
+ async function () {
+ info("Test 6: different count, same date");
+ await task_setCountDate(uri1, c1, d1);
+ await task_setCountDate(uri2, c2, d1);
+ await tagURI(uri1, ["site"]);
+ let context = createContext("site", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri1.spec,
+ title: "bleh",
+ tags: ["site"],
+ }),
+ makeVisitResult(context, {
+ uri: uri2.spec,
+ title: `test visit for ${uri2.spec}`,
+ }),
+ ],
+ });
+ },
+ async function () {
+ info("Test 7: different count, same date");
+ await task_setCountDate(uri1, c2, d1);
+ await task_setCountDate(uri2, c1, d1);
+ await tagURI(uri1, ["site"]);
+ let context = createContext("site", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: uri2.spec,
+ title: `test visit for ${uri2.spec}`,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri1.spec,
+ title: "bleh",
+ tags: ["site"],
+ }),
+ ],
+ });
+ },
+ // There are multiple tests for 8, hence the multiple functions
+ // Bug 426166 section
+ async function () {
+ info("Test 8.1a: same count, same date");
+ await setBookmark(uri3);
+ await setBookmark(uri4);
+ let context = createContext("a", { isPrivate: false });
+ let bookmarkResults = [
+ makeBookmarkResult(context, {
+ uri: uri4.spec,
+ title: "bleh",
+ }),
+ makeBookmarkResult(context, {
+ uri: uri3.spec,
+ title: "bleh",
+ }),
+ ];
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...bookmarkResults,
+ ],
+ });
+
+ context = createContext("aa", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ // We need to continuously redefine the heuristic search result because it
+ // is the only one that changes with the search string.
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...bookmarkResults,
+ ],
+ });
+
+ context = createContext("aaa", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...bookmarkResults,
+ ],
+ });
+
+ context = createContext("aaaa", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...bookmarkResults,
+ ],
+ });
+
+ context = createContext("aaa", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...bookmarkResults,
+ ],
+ });
+
+ context = createContext("aa", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...bookmarkResults,
+ ],
+ });
+
+ context = createContext("a", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...bookmarkResults,
+ ],
+ });
+ },
+];
+
+add_task(async function test_frecency() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
+ // always search in history + bookmarks, no matter what the default is
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.openpage", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.engines", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false);
+ for (let test of tests) {
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+
+ await test();
+ }
+ for (let type of [
+ "history",
+ "bookmark",
+ "openpage",
+ "searches",
+ "engines",
+ "quickactions",
+ ]) {
+ Services.prefs.clearUserPref("browser.urlbar.suggest." + type);
+ Services.prefs.clearUserPref("browser.urlbar.autoFill");
+ }
+});
diff --git a/browser/components/urlbar/tests/unit/test_frecency_alternative_nimbus.js b/browser/components/urlbar/tests/unit/test_frecency_alternative_nimbus.js
new file mode 100644
index 0000000000..c2d0e4bae3
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_frecency_alternative_nimbus.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const ORIGINS_FEATUREGATE = "places.frecency.origins.alternative.featureGate";
+const ORIGINS_DAYSCUTOFF = "places.frecency.origins.alternative.daysCutOff";
+
+add_task(async function () {
+ let reset = await UrlbarTestUtils.initNimbusFeature(
+ {
+ // Empty for sanity check.
+ },
+ "frecency",
+ "config"
+ );
+ Assert.equal(Services.prefs.getBoolPref(ORIGINS_FEATUREGATE), false);
+ Assert.throws(
+ () => Services.prefs.getIntPref(ORIGINS_DAYSCUTOFF),
+ /NS_ERROR_UNEXPECTED/
+ );
+ await reset();
+
+ reset = await UrlbarTestUtils.initNimbusFeature(
+ {
+ originsAlternativeEnable: true,
+ },
+ "frecency",
+ "config"
+ );
+ Assert.equal(Services.prefs.getBoolPref(ORIGINS_FEATUREGATE), true);
+ Assert.ok(Services.prefs.prefHasUserValue(ORIGINS_FEATUREGATE));
+ Assert.throws(
+ () => Services.prefs.getIntPref(ORIGINS_DAYSCUTOFF),
+ /NS_ERROR_UNEXPECTED/
+ );
+ await reset();
+
+ reset = await UrlbarTestUtils.initNimbusFeature(
+ {
+ originsAlternativeEnable: true,
+ originsDaysCutOff: 60,
+ },
+ "frecency",
+ "config"
+ );
+ Assert.equal(Services.prefs.getBoolPref(ORIGINS_FEATUREGATE), true);
+ Assert.ok(Services.prefs.prefHasUserValue(ORIGINS_FEATUREGATE));
+ Assert.equal(Services.prefs.getIntPref(ORIGINS_DAYSCUTOFF, 90), 60);
+ Assert.ok(Services.prefs.prefHasUserValue(ORIGINS_DAYSCUTOFF));
+ await reset();
+});
diff --git a/browser/components/urlbar/tests/unit/test_heuristic_cancel.js b/browser/components/urlbar/tests/unit/test_heuristic_cancel.js
new file mode 100644
index 0000000000..a53786a747
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_heuristic_cancel.js
@@ -0,0 +1,238 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests that old results from UrlbarProviderAutofill do not overwrite results
+ * from UrlbarProviderHeuristicFallback after the autofillable query is
+ * cancelled. See bug 1653436.
+ */
+
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarProviderAutofill: "resource:///modules/UrlbarProviderAutofill.sys.mjs",
+});
+
+/**
+ * A test provider that waits before returning results to simulate a slow DB
+ * lookup.
+ */
+class SlowHeuristicProvider extends TestProvider {
+ get type() {
+ return UrlbarUtils.PROVIDER_TYPE.HEURISTIC;
+ }
+
+ async startQuery(context, add) {
+ this._context = context;
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 300));
+ for (let result of this._results) {
+ add(this, result);
+ }
+ }
+}
+
+/**
+ * A fast provider that alerts the test when it has added its results.
+ */
+class FastHeuristicProvider extends TestProvider {
+ get type() {
+ return UrlbarUtils.PROVIDER_TYPE.HEURISTIC;
+ }
+
+ async startQuery(context, add) {
+ this._context = context;
+ for (let result of this._results) {
+ add(this, result);
+ }
+ Services.obs.notifyObservers(null, "results-added");
+ }
+}
+
+add_task(async function setup() {
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+});
+
+/**
+ * Tests that UrlbarProvidersManager._heuristicProviderTimer is cancelled when
+ * a query is cancelled.
+ */
+add_task(async function timerIsCancelled() {
+ let context = createContext("m", { isPrivate: false });
+ await PlacesTestUtils.promiseAsyncUpdates();
+ info("Manually set up query and then overwrite it.");
+ // slowProvider is a stand-in for a slow UrlbarProviderPlaces returning a
+ // non-heuristic result.
+ let slowProvider = new SlowHeuristicProvider({
+ results: [
+ makeVisitResult(context, {
+ uri: `http://mozilla.org/`,
+ title: `mozilla.org/`,
+ }),
+ ],
+ });
+ UrlbarProvidersManager.registerProvider(slowProvider);
+
+ // fastProvider is a stand-in for a fast Autofill returning a heuristic
+ // result.
+ let fastProvider = new FastHeuristicProvider({
+ results: [
+ makeVisitResult(context, {
+ uri: `http://mozilla.com/`,
+ title: `mozilla.com/`,
+ heuristic: true,
+ }),
+ ],
+ });
+ UrlbarProvidersManager.registerProvider(fastProvider);
+ let firstContext = createContext("m", {
+ providers: [slowProvider.name, fastProvider.name],
+ });
+ let secondContext = createContext("ma", {
+ providers: [slowProvider.name, fastProvider.name],
+ });
+
+ let controller = UrlbarTestUtils.newMockController();
+ let queryRecieved, queryCancelled;
+ const controllerListener = {
+ onQueryResults(queryContext) {
+ Assert.equal(
+ queryContext,
+ secondContext,
+ "Only the second query should finish."
+ );
+ queryRecieved = true;
+ },
+ onQueryCancelled(queryContext) {
+ Assert.equal(
+ queryContext,
+ firstContext,
+ "The first query should be cancelled."
+ );
+ Assert.ok(!queryCancelled, "No more than one query should be cancelled.");
+ queryCancelled = true;
+ },
+ };
+ controller.addQueryListener(controllerListener);
+
+ // Wait until FastProvider sends its results to the providers manager.
+ // Then they will be queued up in a _heuristicProvidersTimer, waiting for
+ // the results from SlowProvider.
+ let resultsAddedPromise = new Promise(resolve => {
+ let observe = async (subject, topic, data) => {
+ Services.obs.removeObserver(observe, "results-added");
+ // Fire the second query to cancel the first.
+ await controller.startQuery(secondContext);
+ resolve();
+ };
+
+ Services.obs.addObserver(observe, "results-added");
+ });
+
+ controller.startQuery(firstContext);
+ await resultsAddedPromise;
+
+ Assert.ok(queryCancelled, "At least one query was cancelled.");
+ Assert.ok(queryRecieved, "At least one query finished.");
+ controller.removeQueryListener(controllerListener);
+});
+
+/**
+ * Tests that old autofill results aren't displayed after a query is cancelled.
+ * See bug 1653436.
+ */
+add_task(async function autofillIsCleared() {
+ /**
+ * Steps:
+ * 1. Start query.
+ * 2. Allow UrlbarProviderAutofill to start _getAutofillResult.
+ * 3. Execute a new query with no autofill match, cancelling the first
+ * query.
+ * 4. Test that the old result from UrlbarProviderAutofill isn't displayed.
+ */
+ await PlacesTestUtils.addVisits("http://example.com");
+
+ let firstContext = createContext("e", {
+ providers: ["Autofill", "HeuristicFallback"],
+ });
+ let secondContext = createContext("em", {
+ providers: ["Autofill", "HeuristicFallback"],
+ });
+
+ info("Sanity check: The first query autofills and the second does not.");
+ await check_results({
+ firstContext,
+ autofilled: "example.com",
+ completed: "http://example.com/",
+ matches: [
+ makeVisitResult(firstContext, {
+ uri: "http://example.com/",
+ title: "example.com",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await check_results({
+ secondContext,
+ matches: [
+ makeSearchResult(secondContext, {
+ engineName: (await Services.search.getDefault()).name,
+ providerName: "HeuristicFallback",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // Refresh our queries
+ firstContext = createContext("e", {
+ providers: ["Autofill", "HeuristicFallback"],
+ });
+ secondContext = createContext("em", {
+ providers: ["Autofill", "HeuristicFallback"],
+ });
+
+ // Set up controller to observe queries.
+ let controller = UrlbarTestUtils.newMockController();
+ let queryRecieved, queryCancelled;
+ const controllerListener = {
+ onQueryResults(queryContext) {
+ Assert.equal(
+ queryContext,
+ secondContext,
+ "Only the second query should finish."
+ );
+ queryRecieved = true;
+ },
+ onQueryCancelled(queryContext) {
+ Assert.equal(
+ queryContext,
+ firstContext,
+ "The first query should be cancelled."
+ );
+ Assert.ok(
+ !UrlbarProviderAutofill._autofillData,
+ "The first result should not have populated autofill data."
+ );
+ Assert.ok(!queryCancelled, "No more than one query should be cancelled.");
+ queryCancelled = true;
+ },
+ };
+ controller.addQueryListener(controllerListener);
+
+ // Intentionally do not await this first query.
+ controller.startQuery(firstContext);
+ await controller.startQuery(secondContext);
+
+ Assert.ok(queryCancelled, "At least one query was cancelled.");
+ Assert.ok(queryRecieved, "At least one query finished.");
+ controller.removeQueryListener(controllerListener);
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_hideSponsoredHistory.js b/browser/components/urlbar/tests/unit/test_hideSponsoredHistory.js
new file mode 100644
index 0000000000..d94a655b22
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_hideSponsoredHistory.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This tests the muxer functionality that hides URLs in history that were
+// originally sponsored.
+
+"use strict";
+
+add_task(async function test() {
+ // Disable search suggestions to avoid hitting the network.
+ UrlbarPrefs.set("suggest.searches", false);
+
+ let engine = await Services.search.getDefault();
+ let pref = "browser.newtabpage.activity-stream.hideTopSitesWithSearchParam";
+
+ // This maps URL search params to objects describing whether a URL with those
+ // params is expected to appear in the search results. Each inner object maps
+ // from a value of the pref to whether the URL is expected to appear given the
+ // pref value.
+ let tests = {
+ "": {
+ "": true,
+ test: true,
+ "test=": true,
+ "test=hide": true,
+ nomatch: true,
+ "nomatch=": true,
+ "nomatch=hide": true,
+ },
+ test: {
+ "": true,
+ test: false,
+ "test=": false,
+ "test=hide": true,
+ nomatch: true,
+ "nomatch=": true,
+ "nomatch=hide": true,
+ },
+ "test=hide": {
+ "": true,
+ test: false,
+ "test=": true,
+ "test=hide": false,
+ nomatch: true,
+ "nomatch=": true,
+ "nomatch=hide": true,
+ },
+ "test=foo&test=hide": {
+ "": true,
+ test: false,
+ "test=": true,
+ "test=hide": false,
+ nomatch: true,
+ "nomatch=": true,
+ "nomatch=hide": true,
+ },
+ };
+
+ for (let [urlParams, expected] of Object.entries(tests)) {
+ for (let [prefValue, shouldAppear] of Object.entries(expected)) {
+ info(
+ "Running test: " +
+ JSON.stringify({ urlParams, prefValue, shouldAppear })
+ );
+
+ // Add a visit to a URL with search params `urlParams`.
+ let url = new URL("http://example.com/");
+ url.search = urlParams;
+ await PlacesTestUtils.addVisits(url);
+
+ // Set the pref to `prefValue`.
+ Services.prefs.setCharPref(pref, prefValue);
+
+ // Set up the context and expected results. If `shouldAppear` is true, a
+ // visit result for the URL should appear.
+ let context = createContext("ample", { isPrivate: false });
+ let expectedResults = [
+ makeSearchResult(context, {
+ heuristic: true,
+ engineName: engine.name,
+ engineIconUri: engine.iconURI?.spec,
+ }),
+ ];
+ if (shouldAppear) {
+ expectedResults.push(
+ makeVisitResult(context, {
+ uri: url.toString(),
+ title: "test visit for " + url,
+ })
+ );
+ }
+
+ // Do a search and check the results.
+ await check_results({
+ context,
+ matches: expectedResults,
+ });
+
+ await PlacesUtils.history.clear();
+ }
+ }
+
+ Services.prefs.clearUserPref(pref);
+});
diff --git a/browser/components/urlbar/tests/unit/test_keywords.js b/browser/components/urlbar/tests/unit/test_keywords.js
new file mode 100644
index 0000000000..de60742b81
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_keywords.js
@@ -0,0 +1,212 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+testEngine_setup();
+
+add_task(async function test_non_keyword() {
+ info("Searching for non-keyworded entry should autoFill it");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ });
+ let context = createContext("moz", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.org/",
+ completed: "http://mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/",
+ fallbackTitle: "mozilla.org",
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://mozilla.org/test/",
+ title: "A bookmark",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_keyword() {
+ info("Searching for keyworded entry should not autoFill it");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ keyword: "moz",
+ });
+ let context = createContext("moz", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://mozilla.org/test/",
+ title: "http://mozilla.org/test/",
+ keyword: "moz",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_more_than_keyword() {
+ info("Searching for more than keyworded entry should autoFill it");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ keyword: "moz",
+ });
+ let context = createContext("mozi", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.org/",
+ completed: "http://mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/",
+ fallbackTitle: "mozilla.org",
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://mozilla.org/test/",
+ title: "A bookmark",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_less_than_keyword() {
+ info("Searching for less than keyworded entry should autoFill it");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ keyword: "moz",
+ });
+ let context = createContext("mo", { isPrivate: false });
+ await check_results({
+ context,
+ search: "mo",
+ autofilled: "mozilla.org/",
+ completed: "http://mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/",
+ fallbackTitle: "mozilla.org",
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://mozilla.org/test/",
+ title: "A bookmark",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_keyword_casing() {
+ info("Searching for keyworded entry is case-insensitive");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ keyword: "moz",
+ });
+ let context = createContext("MoZ", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://mozilla.org/test/",
+ title: "http://mozilla.org/test/",
+ keyword: "MoZ",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_less_then_equal_than_keyword_bug_1124238() {
+ info("Searching for less than keyworded entry should autoFill it");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://mozilla.org/test/"),
+ });
+ await PlacesTestUtils.addVisits("http://mozilla.com/");
+ PlacesTestUtils.addBookmarkWithDetails({
+ uri: Services.io.newURI("http://mozilla.com/"),
+ keyword: "moz",
+ });
+
+ let context = createContext("mo", { isPrivate: false });
+ await check_results({
+ context,
+ search: "mo",
+ autofilled: "mozilla.com/",
+ completed: "http://mozilla.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.com/",
+ title: "test visit for http://mozilla.com/",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/test/",
+ title: "test visit for http://mozilla.org/test/",
+ }),
+ ],
+ });
+
+ // Search with an additional character. As the input matches a keyword, the
+ // completion should equal the keyword and not the URI as before.
+ context = createContext("moz", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://mozilla.com/",
+ title: "http://mozilla.com/",
+ keyword: "moz",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/test/",
+ title: "test visit for http://mozilla.org/test/",
+ }),
+ ],
+ });
+
+ // Search with an additional character. The input doesn't match a keyword
+ // anymore, it should be autofilled.
+ context = createContext("mozi", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.com/",
+ completed: "http://mozilla.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://mozilla.com/",
+ title: "A bookmark",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://mozilla.org/test/",
+ title: "test visit for http://mozilla.org/test/",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_l10nCache.js b/browser/components/urlbar/tests/unit/test_l10nCache.js
new file mode 100644
index 0000000000..e92c75fa01
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_l10nCache.js
@@ -0,0 +1,685 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests L10nCache in UrlbarUtils.jsm.
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ L10nCache: "resource:///modules/UrlbarUtils.sys.mjs",
+});
+
+add_task(async function comprehensive() {
+ // Set up a mock localization.
+ let l10n = initL10n({
+ args0a: "Zero args value",
+ args0b: "Another zero args value",
+ args1a: "One arg value is { $arg1 }",
+ args1b: "Another one arg value is { $arg1 }",
+ args2a: "Two arg values are { $arg1 } and { $arg2 }",
+ args2b: "More two arg values are { $arg1 } and { $arg2 }",
+ args3a: "Three arg values are { $arg1 }, { $arg2 }, and { $arg3 }",
+ args3b: "More three arg values are { $arg1 }, { $arg2 }, and { $arg3 }",
+ attrs1: [".label = attrs1 label has zero args"],
+ attrs2: [
+ ".label = attrs2 label has zero args",
+ ".tooltiptext = attrs2 tooltiptext arg value is { $arg1 }",
+ ],
+ attrs3: [
+ ".label = attrs3 label has zero args",
+ ".tooltiptext = attrs3 tooltiptext arg value is { $arg1 }",
+ ".alt = attrs3 alt arg values are { $arg1 } and { $arg2 }",
+ ],
+ });
+
+ let tests = [
+ // different strings with the same number of args and also the same strings
+ // with different args
+ {
+ obj: {
+ id: "args0a",
+ },
+ expected: {
+ value: "Zero args value",
+ attributes: null,
+ },
+ },
+ {
+ obj: {
+ id: "args0b",
+ },
+ expected: {
+ value: "Another zero args value",
+ attributes: null,
+ },
+ },
+ {
+ obj: {
+ id: "args1a",
+ args: { arg1: "foo1" },
+ },
+ expected: {
+ value: "One arg value is foo1",
+ attributes: null,
+ },
+ },
+ {
+ obj: {
+ id: "args1a",
+ args: { arg1: "foo2" },
+ },
+ expected: {
+ value: "One arg value is foo2",
+ attributes: null,
+ },
+ },
+ {
+ obj: {
+ id: "args1b",
+ args: { arg1: "foo1" },
+ },
+ expected: {
+ value: "Another one arg value is foo1",
+ attributes: null,
+ },
+ },
+ {
+ obj: {
+ id: "args1b",
+ args: { arg1: "foo2" },
+ },
+ expected: {
+ value: "Another one arg value is foo2",
+ attributes: null,
+ },
+ },
+ {
+ obj: {
+ id: "args2a",
+ args: { arg1: "foo1", arg2: "bar1" },
+ },
+ expected: {
+ value: "Two arg values are foo1 and bar1",
+ attributes: null,
+ },
+ },
+ {
+ obj: {
+ id: "args2a",
+ args: { arg1: "foo2", arg2: "bar2" },
+ },
+ expected: {
+ value: "Two arg values are foo2 and bar2",
+ attributes: null,
+ },
+ },
+ {
+ obj: {
+ id: "args2b",
+ args: { arg1: "foo1", arg2: "bar1" },
+ },
+ expected: {
+ value: "More two arg values are foo1 and bar1",
+ attributes: null,
+ },
+ },
+ {
+ obj: {
+ id: "args2b",
+ args: { arg1: "foo2", arg2: "bar2" },
+ },
+ expected: {
+ value: "More two arg values are foo2 and bar2",
+ attributes: null,
+ },
+ },
+ {
+ obj: {
+ id: "args3a",
+ args: { arg1: "foo1", arg2: "bar1", arg3: "baz1" },
+ },
+ expected: {
+ value: "Three arg values are foo1, bar1, and baz1",
+ attributes: null,
+ },
+ },
+ {
+ obj: {
+ id: "args3a",
+ args: { arg1: "foo2", arg2: "bar2", arg3: "baz2" },
+ },
+ expected: {
+ value: "Three arg values are foo2, bar2, and baz2",
+ attributes: null,
+ },
+ },
+ {
+ obj: {
+ id: "args3b",
+ args: { arg1: "foo1", arg2: "bar1", arg3: "baz1" },
+ },
+ expected: {
+ value: "More three arg values are foo1, bar1, and baz1",
+ attributes: null,
+ },
+ },
+ {
+ obj: {
+ id: "args3b",
+ args: { arg1: "foo2", arg2: "bar2", arg3: "baz2" },
+ },
+ expected: {
+ value: "More three arg values are foo2, bar2, and baz2",
+ attributes: null,
+ },
+ },
+
+ // two instances of the same string with their args swapped
+ {
+ obj: {
+ id: "args2a",
+ args: { arg1: "arg A", arg2: "arg B" },
+ },
+ expected: {
+ value: "Two arg values are arg A and arg B",
+ attributes: null,
+ },
+ },
+ {
+ obj: {
+ id: "args2a",
+ args: { arg1: "arg B", arg2: "arg A" },
+ },
+ expected: {
+ value: "Two arg values are arg B and arg A",
+ attributes: null,
+ },
+ },
+
+ // strings with attributes
+ {
+ obj: {
+ id: "attrs1",
+ },
+ expected: {
+ value: null,
+ attributes: {
+ label: "attrs1 label has zero args",
+ },
+ },
+ },
+ {
+ obj: {
+ id: "attrs2",
+ args: {
+ arg1: "arg A",
+ },
+ },
+ expected: {
+ value: null,
+ attributes: {
+ label: "attrs2 label has zero args",
+ tooltiptext: "attrs2 tooltiptext arg value is arg A",
+ },
+ },
+ },
+ {
+ obj: {
+ id: "attrs3",
+ args: {
+ arg1: "arg A",
+ arg2: "arg B",
+ },
+ },
+ expected: {
+ value: null,
+ attributes: {
+ label: "attrs3 label has zero args",
+ tooltiptext: "attrs3 tooltiptext arg value is arg A",
+ alt: "attrs3 alt arg values are arg A and arg B",
+ },
+ },
+ },
+ ];
+
+ let cache = new L10nCache(l10n);
+
+ // Get some non-cached strings.
+ Assert.ok(!cache.get({ id: "uncached1" }), "Uncached string 1");
+ Assert.ok(!cache.get({ id: "uncached2", args: "foo" }), "Uncached string 2");
+
+ // Add each test string and get it back.
+ for (let { obj, expected } of tests) {
+ await cache.add(obj);
+ let message = cache.get(obj);
+ Assert.deepEqual(
+ message,
+ expected,
+ "Expected message for obj: " + JSON.stringify(obj)
+ );
+ }
+
+ // Get each string again to make sure each add didn't somehow mess up the
+ // previously added strings.
+ for (let { obj, expected } of tests) {
+ Assert.deepEqual(
+ cache.get(obj),
+ expected,
+ "Expected message for obj: " + JSON.stringify(obj)
+ );
+ }
+
+ // Delete some of the strings. We'll delete every other one to mix it up.
+ for (let i = 0; i < tests.length; i++) {
+ if (i % 2 == 0) {
+ let { obj } = tests[i];
+ cache.delete(obj);
+ Assert.ok(!cache.get(obj), "Deleted from cache: " + JSON.stringify(obj));
+ }
+ }
+
+ // Get each remaining string.
+ for (let i = 0; i < tests.length; i++) {
+ if (i % 2 != 0) {
+ let { obj, expected } = tests[i];
+ Assert.deepEqual(
+ cache.get(obj),
+ expected,
+ "Expected message for obj: " + JSON.stringify(obj)
+ );
+ }
+ }
+
+ // Clear the cache.
+ cache.clear();
+ for (let { obj } of tests) {
+ Assert.ok(!cache.get(obj), "After cache clear: " + JSON.stringify(obj));
+ }
+
+ // `ensure` each test string and get it back.
+ for (let { obj, expected } of tests) {
+ await cache.ensure(obj);
+ let message = cache.get(obj);
+ Assert.deepEqual(
+ message,
+ expected,
+ "Expected message for obj: " + JSON.stringify(obj)
+ );
+
+ // Call `ensure` again. This time, `add` should not be called.
+ let originalAdd = cache.add;
+ cache.add = () => Assert.ok(false, "add erroneously called");
+ await cache.ensure(obj);
+ cache.add = originalAdd;
+ }
+
+ // Clear the cache again.
+ cache.clear();
+ for (let { obj } of tests) {
+ Assert.ok(!cache.get(obj), "After cache clear: " + JSON.stringify(obj));
+ }
+
+ // `ensureAll` the test strings and get them back.
+ let objects = tests.map(({ obj }) => obj);
+ await cache.ensureAll(objects);
+ for (let { obj, expected } of tests) {
+ let message = cache.get(obj);
+ Assert.deepEqual(
+ message,
+ expected,
+ "Expected message for obj: " + JSON.stringify(obj)
+ );
+ }
+
+ // Ensure the cache is cleared after the app locale changes
+ Assert.greater(cache.size(), 0, "The cache has messages in it.");
+ Services.obs.notifyObservers(null, "intl:app-locales-changed");
+ await l10n.ready;
+ Assert.equal(cache.size(), 0, "The cache is empty on app locale change");
+});
+
+// Tests the `excludeArgsFromCacheKey` option.
+add_task(async function excludeArgsFromCacheKey() {
+ // Set up a mock localization.
+ let l10n = initL10n({
+ args0: "Zero args value",
+ args1: "One arg value is { $arg1 }",
+ attrs0: [".label = attrs0 label has zero args"],
+ attrs1: [
+ ".label = attrs1 label has zero args",
+ ".tooltiptext = attrs1 tooltiptext arg value is { $arg1 }",
+ ],
+ });
+
+ let cache = new L10nCache(l10n);
+
+ // Test cases. For each test case, we cache a string using one or more
+ // methods, `cache.add({ excludeArgsFromCacheKey: true })` and/or
+ // `cache.ensure({ excludeArgsFromCacheKey: true })`. After calling each
+ // method, we call `cache.get()` to get the cached string.
+ //
+ // Test cases are cumulative, so when `cache.add()` is called for a string and
+ // then `cache.ensure()` is called for the same string but with different l10n
+ // argument values, the string should be re-cached with the new values.
+ //
+ // Each item in the tests array is: `{ methods, obj, gets }`
+ //
+ // {array} methods
+ // Array of cache method names, one or more of: "add", "ensure"
+ // Methods are called in the order they are listed.
+ // {object} obj
+ // An l10n object that will be passed to the cache methods:
+ // `{ id, args, excludeArgsFromCacheKey }`
+ // {array} gets
+ // An array of objects that describes a series of calls to `cache.get()` and
+ // the expected return values: `{ obj, expected }`
+ //
+ // {object} obj
+ // An l10n object that will be passed to `cache.get():`
+ // `{ id, args, excludeArgsFromCacheKey }`
+ // {object} expected
+ // The expected return value from `get()`.
+ let tests = [
+ // args0: string with no args and no attributes
+ {
+ methods: ["add", "ensure"],
+ obj: {
+ id: "args0",
+ excludeArgsFromCacheKey: true,
+ },
+ gets: [
+ {
+ obj: { id: "args0" },
+ expected: {
+ value: "Zero args value",
+ attributes: null,
+ },
+ },
+ {
+ obj: { id: "args0", excludeArgsFromCacheKey: true },
+ expected: {
+ value: "Zero args value",
+ attributes: null,
+ },
+ },
+ ],
+ },
+
+ // args1: string with one arg and no attributes
+ {
+ methods: ["add"],
+ obj: {
+ id: "args1",
+ args: { arg1: "ADD" },
+ excludeArgsFromCacheKey: true,
+ },
+ gets: [
+ {
+ obj: { id: "args1" },
+ expected: {
+ value: "One arg value is ADD",
+ attributes: null,
+ },
+ },
+ {
+ obj: { id: "args1", excludeArgsFromCacheKey: true },
+ expected: {
+ value: "One arg value is ADD",
+ attributes: null,
+ },
+ },
+ {
+ obj: {
+ id: "args1",
+ args: { arg1: "some other value" },
+ excludeArgsFromCacheKey: true,
+ },
+ expected: {
+ value: "One arg value is ADD",
+ attributes: null,
+ },
+ },
+ {
+ obj: {
+ id: "args1",
+ args: { arg1: "some other value" },
+ },
+ expected: undefined,
+ },
+ ],
+ },
+ {
+ methods: ["ensure"],
+ obj: {
+ id: "args1",
+ args: { arg1: "ENSURE" },
+ excludeArgsFromCacheKey: true,
+ },
+ gets: [
+ {
+ obj: { id: "args1" },
+ expected: {
+ value: "One arg value is ENSURE",
+ attributes: null,
+ },
+ },
+ {
+ obj: { id: "args1", excludeArgsFromCacheKey: true },
+ expected: {
+ value: "One arg value is ENSURE",
+ attributes: null,
+ },
+ },
+ {
+ obj: {
+ id: "args1",
+ args: { arg1: "some other value" },
+ excludeArgsFromCacheKey: true,
+ },
+ expected: {
+ value: "One arg value is ENSURE",
+ attributes: null,
+ },
+ },
+ {
+ obj: {
+ id: "args1",
+ args: { arg1: "some other value" },
+ },
+ expected: undefined,
+ },
+ ],
+ },
+
+ // attrs0: string with no args and one attribute
+ {
+ methods: ["add", "ensure"],
+ obj: {
+ id: "attrs0",
+ excludeArgsFromCacheKey: true,
+ },
+ gets: [
+ {
+ obj: { id: "attrs0" },
+ expected: {
+ value: null,
+ attributes: {
+ label: "attrs0 label has zero args",
+ },
+ },
+ },
+ {
+ obj: { id: "attrs0", excludeArgsFromCacheKey: true },
+ expected: {
+ value: null,
+ attributes: {
+ label: "attrs0 label has zero args",
+ },
+ },
+ },
+ ],
+ },
+
+ // attrs1: string with one arg and two attributes
+ {
+ methods: ["add"],
+ obj: {
+ id: "attrs1",
+ args: { arg1: "ADD" },
+ excludeArgsFromCacheKey: true,
+ },
+ gets: [
+ {
+ obj: { id: "attrs1" },
+ expected: {
+ value: null,
+ attributes: {
+ label: "attrs1 label has zero args",
+ tooltiptext: "attrs1 tooltiptext arg value is ADD",
+ },
+ },
+ },
+ {
+ obj: { id: "attrs1", excludeArgsFromCacheKey: true },
+ expected: {
+ value: null,
+ attributes: {
+ label: "attrs1 label has zero args",
+ tooltiptext: "attrs1 tooltiptext arg value is ADD",
+ },
+ },
+ },
+ {
+ obj: {
+ id: "attrs1",
+ args: { arg1: "some other value" },
+ excludeArgsFromCacheKey: true,
+ },
+ expected: {
+ value: null,
+ attributes: {
+ label: "attrs1 label has zero args",
+ tooltiptext: "attrs1 tooltiptext arg value is ADD",
+ },
+ },
+ },
+ {
+ obj: {
+ id: "attrs1",
+ args: { arg1: "some other value" },
+ },
+ expected: undefined,
+ },
+ ],
+ },
+ {
+ methods: ["ensure"],
+ obj: {
+ id: "attrs1",
+ args: { arg1: "ENSURE" },
+ excludeArgsFromCacheKey: true,
+ },
+ gets: [
+ {
+ obj: { id: "attrs1" },
+ expected: {
+ value: null,
+ attributes: {
+ label: "attrs1 label has zero args",
+ tooltiptext: "attrs1 tooltiptext arg value is ENSURE",
+ },
+ },
+ },
+ {
+ obj: { id: "attrs1", excludeArgsFromCacheKey: true },
+ expected: {
+ value: null,
+ attributes: {
+ label: "attrs1 label has zero args",
+ tooltiptext: "attrs1 tooltiptext arg value is ENSURE",
+ },
+ },
+ },
+ {
+ obj: {
+ id: "attrs1",
+ args: { arg1: "some other value" },
+ excludeArgsFromCacheKey: true,
+ },
+ expected: {
+ value: null,
+ attributes: {
+ label: "attrs1 label has zero args",
+ tooltiptext: "attrs1 tooltiptext arg value is ENSURE",
+ },
+ },
+ },
+ {
+ obj: {
+ id: "attrs1",
+ args: { arg1: "some other value" },
+ },
+ expected: undefined,
+ },
+ ],
+ },
+ ];
+
+ let sandbox = sinon.createSandbox();
+ let spy = sandbox.spy(cache, "add");
+
+ for (let { methods, obj, gets } of tests) {
+ for (let method of methods) {
+ info(`Calling method '${method}' with l10n obj: ` + JSON.stringify(obj));
+ await cache[method](obj);
+
+ // `add()` should always be called: We either just called it directly, or
+ // `ensure({ excludeArgsFromCacheKey: true })` called it.
+ Assert.ok(
+ spy.calledOnce,
+ "add() should have been called once: " + JSON.stringify(obj)
+ );
+ spy.resetHistory();
+
+ for (let { obj: getObj, expected } of gets) {
+ Assert.deepEqual(
+ cache.get(getObj),
+ expected,
+ "Expected message for get: " + JSON.stringify(getObj)
+ );
+ }
+ }
+ }
+
+ sandbox.restore();
+});
+
+/**
+ * Sets up a mock localization.
+ *
+ * @param {object} pairs
+ * Fluent strings as key-value pairs.
+ * @returns {Localization}
+ * The mock Localization object.
+ */
+function initL10n(pairs) {
+ let source = Object.entries(pairs)
+ .map(([key, value]) => {
+ if (Array.isArray(value)) {
+ value = value.map(s => " \n" + s).join("");
+ }
+ return `${key} = ${value}`;
+ })
+ .join("\n");
+ let registry = new L10nRegistry();
+ registry.registerSources([
+ L10nFileSource.createMock(
+ "test",
+ "app",
+ ["en-US"],
+ "/localization/{locale}",
+ [{ source, path: "/localization/en-US/test.ftl" }]
+ ),
+ ]);
+ return new Localization(["/test.ftl"], true, registry, ["en-US"]);
+}
diff --git a/browser/components/urlbar/tests/unit/test_local_suggest_prefs.js b/browser/components/urlbar/tests/unit/test_local_suggest_prefs.js
new file mode 100644
index 0000000000..2d03cc4c54
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_local_suggest_prefs.js
@@ -0,0 +1,126 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim:set ts=2 sw=2 sts=2 et:
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Test for following preferences related to local suggest.
+// * browser.urlbar.suggest.bookmark
+// * browser.urlbar.suggest.history
+// * browser.urlbar.suggest.openpage
+
+testEngine_setup();
+
+add_task(async function setup() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.engines", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false);
+
+ const uri = Services.io.newURI("http://example.com/");
+
+ await PlacesTestUtils.addVisits([{ uri, title: "example" }]);
+ await PlacesUtils.bookmarks.insert({
+ url: uri,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ });
+ await addOpenPages(uri);
+
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.autoFill");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.engines");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions");
+
+ Services.prefs.clearUserPref("browser.urlbar.suggest.bookmark");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.history");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.openpage");
+ await cleanupPlaces();
+ });
+});
+
+add_task(async function test_prefs() {
+ const testData = [
+ {
+ bookmark: true,
+ history: true,
+ openpage: true,
+ },
+ {
+ bookmark: false,
+ history: true,
+ openpage: true,
+ },
+ {
+ bookmark: true,
+ history: false,
+ openpage: true,
+ },
+ {
+ bookmark: true,
+ history: true,
+ openpage: false,
+ },
+ {
+ bookmark: false,
+ history: false,
+ openpage: true,
+ },
+ {
+ bookmark: false,
+ history: true,
+ openpage: false,
+ },
+ {
+ bookmark: true,
+ history: false,
+ openpage: false,
+ },
+ {
+ bookmark: false,
+ history: false,
+ openpage: false,
+ },
+ ];
+
+ for (const { bookmark, history, openpage } of testData) {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", bookmark);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", history);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.openpage", openpage);
+
+ info(`Test bookmark:${bookmark} history:${history} openpage:${openpage}`);
+
+ const context = createContext("e", { isPrivate: false });
+ const matches = [];
+
+ matches.push(
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ })
+ );
+
+ if (openpage) {
+ matches.push(
+ makeTabSwitchResult(context, {
+ uri: "http://example.com/",
+ title: "example",
+ })
+ );
+ } else if (bookmark) {
+ matches.push(
+ makeBookmarkResult(context, {
+ uri: "http://example.com/",
+ title: "example",
+ })
+ );
+ } else if (history) {
+ matches.push(
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ title: "example",
+ })
+ );
+ }
+
+ await check_results({ context, matches });
+ }
+});
diff --git a/browser/components/urlbar/tests/unit/test_match_javascript.js b/browser/components/urlbar/tests/unit/test_match_javascript.js
new file mode 100644
index 0000000000..6cf7126d41
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_match_javascript.js
@@ -0,0 +1,155 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test for bug 417798 to make sure javascript: URIs don't show up unless the
+ * user searches for javascript: explicitly.
+ */
+
+testEngine_setup();
+
+add_task(async function test_javascript_match() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.engines", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.autoFill.searchEngines");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.engines");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions");
+ });
+
+ let uri1 = Services.io.newURI("http://abc/def");
+ let uri2 = Services.io.newURI("javascript:5");
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri2,
+ title: "Title with javascript:",
+ });
+ await PlacesTestUtils.addVisits([
+ { uri: uri1, title: "Title with javascript:" },
+ ]);
+
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ info("Match non-javascript: with plain search");
+ let context = createContext("a", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: uri1.spec,
+ title: "Title with javascript:",
+ }),
+ ],
+ });
+
+ info("Match non-javascript: with 'javascript'");
+ context = createContext("javascript", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: uri1.spec,
+ title: "Title with javascript:",
+ }),
+ ],
+ });
+
+ info("Match non-javascript with 'javascript:'");
+ context = createContext("javascript:", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: uri1.spec,
+ title: "Title with javascript:",
+ }),
+ ],
+ });
+
+ info("Match nothing with '5 javascript:'");
+ context = createContext("5 javascript:", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Match non-javascript: with 'a javascript:'");
+ context = createContext("a javascript:", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: uri1.spec,
+ title: "Title with javascript:",
+ }),
+ ],
+ });
+
+ info("Match non-javascript: and javascript: with 'javascript: a'");
+ context = createContext("javascript: a", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: "javascript: a",
+ fallbackTitle: "javascript: a",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: uri1.spec,
+ title: "Title with javascript:",
+ }),
+ makeBookmarkResult(context, {
+ uri: uri2.spec,
+ iconUri: "chrome://global/skin/icons/defaultFavicon.svg",
+ title: "Title with javascript:",
+ }),
+ ],
+ });
+
+ info("Match javascript: with 'javascript: 5'");
+ context = createContext("javascript: 5", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: "javascript: 5",
+ fallbackTitle: "javascript: 5",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri2.spec,
+ iconUri: "chrome://global/skin/icons/defaultFavicon.svg",
+ title: "Title with javascript:",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_multi_word_search.js b/browser/components/urlbar/tests/unit/test_multi_word_search.js
new file mode 100644
index 0000000000..7054feb8aa
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_multi_word_search.js
@@ -0,0 +1,126 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test for bug 401869 to allow multiple words separated by spaces to match in
+ * the page title, page url, or bookmark title to be considered a match. All
+ * terms must match but not all terms need to be in the title, etc.
+ *
+ * Test bug 424216 by making sure bookmark titles are always shown if one is
+ * available. Also bug 425056 makes sure matches aren't found partially in the
+ * page title and partially in the bookmark.
+ */
+
+testEngine_setup();
+
+add_task(async function test_match_beginning() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+
+ let uri1 = Services.io.newURI("http://a.b.c/d-e_f/h/t/p");
+ let uri2 = Services.io.newURI("http://d.e.f/g-h_i/h/t/p");
+ let uri3 = Services.io.newURI("http://g.h.i/j-k_l/h/t/p");
+ let uri4 = Services.io.newURI("http://j.k.l/m-n_o/h/t/p");
+ await PlacesTestUtils.addVisits([
+ { uri: uri4, title: "f(o)o b<a>r" },
+ { uri: uri3, title: "f(o)o b<a>r" },
+ { uri: uri2, title: "b(a)r b<a>z" },
+ { uri: uri1, title: "f(o)o b<a>r" },
+ ]);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri3,
+ title: "f(o)o b<a>r",
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri4,
+ title: "b(a)r b<a>z",
+ });
+
+ 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 b<a>r" }),
+ ],
+ });
+
+ 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 b<a>r" }),
+ makeVisitResult(context, { uri: uri2.spec, title: "b(a)r b<a>z" }),
+ ],
+ });
+
+ 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 b<a>z" }),
+ makeVisitResult(context, { uri: uri2.spec, title: "b(a)r b<a>z" }),
+ ],
+ });
+
+ 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 b<a>r" }),
+ ],
+ });
+
+ 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 b<a>z" }),
+ ],
+ });
+
+ info("Match nothing");
+ context = createContext("m o z i", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_muxer.js b/browser/components/urlbar/tests/unit/test_muxer.js
new file mode 100644
index 0000000000..dcea820835
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_muxer.js
@@ -0,0 +1,731 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+let sandbox;
+
+add_task(async function setup() {
+ sandbox = lazy.sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+});
+
+add_task(async function test_muxer() {
+ Assert.throws(
+ () => UrlbarProvidersManager.registerMuxer(),
+ /invalid muxer/,
+ "Should throw with no arguments"
+ );
+ Assert.throws(
+ () => UrlbarProvidersManager.registerMuxer({}),
+ /invalid muxer/,
+ "Should throw with empty object"
+ );
+ Assert.throws(
+ () =>
+ UrlbarProvidersManager.registerMuxer({
+ name: "",
+ }),
+ /invalid muxer/,
+ "Should throw with empty name"
+ );
+ Assert.throws(
+ () =>
+ UrlbarProvidersManager.registerMuxer({
+ name: "test",
+ sort: "no",
+ }),
+ /invalid muxer/,
+ "Should throw with invalid sort"
+ );
+
+ let matches = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: "http://mozilla.org/tab/" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ { url: "http://mozilla.org/bookmark/" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/history/" }
+ ),
+ ];
+
+ let provider = registerBasicTestProvider(matches);
+ let context = createContext(undefined, { providers: [provider.name] });
+ let controller = UrlbarTestUtils.newMockController();
+ /**
+ * A test muxer.
+ */
+ class TestMuxer extends UrlbarMuxer {
+ get name() {
+ return "TestMuxer";
+ }
+ sort(queryContext, unsortedResults) {
+ queryContext.results = [...unsortedResults].sort((a, b) => {
+ if (b.source == UrlbarUtils.RESULT_SOURCE.TABS) {
+ return -1;
+ }
+ if (b.source == UrlbarUtils.RESULT_SOURCE.BOOKMARKS) {
+ return 1;
+ }
+ return a.source == UrlbarUtils.RESULT_SOURCE.BOOKMARKS ? -1 : 1;
+ });
+ }
+ }
+ let muxer = new TestMuxer();
+
+ UrlbarProvidersManager.registerMuxer(muxer);
+ context.muxer = "TestMuxer";
+
+ info("Check results, the order should be: bookmark, history, tab");
+ await UrlbarProvidersManager.startQuery(context, controller);
+ Assert.deepEqual(context.results, [matches[1], matches[2], matches[0]]);
+
+ // Sanity check, should not throw.
+ UrlbarProvidersManager.unregisterMuxer(muxer);
+ UrlbarProvidersManager.unregisterMuxer("TestMuxer"); // no-op.
+});
+
+add_task(async function test_preselectedHeuristic_singleProvider() {
+ let matches = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/a" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/b" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/c" }
+ ),
+ ];
+ matches[1].heuristic = true;
+
+ let provider = registerBasicTestProvider(matches);
+ let context = createContext(undefined, {
+ providers: [provider.name],
+ });
+ let controller = UrlbarTestUtils.newMockController();
+
+ info("Check results, the order should be: b (heuristic), a, c");
+ await UrlbarProvidersManager.startQuery(context, controller);
+ Assert.deepEqual(context.results, [matches[1], matches[0], matches[2]]);
+});
+
+add_task(async function test_preselectedHeuristic_multiProviders() {
+ let matches1 = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/a" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/b" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/c" }
+ ),
+ ];
+
+ let matches2 = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/d" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/e" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/f" }
+ ),
+ ];
+ matches2[1].heuristic = true;
+
+ let provider1 = registerBasicTestProvider(matches1);
+ let provider2 = registerBasicTestProvider(matches2);
+
+ let context = createContext(undefined, {
+ providers: [provider1.name, provider2.name],
+ });
+ let controller = UrlbarTestUtils.newMockController();
+
+ info("Check results, the order should be: e (heuristic), a, b, c, d, f");
+ await UrlbarProvidersManager.startQuery(context, controller);
+ Assert.deepEqual(context.results, [
+ matches2[1],
+ ...matches1,
+ matches2[0],
+ matches2[2],
+ ]);
+});
+
+add_task(async function test_suggestions() {
+ Services.prefs.setIntPref("browser.urlbar.maxHistoricalSearchSuggestions", 1);
+
+ let matches = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/a" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/b" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ {
+ engine: "mozSearch",
+ query: "moz",
+ suggestion: "mozzarella",
+ lowerCaseSuggestion: "mozzarella",
+ }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ {
+ engine: "mozSearch",
+ query: "moz",
+ suggestion: "mozilla",
+ lowerCaseSuggestion: "mozilla",
+ }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ {
+ engine: "mozSearch",
+ query: "moz",
+ providesSearchMode: true,
+ keyword: "@moz",
+ }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/c" }
+ ),
+ ];
+
+ let provider = registerBasicTestProvider(matches);
+
+ let context = createContext(undefined, {
+ providers: [provider.name],
+ });
+ let controller = UrlbarTestUtils.newMockController();
+
+ info("Check results, the order should be: mozzarella, moz, a, b, @moz, c");
+ await UrlbarProvidersManager.startQuery(context, controller);
+ Assert.deepEqual(context.results, [
+ matches[2],
+ matches[3],
+ matches[0],
+ matches[1],
+ matches[4],
+ matches[5],
+ ]);
+
+ Services.prefs.clearUserPref("browser.urlbar.maxHistoricalSearchSuggestions");
+});
+
+add_task(async function test_deduplicate_for_unitConversion() {
+ const searchSuggestion = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ {
+ engine: "Google",
+ query: "10cm to m",
+ suggestion: "= 0.1 meters",
+ }
+ );
+ const searchProvider = registerBasicTestProvider(
+ [searchSuggestion],
+ null,
+ UrlbarUtils.PROVIDER_TYPE.PROFILE
+ );
+
+ const unitConversionSuggestion = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ dynamicType: "unitConversion",
+ output: "0.1 m",
+ input: "10cm to m",
+ }
+ );
+ unitConversionSuggestion.suggestedIndex = 1;
+
+ const unitConversion = registerBasicTestProvider(
+ [unitConversionSuggestion],
+ null,
+ UrlbarUtils.PROVIDER_TYPE.PROFILE,
+ "UnitConversion"
+ );
+
+ const context = createContext(undefined, {
+ providers: [searchProvider.name, unitConversion.name],
+ });
+ const controller = UrlbarTestUtils.newMockController();
+ await UrlbarProvidersManager.startQuery(context, controller);
+ Assert.deepEqual(context.results, [unitConversionSuggestion]);
+});
+
+// These results are used in the badHeuristicGroups tests below. The order of
+// the results in the array isn't important because they all get added at the
+// same time. It's the resultGroups in each test that is important.
+const BAD_HEURISTIC_RESULTS = [
+ // heuristic
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/heuristic-0" }
+ ),
+ { heuristic: true }
+ ),
+ // heuristic
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/heuristic-1" }
+ ),
+ { heuristic: true }
+ ),
+ // non-heuristic
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/non-heuristic-0" }
+ ),
+ // non-heuristic
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/non-heuristic-1" }
+ ),
+];
+
+const BAD_HEURISTIC_RESULTS_FIRST_HEURISTIC = BAD_HEURISTIC_RESULTS[0];
+const BAD_HEURISTIC_RESULTS_GENERAL = [
+ BAD_HEURISTIC_RESULTS[2],
+ BAD_HEURISTIC_RESULTS[3],
+];
+
+add_task(async function test_badHeuristicGroups_multiple_0() {
+ await doBadHeuristicGroupsTest(
+ [
+ // 2 heuristics with child groups
+ {
+ maxResultCount: 2,
+ children: [{ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }],
+ },
+ // infinite general
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ [BAD_HEURISTIC_RESULTS_FIRST_HEURISTIC, ...BAD_HEURISTIC_RESULTS_GENERAL]
+ );
+});
+
+add_task(async function test_badHeuristicGroups_multiple_1() {
+ await doBadHeuristicGroupsTest(
+ [
+ // infinite heuristics with child groups
+ {
+ children: [{ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }],
+ },
+ // infinite general
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ [BAD_HEURISTIC_RESULTS_FIRST_HEURISTIC, ...BAD_HEURISTIC_RESULTS_GENERAL]
+ );
+});
+
+add_task(async function test_badHeuristicGroups_multiple_2() {
+ await doBadHeuristicGroupsTest(
+ [
+ // 2 heuristics
+ {
+ maxResultCount: 2,
+ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST,
+ },
+ // infinite general
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ [BAD_HEURISTIC_RESULTS_FIRST_HEURISTIC, ...BAD_HEURISTIC_RESULTS_GENERAL]
+ );
+});
+
+add_task(async function test_badHeuristicGroups_multiple_3() {
+ await doBadHeuristicGroupsTest(
+ [
+ // infinite heuristics
+ {
+ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST,
+ },
+ // infinite general
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ [BAD_HEURISTIC_RESULTS_FIRST_HEURISTIC, ...BAD_HEURISTIC_RESULTS_GENERAL]
+ );
+});
+
+add_task(async function test_badHeuristicGroups_multiple_4() {
+ await doBadHeuristicGroupsTest(
+ [
+ // 1 heuristic with child groups
+ {
+ maxResultCount: 1,
+ children: [{ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }],
+ },
+ // infinite general
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ // 1 heuristic with child groups
+ {
+ maxResultCount: 1,
+ children: [{ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }],
+ },
+ ],
+ [BAD_HEURISTIC_RESULTS_FIRST_HEURISTIC, ...BAD_HEURISTIC_RESULTS_GENERAL]
+ );
+});
+
+add_task(async function test_badHeuristicGroups_multiple_5() {
+ await doBadHeuristicGroupsTest(
+ [
+ // infinite heuristics with child groups
+ {
+ children: [{ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }],
+ },
+ // infinite general
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ // infinite heuristics with child groups
+ {
+ children: [{ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }],
+ },
+ ],
+ [BAD_HEURISTIC_RESULTS_FIRST_HEURISTIC, ...BAD_HEURISTIC_RESULTS_GENERAL]
+ );
+});
+
+add_task(async function test_badHeuristicGroups_multiple_6() {
+ await doBadHeuristicGroupsTest(
+ [
+ // 1 heuristic
+ {
+ maxResultCount: 1,
+ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST,
+ },
+ // infinite general
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ // 1 heuristic
+ {
+ maxResultCount: 1,
+ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST,
+ },
+ ],
+ [BAD_HEURISTIC_RESULTS_FIRST_HEURISTIC, ...BAD_HEURISTIC_RESULTS_GENERAL]
+ );
+});
+
+add_task(async function test_badHeuristicGroups_multiple_7() {
+ await doBadHeuristicGroupsTest(
+ [
+ // infinite heuristics
+ {
+ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST,
+ },
+ // infinite general
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ // infinite heuristics
+ {
+ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST,
+ },
+ ],
+ [BAD_HEURISTIC_RESULTS_FIRST_HEURISTIC, ...BAD_HEURISTIC_RESULTS_GENERAL]
+ );
+});
+
+add_task(async function test_badHeuristicsGroups_notFirst_0() {
+ await doBadHeuristicGroupsTest(
+ [
+ // infinite general first
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ // 1 heuristic with child groups second
+ {
+ maxResultCount: 1,
+ children: [{ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }],
+ },
+ ],
+ [...BAD_HEURISTIC_RESULTS_GENERAL]
+ );
+});
+
+add_task(async function test_badHeuristicsGroups_notFirst_1() {
+ await doBadHeuristicGroupsTest(
+ [
+ // infinite general first
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ // infinite heuristics with child groups second
+ {
+ children: [{ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }],
+ },
+ ],
+ [...BAD_HEURISTIC_RESULTS_GENERAL]
+ );
+});
+
+add_task(async function test_badHeuristicsGroups_notFirst_2() {
+ await doBadHeuristicGroupsTest(
+ [
+ // infinite general first
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ // 1 heuristic second
+ {
+ maxResultCount: 1,
+ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST,
+ },
+ ],
+ [...BAD_HEURISTIC_RESULTS_GENERAL]
+ );
+});
+
+add_task(async function test_badHeuristicsGroups_notFirst_3() {
+ await doBadHeuristicGroupsTest(
+ [
+ // infinite general first
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ // infinite heuristics second
+ {
+ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST,
+ },
+ ],
+ [...BAD_HEURISTIC_RESULTS_GENERAL]
+ );
+});
+
+add_task(async function test_badHeuristicsGroups_notFirst_4() {
+ await doBadHeuristicGroupsTest(
+ [
+ // 1 general first
+ {
+ maxResultCount: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ // infinite heuristics second
+ {
+ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST,
+ },
+ // infinite general third
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ [...BAD_HEURISTIC_RESULTS_GENERAL]
+ );
+});
+
+/**
+ * Sets the resultGroups pref, performs a search, and then checks the results.
+ * Regardless of the groups, the muxer should include at most one heuristic in
+ * its results and it should always be the first result.
+ *
+ * @param {Array} resultGroups
+ * The result groups.
+ * @param {Array} expectedResults
+ * The expected results.
+ */
+async function doBadHeuristicGroupsTest(resultGroups, expectedResults) {
+ sandbox.stub(UrlbarPrefs, "resultGroups").get(() => {
+ return { children: resultGroups };
+ });
+
+ let provider = registerBasicTestProvider(BAD_HEURISTIC_RESULTS);
+ let context = createContext("foo", { providers: [provider.name] });
+ let controller = UrlbarTestUtils.newMockController();
+ await UrlbarProvidersManager.startQuery(context, controller);
+ Assert.deepEqual(context.results, expectedResults);
+
+ sandbox.restore();
+}
+
+// When `maxRichResults` is positive and taken up by suggested-index result(s),
+// both the heuristic and suggested-index results should be included because we
+// (a) make room for the heuristic and (b) assume all suggested-index results
+// should be included even if it means exceeding `maxRichResults`. The specified
+// `maxRichResults` span will be exceeded in this case.
+add_task(async function roomForHeuristic_suggestedIndex() {
+ let results = [
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://example.com/heuristic" }
+ ),
+ { heuristic: true }
+ ),
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://example.com/suggestedIndex" }
+ ),
+ { suggestedIndex: 1 }
+ ),
+ ];
+
+ UrlbarPrefs.set("maxRichResults", 1);
+
+ let provider = registerBasicTestProvider(results);
+ let context = createContext(undefined, { providers: [provider.name] });
+ await check_results({
+ context,
+ matches: results,
+ });
+
+ UrlbarPrefs.clear("maxRichResults");
+});
+
+// When `maxRichResults` is positive but less than the heuristic's result span,
+// the heuristic should be included because we make room for it even if it means
+// exceeding `maxRichResults`. The specified `maxRichResults` span will be
+// exceeded in this case.
+add_task(async function roomForHeuristic_largeResultSpan() {
+ let results = [
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://example.com/heuristic" }
+ ),
+ { heuristic: true, resultSpan: 2 }
+ ),
+ ];
+
+ UrlbarPrefs.set("maxRichResults", 1);
+
+ let provider = registerBasicTestProvider(results);
+ let context = createContext(undefined, { providers: [provider.name] });
+ await check_results({
+ context,
+ matches: results,
+ });
+
+ UrlbarPrefs.clear("maxRichResults");
+});
+
+// When `maxRichResults` is zero and there are no suggested-index results, the
+// heuristic should not be included.
+add_task(async function roomForHeuristic_maxRichResultsZero() {
+ let results = [
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://example.com/heuristic" }
+ ),
+ { heuristic: true }
+ ),
+ ];
+
+ UrlbarPrefs.set("maxRichResults", 0);
+
+ let provider = registerBasicTestProvider(results);
+ let context = createContext(undefined, { providers: [provider.name] });
+ await check_results({
+ context,
+ matches: [],
+ });
+
+ UrlbarPrefs.clear("maxRichResults");
+});
+
+// When `maxRichResults` is zero and suggested-index results are present,
+// neither the heuristic nor the suggested-index results should be included.
+add_task(async function roomForHeuristic_maxRichResultsZero_suggestedIndex() {
+ let results = [
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://example.com/heuristic" }
+ ),
+ { heuristic: true }
+ ),
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://example.com/suggestedIndex" }
+ ),
+ { suggestedIndex: 1 }
+ ),
+ ];
+
+ UrlbarPrefs.set("maxRichResults", 0);
+
+ let provider = registerBasicTestProvider(results);
+ let context = createContext(undefined, { providers: [provider.name] });
+ await check_results({
+ context,
+ matches: [],
+ });
+
+ UrlbarPrefs.clear("maxRichResults");
+});
diff --git a/browser/components/urlbar/tests/unit/test_protocol_ignore.js b/browser/components/urlbar/tests/unit/test_protocol_ignore.js
new file mode 100644
index 0000000000..2e5096cb46
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_protocol_ignore.js
@@ -0,0 +1,42 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test bug 424509 to make sure searching for "h" doesn't match "http" of urls.
+ */
+
+testEngine_setup();
+
+add_task(async function test_escape() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+
+ let uri1 = Services.io.newURI("http://site/");
+ let uri2 = Services.io.newURI("http://happytimes/");
+ await PlacesTestUtils.addVisits([
+ { uri: uri1, title: "title" },
+ { uri: uri2, title: "title" },
+ ]);
+
+ info("Searching for h matches site and not http://");
+ let context = createContext("h", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: uri2.spec,
+ title: "title",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_protocol_swap.js b/browser/components/urlbar/tests/unit/test_protocol_swap.js
new file mode 100644
index 0000000000..980904001c
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_protocol_swap.js
@@ -0,0 +1,303 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test bug 424717 to make sure searching with an existing location like
+ * http://site/ also matches https://site/ or ftp://site/. Same thing for
+ * ftp://site/ and https://site/.
+ *
+ * Test bug 461483 to make sure a search for "w" doesn't match the "www." from
+ * site subdomains.
+ */
+
+testEngine_setup();
+
+add_task(async function test_swap_protocol() {
+ let uri1 = Services.io.newURI("http://www.site/");
+ let uri2 = Services.io.newURI("http://site/");
+ let uri3 = Services.io.newURI("ftp://ftp.site/");
+ let uri4 = Services.io.newURI("ftp://site/");
+ let uri5 = Services.io.newURI("https://www.site/");
+ let uri6 = Services.io.newURI("https://site/");
+ let uri7 = Services.io.newURI("http://woohoo/");
+ let uri8 = Services.io.newURI("http://wwwwwwacko/");
+ await PlacesTestUtils.addVisits([
+ { uri: uri8, title: "title" },
+ { uri: uri7, title: "title" },
+ { uri: uri6, title: "title" },
+ { uri: uri5, title: "title" },
+ { uri: uri4, title: "title" },
+ { uri: uri3, title: "title" },
+ { uri: uri2, title: "title" },
+ { uri: uri1, title: "title" },
+ ]);
+
+ // Disable autoFill to avoid handling the first result.
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+
+ info("http://www.site matches 'www.site' pages");
+ let searchString = "http://www.site";
+ let context = createContext(searchString, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: `${searchString}/`,
+ title: "title",
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri5.spec, title: "title" }),
+ ],
+ });
+
+ info("http://site matches all sites");
+ searchString = "http://site";
+ context = createContext(searchString, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: `${searchString}/`,
+ title: "title",
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri3.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri4.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri6.spec, title: "title" }),
+ ],
+ });
+
+ info("ftp://ftp.site matches itself");
+ searchString = "ftp://ftp.site";
+ context = createContext(searchString, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: `${searchString}/`,
+ fallbackTitle: `${searchString}/`,
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri3.spec, title: "title" }),
+ ],
+ });
+
+ info("ftp://site matches all sites");
+ searchString = "ftp://site";
+ context = createContext(searchString, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: `${searchString}/`,
+ fallbackTitle: `${searchString}/`,
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri3.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri4.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri6.spec, title: "title" }),
+ ],
+ });
+
+ info("https://www.site matches all sites");
+ searchString = "https://www.sit";
+ context = createContext(searchString, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: `${searchString}/`,
+ fallbackTitle: `${searchString}/`,
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri5.spec, title: "title" }),
+ ],
+ });
+
+ info("https://site matches all sites");
+ searchString = "https://sit";
+ context = createContext(searchString, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: `${searchString}/`,
+ fallbackTitle: `${searchString}/`,
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri3.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri4.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri6.spec, title: "title" }),
+ ],
+ });
+
+ info("www.site matches 'www.site' pages");
+ searchString = "www.site";
+ context = createContext(searchString, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: `http://${searchString}/`,
+ title: "title",
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri5.spec, title: "title" }),
+ ],
+ });
+
+ info("w matches 'w' pages, including 'www'");
+ context = createContext("w", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri5.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri7.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri8.spec, title: "title" }),
+ ],
+ });
+
+ info("http://w matches 'w' pages, including 'www'");
+ searchString = "http://w";
+ context = createContext(searchString, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: `${searchString}/`,
+ fallbackTitle: `${searchString}/`,
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri5.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri7.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri8.spec, title: "title" }),
+ ],
+ });
+
+ info("http://www.w matches nothing");
+ searchString = "http://www.w";
+ context = createContext(searchString, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: `${searchString}/`,
+ fallbackTitle: `${searchString}/`,
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("ww matches no 'ww' pages, including 'www'");
+ context = createContext("ww", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri5.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri8.spec, title: "title" }),
+ ],
+ });
+
+ info("http://ww matches no 'ww' pages, including 'www'");
+ searchString = "http://ww";
+ context = createContext(searchString, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: `${searchString}/`,
+ fallbackTitle: `${searchString}/`,
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri5.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri8.spec, title: "title" }),
+ ],
+ });
+
+ info("http://www.ww matches nothing");
+ searchString = "http://www.ww";
+ context = createContext(searchString, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: `${searchString}/`,
+ fallbackTitle: `${searchString}/`,
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("www matches 'www' pages");
+ context = createContext("www", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri5.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri8.spec, title: "title" }),
+ ],
+ });
+
+ info("http://www matches 'www' pages");
+ searchString = "http://www";
+ context = createContext(searchString, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: `${searchString}/`,
+ fallbackTitle: `${searchString}/`,
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri5.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri8.spec, title: "title" }),
+ ],
+ });
+
+ info("http://www.www matches nothing");
+ searchString = "http://www.www";
+ context = createContext(searchString, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ uri: `${searchString}/`,
+ fallbackTitle: `${searchString}/`,
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_providerAliasEngines.js b/browser/components/urlbar/tests/unit/test_providerAliasEngines.js
new file mode 100644
index 0000000000..14ba368f0d
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providerAliasEngines.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests search engine aliases. See
+ * browser/components/urlbar/tests/browser/browser_tokenAlias.js for tests of
+ * the token alias list (i.e. showing all aliased engines on a "@" query).
+ */
+
+testEngine_setup();
+
+// Basic test that uses two engines, a GET engine and a POST engine, neither
+// providing search suggestions.
+add_task(async function basicGetAndPost() {
+ await SearchTestUtils.installSearchExtension({
+ name: "AliasedGETMozSearch",
+ keyword: "get",
+ search_url: "https://s.example.com/search",
+ });
+ await SearchTestUtils.installSearchExtension({
+ name: "AliasedPOSTMozSearch",
+ keyword: "post",
+ search_url: "https://s.example.com/search",
+ search_url_post_params: "q={searchTerms}",
+ });
+
+ for (let alias of ["get", "post"]) {
+ let context = createContext(alias, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ providerName: "HeuristicFallback",
+ }),
+ ],
+ });
+
+ context = createContext(`${alias} `, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: `Aliased${alias.toUpperCase()}MozSearch`,
+ query: "",
+ alias,
+ heuristic: true,
+ providerName: "AliasEngines",
+ }),
+ ],
+ });
+
+ context = createContext(`${alias} fire`, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: `Aliased${alias.toUpperCase()}MozSearch`,
+ query: "fire",
+ alias,
+ heuristic: true,
+ providerName: "AliasEngines",
+ }),
+ ],
+ });
+
+ context = createContext(`${alias} mozilla`, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: `Aliased${alias.toUpperCase()}MozSearch`,
+ query: "mozilla",
+ alias,
+ heuristic: true,
+ providerName: "AliasEngines",
+ }),
+ ],
+ });
+
+ context = createContext(`${alias} MoZiLlA`, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: `Aliased${alias.toUpperCase()}MozSearch`,
+ query: "MoZiLlA",
+ alias,
+ heuristic: true,
+ providerName: "AliasEngines",
+ }),
+ ],
+ });
+
+ context = createContext(`${alias} mozzarella mozilla`, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: `Aliased${alias.toUpperCase()}MozSearch`,
+ query: "mozzarella mozilla",
+ alias,
+ heuristic: true,
+ providerName: "AliasEngines",
+ }),
+ ],
+ });
+ }
+
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_providerHeuristicFallback.js b/browser/components/urlbar/tests/unit/test_providerHeuristicFallback.js
new file mode 100644
index 0000000000..101f6fb21d
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providerHeuristicFallback.js
@@ -0,0 +1,691 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that visit-url and search engine heuristic results are returned by
+ * UrlbarProviderHeuristicFallback.
+ */
+
+const QUICKACTIONS_PREF = "browser.urlbar.suggest.quickactions";
+const SUGGEST_ENABLED_PREF = "browser.search.suggest.enabled";
+const PRIVATE_SEARCH_PREF = "browser.search.separatePrivateDefault.ui.enabled";
+
+// We make sure that restriction tokens and search terms are correctly
+// recognized when they are separated by each of these different types of spaces
+// and combinations of spaces. U+3000 is the ideographic space in CJK and is
+// commonly used by CJK speakers.
+const TEST_SPACES = [" ", "\u3000", " \u3000", "\u3000 "];
+
+testEngine_setup();
+
+add_task(async function setup() {
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref(QUICKACTIONS_PREF);
+ Services.prefs.clearUserPref(SUGGEST_ENABLED_PREF);
+ Services.prefs.clearUserPref(PRIVATE_SEARCH_PREF);
+ Services.prefs.clearUserPref("keyword.enabled");
+ });
+ Services.prefs.setBoolPref(QUICKACTIONS_PREF, false);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, false);
+ Services.prefs.setBoolPref(PRIVATE_SEARCH_PREF, false);
+});
+
+add_task(async function () {
+ info("visit url, no protocol");
+ let query = "mozilla.org";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${query}/`,
+ fallbackTitle: `http://${query}/`,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ ],
+ });
+
+ info("visit url, no protocol but with 2 dots");
+ query = "www.mozilla.org";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${query}/`,
+ fallbackTitle: `http://${query}/`,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ ],
+ });
+
+ info("visit url, no protocol, e-mail like");
+ query = "a@b.com";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${query}/`,
+ fallbackTitle: `http://${query}/`,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ ],
+ });
+
+ info("visit url, with protocol but with 2 dots");
+ query = "https://www.mozilla.org";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `${query}/`,
+ fallbackTitle: `${query}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // info("visit url, with protocol but with 3 dots");
+ query = "https://www.mozilla.org.tw";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `${query}/`,
+ fallbackTitle: `${query}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("visit url, with protocol");
+ query = "https://mozilla.org";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `${query}/`,
+ fallbackTitle: `${query}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("visit url, about: protocol (no host)");
+ query = "about:nonexistent";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: query,
+ fallbackTitle: query,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("visit url, with non-standard whitespace");
+ query = "https://mozilla.org";
+ context = createContext(`${query}\u2028`, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `${query}/`,
+ fallbackTitle: `${query}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // This is distinct because of how we predict being able to url autofill via
+ // host lookups.
+ info("visit url, host matching visited host but not visited url");
+ await PlacesTestUtils.addVisits([
+ {
+ uri: Services.io.newURI("http://mozilla.org/wine/"),
+ title: "Mozilla Wine",
+ transition: PlacesUtils.history.TRANSITION_TYPED,
+ },
+ ]);
+ query = "mozilla.org/rum";
+ context = createContext(`${query}\u2028`, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${query}`,
+ fallbackTitle: `http://${query}`,
+ iconUri: "page-icon:http://mozilla.org/",
+ heuristic: true,
+ }),
+ ],
+ });
+ await PlacesUtils.history.clear();
+
+ // And hosts with no dot in them are special, due to requiring safelisting.
+ info("unknown host");
+ query = "firefox";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("string with known host");
+ query = "firefox/get";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ Services.prefs.setBoolPref("browser.fixup.domainwhitelist.firefox", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.fixup.domainwhitelist.firefox");
+ });
+
+ info("known host");
+ query = "firefox";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${query}/`,
+ fallbackTitle: `http://${query}/`,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ ],
+ });
+
+ info("url with known host");
+ query = "firefox/get";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${query}`,
+ fallbackTitle: `http://${query}`,
+ iconUri: "page-icon:http://firefox/",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("visit url, host matching visited host but not visited url, known host");
+ Services.prefs.setBoolPref("browser.fixup.domainwhitelist.mozilla", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.fixup.domainwhitelist.mozilla");
+ });
+ query = "mozilla/rum";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${query}`,
+ fallbackTitle: `http://${query}`,
+ iconUri: "page-icon:http://mozilla/",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // ipv4 and ipv6 literal addresses should offer to visit.
+ info("visit url, ipv4 literal");
+ query = "127.0.0.1";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${query}/`,
+ fallbackTitle: `http://${query}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("visit url, ipv6 literal");
+ query = "[2001:db8::1]";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${query}/`,
+ fallbackTitle: `http://${query}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // Setting keyword.enabled to false should always try to visit.
+ let keywordEnabled = Services.prefs.getBoolPref("keyword.enabled");
+ Services.prefs.setBoolPref("keyword.enabled", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("keyword.enabled");
+ });
+ info("visit url, keyword.enabled = false");
+ query = "bacon";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${query}/`,
+ fallbackTitle: `http://${query}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("visit two word query, keyword.enabled = false");
+ query = "bacon lovers";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: query,
+ fallbackTitle: query,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Forced search through a restriction token, keyword.enabled = false");
+ query = "?bacon";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ query: "bacon",
+ }),
+ ],
+ });
+
+ Services.prefs.setBoolPref("keyword.enabled", true);
+ info("visit two word query, keyword.enabled = true");
+ query = "bacon lovers";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ Services.prefs.setBoolPref("keyword.enabled", keywordEnabled);
+
+ info("visit url, scheme+host");
+ query = "http://example";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `${query}/`,
+ fallbackTitle: `${query}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("visit url, scheme+host");
+ query = "ftp://example";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `${query}/`,
+ fallbackTitle: `${query}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("visit url, host+port");
+ query = "example:8080";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${query}/`,
+ fallbackTitle: `http://${query}/`,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("numerical operations that look like urls should search");
+ query = "123/12";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("numerical operations that look like urls should search");
+ query = "123.12/12.1";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ query = "resource:///modules";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: query,
+ fallbackTitle: query,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("access resource://app/modules");
+ query = "resource://app/modules";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: query,
+ fallbackTitle: query,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("protocol with an extra slash");
+ query = "http:///";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("change default engine");
+ let originalTestEngine = Services.search.getEngineByName(
+ SUGGESTIONS_ENGINE_NAME
+ );
+ await SearchTestUtils.installSearchExtension({
+ name: "AliasEngine",
+ keyword: "alias",
+ });
+ let engine2 = Services.search.getEngineByName("AliasEngine");
+ Assert.notEqual(
+ Services.search.defaultEngine,
+ engine2,
+ "New engine shouldn't be the current engine yet"
+ );
+ await Services.search.setDefault(
+ engine2,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ query = "toronto";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: "AliasEngine",
+ heuristic: true,
+ }),
+ ],
+ });
+ await Services.search.setDefault(
+ originalTestEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+
+ info(
+ "Leading search-mode restriction tokens are removed from the search result."
+ );
+ for (let token of UrlbarTokenizer.SEARCH_MODE_RESTRICT) {
+ for (let spaces of TEST_SPACES) {
+ query = token + spaces + "query";
+ info("Testing: " + JSON.stringify({ query, spaces: codePoints(spaces) }));
+ let expectedQuery = query.substring(1).trimStart();
+ context = createContext(query, { isPrivate: false });
+ info(`Searching for "${query}", expecting "${expectedQuery}"`);
+ let payload = {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ query: expectedQuery,
+ alias: token,
+ };
+ if (token == UrlbarTokenizer.RESTRICT.SEARCH) {
+ payload.source = UrlbarUtils.RESULT_SOURCE.SEARCH;
+ payload.engineName = SUGGESTIONS_ENGINE_NAME;
+ }
+ await check_results({
+ context,
+ matches: [makeSearchResult(context, payload)],
+ });
+ }
+ }
+
+ info(
+ "Leading search-mode restriction tokens are removed from the search result with keyword.enabled = false."
+ );
+ Services.prefs.setBoolPref("keyword.enabled", false);
+ for (let token of UrlbarTokenizer.SEARCH_MODE_RESTRICT) {
+ for (let spaces of TEST_SPACES) {
+ query = token + spaces + "query";
+ info("Testing: " + JSON.stringify({ query, spaces: codePoints(spaces) }));
+ let expectedQuery = query.substring(1).trimStart();
+ context = createContext(query, { isPrivate: false });
+ info(`Searching for "${query}", expecting "${expectedQuery}"`);
+ let payload = {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ query: expectedQuery,
+ alias: token,
+ };
+ if (token == UrlbarTokenizer.RESTRICT.SEARCH) {
+ payload.source = UrlbarUtils.RESULT_SOURCE.SEARCH;
+ payload.engineName = SUGGESTIONS_ENGINE_NAME;
+ }
+ await check_results({
+ context,
+ matches: [makeSearchResult(context, payload)],
+ });
+ }
+ }
+ Services.prefs.clearUserPref("keyword.enabled");
+
+ info(
+ "Leading non-search-mode restriction tokens are not removed from the search result."
+ );
+ for (let token of Object.values(UrlbarTokenizer.RESTRICT)) {
+ if (UrlbarTokenizer.SEARCH_MODE_RESTRICT.has(token)) {
+ continue;
+ }
+ for (let spaces of TEST_SPACES) {
+ query = token + spaces + "query";
+ info("Testing: " + JSON.stringify({ query, spaces: codePoints(spaces) }));
+ let expectedQuery = query;
+ context = createContext(query, { isPrivate: false });
+ info(`Searching for "${query}", expecting "${expectedQuery}"`);
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ heuristic: true,
+ query: expectedQuery,
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ ],
+ });
+ }
+ }
+
+ info(
+ "Test the format inputed is user@host, and the host is in domainwhitelist"
+ );
+ Services.prefs.setBoolPref("browser.fixup.domainwhitelist.test-host", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.fixup.domainwhitelist.test-host");
+ });
+
+ query = "any@test-host";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${query}/`,
+ fallbackTitle: `http://${query}/`,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ ],
+ });
+
+ info(
+ "Test the format inputed is user@host, but the host is not in domainwhitelist"
+ );
+ query = "any@not-host";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ heuristic: true,
+ query,
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ ],
+ });
+
+ info(
+ "Test if the format of user:pass@host is handled as visit even if the host is not in domainwhitelist"
+ );
+ query = "user:pass@not-host";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://user:pass@not-host/",
+ fallbackTitle: "http://user:pass@not-host/",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Test if the format of user@ipaddress is handled as visit");
+ query = "user@192.168.0.1";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://user@192.168.0.1/",
+ fallbackTitle: "http://user@192.168.0.1/",
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ heuristic: false,
+ query,
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ ],
+ });
+});
+
+/**
+ * Returns an array of code points in the given string. Each code point is
+ * returned as a hexidecimal string.
+ *
+ * @param {string} str
+ * The code points of this string will be returned.
+ * @returns {Array}
+ * Array of code points in the string, where each is a hexidecimal string.
+ */
+function codePoints(str) {
+ return str.split("").map(s => s.charCodeAt(0).toString(16));
+}
diff --git a/browser/components/urlbar/tests/unit/test_providerHistoryUrlHeuristic.js b/browser/components/urlbar/tests/unit/test_providerHistoryUrlHeuristic.js
new file mode 100644
index 0000000000..6f26fee7cb
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providerHistoryUrlHeuristic.js
@@ -0,0 +1,197 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for the behavior of UrlbarProviderHistoryUrlHeuristic.
+
+add_task(async function setup() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.autoFill");
+ });
+});
+
+add_task(async function test_basic() {
+ await PlacesTestUtils.addVisits([
+ { uri: "https://example.com/", title: "Example COM" },
+ ]);
+
+ const testCases = [
+ {
+ input: "https://example.com/",
+ expected: context => [
+ makeVisitResult(context, {
+ uri: "https://example.com/",
+ title: "Example COM",
+ iconUri: "page-icon:https://example.com/",
+ heuristic: true,
+ providerName: "HistoryUrlHeuristic",
+ }),
+ ],
+ },
+ {
+ input: "https://www.example.com/",
+ expected: context => [
+ makeVisitResult(context, {
+ uri: "https://www.example.com/",
+ title: "Example COM",
+ iconUri: "page-icon:https://example.com/",
+ heuristic: true,
+ providerName: "HistoryUrlHeuristic",
+ }),
+ ],
+ },
+ {
+ input: "http://example.com/",
+ expected: context => [
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ title: "Example COM",
+ iconUri: "page-icon:https://example.com/",
+ heuristic: true,
+ providerName: "HistoryUrlHeuristic",
+ }),
+ makeVisitResult(context, {
+ uri: "https://example.com/",
+ title: "Example COM",
+ iconUri: "page-icon:https://example.com/",
+ providerName: "Places",
+ }),
+ ],
+ },
+ {
+ input: "example.com",
+ expected: context => [
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ title: "Example COM",
+ iconUri: "page-icon:https://example.com/",
+ heuristic: true,
+ providerName: "HistoryUrlHeuristic",
+ }),
+ makeVisitResult(context, {
+ uri: "https://example.com/",
+ title: "Example COM",
+ iconUri: "page-icon:https://example.com/",
+ providerName: "Places",
+ }),
+ ],
+ },
+ {
+ input: "www.example.com",
+ expected: context => [
+ makeVisitResult(context, {
+ uri: "http://www.example.com/",
+ title: "Example COM",
+ iconUri: "page-icon:https://example.com/",
+ heuristic: true,
+ providerName: "HistoryUrlHeuristic",
+ }),
+ ],
+ },
+ {
+ input: "htp:example.com",
+ expected: context => [
+ makeVisitResult(context, {
+ uri: "http://example.com/",
+ title: "Example COM",
+ iconUri: "page-icon:https://example.com/",
+ heuristic: true,
+ providerName: "HistoryUrlHeuristic",
+ }),
+ ],
+ },
+ ];
+
+ for (const { input, expected } of testCases) {
+ info(`Test with "${input}"`);
+ const context = createContext(input, { isPrivate: false });
+ await check_results({
+ context,
+ matches: expected(context),
+ });
+ }
+
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function test_null_title() {
+ await PlacesTestUtils.addVisits([{ uri: "https://example.com/", title: "" }]);
+
+ const context = createContext("https://example.com/", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "https://example.com/",
+ fallbackTitle: "https://example.com/",
+ iconUri: "page-icon:https://example.com/",
+ heuristic: true,
+ providerName: "HeuristicFallback",
+ }),
+ ],
+ });
+
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function test_over_max_length_text() {
+ let uri = "https://example.com/";
+ for (; uri.length < UrlbarUtils.MAX_TEXT_LENGTH; ) {
+ uri += "0123456789";
+ }
+
+ await PlacesTestUtils.addVisits([{ uri, title: "Example MAX" }]);
+
+ const context = createContext(uri, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri,
+ fallbackTitle: uri,
+ iconUri: "page-icon:https://example.com/",
+ heuristic: true,
+ providerName: "HeuristicFallback",
+ }),
+ ],
+ });
+
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function test_unsupported_protocol() {
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "about:robots",
+ title: "Robots!",
+ });
+
+ const context = createContext("about:robots", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "about:robots",
+ fallbackTitle: "about:robots",
+ heuristic: true,
+ providerName: "HeuristicFallback",
+ }),
+ makeBookmarkResult(context, {
+ uri: "about:robots",
+ title: "Robots!",
+ }),
+ makeVisitResult(context, {
+ uri: "about:robots",
+ title: "about:robots",
+ tags: null,
+ providerName: "AboutPages",
+ }),
+ ],
+ });
+
+ await PlacesUtils.bookmarks.eraseEverything();
+});
diff --git a/browser/components/urlbar/tests/unit/test_providerKeywords.js b/browser/components/urlbar/tests/unit/test_providerKeywords.js
new file mode 100644
index 0000000000..3be5fe30c0
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providerKeywords.js
@@ -0,0 +1,360 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test for bug 392143 that puts keyword results into the autocomplete. Makes
+ * sure that multiple parameter queries get spaces converted to +, + converted
+ * to %2B, non-ascii become escaped, and pages in history that match the
+ * keyword uses the page's title.
+ *
+ * Also test for bug 249468 by making sure multiple keyword bookmarks with the
+ * same keyword appear in the list.
+ */
+
+testEngine_setup();
+
+add_task(async function test_keyword_search() {
+ let uri1 = "http://abc/?search=%s";
+ let uri2 = "http://abc/?search=ThisPageIsInHistory";
+ let uri3 = "http://abc/?search=%s&raw=%S";
+ let uri4 = "http://abc/?search=%s&raw=%S&mozcharset=ISO-8859-1";
+ let uri5 = "http://def/?search=%s";
+ let uri6 = "http://ghi/?search=%s&raw=%S";
+ let uri7 = "http://somedomain.example/key2";
+ await PlacesTestUtils.addVisits([
+ { uri: uri1 },
+ { uri: uri2 },
+ { uri: uri3 },
+ { uri: uri6 },
+ { uri: uri7 },
+ ]);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri1,
+ title: "Keyword",
+ keyword: "key",
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri1,
+ title: "Post",
+ keyword: "post",
+ postData: "post_search=%s",
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri3,
+ title: "Encoded",
+ keyword: "encoded",
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri4,
+ title: "Charset",
+ keyword: "charset",
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri2,
+ title: "Noparam",
+ keyword: "noparam",
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri2,
+ title: "Noparam-Post",
+ keyword: "post_noparam",
+ postData: "noparam=1",
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri5,
+ title: "Keyword",
+ keyword: "key2",
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri6,
+ title: "Charset-history",
+ keyword: "charset_history",
+ });
+
+ await PlacesUtils.history.update({
+ url: uri6,
+ annotations: new Map([[PlacesUtils.CHARSET_ANNO, "ISO-8859-1"]]),
+ });
+
+ info("Plain keyword query");
+ let context = createContext("key term", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://abc/?search=term",
+ keyword: "key",
+ title: "abc: term",
+ iconUri: "page-icon:http://abc/?search=%s",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Plain keyword UC");
+ context = createContext("key TERM", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://abc/?search=TERM",
+ keyword: "key",
+ title: "abc: TERM",
+ iconUri: "page-icon:http://abc/?search=%s",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Multi-word keyword query");
+ context = createContext("key multi word", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://abc/?search=multi%20word",
+ keyword: "key",
+ title: "abc: multi word",
+ iconUri: "page-icon:http://abc/?search=%s",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Keyword query with +");
+ context = createContext("key blocking+", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://abc/?search=blocking%2B",
+ keyword: "key",
+ title: "abc: blocking+",
+ iconUri: "page-icon:http://abc/?search=%s",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Unescaped term in query");
+ // ... but note that we call encodeURIComponent() on the query string when we
+ // build the URL, so the expected result will have the ユニコード substring
+ // encoded in the URL.
+ context = createContext("key ユニコード", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://abc/?search=" + encodeURIComponent("ユニコード"),
+ keyword: "key",
+ title: "abc: ユニコード",
+ iconUri: "page-icon:http://abc/?search=%s",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Keyword that happens to match a page");
+ context = createContext("key ThisPageIsInHistory", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://abc/?search=ThisPageIsInHistory",
+ keyword: "key",
+ title: "abc: ThisPageIsInHistory",
+ iconUri: "page-icon:http://abc/?search=%s",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Keyword with partial page match");
+ context = createContext("key ThisPage", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://abc/?search=ThisPage",
+ keyword: "key",
+ title: "abc: ThisPage",
+ iconUri: "page-icon:http://abc/?search=%s",
+ heuristic: true,
+ }),
+ // Only the most recent bookmark for the URL:
+ makeBookmarkResult(context, {
+ uri: "http://abc/?search=ThisPageIsInHistory",
+ title: "Noparam-Post",
+ }),
+ ],
+ });
+
+ // For the keyword with no query terms (with or without space after), the
+ // domain is different from the other tests because otherwise all the other
+ // test bookmarks and history entries would be matches.
+ info("Keyword without query (without space)");
+ context = createContext("key2", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://def/?search=",
+ fallbackTitle: "http://def/?search=",
+ keyword: "key2",
+ iconUri: "page-icon:http://def/?search=%s",
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri5,
+ title: "Keyword",
+ }),
+ ],
+ });
+
+ info("Keyword without query (with space)");
+ context = createContext("key2 ", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://def/?search=",
+ fallbackTitle: "http://def/?search=",
+ keyword: "key2",
+ iconUri: "page-icon:http://def/?search=%s",
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri5,
+ title: "Keyword",
+ }),
+ ],
+ });
+
+ info("POST Keyword");
+ context = createContext("post foo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://abc/?search=foo",
+ keyword: "post",
+ title: "abc: foo",
+ postData: "post_search=foo",
+ iconUri: "page-icon:http://abc/?search=%s",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("escaping with default UTF-8 charset");
+ context = createContext("encoded foé", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://abc/?search=fo%C3%A9&raw=foé",
+ keyword: "encoded",
+ title: "abc: foé",
+ iconUri: "page-icon:http://abc/?search=%s&raw=%S",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("escaping with forced ISO-8859-1 charset");
+ context = createContext("charset foé", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://abc/?search=fo%E9&raw=foé",
+ keyword: "charset",
+ title: "abc: foé",
+ iconUri: "page-icon:http://abc/?search=%s&raw=%S&mozcharset=ISO-8859-1",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("escaping with ISO-8859-1 charset annotated in history");
+ context = createContext("charset_history foé", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://ghi/?search=fo%E9&raw=foé",
+ keyword: "charset_history",
+ title: "ghi: foé",
+ iconUri: "page-icon:http://ghi/?search=%s&raw=%S",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Bug 359809: escaping +, / and @ with default UTF-8 charset");
+ context = createContext("encoded +/@", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://abc/?search=%2B%2F%40&raw=+/@",
+ keyword: "encoded",
+ title: "abc: +/@",
+ iconUri: "page-icon:http://abc/?search=%s&raw=%S",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Bug 359809: escaping +, / and @ with forced ISO-8859-1 charset");
+ context = createContext("charset +/@", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://abc/?search=%2B%2F%40&raw=+/@",
+ keyword: "charset",
+ title: "abc: +/@",
+ iconUri: "page-icon:http://abc/?search=%s&raw=%S&mozcharset=ISO-8859-1",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Bug 1228111 - Keyword with a space in front");
+ context = createContext(" key test", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeKeywordSearchResult(context, {
+ uri: "http://abc/?search=test",
+ keyword: "key",
+ title: "abc: test",
+ iconUri: "page-icon:http://abc/?search=%s",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Bug 1481319 - Keyword with a prefix in front");
+ context = createContext("http://key2", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://key2/",
+ fallbackTitle: "http://key2/",
+ heuristic: true,
+ providerName: "HeuristicFallback",
+ }),
+ makeVisitResult(context, {
+ uri: uri7,
+ title: "test visit for http://somedomain.example/key2",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_providerOmnibox.js b/browser/components/urlbar/tests/unit/test_providerOmnibox.js
new file mode 100644
index 0000000000..134158b5b1
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providerOmnibox.js
@@ -0,0 +1,887 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim:set ts=2 sw=2 sts=2 et:
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { ExtensionSearchHandler } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionSearchHandler.sys.mjs"
+);
+
+let controller = Cc["@mozilla.org/autocomplete/controller;1"].getService(
+ Ci.nsIAutoCompleteController
+);
+
+const SUGGEST_PREF = "browser.urlbar.suggest.searches";
+const SUGGEST_ENABLED_PREF = "browser.search.suggest.enabled";
+
+async function cleanup() {
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+}
+
+add_task(function setup() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, false);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, false);
+
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref(SUGGEST_PREF);
+ Services.prefs.clearUserPref(SUGGEST_ENABLED_PREF);
+ });
+});
+
+add_task(async function test_correct_errors_are_thrown() {
+ let keyword = "foo";
+ let anotherKeyword = "bar";
+ let unregisteredKeyword = "baz";
+
+ // Register a keyword.
+ ExtensionSearchHandler.registerKeyword(keyword, { emit: () => {} });
+
+ // Try registering the keyword again.
+ Assert.throws(
+ () => ExtensionSearchHandler.registerKeyword(keyword, { emit: () => {} }),
+ /The keyword provided is already registered/
+ );
+
+ // Register a different keyword.
+ ExtensionSearchHandler.registerKeyword(anotherKeyword, { emit: () => {} });
+
+ // Try calling handleSearch for an unregistered keyword.
+ let searchData = {
+ keyword: unregisteredKeyword,
+ text: `${unregisteredKeyword} `,
+ };
+ Assert.throws(
+ () => ExtensionSearchHandler.handleSearch(searchData, () => {}),
+ /The keyword provided is not registered/
+ );
+
+ // Try calling handleSearch without a callback.
+ Assert.throws(
+ () => ExtensionSearchHandler.handleSearch(searchData),
+ /The keyword provided is not registered/
+ );
+
+ // Try getting the description for a keyword which isn't registered.
+ Assert.throws(
+ () => ExtensionSearchHandler.getDescription(unregisteredKeyword),
+ /The keyword provided is not registered/
+ );
+
+ // Try setting the default suggestion for a keyword which isn't registered.
+ Assert.throws(
+ () =>
+ ExtensionSearchHandler.setDefaultSuggestion(
+ unregisteredKeyword,
+ "suggestion"
+ ),
+ /The keyword provided is not registered/
+ );
+
+ // Try calling handleInputCancelled when there is no active input session.
+ Assert.throws(
+ () => ExtensionSearchHandler.handleInputCancelled(),
+ /There is no active input session/
+ );
+
+ // Try calling handleInputEntered when there is no active input session.
+ Assert.throws(
+ () =>
+ ExtensionSearchHandler.handleInputEntered(
+ anotherKeyword,
+ `${anotherKeyword} test`,
+ "tab"
+ ),
+ /There is no active input session/
+ );
+
+ // Start a session by calling handleSearch with the registered keyword.
+ searchData = {
+ keyword,
+ text: `${keyword} test`,
+ };
+ ExtensionSearchHandler.handleSearch(searchData, () => {});
+
+ // Try providing suggestions for an unregistered keyword.
+ Assert.throws(
+ () => ExtensionSearchHandler.addSuggestions(unregisteredKeyword, 0, []),
+ /The keyword provided is not registered/
+ );
+
+ // Try providing suggestions for an inactive keyword.
+ Assert.throws(
+ () => ExtensionSearchHandler.addSuggestions(anotherKeyword, 0, []),
+ /The keyword provided is not apart of an active input session/
+ );
+
+ // Try calling handleSearch for an inactive keyword.
+ searchData = {
+ keyword: anotherKeyword,
+ text: `${anotherKeyword} `,
+ };
+ Assert.throws(
+ () => ExtensionSearchHandler.handleSearch(searchData, () => {}),
+ /A different input session is already ongoing/
+ );
+
+ // Try calling addSuggestions with an old callback ID.
+ Assert.throws(
+ () => ExtensionSearchHandler.addSuggestions(keyword, 0, []),
+ /The callback is no longer active for the keyword provided/
+ );
+
+ // Add suggestions with a valid callback ID.
+ ExtensionSearchHandler.addSuggestions(keyword, 1, []);
+
+ // Add suggestions again with a valid callback ID.
+ ExtensionSearchHandler.addSuggestions(keyword, 1, []);
+
+ // Try calling addSuggestions with a future callback ID.
+ Assert.throws(
+ () => ExtensionSearchHandler.addSuggestions(keyword, 2, []),
+ /The callback is no longer active for the keyword provided/
+ );
+
+ // End the input session by calling handleInputCancelled.
+ ExtensionSearchHandler.handleInputCancelled();
+
+ // Try calling handleInputCancelled after the session has ended.
+ Assert.throws(
+ () => ExtensionSearchHandler.handleInputCancelled(),
+ /There is no active input sessio/
+ );
+
+ // Try calling handleSearch that doesn't have a space after the keyword.
+ searchData = {
+ keyword: anotherKeyword,
+ text: `${anotherKeyword}`,
+ };
+ Assert.throws(
+ () => ExtensionSearchHandler.handleSearch(searchData, () => {}),
+ /The text provided must start with/
+ );
+
+ // Try calling handleSearch with text starting with the wrong keyword.
+ searchData = {
+ keyword: anotherKeyword,
+ text: `${keyword} test`,
+ };
+ Assert.throws(
+ () => ExtensionSearchHandler.handleSearch(searchData, () => {}),
+ /The text provided must start with/
+ );
+
+ // Start a new session by calling handleSearch with a different keyword
+ searchData = {
+ keyword: anotherKeyword,
+ text: `${anotherKeyword} test`,
+ };
+ ExtensionSearchHandler.handleSearch(searchData, () => {});
+
+ // Try adding suggestions again with the same callback ID now that the input session has ended.
+ Assert.throws(
+ () => ExtensionSearchHandler.addSuggestions(keyword, 1, []),
+ /The keyword provided is not apart of an active input session/
+ );
+
+ // Add suggestions with a valid callback ID.
+ ExtensionSearchHandler.addSuggestions(anotherKeyword, 2, []);
+
+ // Try adding suggestions with a valid callback ID but a different keyword.
+ Assert.throws(
+ () => ExtensionSearchHandler.addSuggestions(keyword, 2, []),
+ /The keyword provided is not apart of an active input session/
+ );
+
+ // Try adding suggestions with a valid callback ID but an unregistered keyword.
+ Assert.throws(
+ () => ExtensionSearchHandler.addSuggestions(unregisteredKeyword, 2, []),
+ /The keyword provided is not registered/
+ );
+
+ // Set the default suggestion.
+ ExtensionSearchHandler.setDefaultSuggestion(anotherKeyword, {
+ description: "test result",
+ });
+
+ // Try ending the session using handleInputEntered with a different keyword.
+ Assert.throws(
+ () =>
+ ExtensionSearchHandler.handleInputEntered(
+ keyword,
+ `${keyword} test`,
+ "tab"
+ ),
+ /A different input session is already ongoing/
+ );
+
+ // Try calling handleInputEntered with invalid text.
+ Assert.throws(
+ () =>
+ ExtensionSearchHandler.handleInputEntered(anotherKeyword, ` test`, "tab"),
+ /The text provided must start with/
+ );
+
+ // Try calling handleInputEntered with an invalid disposition.
+ Assert.throws(
+ () =>
+ ExtensionSearchHandler.handleInputEntered(
+ anotherKeyword,
+ `${anotherKeyword} test`,
+ "invalid"
+ ),
+ /Invalid "where" argument/
+ );
+
+ // End the session by calling handleInputEntered.
+ ExtensionSearchHandler.handleInputEntered(
+ anotherKeyword,
+ `${anotherKeyword} test`,
+ "tab"
+ );
+
+ // Try calling handleInputEntered after the session has ended.
+ Assert.throws(
+ () =>
+ ExtensionSearchHandler.handleInputEntered(
+ anotherKeyword,
+ `${anotherKeyword} test`,
+ "tab"
+ ),
+ /There is no active input session/
+ );
+
+ // Unregister the keyword.
+ ExtensionSearchHandler.unregisterKeyword(keyword);
+
+ // Try setting the default suggestion for the unregistered keyword.
+ Assert.throws(
+ () =>
+ ExtensionSearchHandler.setDefaultSuggestion(keyword, {
+ description: "test",
+ }),
+ /The keyword provided is not registered/
+ );
+
+ // Try handling a search with the unregistered keyword.
+ searchData = {
+ keyword,
+ text: `${keyword} test`,
+ };
+ Assert.throws(
+ () => ExtensionSearchHandler.handleSearch(searchData, () => {}),
+ /The keyword provided is not registered/
+ );
+
+ // Try unregistering the keyword again.
+ Assert.throws(
+ () => ExtensionSearchHandler.unregisterKeyword(keyword),
+ /The keyword provided is not registered/
+ );
+
+ // Unregister the other keyword.
+ ExtensionSearchHandler.unregisterKeyword(anotherKeyword);
+
+ // Try unregistering the word which was never registered.
+ Assert.throws(
+ () => ExtensionSearchHandler.unregisterKeyword(unregisteredKeyword),
+ /The keyword provided is not registered/
+ );
+
+ // Try setting the default suggestion for a word that was never registered.
+ Assert.throws(
+ () =>
+ ExtensionSearchHandler.setDefaultSuggestion(unregisteredKeyword, {
+ description: "test",
+ }),
+ /The keyword provided is not registered/
+ );
+
+ await cleanup();
+});
+
+add_task(async function test_extension_private_browsing() {
+ let events = [];
+ let mockExtension = {
+ emit: message => events.push(message),
+ privateBrowsingAllowed: false,
+ };
+
+ let keyword = "foo";
+
+ ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
+
+ let searchData = {
+ keyword,
+ text: `${keyword} test`,
+ inPrivateWindow: true,
+ };
+ let result = await ExtensionSearchHandler.handleSearch(searchData);
+ Assert.equal(result, false, "unable to handle search for private window");
+
+ // Try calling handleInputEntered after the session has ended.
+ Assert.throws(
+ () =>
+ ExtensionSearchHandler.handleInputEntered(
+ keyword,
+ `${keyword} test`,
+ "tab"
+ ),
+ /There is no active input session/
+ );
+
+ ExtensionSearchHandler.unregisterKeyword(keyword);
+ await cleanup();
+});
+
+add_task(async function test_extension_private_browsing_allowed() {
+ let extensionName = "Foo Bar";
+ let mockExtension = {
+ name: extensionName,
+ emit: (message, text, id) => {
+ if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
+ ExtensionSearchHandler.addSuggestions(keyword, id, [
+ { content: "foo", description: "first suggestion" },
+ { content: "foobar", description: "second suggestion" },
+ ]);
+ // The API doesn't have a way to notify when addition is complete.
+ do_timeout(1000, () => {
+ controller.stopSearch();
+ });
+ }
+ },
+ privateBrowsingAllowed: true,
+ };
+
+ let keyword = "foo";
+ ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
+
+ let query = `${keyword} foo`;
+ let context = createContext(query, { isPrivate: true });
+ await check_results({
+ context,
+ matches: [
+ makeOmniboxResult(context, {
+ heuristic: true,
+ keyword,
+ description: extensionName,
+ content: query,
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} foobar`,
+ description: "second suggestion",
+ }),
+ ],
+ });
+
+ ExtensionSearchHandler.unregisterKeyword(keyword);
+ await cleanup();
+});
+
+add_task(async function test_correct_events_are_emitted() {
+ let events = [];
+ function checkEvents(expectedEvents) {
+ Assert.equal(
+ events.length,
+ expectedEvents.length,
+ "The correct number of events fired"
+ );
+ expectedEvents.forEach((e, i) =>
+ Assert.equal(e, events[i], `Expected "${e}" event to fire`)
+ );
+ events = [];
+ }
+
+ let mockExtension = { emit: message => events.push(message) };
+
+ let keyword = "foo";
+ let anotherKeyword = "bar";
+
+ ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
+ ExtensionSearchHandler.registerKeyword(anotherKeyword, mockExtension);
+
+ let searchData = {
+ keyword,
+ text: `${keyword} `,
+ };
+ ExtensionSearchHandler.handleSearch(searchData, () => {});
+ checkEvents([ExtensionSearchHandler.MSG_INPUT_STARTED]);
+
+ searchData.text = `${keyword} f`;
+ ExtensionSearchHandler.handleSearch(searchData, () => {});
+ checkEvents([ExtensionSearchHandler.MSG_INPUT_CHANGED]);
+
+ ExtensionSearchHandler.handleInputEntered(keyword, searchData.text, "tab");
+ checkEvents([ExtensionSearchHandler.MSG_INPUT_ENTERED]);
+
+ ExtensionSearchHandler.handleSearch(searchData, () => {});
+ checkEvents([
+ ExtensionSearchHandler.MSG_INPUT_STARTED,
+ ExtensionSearchHandler.MSG_INPUT_CHANGED,
+ ]);
+
+ ExtensionSearchHandler.handleInputCancelled();
+ checkEvents([ExtensionSearchHandler.MSG_INPUT_CANCELLED]);
+
+ ExtensionSearchHandler.handleSearch(
+ {
+ keyword: anotherKeyword,
+ text: `${anotherKeyword} baz`,
+ },
+ () => {}
+ );
+ checkEvents([
+ ExtensionSearchHandler.MSG_INPUT_STARTED,
+ ExtensionSearchHandler.MSG_INPUT_CHANGED,
+ ]);
+
+ ExtensionSearchHandler.handleInputEntered(
+ anotherKeyword,
+ `${anotherKeyword} baz`,
+ "tab"
+ );
+ checkEvents([ExtensionSearchHandler.MSG_INPUT_ENTERED]);
+
+ ExtensionSearchHandler.unregisterKeyword(keyword);
+});
+
+add_task(async function test_removes_suggestion_if_its_content_is_typed_in() {
+ let keyword = "test";
+ let extensionName = "Foo Bar";
+
+ let mockExtension = {
+ name: extensionName,
+ emit(message, text, id) {
+ if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
+ ExtensionSearchHandler.addSuggestions(keyword, id, [
+ { content: "foo", description: "first suggestion" },
+ { content: "bar", description: "second suggestion" },
+ { content: "baz", description: "third suggestion" },
+ ]);
+ // The API doesn't have a way to notify when addition is complete.
+ do_timeout(1000, () => {
+ controller.stopSearch();
+ });
+ }
+ },
+ };
+
+ ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
+
+ let query = `${keyword} unmatched`;
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeOmniboxResult(context, {
+ heuristic: true,
+ keyword,
+ description: extensionName,
+ content: `${keyword} unmatched`,
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} foo`,
+ description: "first suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} bar`,
+ description: "second suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} baz`,
+ description: "third suggestion",
+ }),
+ ],
+ });
+
+ query = `${keyword} foo`;
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeOmniboxResult(context, {
+ heuristic: true,
+ keyword,
+ description: extensionName,
+ content: `${keyword} foo`,
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} bar`,
+ description: "second suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} baz`,
+ description: "third suggestion",
+ }),
+ ],
+ });
+
+ query = `${keyword} bar`;
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeOmniboxResult(context, {
+ heuristic: true,
+ keyword,
+ description: extensionName,
+ content: `${keyword} bar`,
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} foo`,
+ description: "first suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} baz`,
+ description: "third suggestion",
+ }),
+ ],
+ });
+
+ query = `${keyword} baz`;
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeOmniboxResult(context, {
+ heuristic: true,
+ keyword,
+ description: extensionName,
+ content: `${keyword} baz`,
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} foo`,
+ description: "first suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} bar`,
+ description: "second suggestion",
+ }),
+ ],
+ });
+
+ ExtensionSearchHandler.unregisterKeyword(keyword);
+ await cleanup();
+});
+
+add_task(async function test_extension_results_should_come_first() {
+ let keyword = "test";
+ let extensionName = "Omnibox Example";
+
+ let uri = Services.io.newURI(`http://a.com/b`);
+ await PlacesTestUtils.addVisits([{ uri, title: `${keyword} -` }]);
+
+ let mockExtension = {
+ name: extensionName,
+ emit(message, text, id) {
+ if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
+ ExtensionSearchHandler.addSuggestions(keyword, id, [
+ { content: "foo", description: "first suggestion" },
+ { content: "bar", description: "second suggestion" },
+ { content: "baz", description: "third suggestion" },
+ ]);
+ }
+ },
+ };
+
+ ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
+
+ // Start an input session before testing MSG_INPUT_CHANGED.
+ ExtensionSearchHandler.handleSearch(
+ { keyword, text: `${keyword} ` },
+ () => {}
+ );
+
+ let query = `${keyword} -`;
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeOmniboxResult(context, {
+ heuristic: true,
+ keyword,
+ description: extensionName,
+ content: `${keyword} -`,
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} foo`,
+ description: "first suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} bar`,
+ description: "second suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} baz`,
+ description: "third suggestion",
+ }),
+ makeVisitResult(context, {
+ uri: `http://a.com/b`,
+ title: `${keyword} -`,
+ }),
+ ],
+ });
+
+ ExtensionSearchHandler.unregisterKeyword(keyword);
+ await cleanup();
+});
+
+add_task(async function test_setting_the_default_suggestion() {
+ let keyword = "test";
+ let extensionName = "Omnibox Example";
+
+ let mockExtension = {
+ name: extensionName,
+ emit(message, text, id) {
+ if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
+ ExtensionSearchHandler.addSuggestions(keyword, id, []);
+ // The API doesn't have a way to notify when addition is complete.
+ do_timeout(1000, () => {
+ controller.stopSearch();
+ });
+ }
+ },
+ };
+
+ ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
+
+ ExtensionSearchHandler.setDefaultSuggestion(keyword, {
+ description: "hello world",
+ });
+
+ let query = `${keyword} search query`;
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeOmniboxResult(context, {
+ heuristic: true,
+ keyword,
+ description: "hello world",
+ content: query,
+ }),
+ ],
+ });
+
+ ExtensionSearchHandler.setDefaultSuggestion(keyword, {
+ description: "foo bar",
+ });
+
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ searchParam: "enable-actions",
+ matches: [
+ makeOmniboxResult(context, {
+ heuristic: true,
+ keyword,
+ description: "foo bar",
+ content: query,
+ }),
+ ],
+ });
+
+ ExtensionSearchHandler.unregisterKeyword(keyword);
+ await cleanup();
+});
+
+add_task(async function test_maximum_number_of_suggestions_is_enforced() {
+ let keyword = "test";
+ let extensionName = "Omnibox Example";
+
+ let mockExtension = {
+ name: extensionName,
+ emit(message, text, id) {
+ if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
+ ExtensionSearchHandler.addSuggestions(keyword, id, [
+ { content: "a", description: "first suggestion" },
+ { content: "b", description: "second suggestion" },
+ { content: "c", description: "third suggestion" },
+ { content: "d", description: "fourth suggestion" },
+ { content: "e", description: "fifth suggestion" },
+ { content: "f", description: "sixth suggestion" },
+ { content: "g", description: "seventh suggestion" },
+ { content: "h", description: "eigth suggestion" },
+ { content: "i", description: "ninth suggestion" },
+ { content: "j", description: "tenth suggestion" },
+ { content: "k", description: "eleventh suggestion" },
+ { content: "l", description: "twelfth suggestion" },
+ ]);
+ // The API doesn't have a way to notify when addition is complete.
+ do_timeout(1000, () => {
+ controller.stopSearch();
+ });
+ }
+ },
+ };
+
+ ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
+
+ // Start an input session before testing MSG_INPUT_CHANGED.
+ ExtensionSearchHandler.handleSearch(
+ { keyword, text: `${keyword} ` },
+ () => {}
+ );
+
+ let query = `${keyword} #`;
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeOmniboxResult(context, {
+ heuristic: true,
+ keyword,
+ description: extensionName,
+ content: `${keyword} #`,
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} a`,
+ description: "first suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} b`,
+ description: "second suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} c`,
+ description: "third suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} d`,
+ description: "fourth suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} e`,
+ description: "fifth suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} f`,
+ description: "sixth suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} g`,
+ description: "seventh suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} h`,
+ description: "eigth suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} i`,
+ description: "ninth suggestion",
+ }),
+ ],
+ });
+
+ ExtensionSearchHandler.unregisterKeyword(keyword);
+ await cleanup();
+});
+
+add_task(async function conflicting_alias() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+
+ let engine = await addTestSuggestionsEngine();
+ let keyword = "test";
+ engine.alias = keyword;
+ let oldDefaultEngine = await Services.search.getDefault();
+ Services.search.setDefault(engine, Ci.nsISearchService.CHANGE_REASON_UNKNOWN);
+
+ let extensionName = "Omnibox Example";
+
+ let mockExtension = {
+ name: extensionName,
+ emit(message, text, id) {
+ if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
+ ExtensionSearchHandler.addSuggestions(keyword, id, [
+ { content: "foo", description: "first suggestion" },
+ { content: "bar", description: "second suggestion" },
+ { content: "baz", description: "third suggestion" },
+ ]);
+ // The API doesn't have a way to notify when addition is complete.
+ do_timeout(1000, () => {
+ controller.stopSearch();
+ });
+ }
+ },
+ };
+
+ ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
+ let query = `${keyword} unmatched`;
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeOmniboxResult(context, {
+ heuristic: true,
+ keyword,
+ description: extensionName,
+ content: `${keyword} unmatched`,
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} foo`,
+ description: "first suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} bar`,
+ description: "second suggestion",
+ }),
+ makeOmniboxResult(context, {
+ keyword,
+ content: `${keyword} baz`,
+ description: "third suggestion",
+ }),
+ makeSearchResult(context, {
+ query: "unmatched",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias: keyword,
+ suggestion: "unmatched",
+ }),
+ makeSearchResult(context, {
+ query: "unmatched",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias: keyword,
+ suggestion: "unmatched foo",
+ }),
+ makeSearchResult(context, {
+ query: "unmatched",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias: keyword,
+ suggestion: "unmatched bar",
+ }),
+ ],
+ });
+
+ Services.search.setDefault(
+ oldDefaultEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ Services.prefs.setBoolPref(SUGGEST_PREF, false);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, false);
+ await cleanup();
+});
diff --git a/browser/components/urlbar/tests/unit/test_providerOpenTabs.js b/browser/components/urlbar/tests/unit/test_providerOpenTabs.js
new file mode 100644
index 0000000000..6cdf632b42
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providerOpenTabs.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_openTabs() {
+ const userContextId = 5;
+ const url = "http://foo.mozilla.org/";
+ UrlbarProviderOpenTabs.registerOpenTab(url, userContextId, false);
+ UrlbarProviderOpenTabs.registerOpenTab(url, userContextId, false);
+ Assert.equal(
+ UrlbarProviderOpenTabs._openTabs.get(userContextId).length,
+ 2,
+ "Found all the expected tabs"
+ );
+ UrlbarProviderOpenTabs.unregisterOpenTab(url, userContextId, false);
+ Assert.equal(
+ UrlbarProviderOpenTabs._openTabs.get(userContextId).length,
+ 1,
+ "Found all the expected tabs"
+ );
+
+ let context = createContext();
+ let matchCount = 0;
+ let callback = function (provider, match) {
+ matchCount++;
+ Assert.ok(
+ provider instanceof UrlbarProviderOpenTabs,
+ "Got the expected provider"
+ );
+ Assert.equal(
+ match.type,
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ "Got the expected result type"
+ );
+ Assert.equal(match.payload.url, url, "Got the expected url");
+ Assert.equal(match.payload.title, undefined, "Got the expected title");
+ };
+
+ let provider = new UrlbarProviderOpenTabs();
+ await provider.startQuery(context, callback);
+ Assert.equal(matchCount, 1, "Found the expected number of matches");
+ // Sanity check that this doesn't throw.
+ provider.cancelQuery(context);
+});
diff --git a/browser/components/urlbar/tests/unit/test_providerPlaces.js b/browser/components/urlbar/tests/unit/test_providerPlaces.js
new file mode 100644
index 0000000000..c64f3345e1
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providerPlaces.js
@@ -0,0 +1,250 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// This is a simple test to check the Places provider works, it is not
+// intended to check all the edge cases, because that component is already
+// covered by a good amount of tests.
+
+const SUGGEST_PREF = "browser.urlbar.suggest.searches";
+const SUGGEST_ENABLED_PREF = "browser.search.suggest.enabled";
+
+add_task(async function test_places() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+ let engine = await addTestSuggestionsEngine();
+ Services.search.defaultEngine = engine;
+ let oldCurrentEngine = Services.search.defaultEngine;
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(SUGGEST_PREF);
+ Services.prefs.clearUserPref(SUGGEST_ENABLED_PREF);
+ Services.search.defaultEngine = oldCurrentEngine;
+ });
+
+ let controller = UrlbarTestUtils.newMockController();
+ // Also check case insensitivity.
+ let searchString = "MoZ oRg";
+ let context = createContext(searchString, { isPrivate: false });
+
+ // Add entries from multiple sources.
+ await PlacesUtils.bookmarks.insert({
+ url: "https://bookmark.mozilla.org/",
+ title: "Test bookmark",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ PlacesUtils.tagging.tagURI(
+ Services.io.newURI("https://bookmark.mozilla.org/"),
+ ["mozilla", "org", "ham", "moz", "bacon"]
+ );
+ await PlacesTestUtils.addVisits([
+ { uri: "https://history.mozilla.org/", title: "Test history" },
+ { uri: "https://tab.mozilla.org/", title: "Test tab" },
+ ]);
+ UrlbarProviderOpenTabs.registerOpenTab("https://tab.mozilla.org/", 0, false);
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ await controller.startQuery(context);
+
+ info(
+ "Results:\n" +
+ context.results.map(m => `${m.title} - ${m.payload.url}`).join("\n")
+ );
+ Assert.equal(
+ context.results.length,
+ 6,
+ "Found the expected number of matches"
+ );
+
+ Assert.deepEqual(
+ [
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_TYPE.URL,
+ ],
+ context.results.map(m => m.type),
+ "Check result types"
+ );
+
+ Assert.deepEqual(
+ [
+ searchString,
+ searchString + " foo",
+ searchString + " bar",
+ "Test bookmark",
+ "Test tab",
+ "Test history",
+ ],
+ context.results.map(m => m.title),
+ "Check match titles"
+ );
+
+ Assert.deepEqual(
+ context.results[3].payload.tags,
+ ["moz", "mozilla", "org"],
+ "Check tags"
+ );
+
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ UrlbarProviderOpenTabs.unregisterOpenTab(
+ "https://tab.mozilla.org/",
+ 0,
+ false
+ );
+});
+
+add_task(async function test_bookmarkBehaviorDisabled_tagged() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, false);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, false);
+
+ // Disable the bookmark behavior.
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+
+ let controller = UrlbarTestUtils.newMockController();
+ // Also check case insensitivity.
+ let searchString = "MoZ oRg";
+ let context = createContext(searchString, { isPrivate: false });
+
+ // Add a tagged bookmark that's also visited.
+ await PlacesUtils.bookmarks.insert({
+ url: "https://bookmark.mozilla.org/",
+ title: "Test bookmark",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ PlacesUtils.tagging.tagURI(
+ Services.io.newURI("https://bookmark.mozilla.org/"),
+ ["mozilla", "org", "ham", "moz", "bacon"]
+ );
+ await PlacesTestUtils.addVisits("https://bookmark.mozilla.org/");
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ await controller.startQuery(context);
+
+ info(
+ "Results:\n" +
+ context.results.map(m => `${m.title} - ${m.payload.url}`).join("\n")
+ );
+ Assert.equal(
+ context.results.length,
+ 2,
+ "Found the expected number of matches"
+ );
+
+ Assert.deepEqual(
+ [UrlbarUtils.RESULT_TYPE.SEARCH, UrlbarUtils.RESULT_TYPE.URL],
+ context.results.map(m => m.type),
+ "Check result types"
+ );
+
+ Assert.deepEqual(
+ [searchString, "Test bookmark"],
+ context.results.map(m => m.title),
+ "Check match titles"
+ );
+
+ Assert.deepEqual(context.results[1].payload.tags, [], "Check tags");
+
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test_bookmarkBehaviorDisabled_untagged() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, false);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, false);
+
+ // Disable the bookmark behavior.
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+
+ let controller = UrlbarTestUtils.newMockController();
+ // Also check case insensitivity.
+ let searchString = "MoZ oRg";
+ let context = createContext(searchString, { isPrivate: false });
+
+ // Add an *untagged* bookmark that's also visited.
+ await PlacesUtils.bookmarks.insert({
+ url: "https://bookmark.mozilla.org/",
+ title: "Test bookmark",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ await PlacesTestUtils.addVisits("https://bookmark.mozilla.org/");
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ await controller.startQuery(context);
+
+ info(
+ "Results:\n" +
+ context.results.map(m => `${m.title} - ${m.payload.url}`).join("\n")
+ );
+ Assert.equal(
+ context.results.length,
+ 2,
+ "Found the expected number of matches"
+ );
+
+ Assert.deepEqual(
+ [UrlbarUtils.RESULT_TYPE.SEARCH, UrlbarUtils.RESULT_TYPE.URL],
+ context.results.map(m => m.type),
+ "Check result types"
+ );
+
+ Assert.deepEqual(
+ [searchString, "Test bookmark"],
+ context.results.map(m => m.title),
+ "Check match titles"
+ );
+
+ Assert.deepEqual(context.results[1].payload.tags, [], "Check tags");
+
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test_diacritics() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, false);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, false);
+
+ // Enable the bookmark behavior.
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true);
+
+ let controller = UrlbarTestUtils.newMockController();
+ let searchString = "agui";
+ let context = createContext(searchString, { isPrivate: false });
+
+ await PlacesUtils.bookmarks.insert({
+ url: "https://bookmark.mozilla.org/%C3%A3g%CC%83u%C4%A9",
+ title: "Test bookmark with accents in path",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ await controller.startQuery(context);
+
+ info(
+ "Results:\n" +
+ context.results.map(m => `${m.title} - ${m.payload.url}`).join("\n")
+ );
+ Assert.equal(
+ context.results.length,
+ 2,
+ "Found the expected number of matches"
+ );
+
+ Assert.deepEqual(
+ [UrlbarUtils.RESULT_TYPE.SEARCH, UrlbarUtils.RESULT_TYPE.URL],
+ context.results.map(m => m.type),
+ "Check result types"
+ );
+
+ Assert.deepEqual(
+ [searchString, "Test bookmark with accents in path"],
+ context.results.map(m => m.title),
+ "Check match titles"
+ );
+
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+});
diff --git a/browser/components/urlbar/tests/unit/test_providerPlaces_duplicate_entries.js b/browser/components/urlbar/tests/unit/test_providerPlaces_duplicate_entries.js
new file mode 100644
index 0000000000..7533921fc6
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providerPlaces_duplicate_entries.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_duplicates() {
+ const TEST_URL = "https://history.mozilla.org/";
+ await PlacesTestUtils.addVisits([
+ { uri: TEST_URL, title: "Test history" },
+ { uri: TEST_URL + "?#", title: "Test history" },
+ { uri: TEST_URL + "#", title: "Test history" },
+ ]);
+
+ let controller = UrlbarTestUtils.newMockController();
+ let searchString = "^Hist";
+ let context = createContext(searchString, { isPrivate: false });
+ await controller.startQuery(context);
+
+ // The first result will be a search heuristic, which we don't care about for
+ // this test.
+ info(
+ "Results:\n" +
+ context.results.map(m => `${m.title} - ${m.payload.url}`).join("\n")
+ );
+ Assert.equal(
+ context.results.length,
+ 2,
+ "Found the expected number of matches"
+ );
+ Assert.equal(
+ context.results[1].type,
+ UrlbarUtils.RESULT_TYPE.URL,
+ "Should have a history result"
+ );
+ Assert.equal(
+ context.results[1].payload.url,
+ TEST_URL + "#",
+ "Check result URL"
+ );
+
+ await PlacesUtils.history.clear();
+});
diff --git a/browser/components/urlbar/tests/unit/test_providerPlaces_nonEnglish.js b/browser/components/urlbar/tests/unit/test_providerPlaces_nonEnglish.js
new file mode 100644
index 0000000000..2cb5f5797a
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providerPlaces_nonEnglish.js
@@ -0,0 +1,43 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+
+Test autocomplete for non-English URLs
+
+- add a visit for a page with a non-English URL
+- search
+- test number of matches (should be exactly one)
+
+*/
+
+testEngine_setup();
+
+add_task(async function test_autocomplete_non_english() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+
+ let searchTerm = "ユニコード";
+ let unescaped = "http://www.foobar.com/" + searchTerm + "/";
+ let uri = Services.io.newURI(unescaped);
+ await PlacesTestUtils.addVisits(uri);
+ let context = createContext(searchTerm, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: uri.spec,
+ title: `test visit for ${uri.spec}`,
+ }),
+ ],
+ });
+});
diff --git a/browser/components/urlbar/tests/unit/test_providerPreloaded.js b/browser/components/urlbar/tests/unit/test_providerPreloaded.js
new file mode 100644
index 0000000000..0785e3afba
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providerPreloaded.js
@@ -0,0 +1,578 @@
+/**
+ * Test for bug 1211726 - preload list of top web sites for better
+ * autocompletion on empty profiles.
+ */
+
+testEngine_setup();
+
+const PREF_FEATURE_ENABLED = "browser.urlbar.usepreloadedtopurls.enabled";
+const PREF_FEATURE_EXPIRE_DAYS =
+ "browser.urlbar.usepreloadedtopurls.expire_days";
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarProviderPreloadedSites:
+ "resource:///modules/UrlbarProviderPreloadedSites.sys.mjs",
+});
+
+Cu.importGlobalProperties(["fetch"]);
+
+let yahoooURI = "https://yahooo.com/";
+let gooogleURI = "https://gooogle.com/";
+
+UrlbarProviderPreloadedSites.populatePreloadedSiteStorage([
+ [yahoooURI, "Yahooo"],
+ [gooogleURI, "Gooogle"],
+]);
+
+async function assert_feature_works(condition) {
+ info("List Results do appear " + condition);
+ let context = createContext("ooo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ heuristic: true,
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ providerName: "HeuristicFallback",
+ }),
+ makeVisitResult(context, {
+ uri: yahoooURI,
+ title: "Yahooo",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ tags: null,
+ iconUri: `page-icon:${yahoooURI}`,
+ providerName: "PreloadedSites",
+ }),
+ makeVisitResult(context, {
+ uri: gooogleURI,
+ title: "Gooogle",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ tags: null,
+ iconUri: `page-icon:${gooogleURI}`,
+ providerName: "PreloadedSites",
+ }),
+ ],
+ });
+
+ info("Autofill does appear " + condition);
+ context = createContext("gooo", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "gooogle.com/",
+ completed: gooogleURI,
+ matches: [
+ makeVisitResult(context, {
+ uri: gooogleURI,
+ fallbackTitle: gooogleURI.slice(0, -1), // Trim trailing slash.
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ iconUri: `page-icon:${gooogleURI}`,
+ providerName: "PreloadedSites",
+ heuristic: true,
+ }),
+ ],
+ });
+}
+
+async function assert_feature_does_not_appear(condition) {
+ info("List Results don't appear " + condition);
+ let context = createContext("ooo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ heuristic: true,
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ providerName: "HeuristicFallback",
+ }),
+ ],
+ });
+
+ info("Autofill doesn't appear " + condition);
+ context = createContext("gooo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ heuristic: true,
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ providerName: "HeuristicFallback",
+ }),
+ ],
+ });
+}
+
+add_task(async function test_it_works() {
+ // Not expired but OFF
+ Services.prefs.setIntPref(PREF_FEATURE_EXPIRE_DAYS, 14);
+ Services.prefs.setBoolPref(PREF_FEATURE_ENABLED, false);
+ await assert_feature_does_not_appear("when OFF by prefs");
+
+ // Now turn it ON
+ Services.prefs.setBoolPref(PREF_FEATURE_ENABLED, true);
+ await assert_feature_works("when ON by prefs");
+
+ // And expire
+ Services.prefs.setIntPref(PREF_FEATURE_EXPIRE_DAYS, 0);
+ await assert_feature_does_not_appear("when expired");
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_sorting_against_bookmark() {
+ let boookmarkURI = "https://boookmark.com/";
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: boookmarkURI,
+ title: "Boookmark",
+ });
+
+ Services.prefs.setBoolPref(PREF_FEATURE_ENABLED, true);
+ Services.prefs.setIntPref(PREF_FEATURE_EXPIRE_DAYS, 14);
+
+ info("Preloaded Top Sites are placed lower than Bookmarks");
+ let context = createContext("ooo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: boookmarkURI,
+ title: "Boookmark",
+ }),
+ makeVisitResult(context, {
+ uri: yahoooURI,
+ title: "Yahooo",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ tags: null,
+ iconUri: `page-icon:${yahoooURI}`,
+ providerName: "PreloadedSites",
+ }),
+ makeVisitResult(context, {
+ uri: gooogleURI,
+ title: "Gooogle",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ tags: null,
+ iconUri: `page-icon:${gooogleURI}`,
+ providerName: "PreloadedSites",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_sorting_against_history() {
+ let histoooryURI = "https://histooory.com/";
+ await PlacesTestUtils.addVisits({ uri: histoooryURI, title: "Histooory" });
+
+ Services.prefs.setBoolPref(PREF_FEATURE_ENABLED, true);
+ Services.prefs.setIntPref(PREF_FEATURE_EXPIRE_DAYS, 14);
+
+ info("Preloaded Top Sites are placed lower than History entries");
+ let context = createContext("ooo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: histoooryURI,
+ title: "Histooory",
+ }),
+ makeVisitResult(context, {
+ uri: yahoooURI,
+ title: "Yahooo",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ tags: null,
+ iconUri: `page-icon:${yahoooURI}`,
+ providerName: "PreloadedSites",
+ }),
+ makeVisitResult(context, {
+ uri: gooogleURI,
+ title: "Gooogle",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ tags: null,
+ iconUri: `page-icon:${gooogleURI}`,
+ providerName: "PreloadedSites",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_scheme_and_www() {
+ // Order is important to check sorting
+ let sites = [
+ ["https://www.ooops-https-www.com/", "Ooops"],
+ ["https://ooops-https.com/", "Ooops"],
+ ["HTTP://ooops-HTTP.com/", "Ooops"],
+ ["HTTP://www.ooops-HTTP-www.com/", "Ooops"],
+ ["https://foo.com/", "Title with www"],
+ ["https://www.bar.com/", "Tile"],
+ ];
+
+ let titlesMap = new Map(sites);
+
+ UrlbarProviderPreloadedSites.populatePreloadedSiteStorage(sites);
+
+ // No matches when just typing the protocol.
+ let context = createContext("https://", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("www.", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "www.ooops-https-www.com/",
+ completed: "https://www.ooops-https-www.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www.ooops-https-www.com/",
+ fallbackTitle: "https://www.ooops-https-www.com",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ iconUri: "page-icon:https://www.ooops-https-www.com/",
+ providerName: "PreloadedSites",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://www.ooops-http-www.com/",
+ title: titlesMap.get("HTTP://www.ooops-HTTP-www.com/"),
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ tags: null,
+ iconUri: "page-icon:http://www.ooops-http-www.com/",
+ providerName: "PreloadedSites",
+ }),
+ makeVisitResult(context, {
+ uri: "https://www.bar.com/",
+ title: titlesMap.get("https://www.bar.com/"),
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ tags: null,
+ iconUri: "page-icon:https://www.bar.com/",
+ providerName: "PreloadedSites",
+ }),
+ ],
+ });
+
+ context = createContext("http://www.", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "http://www.ooops-http-www.com/",
+ completed: "http://www.ooops-http-www.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://www.ooops-http-www.com/",
+ fallbackTitle: "www.ooops-http-www.com",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ iconUri: "page-icon:http://www.ooops-http-www.com/",
+ providerName: "PreloadedSites",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("ftp://ooops", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "ftp://ooops/",
+ fallbackTitle: "ftp://ooops/",
+ providerName: "HeuristicFallback",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("ww", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "www.ooops-https-www.com/",
+ completed: "https://www.ooops-https-www.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www.ooops-https-www.com/",
+ fallbackTitle: "https://www.ooops-https-www.com",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ iconUri: "page-icon:https://www.ooops-https-www.com/",
+ providerName: "PreloadedSites",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://www.ooops-http-www.com/",
+ title: titlesMap.get("HTTP://www.ooops-HTTP-www.com/"),
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ tags: null,
+ iconUri: "page-icon:http://www.ooops-http-www.com/",
+ providerName: "PreloadedSites",
+ }),
+ makeVisitResult(context, {
+ uri: "https://foo.com/",
+ title: titlesMap.get("https://foo.com/"),
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ tags: null,
+ iconUri: "page-icon:https://foo.com/",
+ providerName: "PreloadedSites",
+ }),
+ makeVisitResult(context, {
+ uri: "https://www.bar.com/",
+ title: titlesMap.get("https://www.bar.com/"),
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ tags: null,
+ iconUri: "page-icon:https://www.bar.com/",
+ providerName: "PreloadedSites",
+ }),
+ ],
+ });
+
+ context = createContext("ooops", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "ooops-https-www.com/",
+ completed: "https://www.ooops-https-www.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www.ooops-https-www.com/",
+ fallbackTitle: "https://www.ooops-https-www.com",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ iconUri: "page-icon:https://www.ooops-https-www.com/",
+ providerName: "PreloadedSites",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://ooops-https.com/",
+ title: titlesMap.get("https://ooops-https.com/"),
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ tags: null,
+ iconUri: "page-icon:https://ooops-https.com/",
+ providerName: "PreloadedSites",
+ }),
+ makeVisitResult(context, {
+ uri: "http://ooops-http.com/",
+ title: titlesMap.get("HTTP://ooops-HTTP.com/"),
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ tags: null,
+ iconUri: "page-icon:http://ooops-http.com/",
+ providerName: "PreloadedSites",
+ }),
+ makeVisitResult(context, {
+ uri: "http://www.ooops-http-www.com/",
+ title: titlesMap.get("HTTP://www.ooops-HTTP-www.com/"),
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ tags: null,
+ iconUri: "page-icon:http://www.ooops-http-www.com/",
+ providerName: "PreloadedSites",
+ }),
+ ],
+ });
+
+ context = createContext("www.ooops", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "www.ooops-https-www.com/",
+ completed: "https://www.ooops-https-www.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www.ooops-https-www.com/",
+ fallbackTitle: "https://www.ooops-https-www.com",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ iconUri: "page-icon:https://www.ooops-https-www.com/",
+ providerName: "PreloadedSites",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://www.ooops-http-www.com/",
+ title: titlesMap.get("HTTP://www.ooops-HTTP-www.com/"),
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ tags: null,
+ iconUri: "page-icon:http://www.ooops-http-www.com/",
+ providerName: "PreloadedSites",
+ }),
+ ],
+ });
+
+ context = createContext("ooops-https-www", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "ooops-https-www.com/",
+ completed: "https://www.ooops-https-www.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www.ooops-https-www.com/",
+ fallbackTitle: "https://www.ooops-https-www.com",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ iconUri: "page-icon:https://www.ooops-https-www.com/",
+ providerName: "PreloadedSites",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("www.ooops-https.", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://www.ooops-https./",
+ fallbackTitle: "http://www.ooops-https./",
+ providerName: "HeuristicFallback",
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ providerName: "HeuristicFallback",
+ }),
+ ],
+ });
+
+ context = createContext("https://ooops", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "https://ooops-https-www.com/",
+ completed: "https://www.ooops-https-www.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www.ooops-https-www.com/",
+ fallbackTitle: "https://www.ooops-https-www.com",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ iconUri: "page-icon:https://www.ooops-https-www.com/",
+ providerName: "PreloadedSites",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://ooops-https.com/",
+ title: titlesMap.get("https://ooops-https.com/"),
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ tags: null,
+ iconUri: "page-icon:https://ooops-https.com/",
+ providerName: "PreloadedSites",
+ }),
+ ],
+ });
+
+ context = createContext("https://www.ooops", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "https://www.ooops-https-www.com/",
+ completed: "https://www.ooops-https-www.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www.ooops-https-www.com/",
+ fallbackTitle: "https://www.ooops-https-www.com",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ iconUri: "page-icon:https://www.ooops-https-www.com/",
+ providerName: "PreloadedSites",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("http://www.ooops-http.", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://www.ooops-http./",
+ fallbackTitle: "http://www.ooops-http./",
+ providerName: "HeuristicFallback",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("http://ooops-https", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://ooops-https/",
+ fallbackTitle: "http://ooops-https/",
+ providerName: "HeuristicFallback",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_data_file() {
+ let response = await fetch(
+ "chrome://browser/content/urlbar/preloaded-top-urls.json"
+ );
+
+ info("Source file is supplied and fetched OK");
+ Assert.ok(response.ok);
+
+ info("The JSON is parsed");
+ let sites = await response.json();
+
+ // Add test site so this test doesn't depend on the contents of the data file.
+ sites.push(["https://www.example.com/", "Example"]);
+
+ info("Storage is populated");
+ UrlbarProviderPreloadedSites.populatePreloadedSiteStorage(sites);
+
+ let lastSite = sites.pop();
+ let uri = Services.io.newURI(lastSite[0]);
+
+ info("Storage is populated from JSON correctly");
+ let context = createContext(uri.host, { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: uri.host + "/",
+ completed: uri.spec,
+ matches: [
+ makeVisitResult(context, {
+ uri: uri.spec,
+ fallbackTitle: uri.spec.slice(0, -1), // Trim trailing slash.
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ iconUri: `page-icon:${uri.spec}`,
+ providerName: "PreloadedSites",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_partial_scheme() {
+ // "tt" should not result in a match of "ttps://whatever.com/".
+ let testUrl = "http://www.ttt.com/";
+ UrlbarProviderPreloadedSites.populatePreloadedSiteStorage([
+ [testUrl, "Test"],
+ ]);
+ let context = createContext("tt", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "ttt.com/",
+ completed: testUrl,
+ matches: [
+ makeVisitResult(context, {
+ uri: testUrl,
+ fallbackTitle: "www.ttt.com",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ iconUri: `page-icon:${testUrl}`,
+ heuristic: true,
+ providerName: "PreloadedSites",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_providerTabToSearch.js b/browser/components/urlbar/tests/unit/test_providerTabToSearch.js
new file mode 100644
index 0000000000..fe625f7bb9
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providerTabToSearch.js
@@ -0,0 +1,535 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests UrlbarProviderTabToSearch. See also
+ * browser/components/urlbar/tests/browser/browser_tabToSearch.js
+ */
+
+"use strict";
+
+let testEngine;
+
+add_task(async function init() {
+ // Disable search suggestions for a less verbose test.
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", false);
+ // Disable tab-to-search onboarding results. Those are covered in
+ // browser/components/urlbar/tests/browser/browser_tabToSearch.js.
+ Services.prefs.setIntPref(
+ "browser.urlbar.tabToSearch.onboard.interactionsLeft",
+ 0
+ );
+ await SearchTestUtils.installSearchExtension({ name: "Test" });
+ testEngine = await Services.search.getEngineByName("Test");
+
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref(
+ "browser.urlbar.tabToSearch.onboard.interactionsLeft"
+ );
+ Services.prefs.clearUserPref("browser.search.suggest.enabled");
+ });
+});
+
+// Tests that tab-to-search results appear when the engine's result domain is
+// autofilled.
+add_task(async function basic() {
+ await PlacesTestUtils.addVisits(["https://example.com/"]);
+ let context = createContext("examp", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ completed: "https://example.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://example.com/",
+ title: "test visit for https://example.com/",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ makeSearchResult(context, {
+ engineName: testEngine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(testEngine.searchUrlDomain),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ ],
+ });
+
+ info("Repeat the search but with tab-to-search disabled through pref.");
+ Services.prefs.setBoolPref("browser.urlbar.suggest.engines", false);
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ completed: "https://example.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://example.com/",
+ title: "test visit for https://example.com/",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ ],
+ });
+ Services.prefs.clearUserPref("browser.urlbar.suggest.engines");
+
+ await cleanupPlaces();
+});
+
+// Tests that tab-to-search results are shown when the typed string matches an
+// engine domain even when there is no autofill.
+add_task(async function noAutofill() {
+ // Note we are not adding any history visits.
+ let context = createContext("examp", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: Services.search.defaultEngine.name,
+ engineIconUri: Services.search.defaultEngine.iconURI?.spec,
+ heuristic: true,
+ providerName: "HeuristicFallback",
+ }),
+ makeSearchResult(context, {
+ engineName: testEngine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(testEngine.searchUrlDomain),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ ],
+ });
+});
+
+// Tests that tab-to-search results are not shown when the typed string matches
+// an engine domain, but something else is being autofilled.
+add_task(async function autofillDoesNotMatchEngine() {
+ await PlacesTestUtils.addVisits(["https://example.test.ca/"]);
+ let context = createContext("example", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "example.test.ca/",
+ completed: "https://example.test.ca/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://example.test.ca/",
+ title: "test visit for https://example.test.ca/",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+// Tests that www. is ignored for the purposes of matching autofill to
+// tab-to-search.
+add_task(async function ignoreWww() {
+ // The history result has www., the engine does not.
+ await PlacesTestUtils.addVisits(["https://www.example.com/"]);
+ let context = createContext("www.examp", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "www.example.com/",
+ completed: "https://www.example.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www.example.com/",
+ title: "test visit for https://www.example.com/",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ makeSearchResult(context, {
+ engineName: testEngine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(testEngine.searchUrlDomain),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+
+ // The engine has www., the history result does not.
+ await PlacesTestUtils.addVisits(["https://foo.bar/"]);
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: "TestWww",
+ search_url: "https://www.foo.bar/",
+ },
+ { skipUnload: true }
+ );
+ let wwwTestEngine = Services.search.getEngineByName("TestWww");
+ context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "foo.bar/",
+ completed: "https://foo.bar/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://foo.bar/",
+ title: "test visit for https://foo.bar/",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ makeSearchResult(context, {
+ engineName: wwwTestEngine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(
+ wwwTestEngine.searchUrlDomain
+ ),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+
+ // Both the engine and the history result have www.
+ await PlacesTestUtils.addVisits(["https://www.foo.bar/"]);
+ context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "foo.bar/",
+ completed: "https://www.foo.bar/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www.foo.bar/",
+ title: "test visit for https://www.foo.bar/",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ makeSearchResult(context, {
+ engineName: wwwTestEngine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(
+ wwwTestEngine.searchUrlDomain
+ ),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+
+ await extension.unload();
+});
+
+// Tests that when a user's query causes autofill to replace one engine's domain
+// with another, the correct tab-to-search results are shown.
+add_task(async function conflictingEngines() {
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits([
+ "https://foobar.com/",
+ "https://foo.com/",
+ ]);
+ }
+ let extension1 = await SearchTestUtils.installSearchExtension(
+ {
+ name: "TestFooBar",
+ search_url: "https://foobar.com/",
+ },
+ { skipUnload: true }
+ );
+ let extension2 = await SearchTestUtils.installSearchExtension(
+ {
+ name: "TestFoo",
+ search_url: "https://foo.com/",
+ },
+ { skipUnload: true }
+ );
+ let fooBarTestEngine = Services.search.getEngineByName("TestFooBar");
+ let fooTestEngine = Services.search.getEngineByName("TestFoo");
+
+ // Search for "foo", autofilling foo.com. Observe that the foo.com
+ // tab-to-search result is shown, even though the foobar.com engine was added
+ // first (and thus enginesForDomainPrefix puts it earlier in its returned
+ // array.)
+ let context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "foo.com/",
+ completed: "https://foo.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://foo.com/",
+ title: "test visit for https://foo.com/",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ makeSearchResult(context, {
+ engineName: fooTestEngine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(
+ fooTestEngine.searchUrlDomain
+ ),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ makeVisitResult(context, {
+ uri: "https://foobar.com/",
+ title: "test visit for https://foobar.com/",
+ providerName: "Places",
+ }),
+ ],
+ });
+
+ // Search for "foob", autofilling foobar.com. Observe that the foo.com
+ // tab-to-search result is replaced with the foobar.com tab-to-search result.
+ context = createContext("foob", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "foobar.com/",
+ completed: "https://foobar.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://foobar.com/",
+ title: "test visit for https://foobar.com/",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ makeSearchResult(context, {
+ engineName: fooBarTestEngine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(
+ fooBarTestEngine.searchUrlDomain
+ ),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+ await extension1.unload();
+ await extension2.unload();
+});
+
+add_task(async function multipleEnginesForHostname() {
+ info(
+ "In case of multiple engines only one tab-to-search result should be returned"
+ );
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: "TestMaps",
+ search_url: "https://example.com/maps/",
+ },
+ { skipUnload: true }
+ );
+
+ let context = createContext("examp", { isPrivate: false });
+ let maxResultCount = UrlbarPrefs.get("maxRichResults");
+
+ // Add enough visits to autofill example.com.
+ for (let i = 0; i < maxResultCount; i++) {
+ await PlacesTestUtils.addVisits("https://example.com/");
+ }
+
+ // Add enough visits to other URLs matching our query to fill up the list of
+ // results.
+ let otherVisitResults = [];
+ for (let i = 0; i < maxResultCount; i++) {
+ let url = "https://mochi.test:8888/example/" + i;
+ await PlacesTestUtils.addVisits(url);
+ otherVisitResults.unshift(
+ makeVisitResult(context, {
+ uri: url,
+ title: "test visit for " + url,
+ })
+ );
+ }
+
+ await check_results({
+ context,
+ autofilled: "example.com/",
+ completed: "https://example.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://example.com/",
+ title: "test visit for https://example.com/",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ makeSearchResult(context, {
+ engineName: testEngine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(testEngine.searchUrlDomain),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ // There should be `maxResultCount` - 2 other visit results. If this fails
+ // because there are actually `maxResultCount` - 3 other results, then the
+ // muxer is improperly including both TabToSearch results in its
+ // calculation of the total available result span instead of only one, so
+ // one fewer visit result appears than expected.
+ ...otherVisitResults.slice(0, maxResultCount - 2),
+ ],
+ });
+ await cleanupPlaces();
+ await extension.unload();
+});
+
+add_task(async function test_casing() {
+ info("Tab-to-search results appear also in case of different casing.");
+ await PlacesTestUtils.addVisits(["https://example.com/"]);
+ let context = createContext("eXAm", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "eXAmple.com/",
+ completed: "https://example.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://example.com/",
+ title: "test visit for https://example.com/",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ makeSearchResult(context, {
+ engineName: testEngine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(testEngine.searchUrlDomain),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_publicSuffix() {
+ info("Tab-to-search results appear also in case of partial host match.");
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: "MyTest",
+ search_url: "https://test.mytest.it/",
+ },
+ { skipUnload: true }
+ );
+ let engine = Services.search.getEngineByName("MyTest");
+ await PlacesTestUtils.addVisits(["https://test.mytest.it/"]);
+ let context = createContext("my", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: Services.search.defaultEngine.name,
+ engineIconUri: Services.search.defaultEngine.iconURI?.spec,
+ heuristic: true,
+ providerName: "HeuristicFallback",
+ }),
+ makeSearchResult(context, {
+ engineName: engine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(engine.searchUrlDomain),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ satisfiesAutofillThreshold: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://test.mytest.it/",
+ title: "test visit for https://test.mytest.it/",
+ providerName: "Places",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+ await extension.unload();
+});
+
+add_task(async function test_publicSuffixIsHost() {
+ info("Tab-to-search results does not appear in case we autofill a suffix.");
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: "SuffixTest",
+ search_url: "https://somesuffix.com.mx/",
+ },
+ { skipUnload: true }
+ );
+
+ // The top level domain will be autofilled, not the full domain.
+ await PlacesTestUtils.addVisits(["https://com.mx/"]);
+ let context = createContext("co", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "com.mx/",
+ completed: "https://com.mx/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://com.mx/",
+ title: "test visit for https://com.mx/",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+ await extension.unload();
+});
+
+add_task(async function test_disabledEngine() {
+ info("Tab-to-search results does not appear for a Pref-disabled engine.");
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: "Disabled",
+ search_url: "https://disabled.com/",
+ },
+ { skipUnload: true }
+ );
+ let engine = Services.search.getEngineByName("Disabled");
+ await PlacesTestUtils.addVisits(["https://disabled.com/"]);
+ let context = createContext("dis", { isPrivate: false });
+
+ info("Sanity check that the engine would appear.");
+ await check_results({
+ context,
+ autofilled: "disabled.com/",
+ completed: "https://disabled.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://disabled.com/",
+ title: "test visit for https://disabled.com/",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ makeSearchResult(context, {
+ engineName: engine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(engine.searchUrlDomain),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ ],
+ });
+
+ info("Now disable the engine.");
+ Services.prefs.setCharPref("browser.search.hiddenOneOffs", engine.name);
+ await check_results({
+ context,
+ autofilled: "disabled.com/",
+ completed: "https://disabled.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://disabled.com/",
+ title: "test visit for https://disabled.com/",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ ],
+ });
+ Services.prefs.clearUserPref("browser.search.hiddenOneOffs");
+
+ await cleanupPlaces();
+ await extension.unload();
+});
diff --git a/browser/components/urlbar/tests/unit/test_providerTabToSearch_partialHost.js b/browser/components/urlbar/tests/unit/test_providerTabToSearch_partialHost.js
new file mode 100644
index 0000000000..d2391c0f43
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providerTabToSearch_partialHost.js
@@ -0,0 +1,214 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Search engine origins are autofilled normally when they get over the
+// threshold, though certain origins redirect to localized subdomains, that
+// the user is unlikely to type, for example wikipedia.org => en.wikipedia.org.
+// We should get a tab to search result also for these cases, where a normal
+// autofill wouldn't happen.
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarProviderAutofill: "resource:///modules/UrlbarProviderAutofill.sys.mjs",
+});
+
+add_task(async function setup() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false);
+ // Disable tab-to-search onboarding results.
+ Services.prefs.setIntPref(
+ "browser.urlbar.tabToSearch.onboard.interactionsLeft",
+ 0
+ );
+ Services.prefs.setBoolPref(
+ "browser.search.separatePrivateDefault.ui.enabled",
+ false
+ );
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions");
+ Services.prefs.clearUserPref(
+ "browser.search.separatePrivateDefault.ui.enabled"
+ );
+ Services.prefs.clearUserPref(
+ "browser.urlbar.tabToSearch.onboard.interactionsLeft"
+ );
+ });
+});
+
+add_task(async function test() {
+ let url = "https://en.example.com/";
+ await SearchTestUtils.installSearchExtension(
+ {
+ name: "TestEngine",
+ search_url: url,
+ },
+ { setAsDefault: true }
+ );
+
+ // Make sure the engine domain would be autofilled.
+ await PlacesUtils.bookmarks.insert({
+ url,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "bookmark",
+ });
+
+ info("Test matching cases");
+
+ for (let searchStr of ["ex", "example.c"]) {
+ info("Searching for " + searchStr);
+ let context = createContext(searchStr, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: Services.search.defaultEngine.name,
+ providerName: "HeuristicFallback",
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: "TestEngine",
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
+ uri: "en.example.",
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ satisfiesAutofillThreshold: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: url,
+ title: "bookmark",
+ }),
+ ],
+ });
+ }
+
+ info("Test a www engine");
+ let url2 = "https://www.it.mochi.com/";
+ await SearchTestUtils.installSearchExtension({
+ name: "TestEngine2",
+ search_url: url2,
+ });
+
+ let engine2 = Services.search.getEngineByName("TestEngine2");
+ // Make sure the engine domain would be autofilled.
+ await PlacesUtils.bookmarks.insert({
+ url: url2,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "bookmark",
+ });
+
+ for (let searchStr of ["mo", "mochi.c"]) {
+ info("Searching for " + searchStr);
+ let context = createContext(searchStr, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: Services.search.defaultEngine.name,
+ providerName: "HeuristicFallback",
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: engine2.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
+ uri: "www.it.mochi.",
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ satisfiesAutofillThreshold: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: url2,
+ title: "bookmark",
+ }),
+ ],
+ });
+ }
+
+ info("Test non-matching cases");
+
+ for (let searchStr of ["www.en", "www.ex", "https://ex"]) {
+ info("Searching for " + searchStr);
+ let context = createContext(searchStr, { isPrivate: false });
+ // We don't want to generate all the possible results here, just check
+ // the heuristic result is not autofill.
+ let controller = UrlbarTestUtils.newMockController();
+ await UrlbarProvidersManager.startQuery(context, controller);
+ Assert.ok(context.results[0].heuristic, "Check heuristic result");
+ Assert.notEqual(context.results[0].providerName, "Autofill");
+ }
+
+ info("Tab-to-search is not shown when an unrelated site is autofilled.");
+ let wikiUrl = "https://wikipedia.org/";
+ await SearchTestUtils.installSearchExtension({
+ name: "FakeWikipedia",
+ search_url: url,
+ });
+ let wikiEngine = Services.search.getEngineByName("TestEngine");
+
+ // Make sure that wikiUrl will pass getTopHostOverThreshold.
+ await PlacesUtils.bookmarks.insert({
+ url: wikiUrl,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "Wikipedia",
+ });
+
+ // Make sure an unrelated www site is autofilled.
+ let wwwUrl = "https://www.example.com";
+ await PlacesUtils.bookmarks.insert({
+ url: wwwUrl,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "Example",
+ });
+
+ let searchStr = "w";
+ let context = createContext(searchStr, {
+ isPrivate: false,
+ sources: [UrlbarUtils.RESULT_SOURCE.BOOKMARKS],
+ });
+ let host = await UrlbarProviderAutofill.getTopHostOverThreshold(context, [
+ wikiEngine.searchUrlDomain,
+ ]);
+ Assert.equal(
+ host,
+ wikiEngine.searchUrlDomain,
+ "The search satisfies the autofill threshold requirement."
+ );
+ await check_results({
+ context,
+ autofilled: "www.example.com/",
+ completed: "https://www.example.com/",
+ matches: [
+ makeVisitResult(context, {
+ uri: `${wwwUrl}/`,
+ title: "Example",
+ heuristic: true,
+ providerName: "Autofill",
+ }),
+ // Note that tab-to-search is not shown.
+ makeBookmarkResult(context, {
+ uri: wikiUrl,
+ title: "Wikipedia",
+ }),
+ makeBookmarkResult(context, {
+ uri: url2,
+ title: "bookmark",
+ }),
+ ],
+ });
+
+ info("Restricting to history should not autofill our bookmark");
+ context = createContext("ex", {
+ isPrivate: false,
+ sources: [UrlbarUtils.RESULT_SOURCE.HISTORY],
+ });
+ let controller = UrlbarTestUtils.newMockController();
+ await UrlbarProvidersManager.startQuery(context, controller);
+ Assert.ok(context.results[0].heuristic, "Check heuristic result");
+ Assert.notEqual(context.results[0].providerName, "Autofill");
+
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_providersManager.js b/browser/components/urlbar/tests/unit/test_providersManager.js
new file mode 100644
index 0000000000..8446ed0675
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providersManager.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_providers() {
+ Assert.throws(
+ () => UrlbarProvidersManager.registerProvider(),
+ /invalid provider/,
+ "Should throw with no arguments"
+ );
+ Assert.throws(
+ () => UrlbarProvidersManager.registerProvider({}),
+ /invalid provider/,
+ "Should throw with empty object"
+ );
+ Assert.throws(
+ () =>
+ UrlbarProvidersManager.registerProvider({
+ name: "",
+ }),
+ /invalid provider/,
+ "Should throw with empty name"
+ );
+ Assert.throws(
+ () =>
+ UrlbarProvidersManager.registerProvider({
+ name: "test",
+ startQuery: "no",
+ }),
+ /invalid provider/,
+ "Should throw with invalid startQuery"
+ );
+ Assert.throws(
+ () =>
+ UrlbarProvidersManager.registerProvider({
+ name: "test",
+ startQuery: () => {},
+ cancelQuery: "no",
+ }),
+ /invalid provider/,
+ "Should throw with invalid cancelQuery"
+ );
+
+ let match = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: "http://mozilla.org/foo/" }
+ );
+
+ let provider = registerBasicTestProvider([match]);
+ let context = createContext(undefined, { providers: [provider.name] });
+ let controller = UrlbarTestUtils.newMockController();
+ let resultsPromise = promiseControllerNotification(
+ controller,
+ "onQueryResults"
+ );
+
+ await UrlbarProvidersManager.startQuery(context, controller);
+ // Sanity check that this doesn't throw. It should be a no-op since we await
+ // for startQuery.
+ UrlbarProvidersManager.cancelQuery(context);
+
+ let params = await resultsPromise;
+ Assert.deepEqual(params[0].results, [match]);
+});
+
+add_task(async function test_criticalSection() {
+ // Just a sanity check, this shouldn't throw.
+ await UrlbarProvidersManager.runInCriticalSection(async () => {
+ let db = await PlacesUtils.promiseLargeCacheDBConnection();
+ await db.execute(`PRAGMA page_cache`);
+ });
+});
diff --git a/browser/components/urlbar/tests/unit/test_providersManager_filtering.js b/browser/components/urlbar/tests/unit/test_providersManager_filtering.js
new file mode 100644
index 0000000000..a631bfa204
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providersManager_filtering.js
@@ -0,0 +1,407 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_filtering_disable_only_source() {
+ let match = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: "http://mozilla.org/foo/" }
+ );
+ let provider = registerBasicTestProvider([match]);
+ let context = createContext(undefined, { providers: [provider.name] });
+ let controller = UrlbarTestUtils.newMockController();
+
+ info("Disable the only available source, should get no matches");
+ Services.prefs.setBoolPref("browser.urlbar.suggest.openpage", false);
+ let promise = Promise.race([
+ promiseControllerNotification(controller, "onQueryResults", false),
+ promiseControllerNotification(controller, "onQueryFinished"),
+ ]);
+ await controller.startQuery(context);
+ await promise;
+ Services.prefs.clearUserPref("browser.urlbar.suggest.openpage");
+ UrlbarProvidersManager.unregisterProvider({ name: provider.name });
+});
+
+add_task(async function test_filtering_disable_one_source() {
+ let matches = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: "http://mozilla.org/foo/" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/foo/" }
+ ),
+ ];
+ let provider = registerBasicTestProvider(matches);
+ let context = createContext(undefined, { providers: [provider.name] });
+ let controller = UrlbarTestUtils.newMockController();
+
+ info("Disable one of the sources, should get a single match");
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ let promise = Promise.all([
+ promiseControllerNotification(controller, "onQueryResults"),
+ promiseControllerNotification(controller, "onQueryFinished"),
+ ]);
+ await controller.startQuery(context, controller);
+ await promise;
+ Assert.deepEqual(context.results, matches.slice(0, 1));
+ Services.prefs.clearUserPref("browser.urlbar.suggest.history");
+ UrlbarProvidersManager.unregisterProvider(provider);
+});
+
+add_task(async function test_filtering_restriction_token() {
+ let matches = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: "http://mozilla.org/foo/" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/foo/" }
+ ),
+ ];
+ let provider = registerBasicTestProvider(matches);
+ let context = createContext(`foo ${UrlbarTokenizer.RESTRICT.OPENPAGE}`, {
+ providers: [provider.name],
+ });
+ let controller = UrlbarTestUtils.newMockController();
+
+ info("Use a restriction character, should get a single match");
+ let promise = Promise.all([
+ promiseControllerNotification(controller, "onQueryResults"),
+ promiseControllerNotification(controller, "onQueryFinished"),
+ ]);
+ await controller.startQuery(context, controller);
+ await promise;
+ Assert.deepEqual(context.results, matches.slice(0, 1));
+ UrlbarProvidersManager.unregisterProvider(provider);
+});
+
+add_task(async function test_filter_javascript() {
+ let match = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: "http://mozilla.org/foo/" }
+ );
+ let jsMatch = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "javascript:foo" }
+ );
+ let provider = registerBasicTestProvider([match, jsMatch]);
+ let context = createContext(undefined, { providers: [provider.name] });
+ let controller = UrlbarTestUtils.newMockController();
+
+ info("By default javascript should be filtered out");
+ let promise = promiseControllerNotification(controller, "onQueryResults");
+ await controller.startQuery(context, controller);
+ await promise;
+ Assert.deepEqual(context.results, [match]);
+
+ info("Except when the user explicitly starts the search with javascript:");
+ context = createContext(`javascript: ${UrlbarTokenizer.RESTRICT.HISTORY}`, {
+ providers: [provider.name],
+ });
+ promise = promiseControllerNotification(controller, "onQueryResults");
+ await controller.startQuery(context, controller);
+ await promise;
+ Assert.deepEqual(context.results, [jsMatch]);
+
+ info("Disable javascript filtering");
+ Services.prefs.setBoolPref("browser.urlbar.filter.javascript", false);
+ context = createContext(undefined, { providers: [provider.name] });
+ promise = promiseControllerNotification(controller, "onQueryResults");
+ await controller.startQuery(context, controller);
+ await promise;
+ Assert.deepEqual(context.results, [match, jsMatch]);
+ Services.prefs.clearUserPref("browser.urlbar.filter.javascript");
+ UrlbarProvidersManager.unregisterProvider(provider);
+});
+
+add_task(async function test_filter_isActive() {
+ let goodMatches = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: "http://mozilla.org/foo/" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/foo/" }
+ ),
+ ];
+ let provider = registerBasicTestProvider(goodMatches);
+
+ let badMatches = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ { url: "http://mozilla.org/foo/" }
+ ),
+ ];
+ /**
+ * A test provider that should not be invoked.
+ */
+ class NoInvokeProvider extends UrlbarProvider {
+ get name() {
+ return "BadProvider";
+ }
+ get type() {
+ return UrlbarUtils.PROVIDER_TYPE.PROFILE;
+ }
+ isActive(context) {
+ info("Acceptable sources: " + context.sources);
+ return context.sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS);
+ }
+ async startQuery(context, add) {
+ Assert.ok(false, "Provider should no be invoked");
+ for (const match of badMatches) {
+ add(this, match);
+ }
+ }
+ }
+ let badProvider = new NoInvokeProvider();
+ UrlbarProvidersManager.registerProvider(badProvider);
+
+ let context = createContext(undefined, {
+ sources: [UrlbarUtils.RESULT_SOURCE.TABS],
+ providers: [provider.name, "BadProvider"],
+ });
+ let controller = UrlbarTestUtils.newMockController();
+
+ info("Only tabs should be returned");
+ let promise = promiseControllerNotification(controller, "onQueryResults");
+ await controller.startQuery(context, controller);
+ await promise;
+ Assert.deepEqual(context.results.length, 1, "Should find only one match");
+ Assert.deepEqual(
+ context.results[0].source,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ "Should find only a tab match"
+ );
+ UrlbarProvidersManager.unregisterProvider(provider);
+ UrlbarProvidersManager.unregisterProvider(badProvider);
+});
+
+add_task(async function test_filter_queryContext() {
+ let provider = registerBasicTestProvider();
+
+ /**
+ * A test provider that should not be invoked because of queryContext.providers.
+ */
+ class NoInvokeProvider extends UrlbarProvider {
+ get name() {
+ return "BadProvider";
+ }
+ get type() {
+ return UrlbarUtils.PROVIDER_TYPE.PROFILE;
+ }
+ isActive(context) {
+ return true;
+ }
+ async startQuery(context, add) {
+ Assert.ok(false, "Provider should no be invoked");
+ }
+ }
+ let badProvider = new NoInvokeProvider();
+ UrlbarProvidersManager.registerProvider(badProvider);
+
+ let context = createContext(undefined, {
+ providers: [provider.name],
+ });
+ let controller = UrlbarTestUtils.newMockController();
+
+ await controller.startQuery(context, controller);
+ UrlbarProvidersManager.unregisterProvider(provider);
+ UrlbarProvidersManager.unregisterProvider(badProvider);
+});
+
+add_task(async function test_nofilter_heuristic() {
+ // Checks that even if a provider returns a result that should be filtered out
+ // it will still be invoked if it's of type heuristic, and only the heuristic
+ // result is returned.
+ let matches = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: "http://mozilla.org/foo/" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: "http://mozilla.org/foo2/" }
+ ),
+ ];
+ matches[0].heuristic = true;
+ let provider = registerBasicTestProvider(
+ matches,
+ undefined,
+ UrlbarUtils.PROVIDER_TYPE.HEURISTIC
+ );
+
+ let context = createContext(undefined, {
+ sources: [UrlbarUtils.RESULT_SOURCE.SEARCH],
+ providers: [provider.name],
+ });
+ let controller = UrlbarTestUtils.newMockController();
+
+ // Disable search matches through prefs.
+ Services.prefs.setBoolPref("browser.urlbar.suggest.openpage", false);
+ info("Only 1 heuristic tab result should be returned");
+ let promise = promiseControllerNotification(controller, "onQueryResults");
+ await controller.startQuery(context, controller);
+ await promise;
+ Services.prefs.clearUserPref("browser.urlbar.suggest.openpage");
+ Assert.deepEqual(context.results.length, 1, "Should find only one match");
+ Assert.deepEqual(
+ context.results[0].source,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ "Should find only a tab match"
+ );
+ UrlbarProvidersManager.unregisterProvider(provider);
+});
+
+add_task(async function test_nofilter_restrict() {
+ // Checks that even if a pref is disabled, we still return results on a
+ // restriction token.
+ let matches = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: "http://mozilla.org/foo_tab/" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
+ { url: "http://mozilla.org/foo_bookmark/" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://mozilla.org/foo_history/" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ { engine: "noengine" }
+ ),
+ ];
+ /**
+ * A test provider.
+ */
+ class TestProvider extends UrlbarProvider {
+ get name() {
+ return "MyProvider";
+ }
+ get type() {
+ return UrlbarUtils.PROVIDER_TYPE.PROFILE;
+ }
+ isActive(context) {
+ Assert.equal(context.sources.length, 1, "Check acceptable sources");
+ return true;
+ }
+ async startQuery(context, add) {
+ Assert.ok(true, "expected provider was invoked");
+ for (let match of matches) {
+ add(this, match);
+ }
+ }
+ }
+ let provider = new TestProvider();
+ UrlbarProvidersManager.registerProvider(provider);
+
+ let typeToPropertiesMap = new Map([
+ ["HISTORY", { source: "HISTORY", pref: "history" }],
+ ["BOOKMARK", { source: "BOOKMARKS", pref: "bookmark" }],
+ ["OPENPAGE", { source: "TABS", pref: "openpage" }],
+ ["SEARCH", { source: "SEARCH", pref: "searches" }],
+ ]);
+ for (let [type, token] of Object.entries(UrlbarTokenizer.RESTRICT)) {
+ let properties = typeToPropertiesMap.get(type);
+ if (!properties) {
+ continue;
+ }
+ info("Restricting on " + type);
+ let context = createContext(token + " foo", {
+ providers: ["MyProvider"],
+ });
+ let controller = UrlbarTestUtils.newMockController();
+ // Disable the corresponding pref.
+ const pref = "browser.urlbar.suggest." + properties.pref;
+ info("Disabling " + pref);
+ Services.prefs.setBoolPref(pref, false);
+ await controller.startQuery(context, controller);
+ Assert.equal(context.results.length, 1, "Should find one result");
+ Assert.equal(
+ context.results[0].source,
+ UrlbarUtils.RESULT_SOURCE[properties.source],
+ "Check result source"
+ );
+ Services.prefs.clearUserPref(pref);
+ }
+ UrlbarProvidersManager.unregisterProvider(provider);
+});
+
+add_task(async function test_filter_priority() {
+ /**
+ * A test provider.
+ */
+ class TestProvider extends UrlbarTestUtils.TestProvider {
+ constructor(priority, shouldBeInvoked, namePart = "") {
+ super();
+ this._priority = priority;
+ this._name = `${priority}` + namePart;
+ this._shouldBeInvoked = shouldBeInvoked;
+ }
+ async startQuery(context, add) {
+ Assert.ok(this._shouldBeInvoked, `${this.name} was invoked`);
+ }
+ }
+
+ // Test all possible orderings of the providers to make sure the logic that
+ // finds the highest priority providers is correct.
+ let providerPerms = permute([
+ new TestProvider(0, false),
+ new TestProvider(1, false),
+ new TestProvider(2, true, "a"),
+ new TestProvider(2, true, "b"),
+ ]);
+ for (let providers of providerPerms) {
+ for (let provider of providers) {
+ UrlbarProvidersManager.registerProvider(provider);
+ }
+ let providerNames = providers.map(p => p.name);
+ let context = createContext(undefined, { providers: providerNames });
+ let controller = UrlbarTestUtils.newMockController();
+ await controller.startQuery(context, controller);
+ for (let name of providerNames) {
+ UrlbarProvidersManager.unregisterProvider({ name });
+ }
+ }
+});
+
+function permute(objects) {
+ if (objects.length <= 1) {
+ return [objects];
+ }
+ let perms = [];
+ for (let i = 0; i < objects.length; i++) {
+ let otherObjects = objects.slice();
+ otherObjects.splice(i, 1);
+ let otherPerms = permute(otherObjects);
+ for (let perm of otherPerms) {
+ perm.unshift(objects[i]);
+ }
+ perms = perms.concat(otherPerms);
+ }
+ return perms;
+}
diff --git a/browser/components/urlbar/tests/unit/test_providersManager_maxResults.js b/browser/components/urlbar/tests/unit/test_providersManager_maxResults.js
new file mode 100644
index 0000000000..b30b9352cd
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providersManager_maxResults.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_maxResults() {
+ const MATCHES_LENGTH = 20;
+ let matches = [];
+ for (let i = 0; i < MATCHES_LENGTH; i++) {
+ matches.push(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ { url: `http://mozilla.org/foo/${i}` }
+ )
+ );
+ }
+ let provider = registerBasicTestProvider(matches);
+ let context = createContext(undefined, { providers: [provider.name] });
+ let controller = UrlbarTestUtils.newMockController();
+
+ async function test_count(count) {
+ let promise = promiseControllerNotification(controller, "onQueryFinished");
+ context.maxResults = count;
+ await controller.startQuery(context);
+ await promise;
+ Assert.equal(
+ context.results.length,
+ Math.min(MATCHES_LENGTH, count),
+ "Check count"
+ );
+ Assert.deepEqual(context.results, matches.slice(0, count), "Check results");
+ }
+ await test_count(10);
+ await test_count(1);
+ await test_count(30);
+});
diff --git a/browser/components/urlbar/tests/unit/test_queryScorer.js b/browser/components/urlbar/tests/unit/test_queryScorer.js
new file mode 100644
index 0000000000..1d6171eac4
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_queryScorer.js
@@ -0,0 +1,405 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ QueryScorer: "resource:///modules/UrlbarProviderInterventions.sys.mjs",
+});
+
+const DISTANCE_THRESHOLD = 1;
+
+const DOCUMENTS = {
+ clear: [
+ "cache firefox",
+ "clear cache firefox",
+ "clear cache in firefox",
+ "clear cookies firefox",
+ "clear firefox cache",
+ "clear history firefox",
+ "cookies firefox",
+ "delete cookies firefox",
+ "delete history firefox",
+ "firefox cache",
+ "firefox clear cache",
+ "firefox clear cookies",
+ "firefox clear history",
+ "firefox cookie",
+ "firefox cookies",
+ "firefox delete cookies",
+ "firefox delete history",
+ "firefox history",
+ "firefox not loading pages",
+ "history firefox",
+ "how to clear cache",
+ "how to clear history",
+ ],
+ refresh: [
+ "firefox crashing",
+ "firefox keeps crashing",
+ "firefox not responding",
+ "firefox not working",
+ "firefox refresh",
+ "firefox slow",
+ "how to reset firefox",
+ "refresh firefox",
+ "reset firefox",
+ ],
+ update: [
+ "download firefox",
+ "download mozilla",
+ "firefox browser",
+ "firefox download",
+ "firefox for mac",
+ "firefox for windows",
+ "firefox free download",
+ "firefox install",
+ "firefox installer",
+ "firefox latest version",
+ "firefox mac",
+ "firefox quantum",
+ "firefox update",
+ "firefox version",
+ "firefox windows",
+ "get firefox",
+ "how to update firefox",
+ "install firefox",
+ "mozilla download",
+ "mozilla firefox 2019",
+ "mozilla firefox 2020",
+ "mozilla firefox download",
+ "mozilla firefox for mac",
+ "mozilla firefox for windows",
+ "mozilla firefox free download",
+ "mozilla firefox mac",
+ "mozilla firefox update",
+ "mozilla firefox windows",
+ "mozilla update",
+ "update firefox",
+ "update mozilla",
+ "www.firefox.com",
+ ],
+};
+
+const VARIATIONS = new Map([["firefox", ["fire fox", "fox fire", "foxfire"]]]);
+
+let tests = [
+ {
+ query: "firefox",
+ matches: [
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "bogus",
+ matches: [
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "no match",
+ matches: [
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+
+ // clear
+ {
+ query: "firefox histo",
+ matches: [
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefox histor",
+ matches: [
+ { id: "clear", score: 1 },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefox history",
+ matches: [
+ { id: "clear", score: 0 },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefox history we'll keep matching once we match",
+ matches: [
+ { id: "clear", score: 0 },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+
+ {
+ query: "firef history",
+ matches: [
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefo history",
+ matches: [
+ { id: "clear", score: 1 },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefo histor",
+ matches: [
+ { id: "clear", score: 2 },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefo histor we'll keep matching once we match",
+ matches: [
+ { id: "clear", score: 2 },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+
+ {
+ query: "fire fox history",
+ matches: [
+ { id: "clear", score: 0 },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "fox fire history",
+ matches: [
+ { id: "clear", score: 0 },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "foxfire history",
+ matches: [
+ { id: "clear", score: 0 },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+
+ // refresh
+ {
+ query: "firefox sl",
+ matches: [
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefox slo",
+ matches: [
+ { id: "refresh", score: 1 },
+ { id: "clear", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefox slow",
+ matches: [
+ { id: "refresh", score: 0 },
+ { id: "clear", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefox slow we'll keep matching once we match",
+ matches: [
+ { id: "refresh", score: 0 },
+ { id: "clear", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+
+ {
+ query: "firef slow",
+ matches: [
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefo slow",
+ matches: [
+ { id: "refresh", score: 1 },
+ { id: "clear", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefo slo",
+ matches: [
+ { id: "refresh", score: 2 },
+ { id: "clear", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefo slo we'll keep matching once we match",
+ matches: [
+ { id: "refresh", score: 2 },
+ { id: "clear", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+
+ {
+ query: "fire fox slow",
+ matches: [
+ { id: "refresh", score: 0 },
+ { id: "clear", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "fox fire slow",
+ matches: [
+ { id: "refresh", score: 0 },
+ { id: "clear", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "foxfire slow",
+ matches: [
+ { id: "refresh", score: 0 },
+ { id: "clear", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+
+ // update
+ {
+ query: "firefox upda",
+ matches: [
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefox updat",
+ matches: [
+ { id: "update", score: 1 },
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ ],
+ },
+ {
+ query: "firefox update",
+ matches: [
+ { id: "update", score: 0 },
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ ],
+ },
+ {
+ query: "firefox update we'll keep matching once we match",
+ matches: [
+ { id: "update", score: 0 },
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ ],
+ },
+
+ {
+ query: "firef update",
+ matches: [
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ { id: "update", score: Infinity },
+ ],
+ },
+ {
+ query: "firefo update",
+ matches: [
+ { id: "update", score: 1 },
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ ],
+ },
+ {
+ query: "firefo updat",
+ matches: [
+ { id: "update", score: 2 },
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ ],
+ },
+ {
+ query: "firefo updat we'll keep matching once we match",
+ matches: [
+ { id: "update", score: 2 },
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ ],
+ },
+
+ {
+ query: "fire fox update",
+ matches: [
+ { id: "update", score: 0 },
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ ],
+ },
+ {
+ query: "fox fire update",
+ matches: [
+ { id: "update", score: 0 },
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ ],
+ },
+ {
+ query: "foxfire update",
+ matches: [
+ { id: "update", score: 0 },
+ { id: "clear", score: Infinity },
+ { id: "refresh", score: Infinity },
+ ],
+ },
+];
+
+add_task(async function test() {
+ let qs = new QueryScorer({
+ distanceThreshold: DISTANCE_THRESHOLD,
+ variations: VARIATIONS,
+ });
+
+ for (let [id, phrases] of Object.entries(DOCUMENTS)) {
+ qs.addDocument({ id, phrases });
+ }
+
+ for (let { query, matches } of tests) {
+ let actual = qs
+ .score(query)
+ .map(result => ({ id: result.document.id, score: result.score }));
+ Assert.deepEqual(actual, matches, `Query: "${query}"`);
+ }
+});
diff --git a/browser/components/urlbar/tests/unit/test_query_url.js b/browser/components/urlbar/tests/unit/test_query_url.js
new file mode 100644
index 0000000000..e49c966d76
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_query_url.js
@@ -0,0 +1,121 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const PLACES_PROVIDERNAME = "Places";
+
+testEngine_setup();
+
+add_task(async function test_no_slash() {
+ info("Searching for host match without slash should match host");
+ await PlacesTestUtils.addVisits([
+ { uri: "http://file.org/test/" },
+ { uri: "file:///c:/test.html" },
+ ]);
+ let context = createContext("file", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "file.org/",
+ completed: "http://file.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://file.org/",
+ fallbackTitle: "file.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "file:///c:/test.html",
+ title: "test visit for file:///c:/test.html",
+ iconUri: UrlbarUtils.ICON.DEFAULT,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ makeVisitResult(context, {
+ uri: "http://file.org/test/",
+ title: "test visit for http://file.org/test/",
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_w_slash() {
+ info("Searching match with slash at the end should match url");
+ await PlacesTestUtils.addVisits(
+ {
+ uri: Services.io.newURI("http://file.org/test/"),
+ },
+ {
+ uri: Services.io.newURI("file:///c:/test.html"),
+ }
+ );
+ let context = createContext("file.org/", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "file.org/",
+ completed: "http://file.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://file.org/",
+ fallbackTitle: "file.org/",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://file.org/test/",
+ title: "test visit for http://file.org/test/",
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_middle() {
+ info("Searching match with slash in the middle should match url");
+ await PlacesTestUtils.addVisits(
+ {
+ uri: Services.io.newURI("http://file.org/test/"),
+ },
+ {
+ uri: Services.io.newURI("file:///c:/test.html"),
+ }
+ );
+ let context = createContext("file.org/t", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "file.org/test/",
+ completed: "http://file.org/test/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://file.org/test/",
+ title: "test visit for http://file.org/test/",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_nonhost() {
+ info("Searching for non-host match without slash should not match url");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("file:///c:/test.html"),
+ });
+ let context = createContext("file", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "file:///c:/test.html",
+ title: "test visit for file:///c:/test.html",
+ iconUri: UrlbarUtils.ICON.DEFAULT,
+ providerName: PLACES_PROVIDERNAME,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_quickactions.js b/browser/components/urlbar/tests/unit/test_quickactions.js
new file mode 100644
index 0000000000..ddeb5a7561
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_quickactions.js
@@ -0,0 +1,126 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarProviderQuickActions:
+ "resource:///modules/UrlbarProviderQuickActions.sys.mjs",
+});
+
+let expectedMatch = (key, inputLength) => ({
+ type: UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ source: UrlbarUtils.RESULT_SOURCE.ACTIONS,
+ heuristic: false,
+ payload: {
+ results: [{ key }],
+ dynamicType: "quickactions",
+ helpUrl: UrlbarProviderQuickActions.helpUrl,
+ inputLength,
+ },
+});
+
+testEngine_setup();
+
+add_task(async function init() {
+ UrlbarPrefs.set("quickactions.enabled", true);
+ UrlbarPrefs.set("suggest.quickactions", true);
+
+ UrlbarProviderQuickActions.addAction("newaction", {
+ commands: ["newaction"],
+ });
+
+ registerCleanupFunction(async () => {
+ UrlbarPrefs.clear("quickactions.enabled");
+ UrlbarPrefs.clear("suggest.quickactions");
+ UrlbarProviderQuickActions.removeAction("newaction");
+ });
+});
+
+add_task(async function nomatch() {
+ let context = createContext("this doesnt match", {
+ providers: [UrlbarProviderQuickActions.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [],
+ });
+});
+
+add_task(async function quickactions_disabled() {
+ UrlbarPrefs.set("suggest.quickactions", false);
+ let context = createContext("new", {
+ providers: [UrlbarProviderQuickActions.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [],
+ });
+});
+
+add_task(async function quickactions_match() {
+ UrlbarPrefs.set("suggest.quickactions", true);
+ let context = createContext("new", {
+ providers: [UrlbarProviderQuickActions.name],
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [expectedMatch("newaction", 3)],
+ });
+});
+
+add_task(async function duplicate_matches() {
+ UrlbarProviderQuickActions.addAction("testaction", {
+ commands: ["testaction", "test"],
+ });
+
+ let context = createContext("testaction", {
+ providers: [UrlbarProviderQuickActions.name],
+ isPrivate: false,
+ });
+
+ await check_results({
+ context,
+ matches: [expectedMatch("testaction", 10)],
+ });
+
+ UrlbarProviderQuickActions.removeAction("testaction");
+});
+
+add_task(async function remove_action() {
+ UrlbarProviderQuickActions.addAction("testaction", {
+ commands: ["testaction"],
+ });
+ UrlbarProviderQuickActions.removeAction("testaction");
+
+ let context = createContext("test", {
+ providers: [UrlbarProviderQuickActions.name],
+ isPrivate: false,
+ });
+
+ await check_results({
+ context,
+ matches: [],
+ });
+});
+
+add_task(async function minimum_search_string() {
+ let searchString = "newa";
+ for (let minimumSearchString of [0, 3]) {
+ UrlbarPrefs.set("quickactions.minimumSearchString", minimumSearchString);
+ for (let i = 1; i < 4; i++) {
+ let context = createContext(searchString.substring(0, i), {
+ providers: [UrlbarProviderQuickActions.name],
+ isPrivate: false,
+ });
+ let matches =
+ i >= minimumSearchString ? [expectedMatch("newaction", i)] : [];
+ await check_results({ context, matches });
+ }
+ }
+ UrlbarPrefs.clear("quickactions.minimumSearchString");
+});
diff --git a/browser/components/urlbar/tests/unit/test_remote_tabs.js b/browser/components/urlbar/tests/unit/test_remote_tabs.js
new file mode 100644
index 0000000000..5b834c876e
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_remote_tabs.js
@@ -0,0 +1,695 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim:set ts=2 sw=2 sts=2 et:
+ */
+"use strict";
+
+const { Weave } = ChromeUtils.importESModule(
+ "resource://services-sync/main.sys.mjs"
+);
+
+// A mock "Tabs" engine which autocomplete will use instead of the real
+// engine. We pass a constructor that Sync creates.
+function MockTabsEngine() {
+ this.clients = null; // We'll set this dynamically
+}
+
+MockTabsEngine.prototype = {
+ name: "tabs",
+
+ startTracking() {},
+ getAllClients() {
+ return this.clients;
+ },
+};
+
+// A clients engine that doesn't need to be a constructor.
+let MockClientsEngine = {
+ getClientType(guid) {
+ Assert.ok(guid.endsWith("desktop") || guid.endsWith("mobile"));
+ return guid.endsWith("mobile") ? "phone" : "desktop";
+ },
+ remoteClientExists(id) {
+ return true;
+ },
+ getClientName(id) {
+ return id.endsWith("mobile") ? "My Phone" : "My Desktop";
+ },
+};
+
+// Configure the singleton engine for a test.
+function configureEngine(clients) {
+ // Configure the instance Sync created.
+ let engine = Weave.Service.engineManager.get("tabs");
+ engine.clients = clients;
+ Weave.Service.clientsEngine = MockClientsEngine;
+ // Send an observer that pretends the engine just finished a sync.
+ Services.obs.notifyObservers(null, "weave:engine:sync:finish", "tabs");
+}
+
+testEngine_setup();
+
+add_task(async function setup() {
+ // Tell Sync about the mocks.
+ Weave.Service.engineManager.register(MockTabsEngine);
+
+ // Tell the Sync XPCOM service it is initialized.
+ let weaveXPCService = Cc["@mozilla.org/weave/service;1"].getService(
+ Ci.nsISupports
+ ).wrappedJSObject;
+ weaveXPCService.ready = true;
+
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("services.sync.username");
+ Services.prefs.clearUserPref("services.sync.registerEngines");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions");
+ await cleanupPlaces();
+ });
+
+ Services.prefs.setCharPref("services.sync.username", "someone@somewhere.com");
+ Services.prefs.setCharPref("services.sync.registerEngines", "");
+ // Avoid hitting the network.
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false);
+});
+
+add_task(async function test_minimal() {
+ // The minimal client and tabs info we can get away with.
+ configureEngine([
+ {
+ id: "desktop",
+ tabs: [
+ {
+ urlHistory: ["http://example.com/"],
+ },
+ ],
+ },
+ ]);
+
+ let query = "ex";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://example.com/",
+ device: "My Desktop",
+ }),
+ ],
+ });
+});
+
+add_task(async function test_maximal() {
+ // Every field that could possibly exist on a remote record.
+ configureEngine([
+ {
+ id: "mobile",
+ tabs: [
+ {
+ urlHistory: ["http://example.com/"],
+ title: "An Example",
+ icon: "http://favicon",
+ },
+ ],
+ },
+ ]);
+
+ let query = "ex";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://example.com/",
+ device: "My Phone",
+ title: "An Example",
+ iconUri: "moz-anno:favicon:http://favicon/",
+ }),
+ ],
+ });
+});
+
+add_task(async function test_noShowIcons() {
+ Services.prefs.setBoolPref("services.sync.syncedTabs.showRemoteIcons", false);
+ configureEngine([
+ {
+ id: "mobile",
+ tabs: [
+ {
+ urlHistory: ["http://example.com/"],
+ title: "An Example",
+ icon: "http://favicon",
+ },
+ ],
+ },
+ ]);
+
+ let query = "ex";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://example.com/",
+ device: "My Phone",
+ title: "An Example",
+ // expecting the default favicon due to that pref.
+ iconUri: "",
+ }),
+ ],
+ });
+ Services.prefs.clearUserPref("services.sync.syncedTabs.showRemoteIcons");
+});
+
+add_task(async function test_dontMatchSyncedTabs() {
+ Services.prefs.setBoolPref("services.sync.syncedTabs.showRemoteTabs", false);
+ configureEngine([
+ {
+ id: "mobile",
+ tabs: [
+ {
+ urlHistory: ["http://example.com/"],
+ title: "An Example",
+ icon: "http://favicon",
+ },
+ ],
+ },
+ ]);
+
+ let context = createContext("ex", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ Services.prefs.clearUserPref("services.sync.syncedTabs.showRemoteTabs");
+});
+
+add_task(async function test_tabsDisabledInUrlbar() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.remotetab", false);
+ configureEngine([
+ {
+ id: "mobile",
+ tabs: [
+ {
+ urlHistory: ["http://example.com/"],
+ title: "An Example",
+ icon: "http://favicon",
+ },
+ ],
+ },
+ ]);
+
+ let context = createContext("ex", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ Services.prefs.clearUserPref("browser.urlbar.suggest.remotetab");
+});
+
+add_task(async function test_matches_title() {
+ // URL doesn't match search expression, should still match the title.
+ configureEngine([
+ {
+ id: "mobile",
+ tabs: [
+ {
+ urlHistory: ["http://foo.com/"],
+ title: "An Example",
+ },
+ ],
+ },
+ ]);
+
+ let query = "ex";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.com/",
+ device: "My Phone",
+ title: "An Example",
+ }),
+ ],
+ });
+});
+
+add_task(async function test_localtab_matches_override() {
+ // We have an open tab to the same page on a remote device, only "switch to
+ // tab" should appear as duplicate detection removed the remote one.
+
+ // First set up Sync to have the page as a remote tab.
+ configureEngine([
+ {
+ id: "mobile",
+ tabs: [
+ {
+ urlHistory: ["http://foo.com/"],
+ title: "An Example",
+ },
+ ],
+ },
+ ]);
+
+ // Set up Places to think the tab is open locally.
+ let uri = Services.io.newURI("http://foo.com/");
+ await PlacesTestUtils.addVisits([{ uri, title: "An Example" }]);
+ await addOpenPages(uri, 1);
+
+ let query = "ex";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeTabSwitchResult(context, {
+ uri: "http://foo.com/",
+ title: "An Example",
+ }),
+ ],
+ });
+
+ await removeOpenPages(uri, 1);
+ await cleanupPlaces();
+});
+
+add_task(async function test_remotetab_matches_override() {
+ // If we have an history result to the same page, we should only get the
+ // remote tab match.
+ let url = "http://foo.remote.com/";
+ // First set up Sync to have the page as a remote tab.
+ configureEngine([
+ {
+ id: "mobile",
+ tabs: [
+ {
+ urlHistory: [url],
+ title: "An Example",
+ },
+ ],
+ },
+ ]);
+
+ // Set up Places to think the tab is in history.
+ await PlacesTestUtils.addVisits(url);
+
+ let query = "ex";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/",
+ device: "My Phone",
+ title: "An Example",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_mixed_result_types() {
+ // In case we have many results, non-remote results should flex to the bottom.
+ let url = "http://foo.remote.com/";
+ let tabs = Array(6)
+ .fill(0)
+ .map((e, i) => ({
+ urlHistory: [`${url}${i}`],
+ title: "A title",
+ lastUsed: Math.floor(Date.now() / 1000) - i * 86400, // i days ago.
+ }));
+ // First set up Sync to have the page as a remote tab.
+ configureEngine([{ id: "mobile", tabs }]);
+
+ // Register the page as an open tab.
+ let openTabUrl = url + "openpage/";
+ let uri = Services.io.newURI(openTabUrl);
+ await PlacesTestUtils.addVisits([{ uri, title: "An Example" }]);
+ await addOpenPages(uri, 1);
+
+ // Also add a local history result.
+ let historyUrl = url + "history/";
+ await PlacesTestUtils.addVisits(historyUrl);
+
+ let query = "rem";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/0",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[0].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/1",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[1].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/2",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[2].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/3",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[3].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/4",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[4].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/5",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[5].lastUsed,
+ }),
+ makeVisitResult(context, {
+ uri: historyUrl,
+ title: "test visit for " + historyUrl,
+ }),
+ makeTabSwitchResult(context, {
+ uri: openTabUrl,
+ title: "An Example",
+ }),
+ ],
+ });
+ await removeOpenPages(uri, 1);
+ await cleanupPlaces();
+});
+
+add_task(async function test_many_remotetab_results() {
+ let url = "http://foo.remote.com/";
+ let tabs = Array(8)
+ .fill(0)
+ .map((e, i) => ({
+ urlHistory: [`${url}${i}`],
+ title: "A title",
+ lastUsed: Math.floor(Date.now() / 1000) - i * 86400, // i days old.
+ }));
+
+ // First set up Sync to have the page as a remote tab.
+ configureEngine([
+ {
+ id: "mobile",
+ tabs,
+ },
+ ]);
+
+ let query = "rem";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/0",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[0].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/1",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[1].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/2",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[2].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/3",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[3].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/4",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[4].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/5",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[5].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/6",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[6].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/7",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[7].lastUsed,
+ }),
+ ],
+ });
+});
+
+add_task(async function multiple_clients() {
+ let url = "http://foo.remote.com/";
+ let mobileTabs = Array(2)
+ .fill(0)
+ .map((e, i) => ({
+ urlHistory: [`${url}mobile/${i}`],
+ lastUsed: Date.now() / 1000 - 4 * 86400, // 4 days old (past threshold)
+ }));
+
+ let desktopTabs = Array(3)
+ .fill(0)
+ .map((e, i) => ({
+ urlHistory: [`${url}desktop/${i}`],
+ lastUsed: Date.now() / 1000 - 1, // Fresh tabs
+ }));
+
+ // mobileTabs has the most recent tab, making it the most recent client. The
+ // rest of its tabs are stale. The tabs in desktopTabs are fresh, but not
+ // as fresh as the most recent tab in mobileTab.
+ mobileTabs.push({
+ urlHistory: [`${url}mobile/fresh`],
+ lastUsed: Date.now() / 1000,
+ });
+
+ configureEngine([
+ {
+ id: "mobile",
+ tabs: mobileTabs,
+ },
+ {
+ id: "desktop",
+ tabs: desktopTabs,
+ },
+ ]);
+
+ // We expect that we will show the recent tab from mobileTabs, then all the
+ // tabs from desktopTabs, then the remaining tabs from mobileTabs.
+ let query = "rem";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/mobile/fresh",
+ device: "My Phone",
+ lastUsed: mobileTabs[2].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/desktop/0",
+ device: "My Desktop",
+ lastUsed: desktopTabs[0].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/desktop/1",
+ device: "My Desktop",
+ lastUsed: desktopTabs[1].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/desktop/2",
+ device: "My Desktop",
+ lastUsed: desktopTabs[2].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/mobile/0",
+ device: "My Phone",
+ lastUsed: mobileTabs[0].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/mobile/1",
+ device: "My Phone",
+ lastUsed: mobileTabs[1].lastUsed,
+ }),
+ ],
+ });
+});
+
+add_task(async function test_restrictionCharacter() {
+ let url = "http://foo.remote.com/";
+ let tabs = Array(5)
+ .fill(0)
+ .map((e, i) => ({
+ urlHistory: [`${url}${i}`],
+ title: "A title",
+ lastUsed: Math.floor(Date.now() / 1000) - i,
+ }));
+ configureEngine([
+ {
+ id: "mobile",
+ tabs,
+ },
+ ]);
+
+ // Also add an open page.
+ let openTabUrl = url + "openpage/";
+ let uri = Services.io.newURI(openTabUrl);
+ await PlacesTestUtils.addVisits([{ uri, title: "An Example" }]);
+ await addOpenPages(uri, 1);
+
+ // We expect the open tab to flex to the bottom.
+ let query = UrlbarTokenizer.RESTRICT.OPENPAGE;
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/0",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[0].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/1",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[1].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/2",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[2].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/3",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[3].lastUsed,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/4",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[4].lastUsed,
+ }),
+ makeTabSwitchResult(context, {
+ uri: openTabUrl,
+ title: "An Example",
+ }),
+ ],
+ });
+ await removeOpenPages(uri, 1);
+ await cleanupPlaces();
+});
+
+add_task(async function test_duplicate_remote_tabs() {
+ let url = "http://foo.remote.com/";
+ let tabs = Array(3)
+ .fill(0)
+ .map((e, i) => ({
+ urlHistory: [url],
+ title: "A title",
+ lastUsed: Math.floor(Date.now() / 1000),
+ }));
+ configureEngine([
+ {
+ id: "mobile",
+ tabs,
+ },
+ ]);
+
+ // We expect the duplicate tabs to be deduped.
+ let query = UrlbarTokenizer.RESTRICT.OPENPAGE;
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeRemoteTabResult(context, {
+ uri: "http://foo.remote.com/",
+ device: "My Phone",
+ title: "A title",
+ lastUsed: tabs[0].lastUsed,
+ }),
+ ],
+ });
+});
diff --git a/browser/components/urlbar/tests/unit/test_resultGroups.js b/browser/components/urlbar/tests/unit/test_resultGroups.js
new file mode 100644
index 0000000000..e385f7d2fb
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_resultGroups.js
@@ -0,0 +1,1576 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests the muxer's result groups composition logic: child groups,
+// `availableSpan`, `maxResultCount`, flex, etc. The purpose of this test is to
+// check the composition logic, not every possible result type or group.
+
+"use strict";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+// The possible limit-related properties in result groups.
+const LIMIT_KEYS = ["availableSpan", "maxResultCount"];
+
+// Most of this test adds tasks using `add_resultGroupsLimit_tasks`. It works
+// like this. Instead of defining `maxResultCount` or `availableSpan` in their
+// result groups, tasks define a `limit` property. The value of this property is
+// a number just like any of the values for the limit-related properties. At
+// runtime, `add_resultGroupsLimit_tasks` adds multiple tasks, one for each key
+// in `LIMIT_KEYS`. In each of these tasks, the `limit` property is replaced
+// with the actual limit key. This allows us to run checks against each of the
+// limit keys using essentially the same task.
+
+const MAX_RICH_RESULTS_PREF = "browser.urlbar.maxRichResults";
+
+// For simplicity, most of the flex tests below assume that this is 10, so
+// you'll need to update them if you change this.
+const MAX_RESULTS = 10;
+
+let sandbox;
+
+add_task(async function setup() {
+ // Set a specific maxRichResults for sanity's sake.
+ Services.prefs.setIntPref(MAX_RICH_RESULTS_PREF, MAX_RESULTS);
+
+ sandbox = lazy.sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "empty root",
+ resultGroups: {},
+ providerResults: [...makeHistoryResults(1)],
+ expectedResultIndexes: [],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "root with empty children",
+ resultGroups: {
+ children: [],
+ },
+ providerResults: [...makeHistoryResults(1)],
+ expectedResultIndexes: [],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "root no match",
+ resultGroups: {
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ providerResults: [...makeHistoryResults(1)],
+ expectedResultIndexes: [],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "children no match",
+ resultGroups: {
+ children: [{ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION }],
+ },
+ providerResults: [...makeHistoryResults(1)],
+ expectedResultIndexes: [],
+});
+
+add_resultGroupsLimit_tasks({
+ // The actual max result count on the root is always context.maxResults and
+ // limit is ignored, so we expect the result in this case.
+ testName: "root limit: 0",
+ resultGroups: {
+ limit: 0,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ providerResults: [...makeHistoryResults(1)],
+ expectedResultIndexes: [0],
+});
+
+add_resultGroupsLimit_tasks({
+ // The actual max result count on the root is always context.maxResults and
+ // limit is ignored, so we expect the result in this case.
+ testName: "root limit: 0 with children",
+ resultGroups: {
+ limit: 0,
+ children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }],
+ },
+ providerResults: [...makeHistoryResults(1)],
+ expectedResultIndexes: [0],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "child limit: 0",
+ resultGroups: {
+ children: [
+ {
+ limit: 0,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ providerResults: [...makeHistoryResults(1)],
+ expectedResultIndexes: [],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "root group",
+ resultGroups: {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ providerResults: [...makeHistoryResults(1)],
+ expectedResultIndexes: [...makeIndexRange(0, 1)],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "root group multiple",
+ resultGroups: {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ providerResults: [...makeHistoryResults(2)],
+ expectedResultIndexes: [...makeIndexRange(0, 2)],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "child group multiple",
+ resultGroups: {
+ children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }],
+ },
+ providerResults: [...makeHistoryResults(2)],
+ expectedResultIndexes: [0, 1],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "simple limit",
+ resultGroups: {
+ children: [
+ {
+ limit: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ providerResults: [...makeHistoryResults(2)],
+ expectedResultIndexes: [0],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "limit siblings",
+ resultGroups: {
+ children: [
+ {
+ limit: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ { group: UrlbarUtils.RESULT_GROUP.GENERAL },
+ ],
+ },
+ providerResults: [...makeHistoryResults(2)],
+ expectedResultIndexes: [0, 1],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "limit nested",
+ resultGroups: {
+ children: [
+ {
+ limit: 1,
+ children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }],
+ },
+ ],
+ },
+ providerResults: [...makeHistoryResults(2)],
+ expectedResultIndexes: [0],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "limit nested siblings",
+ resultGroups: {
+ children: [
+ {
+ limit: 1,
+ children: [
+ { group: UrlbarUtils.RESULT_GROUP.GENERAL },
+ { group: UrlbarUtils.RESULT_GROUP.GENERAL },
+ ],
+ },
+ ],
+ },
+ providerResults: [...makeHistoryResults(2)],
+ expectedResultIndexes: [0],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "limit nested uncle",
+ resultGroups: {
+ children: [
+ {
+ limit: 1,
+ children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }],
+ },
+ { group: UrlbarUtils.RESULT_GROUP.GENERAL },
+ ],
+ },
+ providerResults: [...makeHistoryResults(2)],
+ expectedResultIndexes: [0, 1],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "limit nested override bad",
+ resultGroups: {
+ children: [
+ {
+ limit: 1,
+ children: [
+ {
+ limit: 99,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ ],
+ },
+ providerResults: [...makeHistoryResults(2)],
+ expectedResultIndexes: [0],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "limit nested override good",
+ resultGroups: {
+ children: [
+ {
+ limit: 99,
+ children: [
+ {
+ limit: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ ],
+ },
+ providerResults: [...makeHistoryResults(2)],
+ expectedResultIndexes: [0],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "multiple groups",
+ resultGroups: {
+ children: [
+ { group: UrlbarUtils.RESULT_GROUP.GENERAL },
+ { group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(2),
+ ...makeHistoryResults(2),
+ ],
+ expectedResultIndexes: [...makeIndexRange(2, 2), ...makeIndexRange(0, 2)],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "multiple groups limit 1",
+ resultGroups: {
+ children: [
+ {
+ limit: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ { group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(2),
+ ...makeHistoryResults(2),
+ ],
+ expectedResultIndexes: [...makeIndexRange(2, 1), ...makeIndexRange(0, 2)],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "multiple groups limit 2",
+ resultGroups: {
+ children: [
+ { group: UrlbarUtils.RESULT_GROUP.GENERAL },
+ {
+ limit: 1,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(2),
+ ...makeHistoryResults(2),
+ ],
+ expectedResultIndexes: [...makeIndexRange(2, 2), ...makeIndexRange(0, 1)],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "multiple groups limit 3",
+ resultGroups: {
+ children: [
+ {
+ limit: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ { group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION },
+ { group: UrlbarUtils.RESULT_GROUP.GENERAL },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(2),
+ ...makeHistoryResults(2),
+ ],
+ expectedResultIndexes: [
+ ...makeIndexRange(2, 1),
+ ...makeIndexRange(0, 2),
+ ...makeIndexRange(3, 1),
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "multiple groups limit 4",
+ resultGroups: {
+ children: [
+ { group: UrlbarUtils.RESULT_GROUP.GENERAL },
+ {
+ limit: 1,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ { group: UrlbarUtils.RESULT_GROUP.GENERAL },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(2),
+ ...makeHistoryResults(2),
+ ],
+ expectedResultIndexes: [...makeIndexRange(2, 2), ...makeIndexRange(0, 1)],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "multiple groups nested 1",
+ resultGroups: {
+ children: [
+ {
+ children: [
+ { group: UrlbarUtils.RESULT_GROUP.GENERAL },
+ { group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION },
+ ],
+ },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(2),
+ ...makeHistoryResults(2),
+ ],
+ expectedResultIndexes: [...makeIndexRange(2, 2), ...makeIndexRange(0, 2)],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "multiple groups nested 2",
+ resultGroups: {
+ children: [
+ {
+ children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }],
+ },
+ {
+ children: [{ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION }],
+ },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(2),
+ ...makeHistoryResults(2),
+ ],
+ expectedResultIndexes: [...makeIndexRange(2, 2), ...makeIndexRange(0, 2)],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "multiple groups nested limit 1",
+ resultGroups: {
+ children: [
+ {
+ limit: 1,
+ children: [
+ { group: UrlbarUtils.RESULT_GROUP.GENERAL },
+ { group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION },
+ ],
+ },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(2),
+ ...makeHistoryResults(2),
+ ],
+ expectedResultIndexes: [...makeIndexRange(2, 1)],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "multiple groups nested limit 2",
+ resultGroups: {
+ children: [
+ {
+ children: [
+ {
+ limit: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ {
+ children: [{ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION }],
+ },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(2),
+ ...makeHistoryResults(2),
+ ],
+ expectedResultIndexes: [...makeIndexRange(2, 1), ...makeIndexRange(0, 2)],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "multiple groups nested limit 3",
+ resultGroups: {
+ children: [
+ {
+ children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }],
+ },
+ {
+ children: [
+ {
+ limit: 1,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(2),
+ ...makeHistoryResults(2),
+ ],
+ expectedResultIndexes: [...makeIndexRange(2, 2), ...makeIndexRange(0, 1)],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "multiple groups nested limit 4",
+ resultGroups: {
+ children: [
+ {
+ children: [
+ {
+ limit: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ {
+ children: [{ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION }],
+ },
+ {
+ children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }],
+ },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(2),
+ ...makeHistoryResults(2),
+ ],
+ expectedResultIndexes: [
+ ...makeIndexRange(2, 1),
+ ...makeIndexRange(0, 2),
+ ...makeIndexRange(3, 1),
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "multiple groups nested limit 5",
+ resultGroups: {
+ children: [
+ {
+ children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }],
+ },
+ {
+ children: [
+ {
+ limit: 1,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ {
+ children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }],
+ },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(2),
+ ...makeHistoryResults(2),
+ ],
+ expectedResultIndexes: [...makeIndexRange(2, 2), ...makeIndexRange(0, 1)],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "multiple groups nested limit 6",
+ resultGroups: {
+ children: [
+ {
+ children: [
+ {
+ limit: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ { group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION },
+ { group: UrlbarUtils.RESULT_GROUP.GENERAL },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(2),
+ ...makeHistoryResults(2),
+ ],
+ expectedResultIndexes: [
+ ...makeIndexRange(2, 1),
+ ...makeIndexRange(0, 2),
+ ...makeIndexRange(3, 1),
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "flex 1",
+ resultGroups: {
+ flexChildren: true,
+ children: [
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(MAX_RESULTS),
+ ...makeHistoryResults(MAX_RESULTS),
+ ],
+ expectedResultIndexes: [
+ // general/history: round(10 * (1 / (1 + 1))) = 5
+ ...makeIndexRange(MAX_RESULTS, 5),
+ // remote suggestions: round(10 * (1 / (1 + 1))) = 5
+ ...makeIndexRange(0, 5),
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "flex 2",
+ resultGroups: {
+ flexChildren: true,
+ children: [
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(MAX_RESULTS),
+ ...makeHistoryResults(MAX_RESULTS),
+ ],
+ expectedResultIndexes: [
+ // general/history: round(10 * (2 / 3)) = 7
+ ...makeIndexRange(MAX_RESULTS, 7),
+ // remote suggestions: round(10 * (1 / 3)) = 3
+ ...makeIndexRange(0, 3),
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "flex 3",
+ resultGroups: {
+ flexChildren: true,
+ children: [
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(MAX_RESULTS),
+ ...makeHistoryResults(MAX_RESULTS),
+ ],
+ expectedResultIndexes: [
+ // general/history: round(10 * (1 / 3)) = 3
+ ...makeIndexRange(MAX_RESULTS, 3),
+ // remote suggestions: round(10 * (2 / 3)) = 7
+ ...makeIndexRange(0, 7),
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "flex 4",
+ resultGroups: {
+ flexChildren: true,
+ children: [
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ },
+ ],
+ },
+ providerResults: [
+ ...makeFormHistoryResults(MAX_RESULTS),
+ ...makeRemoteSuggestionResults(MAX_RESULTS),
+ ...makeHistoryResults(MAX_RESULTS),
+ ],
+ expectedResultIndexes: [
+ // general/history: round(10 * (1 / 3)) = 3, and then incremented to 4 so
+ // that the total result span is 10 instead of 9. This group is incremented
+ // because the fractional part of its unrounded ideal max result count is
+ // 0.33 (since 10 * (1 / 3) = 3.33), the same as the other two groups, and
+ // this group is first.
+ ...makeIndexRange(2 * MAX_RESULTS, 4),
+ // remote suggestions: round(10 * (1 / 3)) = 3
+ ...makeIndexRange(MAX_RESULTS, 3),
+ // form history: round(10 * (1 / 3)) = 3
+ // The first three form history results dupe the three remote suggestions,
+ // so they should not be included.
+ ...makeIndexRange(3, 3),
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "flex 5",
+ resultGroups: {
+ flexChildren: true,
+ children: [
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ },
+ ],
+ },
+ providerResults: [
+ ...makeFormHistoryResults(MAX_RESULTS),
+ ...makeRemoteSuggestionResults(MAX_RESULTS),
+ ...makeHistoryResults(MAX_RESULTS),
+ ],
+ expectedResultIndexes: [
+ // general/history: round(10 * (2 / 4)) = 5
+ ...makeIndexRange(2 * MAX_RESULTS, 5),
+ // remote suggestions: round(10 * (1 / 4)) = 3
+ ...makeIndexRange(MAX_RESULTS, 3),
+ // form history: round(10 * (1 / 4)) = 3, but context.maxResults is 10, so 2
+ // The first three form history results dupe the three remote suggestions,
+ // so they should not be included.
+ ...makeIndexRange(3, 2),
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "flex 6",
+ resultGroups: {
+ flexChildren: true,
+ children: [
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ },
+ ],
+ },
+ providerResults: [
+ ...makeFormHistoryResults(MAX_RESULTS),
+ ...makeRemoteSuggestionResults(MAX_RESULTS),
+ ...makeHistoryResults(MAX_RESULTS),
+ ],
+ expectedResultIndexes: [
+ // general/history: round(10 * (1 / 4)) = 3
+ ...makeIndexRange(2 * MAX_RESULTS, 3),
+ // remote suggestions: round(10 * (2 / 4)) = 5
+ ...makeIndexRange(MAX_RESULTS, 5),
+ // form history: round(10 * (1 / 4)) = 3, but context.maxResults is 10, so 2
+ // The first five form history results dupe the five remote suggestions, so
+ // they should not be included.
+ ...makeIndexRange(5, 2),
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "flex 7",
+ resultGroups: {
+ flexChildren: true,
+ children: [
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ },
+ ],
+ },
+ providerResults: [
+ ...makeFormHistoryResults(MAX_RESULTS),
+ ...makeRemoteSuggestionResults(MAX_RESULTS),
+ ...makeHistoryResults(MAX_RESULTS),
+ ],
+ expectedResultIndexes: [
+ // general/history: round(10 * (1 / 4)) = 3
+ ...makeIndexRange(2 * MAX_RESULTS, 3),
+ // remote suggestions: round(10 * (1 / 4)) = 3, and then decremented to 2 so
+ // that the total result span is 10 instead of 11. This group is decremented
+ // because the fractional part of its unrounded ideal max result count is
+ // 0.5 (since 10 * (1 / 4) = 2.5), the same as the previous group, and the
+ // next group's fractional part is zero.
+ ...makeIndexRange(MAX_RESULTS, 2),
+ // form history: round(10 * (2 / 4)) = 5
+ // The first 2 form history results dupe the three remote suggestions, so
+ // they should not be included.
+ ...makeIndexRange(2, 5),
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "flex overfill 1",
+ resultGroups: {
+ flexChildren: true,
+ children: [
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ },
+ ],
+ },
+ providerResults: [
+ ...makeFormHistoryResults(MAX_RESULTS),
+ ...makeHistoryResults(MAX_RESULTS),
+ ],
+ expectedResultIndexes: [
+ // general/history: round(10 * (2 / (2 + 0 + 1))) = 7
+ ...makeIndexRange(MAX_RESULTS, 7),
+ // form history: round(10 * (1 / (2 + 0 + 1))) = 3
+ ...makeIndexRange(0, 3),
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "flex overfill 2",
+ resultGroups: {
+ flexChildren: true,
+ children: [
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ },
+ ],
+ },
+ providerResults: [
+ ...makeFormHistoryResults(MAX_RESULTS),
+ ...makeRemoteSuggestionResults(1),
+ ...makeHistoryResults(MAX_RESULTS),
+ ],
+ expectedResultIndexes: [
+ // general/history: round(9 * (2 / (2 + 0 + 1))) = 6
+ ...makeIndexRange(MAX_RESULTS + 1, 6),
+ // remote suggestions
+ ...makeIndexRange(MAX_RESULTS, 1),
+ // form history: round(9 * (1 / (2 + 0 + 1))) = 3
+ // The first form history result dupes the remote suggestion, so it should
+ // not be included.
+ ...makeIndexRange(1, 3),
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "flex nested limit 1",
+ resultGroups: {
+ children: [
+ {
+ limit: 5,
+ flexChildren: true,
+ children: [
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(MAX_RESULTS),
+ ...makeHistoryResults(MAX_RESULTS),
+ ],
+ expectedResultIndexes: [
+ // general/history: round(5 * (2 / (2 + 1))) = 3
+ ...makeIndexRange(MAX_RESULTS, 3),
+ // remote suggestions: round(5 * (1 / (2 + 1))) = 2
+ ...makeIndexRange(0, 2),
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "flex nested limit 2",
+ resultGroups: {
+ children: [
+ {
+ limit: 7,
+ flexChildren: true,
+ children: [
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(MAX_RESULTS),
+ ...makeHistoryResults(MAX_RESULTS),
+ ],
+ expectedResultIndexes: [
+ // general: round(7 * (1 / (2 + 1))) = 2
+ ...makeIndexRange(MAX_RESULTS, 2),
+ // remote suggestions: round(7 * (2 / (2 + 1))) = 5
+ ...makeIndexRange(0, 5),
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "flex nested limit 3",
+ resultGroups: {
+ children: [
+ {
+ limit: 7,
+ flexChildren: true,
+ children: [
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ {
+ limit: 3,
+ flexChildren: true,
+ children: [
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(MAX_RESULTS),
+ ...makeFormHistoryResults(MAX_RESULTS),
+ ...makeHistoryResults(MAX_RESULTS),
+ ],
+ expectedResultIndexes: [
+ // general: round(7 * (1 / (2 + 1))) = 2
+ ...makeIndexRange(2 * MAX_RESULTS, 2),
+ // remote suggestions: round(7 * (2 / (2 + 1))) = 5
+ ...makeIndexRange(0, 5),
+ // form history: round(3 * (2 / (2 + 1))) = 2
+ // The first five form history results dupe the five remote suggestions, so
+ // they should not be included.
+ ...makeIndexRange(MAX_RESULTS + 5, 2),
+ // general: round(3 * (1 / (2 + 1))) = 1
+ ...makeIndexRange(2 * MAX_RESULTS + 2, 1),
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "flex nested limit 4",
+ resultGroups: {
+ children: [
+ {
+ limit: 7,
+ flexChildren: true,
+ children: [
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ {
+ limit: 3,
+ children: [
+ { group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY },
+ { group: UrlbarUtils.RESULT_GROUP.GENERAL },
+ ],
+ },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(MAX_RESULTS),
+ ...makeFormHistoryResults(MAX_RESULTS),
+ ...makeHistoryResults(MAX_RESULTS),
+ ],
+ expectedResultIndexes: [
+ // general: round(7 * (1 / (2 + 1))) = 2
+ ...makeIndexRange(2 * MAX_RESULTS, 2),
+ // remote suggestions: round(7 * (2 / (2 + 1))) = 5
+ ...makeIndexRange(0, 5),
+ // form history: round(3 * (2 / (2 + 1))) = 2
+ // The first five form history results dupe the five remote suggestions, so
+ // they should not be included.
+ ...makeIndexRange(MAX_RESULTS + 5, 3),
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "flex nested limit 5",
+ resultGroups: {
+ children: [
+ {
+ limit: 7,
+ flexChildren: true,
+ children: [
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ {
+ limit: 3,
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(MAX_RESULTS),
+ ...makeFormHistoryResults(MAX_RESULTS),
+ ...makeHistoryResults(MAX_RESULTS),
+ ],
+ expectedResultIndexes: [
+ // general: round(7 * (1 / (2 + 1))) = 2
+ ...makeIndexRange(2 * MAX_RESULTS, 2),
+ // remote suggestions: round(7 * (2 / (2 + 1))) = 5
+ ...makeIndexRange(0, 5),
+ // form history: round(3 * (2 / (2 + 1))) = 2
+ // The first five form history results dupe the five remote suggestions, so
+ // they should not be included.
+ ...makeIndexRange(MAX_RESULTS + 5, 3),
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "flex nested",
+ resultGroups: {
+ flexChildren: true,
+ children: [
+ {
+ flex: 2,
+ flexChildren: true,
+ children: [
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ {
+ flex: 1,
+ flexChildren: true,
+ children: [
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(MAX_RESULTS),
+ ...makeFormHistoryResults(MAX_RESULTS),
+ ...makeHistoryResults(MAX_RESULTS),
+ ],
+ expectedResultIndexes: [
+ // outer 1: general & remote suggestions: round(10 * (2 / (2 + 1))) = 7
+ // inner 1: general: round(7 * (1 / (2 + 1))) = 2
+ ...makeIndexRange(2 * MAX_RESULTS, 2),
+ // inner 2: remote suggestions: round(7 * (2 / (2 + 1))) = 5
+ ...makeIndexRange(0, 5),
+
+ // outer 2: form history & general: round(10 * (1 / (2 + 1))) = 3
+ // inner 1: form history: round(3 * (2 / (2 + 1))) = 2
+ // The first five form history results dupe the five remote suggestions, so
+ // they should not be included.
+ ...makeIndexRange(MAX_RESULTS + 5, 2),
+ // inner 2: general: round(3 * (1 / (2 + 1))) = 1
+ ...makeIndexRange(2 * MAX_RESULTS + 2, 1),
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "flex nested overfill 1",
+ resultGroups: {
+ flexChildren: true,
+ children: [
+ {
+ flex: 2,
+ flexChildren: true,
+ children: [
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ {
+ flex: 1,
+ flexChildren: true,
+ children: [
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(MAX_RESULTS),
+ ...makeFormHistoryResults(MAX_RESULTS),
+ ],
+ expectedResultIndexes: [
+ // outer 1: general & remote suggestions: round(10 * (2 / (2 + 1))) = 7
+ // inner 1: general: no results
+ // inner 2: remote suggestions: round(7 * (2 / (2 + 0))) = 7
+ ...makeIndexRange(0, 7),
+
+ // outer 2: form history & general: round(10 * (1 / (2 + 1))) = 3
+ // inner 1: form history: round(3 * (2 / (2 + 0))) = 3
+ // The first seven form history results dupe the seven remote suggestions,
+ // so they should not be included.
+ ...makeIndexRange(MAX_RESULTS + 7, 3),
+ // inner 2: general: no results
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "flex nested overfill 2",
+ resultGroups: {
+ flexChildren: true,
+ children: [
+ {
+ flex: 2,
+ flexChildren: true,
+ children: [
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ {
+ flex: 1,
+ flexChildren: true,
+ children: [
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ ],
+ },
+ providerResults: [...makeFormHistoryResults(MAX_RESULTS)],
+ expectedResultIndexes: [
+ // outer 1: general & remote suggestions: round(10 * (2 / (2 + 1))) = 7
+ // inner 1: general: no results
+ // inner 2: remote suggestions: no results
+
+ // outer 2: form history & general: round(10 * (1 / (0 + 1))) = 10
+ // inner 1: form history: round(10 * (2 / (2 + 0))) = 10
+ ...makeIndexRange(0, MAX_RESULTS),
+ // inner 2: general: no results
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "flex nested overfill 3",
+ resultGroups: {
+ flexChildren: true,
+ children: [
+ {
+ flex: 2,
+ flexChildren: true,
+ children: [
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ {
+ flex: 1,
+ flexChildren: true,
+ children: [
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ ],
+ },
+ providerResults: [...makeRemoteSuggestionResults(MAX_RESULTS)],
+ expectedResultIndexes: [
+ // outer 1: general & remote suggestions: round(10 * (2 / (2 + 0))) = 10
+ // inner 1: general: no results
+ // inner 2: remote suggestions: round(10 * (2 / (2 + 0))) = 10
+ ...makeIndexRange(0, MAX_RESULTS),
+
+ // outer 2: form history & general: round(10 * (1 / (2 + 1))) = 3
+ // inner 1: form history: no results
+ // inner 2: general: no results
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "limit ignored with flex",
+ resultGroups: {
+ flexChildren: true,
+ children: [
+ {
+ limit: 1,
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ providerResults: [
+ ...makeRemoteSuggestionResults(MAX_RESULTS),
+ ...makeHistoryResults(MAX_RESULTS),
+ ],
+ expectedResultIndexes: [
+ // general/history: round(10 * (2 / (2 + 1))) = 7 -- limit ignored
+ ...makeIndexRange(MAX_RESULTS, 7),
+ // remote suggestions: round(10 * (1 / (2 + 1))) = 3
+ ...makeIndexRange(0, 3),
+ ],
+});
+
+add_resultGroupsLimit_tasks({
+ testName: "resultSpan = 3 followed by others",
+ resultGroups: {
+ children: [
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ providerResults: [
+ // max results remote suggestions
+ ...makeRemoteSuggestionResults(MAX_RESULTS),
+ // 1 history with resultSpan = 3
+ Object.assign(makeHistoryResults(1)[0], { resultSpan: 3 }),
+ ],
+ expectedResultIndexes: [
+ // general/history: 1
+ ...makeIndexRange(MAX_RESULTS, 1),
+ // remote suggestions: maxResults - resultSpan of 3 = 10 - 3 = 7
+ ...makeIndexRange(0, 7),
+ ],
+});
+
+add_resultGroups_task({
+ testName: "maxResultCount: 1, availableSpan: 3",
+ resultGroups: {
+ children: [
+ {
+ maxResultCount: 1,
+ availableSpan: 3,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ providerResults: [...makeHistoryResults(MAX_RESULTS)],
+ expectedResultIndexes: [0],
+});
+
+add_resultGroups_task({
+ testName: "maxResultCount: 1, availableSpan: 3, resultSpan = 3",
+ resultGroups: {
+ children: [
+ {
+ maxResultCount: 1,
+ availableSpan: 3,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ providerResults: [
+ Object.assign(makeHistoryResults(1)[0], { resultSpan: 3 }),
+ Object.assign(makeHistoryResults(1)[0], { resultSpan: 3 }),
+ Object.assign(makeHistoryResults(1)[0], { resultSpan: 3 }),
+ ],
+ expectedResultIndexes: [0],
+});
+
+add_resultGroups_task({
+ testName: "maxResultCount: 3, availableSpan: 1",
+ resultGroups: {
+ children: [
+ {
+ maxResultCount: 3,
+ availableSpan: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ providerResults: [...makeHistoryResults(MAX_RESULTS)],
+ expectedResultIndexes: [0],
+});
+
+add_resultGroups_task({
+ testName: "maxResultCount: 3, availableSpan: 1, resultSpan = 3",
+ resultGroups: {
+ children: [
+ {
+ maxResultCount: 3,
+ availableSpan: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ providerResults: [Object.assign(makeHistoryResults(1)[0], { resultSpan: 3 })],
+ expectedResultIndexes: [],
+});
+
+add_resultGroups_task({
+ testName: "availableSpan: 1",
+ resultGroups: {
+ children: [
+ {
+ availableSpan: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ providerResults: [...makeHistoryResults(MAX_RESULTS)],
+ expectedResultIndexes: [0],
+});
+
+add_resultGroups_task({
+ testName: "availableSpan: 1, resultSpan = 3",
+ resultGroups: {
+ children: [
+ {
+ availableSpan: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ providerResults: [Object.assign(makeHistoryResults(1)[0], { resultSpan: 3 })],
+ expectedResultIndexes: [],
+});
+
+add_resultGroups_task({
+ testName: "availableSpan: 3, resultSpan = 2 and resultSpan = 1",
+ resultGroups: {
+ children: [
+ {
+ availableSpan: 3,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ providerResults: [
+ makeHistoryResults(1)[0],
+ Object.assign(makeHistoryResults(1)[0], { resultSpan: 2 }),
+ makeHistoryResults(1)[0],
+ ],
+ expectedResultIndexes: [0, 1],
+});
+
+add_resultGroups_task({
+ testName: "availableSpan: 3, resultSpan = 1 and resultSpan = 2",
+ resultGroups: {
+ children: [
+ {
+ availableSpan: 3,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ },
+ providerResults: [
+ Object.assign(makeHistoryResults(1)[0], { resultSpan: 2 }),
+ makeHistoryResults(1)[0],
+ makeHistoryResults(1)[0],
+ ],
+ expectedResultIndexes: [0, 1],
+});
+
+/**
+ * Adds a single test task.
+ *
+ * @param {object} options
+ * The options for the test
+ * @param {string} options.testName
+ * This name is logged with `info` as the task starts.
+ * @param {object} options.resultGroups
+ * browser.urlbar.resultGroups is set to this value as the task starts.
+ * @param {Array} options.providerResults
+ * Array of result objects that the test provider will add.
+ * @param {Array} options.expectedResultIndexes
+ * Array of indexes in `providerResults` of the expected final results.
+ */
+function add_resultGroups_task({
+ testName,
+ resultGroups,
+ providerResults,
+ expectedResultIndexes,
+}) {
+ let func = async () => {
+ info(`Running resultGroups test: ${testName}`);
+ info(`Setting result groups: ` + JSON.stringify(resultGroups));
+ setResultGroups(resultGroups);
+ let provider = registerBasicTestProvider(providerResults);
+ let context = createContext("foo", { providers: [provider.name] });
+ let controller = UrlbarTestUtils.newMockController();
+ await UrlbarProvidersManager.startQuery(context, controller);
+ UrlbarProvidersManager.unregisterProvider(provider);
+ let expectedResults = expectedResultIndexes.map(i => providerResults[i]);
+ Assert.deepEqual(context.results, expectedResults);
+ setResultGroups(null);
+ };
+ Object.defineProperty(func, "name", { value: testName });
+ add_task(func);
+}
+
+/**
+ * Adds test tasks for each of the keys in `LIMIT_KEYS`.
+ *
+ * @param {object} options
+ * The options for the test
+ * @param {string} options.testName
+ * The name of the test.
+ * @param {object} options.resultGroups
+ * The resultGroups object to set.
+ * @param {Array} options.providerResults
+ * The results to return from the test
+ * @param {Array} options.expectedResultIndexes
+ * Indexes of the expected results within {@link providerResults}
+ */
+function add_resultGroupsLimit_tasks({
+ testName,
+ resultGroups,
+ providerResults,
+ expectedResultIndexes,
+}) {
+ for (let key of LIMIT_KEYS) {
+ add_resultGroups_task({
+ testName: `${testName} (limit: ${key})`,
+ resultGroups: replaceLimitWithKey(resultGroups, key),
+ providerResults,
+ expectedResultIndexes,
+ });
+ }
+}
+
+function replaceLimitWithKey(group, key) {
+ group = { ...group };
+ if ("limit" in group) {
+ group[key] = group.limit;
+ delete group.limit;
+ }
+ for (let i = 0; i < group.children?.length; i++) {
+ group.children[i] = replaceLimitWithKey(group.children[i], key);
+ }
+ return group;
+}
+
+function makeHistoryResults(count) {
+ let results = [];
+ for (let i = 0; i < count; i++) {
+ results.push(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://example.com/" + i }
+ )
+ );
+ }
+ return results;
+}
+
+function makeRemoteSuggestionResults(count) {
+ let results = [];
+ for (let i = 0; i < count; i++) {
+ results.push(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ {
+ engine: "test",
+ query: "test",
+ suggestion: "test " + i,
+ lowerCaseSuggestion: "test " + i,
+ }
+ )
+ );
+ }
+ return results;
+}
+
+function makeFormHistoryResults(count) {
+ let results = [];
+ for (let i = 0; i < count; i++) {
+ results.push(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ {
+ engine: "test",
+ suggestion: "test " + i,
+ lowerCaseSuggestion: "test " + i,
+ }
+ )
+ );
+ }
+ return results;
+}
+
+function makeIndexRange(startIndex, count) {
+ let indexes = [];
+ for (let i = startIndex; i < startIndex + count; i++) {
+ indexes.push(i);
+ }
+ return indexes;
+}
+
+function setResultGroups(resultGroups) {
+ sandbox.restore();
+ if (resultGroups) {
+ sandbox.stub(UrlbarPrefs, "resultGroups").get(() => resultGroups);
+ }
+}
diff --git a/browser/components/urlbar/tests/unit/test_search_engine_host.js b/browser/components/urlbar/tests/unit/test_search_engine_host.js
new file mode 100644
index 0000000000..8135566547
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_search_engine_host.js
@@ -0,0 +1,98 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let engine;
+
+add_task(async function test_searchEngine_autoFill() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ await SearchTestUtils.installSearchExtension({
+ name: "MySearchEngine",
+ search_url: "https://my.search.com/",
+ });
+ engine = Services.search.getEngineByName("MySearchEngine");
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.autoFill.searchEngines");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+
+ // Add an uri that matches the search string with high frecency.
+ let uri = Services.io.newURI("http://www.example.com/my/");
+ let visits = [];
+ for (let i = 0; i < 100; ++i) {
+ visits.push({ uri, title: "Terms - SearchEngine Search" });
+ }
+ await PlacesTestUtils.addVisits(visits);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri,
+ title: "Example bookmark",
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ ok(
+ (await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: uri,
+ })) > 10000,
+ "Added URI should have expected high frecency"
+ );
+
+ info(
+ "Check search domain is autoFilled even if there's an higher frecency match"
+ );
+ let context = createContext("my", { isPrivate: false });
+ await check_results({
+ search: "my",
+ autofilled: "my.search.com/",
+ matches: [
+ makePrioritySearchResult(context, {
+ engineName: "MySearchEngine",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_searchEngine_noautoFill() {
+ Services.prefs.setIntPref(
+ "browser.urlbar.tabToSearch.onboard.interactionsLeft",
+ 0
+ );
+ await PlacesTestUtils.addVisits(
+ Services.io.newURI("http://my.search.com/samplepage/")
+ );
+
+ info("Check search domain is not autoFilled if it matches a visited domain");
+ let context = createContext("my", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "my.search.com/",
+ completed: "http://my.search.com/",
+ matches: [
+ // Note this result is a normal Autofill result and not a priority engine.
+ makeVisitResult(context, {
+ uri: "http://my.search.com/",
+ fallbackTitle: "my.search.com",
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: engine.name,
+ engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
+ uri: UrlbarUtils.stripPublicSuffixFromHost(engine.searchUrlDomain),
+ providesSearchMode: true,
+ query: "",
+ providerName: "TabToSearch",
+ }),
+ makeVisitResult(context, {
+ uri: "http://my.search.com/samplepage/",
+ title: "test visit for http://my.search.com/samplepage/",
+ providerName: "Places",
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+ Services.prefs.clearUserPref(
+ "browser.urlbar.tabToSearch.onboard.interactionsLeft"
+ );
+});
diff --git a/browser/components/urlbar/tests/unit/test_search_engine_restyle.js b/browser/components/urlbar/tests/unit/test_search_engine_restyle.js
new file mode 100644
index 0000000000..5bfdd66416
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_search_engine_restyle.js
@@ -0,0 +1,124 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+testEngine_setup();
+
+const engineDomain = "s.example.com";
+add_task(async function setup() {
+ Services.prefs.setBoolPref("browser.urlbar.restyleSearches", true);
+ await SearchTestUtils.installSearchExtension({
+ name: "MozSearch",
+ search_url: `https://${engineDomain}/search`,
+ });
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.restyleSearches");
+ });
+});
+
+add_task(async function test_searchEngine() {
+ let uri = Services.io.newURI(`https://${engineDomain}/search?q=Terms`);
+ await PlacesTestUtils.addVisits({
+ uri,
+ title: "Terms - SearchEngine Search",
+ });
+
+ info("Past search terms should be styled.");
+ let context = createContext("term", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeFormHistoryResult(context, {
+ engineName: "MozSearch",
+ suggestion: "Terms",
+ }),
+ ],
+ });
+
+ info(
+ "Searching for a superset of the search string in history should not restyle."
+ );
+ context = createContext("Terms Foo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ info("Bookmarked past searches should not be restyled");
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri,
+ title: "Terms - SearchEngine Search",
+ });
+
+ context = createContext("term", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri.spec,
+ title: "Terms - SearchEngine Search",
+ }),
+ ],
+ });
+
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ info("Past search terms should not be styled if restyling is disabled");
+ Services.prefs.setBoolPref("browser.urlbar.restyleSearches", false);
+ context = createContext("term", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: uri.spec,
+ title: "Terms - SearchEngine Search",
+ }),
+ ],
+ });
+ Services.prefs.setBoolPref("browser.urlbar.restyleSearches", true);
+
+ await cleanupPlaces();
+});
+
+add_task(async function test_extraneousParameters() {
+ info("SERPs in history with extraneous parameters should not be restyled.");
+ let uri = Services.io.newURI(
+ `https://${engineDomain}/search?q=Terms&p=2&type=img`
+ );
+ await PlacesTestUtils.addVisits({
+ uri,
+ title: "Terms - SearchEngine Search",
+ });
+
+ let context = createContext("term", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: uri.spec,
+ title: "Terms - SearchEngine Search",
+ }),
+ ],
+ });
+});
diff --git a/browser/components/urlbar/tests/unit/test_search_suggestions.js b/browser/components/urlbar/tests/unit/test_search_suggestions.js
new file mode 100644
index 0000000000..1a49ff2e7d
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_search_suggestions.js
@@ -0,0 +1,2078 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that search engine suggestions are returned by
+ * UrlbarProviderSearchSuggestions.
+ */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+const SUGGEST_PREF = "browser.urlbar.suggest.searches";
+const SUGGEST_ENABLED_PREF = "browser.search.suggest.enabled";
+const PRIVATE_ENABLED_PREF = "browser.search.suggest.enabled.private";
+const PRIVATE_SEARCH_PREF = "browser.search.separatePrivateDefault.ui.enabled";
+const TAB_TO_SEARCH_PREF = "browser.urlbar.suggest.engines";
+const TRENDING_PREF = "browser.urlbar.trending.featureGate";
+const QUICKACTIONS_PREF = "browser.urlbar.suggest.quickactions";
+const MAX_RICH_RESULTS_PREF = "browser.urlbar.maxRichResults";
+const MAX_FORM_HISTORY_PREF = "browser.urlbar.maxHistoricalSearchSuggestions";
+const SHOW_SEARCH_SUGGESTIONS_FIRST_PREF =
+ "browser.urlbar.showSearchSuggestionsFirst";
+const SEARCH_STRING = "hello";
+
+const MAX_RESULTS = Services.prefs.getIntPref(MAX_RICH_RESULTS_PREF, 10);
+
+var suggestionsFn;
+var previousSuggestionsFn;
+let port;
+let sandbox;
+
+/**
+ * Set the current suggestion funciton.
+ *
+ * @param {Function} fn
+ * A function that that a search string and returns an array of strings that
+ * will be used as search suggestions.
+ * Note: `fn` should return > 0 suggestions in most cases. Otherwise, you may
+ * encounter unexpected behaviour with UrlbarProviderSuggestion's
+ * _lastLowResultsSearchSuggestion safeguard.
+ */
+function setSuggestionsFn(fn) {
+ previousSuggestionsFn = suggestionsFn;
+ suggestionsFn = fn;
+}
+
+async function cleanup() {
+ Services.prefs.clearUserPref("browser.urlbar.autoFill");
+ Services.prefs.clearUserPref("browser.urlbar.autoFill.searchEngines");
+ Services.prefs.clearUserPref(SUGGEST_PREF);
+ Services.prefs.clearUserPref(SUGGEST_ENABLED_PREF);
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+ sandbox.restore();
+}
+
+async function cleanUpSuggestions() {
+ await cleanup();
+ if (previousSuggestionsFn) {
+ suggestionsFn = previousSuggestionsFn;
+ previousSuggestionsFn = null;
+ }
+}
+
+function makeFormHistoryResults(context, count) {
+ let results = [];
+ for (let i = 0; i < count; i++) {
+ results.push(
+ makeFormHistoryResult(context, {
+ suggestion: `${SEARCH_STRING} world Form History ${i}`,
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ })
+ );
+ }
+ return results;
+}
+
+function makeRemoteSuggestionResults(
+ context,
+ { suggestionPrefix = SEARCH_STRING, query = undefined } = {}
+) {
+ // The suggestions function in `setup` returns:
+ // [searchString, searchString + "foo", searchString + "bar"]
+ // But when the heuristic is a search result, the muxer discards suggestion
+ // results that match the search string, and therefore we expect only two
+ // remote suggestion results, the "foo" and "bar" ones.
+ return [
+ makeSearchResult(context, {
+ query,
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: suggestionPrefix + " foo",
+ }),
+ makeSearchResult(context, {
+ query,
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: suggestionPrefix + " bar",
+ }),
+ ];
+}
+
+function setResultGroups(groups) {
+ sandbox.restore();
+ sandbox.stub(UrlbarPrefs, "resultGroups").get(() => {
+ return {
+ children: [
+ // heuristic
+ {
+ maxResultCount: 1,
+ children: [
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_EXTENSION },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_SEARCH_TIP },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_OMNIBOX },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_AUTOFILL },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TOKEN_ALIAS_ENGINE },
+ { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK },
+ ],
+ },
+ // extensions using the omnibox API
+ {
+ group: UrlbarUtils.RESULT_GROUP.OMNIBOX,
+ },
+ ...groups,
+ ],
+ };
+ });
+}
+
+add_task(async function setup() {
+ sandbox = lazy.sinon.createSandbox();
+
+ let engine = await addTestSuggestionsEngine(searchStr => {
+ return suggestionsFn(searchStr);
+ });
+ port = engine.getSubmission("").uri.port;
+
+ setSuggestionsFn(searchStr => {
+ let suffixes = ["foo", "bar"];
+ return [searchStr].concat(suffixes.map(s => searchStr + " " + s));
+ });
+
+ // Install the test engine.
+ let oldDefaultEngine = await Services.search.getDefault();
+ registerCleanupFunction(async () => {
+ Services.search.setDefault(
+ oldDefaultEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ Services.prefs.clearUserPref(PRIVATE_SEARCH_PREF);
+ Services.prefs.clearUserPref(TRENDING_PREF);
+ Services.prefs.clearUserPref(QUICKACTIONS_PREF);
+ Services.prefs.clearUserPref(TAB_TO_SEARCH_PREF);
+ sandbox.restore();
+ });
+ Services.search.setDefault(engine, Ci.nsISearchService.CHANGE_REASON_UNKNOWN);
+ Services.prefs.setBoolPref(PRIVATE_SEARCH_PREF, false);
+ Services.prefs.setBoolPref(TRENDING_PREF, false);
+ Services.prefs.setBoolPref(QUICKACTIONS_PREF, false);
+ // Tab-to-search engines can introduce unexpected results, espescially because
+ // they depend on real en-US engines.
+ Services.prefs.setBoolPref(TAB_TO_SEARCH_PREF, false);
+
+ // Add MAX_RESULTS form history.
+ let context = createContext(SEARCH_STRING, { isPrivate: false });
+ let entries = makeFormHistoryResults(context, MAX_RESULTS).map(r => ({
+ value: r.payload.suggestion,
+ source: SUGGESTIONS_ENGINE_NAME,
+ }));
+ await UrlbarTestUtils.formHistory.add(entries);
+});
+
+add_task(async function disabled_urlbarSuggestions() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, false);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+ let context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanUpSuggestions();
+});
+
+add_task(async function disabled_allSuggestions() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, false);
+ let context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanUpSuggestions();
+});
+
+add_task(async function disabled_privateWindow() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+ Services.prefs.setBoolPref(PRIVATE_ENABLED_PREF, false);
+ let context = createContext(SEARCH_STRING, { isPrivate: true });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanUpSuggestions();
+});
+
+add_task(async function disabled_urlbarSuggestions_withRestrictionToken() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, false);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+ let context = createContext(
+ `${UrlbarTokenizer.RESTRICT.SEARCH} ${SEARCH_STRING}`,
+ { isPrivate: false }
+ );
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ query: SEARCH_STRING,
+ alias: UrlbarTokenizer.RESTRICT.SEARCH,
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 3),
+ ...makeRemoteSuggestionResults(context, {
+ query: SEARCH_STRING,
+ }),
+ ],
+ });
+ await cleanUpSuggestions();
+});
+
+add_task(
+ async function disabled_urlbarSuggestions_withRestrictionToken_private() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, false);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+ Services.prefs.setBoolPref(PRIVATE_ENABLED_PREF, false);
+ let context = createContext(
+ `${UrlbarTokenizer.RESTRICT.SEARCH} ${SEARCH_STRING}`,
+ { isPrivate: true }
+ );
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ query: SEARCH_STRING,
+ alias: UrlbarTokenizer.RESTRICT.SEARCH,
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanUpSuggestions();
+ }
+);
+
+add_task(
+ async function disabled_urlbarSuggestions_withRestrictionToken_private_enabled() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, false);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+ Services.prefs.setBoolPref(PRIVATE_ENABLED_PREF, true);
+ let context = createContext(
+ `${UrlbarTokenizer.RESTRICT.SEARCH} ${SEARCH_STRING}`,
+ { isPrivate: true }
+ );
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ query: SEARCH_STRING,
+ alias: UrlbarTokenizer.RESTRICT.SEARCH,
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 3),
+ ...makeRemoteSuggestionResults(context, {
+ query: SEARCH_STRING,
+ }),
+ ],
+ });
+ await cleanUpSuggestions();
+ }
+);
+
+add_task(async function enabled_by_pref_privateWindow() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+ Services.prefs.setBoolPref(PRIVATE_ENABLED_PREF, true);
+ let context = createContext(SEARCH_STRING, { isPrivate: true });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 3),
+ ...makeRemoteSuggestionResults(context),
+ ],
+ });
+ await cleanUpSuggestions();
+
+ Services.prefs.clearUserPref(PRIVATE_ENABLED_PREF);
+});
+
+add_task(async function singleWordQuery() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+ let context = createContext(SEARCH_STRING, { isPrivate: false });
+
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 3),
+ ...makeRemoteSuggestionResults(context),
+ ],
+ });
+
+ await cleanUpSuggestions();
+});
+
+add_task(async function multiWordQuery() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+ const query = `${SEARCH_STRING} world`;
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 3),
+ ...makeRemoteSuggestionResults(context, {
+ suggestionPrefix: query,
+ }),
+ ],
+ });
+
+ await cleanUpSuggestions();
+});
+
+add_task(async function suffixMatch() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+
+ setSuggestionsFn(searchStr => {
+ let prefixes = ["baz", "quux"];
+ return prefixes.map(p => p + " " + searchStr);
+ });
+
+ let context = createContext(SEARCH_STRING, { isPrivate: false });
+
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 3),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: "baz " + SEARCH_STRING,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: "quux " + SEARCH_STRING,
+ }),
+ ],
+ });
+
+ await cleanUpSuggestions();
+});
+
+add_task(async function remoteSuggestionsDupeSearchString() {
+ Services.prefs.setIntPref(MAX_FORM_HISTORY_PREF, 0);
+
+ // Return remote suggestions with the trimmed search string, the uppercased
+ // search string, and the search string with a trailing space, plus the usual
+ // "foo" and "bar" suggestions.
+ setSuggestionsFn(searchStr => {
+ let suffixes = ["foo", "bar"];
+ return [searchStr.trim(), searchStr.toUpperCase(), searchStr + " "].concat(
+ suffixes.map(s => searchStr + " " + s)
+ );
+ });
+
+ // Do a search with a trailing space. All the variations of the search string
+ // with regard to spaces and case should be discarded from the remote
+ // suggestions, leaving only the usual "foo" and "bar" suggestions.
+ let query = SEARCH_STRING + " ";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ query,
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeRemoteSuggestionResults(context),
+ ],
+ });
+
+ await cleanUpSuggestions();
+ Services.prefs.clearUserPref(MAX_FORM_HISTORY_PREF);
+});
+
+add_task(async function queryIsNotASubstring() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+
+ setSuggestionsFn(searchStr => {
+ return ["aaa", "bbb"];
+ });
+
+ let context = createContext(SEARCH_STRING, { isPrivate: false });
+
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 3),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: "aaa",
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: "bbb",
+ }),
+ ],
+ });
+
+ await cleanUpSuggestions();
+});
+
+add_task(async function restrictToken() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+
+ // Add a visit and a bookmark. Actually, make the bookmark visited too so
+ // that it's guaranteed, with its higher frecency, to appear above the search
+ // suggestions.
+ await PlacesTestUtils.addVisits([
+ {
+ uri: Services.io.newURI(`http://example.com/${SEARCH_STRING}-visit`),
+ title: `${SEARCH_STRING} visit`,
+ },
+ {
+ uri: Services.io.newURI(`http://example.com/${SEARCH_STRING}-bookmark`),
+ title: `${SEARCH_STRING} bookmark`,
+ },
+ ]);
+
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: Services.io.newURI(`http://example.com/${SEARCH_STRING}-bookmark`),
+ title: `${SEARCH_STRING} bookmark`,
+ });
+
+ let context = createContext(SEARCH_STRING, { isPrivate: false });
+
+ // Do an unrestricted search to make sure everything appears in it, including
+ // the visit and bookmark.
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 5),
+ ...makeRemoteSuggestionResults(context),
+ makeBookmarkResult(context, {
+ uri: `http://example.com/${SEARCH_STRING}-bookmark`,
+ title: `${SEARCH_STRING} bookmark`,
+ }),
+ makeVisitResult(context, {
+ uri: `http://example.com/${SEARCH_STRING}-visit`,
+ title: `${SEARCH_STRING} visit`,
+ }),
+ ],
+ });
+
+ // Now do a restricted search to make sure only suggestions appear.
+ context = createContext(
+ `${UrlbarTokenizer.RESTRICT.SEARCH} ${SEARCH_STRING}`,
+ {
+ isPrivate: false,
+ }
+ );
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias: UrlbarTokenizer.RESTRICT.SEARCH,
+ query: SEARCH_STRING,
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 3),
+ ...makeRemoteSuggestionResults(context, {
+ suggestionPrefix: SEARCH_STRING,
+ query: SEARCH_STRING,
+ }),
+ ],
+ });
+
+ // Typing the search restriction char shows the Search Engine entry and local
+ // results.
+ context = createContext(UrlbarTokenizer.RESTRICT.SEARCH, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ query: "",
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 1),
+ ],
+ });
+
+ // Also if followed by multiple spaces.
+ context = createContext(`${UrlbarTokenizer.RESTRICT.SEARCH} `, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias: UrlbarTokenizer.RESTRICT.SEARCH,
+ query: "",
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 1),
+ ],
+ });
+
+ // If followed by any char we should fetch suggestions.
+ // Note this uses "h" to match form history.
+ context = createContext(`${UrlbarTokenizer.RESTRICT.SEARCH}h`, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ query: "h",
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 3),
+ ...makeRemoteSuggestionResults(context, {
+ suggestionPrefix: "h",
+ query: "h",
+ }),
+ ],
+ });
+
+ // Also if followed by a space and single char.
+ context = createContext(`${UrlbarTokenizer.RESTRICT.SEARCH} h`, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias: UrlbarTokenizer.RESTRICT.SEARCH,
+ query: "h",
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 3),
+ ...makeRemoteSuggestionResults(context, {
+ suggestionPrefix: "h",
+ query: "h",
+ }),
+ ],
+ });
+
+ // Leading search-mode restriction tokens are removed.
+ context = createContext(
+ `${UrlbarTokenizer.RESTRICT.BOOKMARK} ${SEARCH_STRING}`,
+ { isPrivate: false }
+ );
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ query: SEARCH_STRING,
+ alias: UrlbarTokenizer.RESTRICT.BOOKMARK,
+ }),
+ makeBookmarkResult(context, {
+ uri: `http://example.com/${SEARCH_STRING}-bookmark`,
+ title: `${SEARCH_STRING} bookmark`,
+ }),
+ ],
+ });
+
+ // Non-search-mode restriction tokens remain in the query and heuristic search
+ // result.
+ let token;
+ for (let t of Object.values(UrlbarTokenizer.RESTRICT)) {
+ if (!UrlbarTokenizer.SEARCH_MODE_RESTRICT.has(t)) {
+ token = t;
+ break;
+ }
+ }
+ Assert.ok(
+ token,
+ "Non-search-mode restrict token exists -- if not, you can probably remove me!"
+ );
+ context = createContext(token, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanUpSuggestions();
+});
+
+add_task(async function mixup_frecency() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+
+ // At most, we should have 22 results in this subtest. We set this to 30 to
+ // make we're not cutting off any results and we are actually getting 22.
+ Services.prefs.setIntPref(MAX_RICH_RESULTS_PREF, 30);
+
+ // Add a visit and a bookmark. Actually, make the bookmark visited too so
+ // that it's guaranteed, with its higher frecency, to appear above the search
+ // suggestions.
+ await PlacesTestUtils.addVisits([
+ {
+ uri: Services.io.newURI("http://example.com/lo0"),
+ title: `${SEARCH_STRING} low frecency 0`,
+ },
+ {
+ uri: Services.io.newURI("http://example.com/lo1"),
+ title: `${SEARCH_STRING} low frecency 1`,
+ },
+ {
+ uri: Services.io.newURI("http://example.com/lo2"),
+ title: `${SEARCH_STRING} low frecency 2`,
+ },
+ {
+ uri: Services.io.newURI("http://example.com/lo3"),
+ title: `${SEARCH_STRING} low frecency 3`,
+ },
+ {
+ uri: Services.io.newURI("http://example.com/lo4"),
+ title: `${SEARCH_STRING} low frecency 4`,
+ },
+ ]);
+
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: Services.io.newURI("http://example.com/hi0"),
+ title: `${SEARCH_STRING} high frecency 0`,
+ transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ },
+ {
+ uri: Services.io.newURI("http://example.com/hi1"),
+ title: `${SEARCH_STRING} high frecency 1`,
+ transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ },
+ {
+ uri: Services.io.newURI("http://example.com/hi2"),
+ title: `${SEARCH_STRING} high frecency 2`,
+ transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ },
+ {
+ uri: Services.io.newURI("http://example.com/hi3"),
+ title: `${SEARCH_STRING} high frecency 3`,
+ transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ },
+ ]);
+ }
+
+ for (let i = 0; i < 4; i++) {
+ let href = `http://example.com/hi${i}`;
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: href,
+ title: `${SEARCH_STRING} high frecency ${i}`,
+ });
+ }
+
+ // Do an unrestricted search to make sure everything appears in it, including
+ // the visit and bookmark.
+ let context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS),
+ ...makeRemoteSuggestionResults(context),
+ makeBookmarkResult(context, {
+ uri: "http://example.com/hi3",
+ title: `${SEARCH_STRING} high frecency 3`,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://example.com/hi2",
+ title: `${SEARCH_STRING} high frecency 2`,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://example.com/hi1",
+ title: `${SEARCH_STRING} high frecency 1`,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://example.com/hi0",
+ title: `${SEARCH_STRING} high frecency 0`,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo4",
+ title: `${SEARCH_STRING} low frecency 4`,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo3",
+ title: `${SEARCH_STRING} low frecency 3`,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo2",
+ title: `${SEARCH_STRING} low frecency 2`,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo1",
+ title: `${SEARCH_STRING} low frecency 1`,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo0",
+ title: `${SEARCH_STRING} low frecency 0`,
+ }),
+ ],
+ });
+
+ // Change the mixup.
+ setResultGroups([
+ // 1 suggestion
+ {
+ maxResultCount: 1,
+ children: [
+ { group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY },
+ { group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION },
+ ],
+ },
+ // 5 general
+ {
+ maxResultCount: 5,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ // 1 suggestion
+ {
+ maxResultCount: 1,
+ children: [
+ { group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY },
+ { group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION },
+ ],
+ },
+ // remaining general
+ { group: UrlbarUtils.RESULT_GROUP.GENERAL },
+ // remaining suggestions
+ { group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY },
+ { group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION },
+ ]);
+
+ // Do an unrestricted search to make sure everything appears in it, including
+ // the visits and bookmarks.
+ context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, 1),
+ makeBookmarkResult(context, {
+ uri: "http://example.com/hi3",
+ title: `${SEARCH_STRING} high frecency 3`,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://example.com/hi2",
+ title: `${SEARCH_STRING} high frecency 2`,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://example.com/hi1",
+ title: `${SEARCH_STRING} high frecency 1`,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://example.com/hi0",
+ title: `${SEARCH_STRING} high frecency 0`,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo4",
+ title: `${SEARCH_STRING} low frecency 4`,
+ }),
+ ...makeFormHistoryResults(context, 2).slice(1),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo3",
+ title: `${SEARCH_STRING} low frecency 3`,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo2",
+ title: `${SEARCH_STRING} low frecency 2`,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo1",
+ title: `${SEARCH_STRING} low frecency 1`,
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/lo0",
+ title: `${SEARCH_STRING} low frecency 0`,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS).slice(2),
+ ...makeRemoteSuggestionResults(context),
+ ],
+ });
+
+ Services.prefs.clearUserPref(MAX_RICH_RESULTS_PREF);
+ await cleanUpSuggestions();
+});
+
+add_task(async function prohibit_suggestions() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(
+ `browser.fixup.domainwhitelist.${SEARCH_STRING}`,
+ false
+ );
+
+ let context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 3),
+ ...makeRemoteSuggestionResults(context),
+ ],
+ });
+
+ Services.prefs.setBoolPref(
+ `browser.fixup.domainwhitelist.${SEARCH_STRING}`,
+ true
+ );
+ registerCleanupFunction(() => {
+ Services.prefs.setBoolPref(
+ `browser.fixup.domainwhitelist.${SEARCH_STRING}`,
+ false
+ );
+ });
+ context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${SEARCH_STRING}/`,
+ fallbackTitle: `http://${SEARCH_STRING}/`,
+ iconUri: "",
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 2),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: false,
+ }),
+ ],
+ });
+
+ // When using multiple words, we should still get suggestions:
+ let query = `${SEARCH_STRING} world`;
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 3),
+ ...makeRemoteSuggestionResults(context, { suggestionPrefix: query }),
+ ],
+ });
+
+ // Clear the whitelist for SEARCH_STRING and try preferring DNS for any single
+ // word instead:
+ Services.prefs.setBoolPref(
+ `browser.fixup.domainwhitelist.${SEARCH_STRING}`,
+ false
+ );
+ Services.prefs.setBoolPref("browser.fixup.dns_first_for_single_words", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.fixup.dns_first_for_single_words");
+ });
+
+ context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: `http://${SEARCH_STRING}/`,
+ fallbackTitle: `http://${SEARCH_STRING}/`,
+ iconUri: "",
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 2),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: false,
+ }),
+ ],
+ });
+
+ context = createContext("somethingelse", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://somethingelse/",
+ fallbackTitle: "http://somethingelse/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: false,
+ }),
+ ],
+ });
+
+ // When using multiple words, we should still get suggestions:
+ query = `${SEARCH_STRING} world`;
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 3),
+ ...makeRemoteSuggestionResults(context, { suggestionPrefix: query }),
+ ],
+ });
+
+ Services.prefs.clearUserPref("browser.fixup.dns_first_for_single_words");
+
+ context = createContext("http://1.2.3.4/", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://1.2.3.4/",
+ fallbackTitle: "http://1.2.3.4/",
+ iconUri: "page-icon:http://1.2.3.4/",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("[2001::1]:30", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://[2001::1]:30/",
+ fallbackTitle: "http://[2001::1]:30/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("user:pass@test", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://user:pass@test/",
+ fallbackTitle: "http://user:pass@test/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("data:text/plain,Content", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "data:text/plain,Content",
+ fallbackTitle: "data:text/plain,Content",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("a", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ await cleanUpSuggestions();
+});
+
+add_task(async function uri_like_queries() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+
+ // We should not fetch any suggestions for an actual URL.
+ let query = "mozilla.org";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ fallbackTitle: `http://${query}/`,
+ uri: `http://${query}/`,
+ iconUri: "",
+ heuristic: true,
+ }),
+ makeSearchResult(context, { query, engineName: SUGGESTIONS_ENGINE_NAME }),
+ ],
+ });
+
+ // We should also not fetch suggestions for a partially-typed URL.
+ query = "mozilla.o";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ // Now trying queries that could be confused for URLs. They should return
+ // results.
+ const uriLikeQueries = [
+ "mozilla.org is a great website",
+ "I like mozilla.org",
+ "a/b testing",
+ "he/him",
+ "Google vs.",
+ "5.8 cm",
+ ];
+ for (query of uriLikeQueries) {
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeRemoteSuggestionResults(context, {
+ suggestionPrefix: query,
+ }),
+ ],
+ });
+ }
+
+ await cleanUpSuggestions();
+});
+
+add_task(async function avoid_remote_url_suggestions_1() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ setSuggestionsFn(searchStr => {
+ let suffixes = [".com", "/test", ":1]", "@test", ". com"];
+ return suffixes.map(s => searchStr + s);
+ });
+
+ const query = "test";
+
+ await UrlbarTestUtils.formHistory.add([`${query}.com`]);
+
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeFormHistoryResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: `${query}.com`,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: `${query}. com`,
+ }),
+ ],
+ });
+
+ await cleanUpSuggestions();
+ await UrlbarTestUtils.formHistory.remove([`${query}.com`]);
+});
+
+add_task(async function avoid_remote_url_suggestions_2() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
+
+ setSuggestionsFn(searchStr => {
+ let suffixes = ["ed", "eds"];
+ return suffixes.map(s => searchStr + s);
+ });
+
+ let context = createContext("htt", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: "htted",
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: "htteds",
+ }),
+ ],
+ });
+
+ context = createContext("ftp", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: "ftped",
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: "ftpeds",
+ }),
+ ],
+ });
+
+ context = createContext("http", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: "httped",
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: "httpeds",
+ }),
+ ],
+ });
+
+ context = createContext("http:", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("https", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: "httpsed",
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: "httpseds",
+ }),
+ ],
+ });
+
+ context = createContext("https:", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("httpd", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: "httpded",
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: "httpdeds",
+ }),
+ ],
+ });
+
+ // Check FTP disabled
+ context = createContext("ftp:", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("ftp:/", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("ftp://", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("ftp://test", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "ftp://test/",
+ fallbackTitle: "ftp://test/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("http:/", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("https:/", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("http://", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("https://", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("http://www", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://www/",
+ fallbackTitle: "http://www/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("https://www", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "https://www/",
+ fallbackTitle: "https://www/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("http://test", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://test/",
+ fallbackTitle: "http://test/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("https://test", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "https://test/",
+ fallbackTitle: "https://test/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("http://www.test", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://www.test/",
+ fallbackTitle: "http://www.test/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("http://www.test.com", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "http://www.test.com/",
+ fallbackTitle: "http://www.test.com/",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("file", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: "fileed",
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: "fileeds",
+ }),
+ ],
+ });
+
+ context = createContext("file:", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("file:///Users", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ uri: "file:///Users",
+ fallbackTitle: "file:///Users",
+ iconUri: "",
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("moz-test://", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("moz+test://", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ context = createContext("about", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: "abouted",
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: "abouteds",
+ }),
+ ],
+ });
+
+ await cleanUpSuggestions();
+});
+
+add_task(async function restrict_remote_suggestions_after_no_results() {
+ // We don't fetch remote suggestions if a query with a length over
+ // maxCharsForSearchSuggestions returns 0 results. We set it to 4 here to
+ // avoid constructing a 100+ character string.
+ Services.prefs.setIntPref("browser.urlbar.maxCharsForSearchSuggestions", 4);
+ setSuggestionsFn(searchStr => {
+ return [];
+ });
+
+ const query = SEARCH_STRING.substring(0, SEARCH_STRING.length - 1);
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 1),
+ ],
+ });
+
+ context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 1),
+ // Because the previous search returned no suggestions, we will not fetch
+ // remote suggestions for this query that is just a longer version of the
+ // previous query.
+ ],
+ });
+
+ // Do one more search before resetting maxCharsForSearchSuggestions to reset
+ // the search suggestion provider's _lastLowResultsSearchSuggestion property.
+ // Otherwise it will be stuck at SEARCH_STRING, which interferes with
+ // subsequent tests.
+ context = createContext("not the search string", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ Services.prefs.clearUserPref("browser.urlbar.maxCharsForSearchSuggestions");
+
+ await cleanUpSuggestions();
+});
+
+add_task(async function formHistory() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+
+ // `maxHistoricalSearchSuggestions` is no longer treated as a max count but as
+ // a boolean: If it's zero, then the user has opted out of form history so we
+ // shouldn't include any at all; if it's non-zero, then we include form
+ // history according to the limits specified in the muxer's result groups.
+
+ // zero => no form history
+ Services.prefs.setIntPref(MAX_FORM_HISTORY_PREF, 0);
+ let context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeRemoteSuggestionResults(context),
+ ],
+ });
+
+ // non-zero => allow form history
+ Services.prefs.setIntPref(MAX_FORM_HISTORY_PREF, 1);
+ context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 3),
+ ...makeRemoteSuggestionResults(context),
+ ],
+ });
+
+ // non-zero => allow form history
+ Services.prefs.setIntPref(MAX_FORM_HISTORY_PREF, 2);
+ context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 3),
+ ...makeRemoteSuggestionResults(context),
+ ],
+ });
+
+ Services.prefs.clearUserPref(MAX_FORM_HISTORY_PREF);
+
+ // Do a search for exactly the suggestion of the first form history result.
+ // The heuristic's query should be the suggestion; the first form history
+ // result should not be included since it dupes the heuristic; the other form
+ // history results should not be included since they don't match; and both
+ // remote suggestions should be included.
+ let firstSuggestion = makeFormHistoryResults(context, 1)[0].payload
+ .suggestion;
+ context = createContext(firstSuggestion, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeRemoteSuggestionResults(context, {
+ suggestionPrefix: firstSuggestion,
+ }),
+ ],
+ });
+
+ // Do the same search but in uppercase with a trailing space. We should get
+ // the same results, i.e., the form history result dupes the trimmed search
+ // string so it shouldn't be included.
+ let query = firstSuggestion.toUpperCase() + " ";
+ context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ query,
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeRemoteSuggestionResults(context, {
+ suggestionPrefix: firstSuggestion.toUpperCase(),
+ }),
+ ],
+ });
+
+ // Add a form history entry that dupes the first remote suggestion and do a
+ // search that triggers both. The form history should be included but the
+ // remote suggestion should not since it dupes the form history.
+ let suggestionPrefix = "dupe";
+ let dupeSuggestion = makeRemoteSuggestionResults(context, {
+ suggestionPrefix,
+ })[0].payload.suggestion;
+ Assert.ok(dupeSuggestion, "Sanity check: dupeSuggestion is defined");
+ await UrlbarTestUtils.formHistory.add([dupeSuggestion]);
+
+ context = createContext(suggestionPrefix, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: dupeSuggestion,
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ ...makeRemoteSuggestionResults(context, { suggestionPrefix }).slice(1),
+ ],
+ });
+
+ await UrlbarTestUtils.formHistory.remove([dupeSuggestion]);
+
+ // Add these form history strings to use below.
+ let formHistoryStrings = ["foo", "FOO ", "foobar", "fooquux"];
+ await UrlbarTestUtils.formHistory.add(formHistoryStrings);
+
+ // Search for "foo". "foo" and "FOO " shouldn't be included since they dupe
+ // the heuristic. Both "foobar" and "fooquux" should be included even though
+ // the max form history count is only two and there are four matching form
+ // history results (including the discarded "foo" and "FOO ").
+ context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "foobar",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "fooquux",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ ...makeRemoteSuggestionResults(context, {
+ suggestionPrefix: "foo",
+ }),
+ ],
+ });
+
+ // Add a visit that matches "foo" and will autofill so that the heuristic is
+ // not a search result. Now the "foo" and "foobar" form history should be
+ // included. The "foo" remote suggestion should not be included since it
+ // dupes the "foo" form history.
+ await PlacesTestUtils.addVisits("http://foo.example.com/");
+ context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ uri: "http://foo.example.com/",
+ title: "test visit for http://foo.example.com/",
+ heuristic: true,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "foo",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "foobar",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "fooquux",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ ...makeRemoteSuggestionResults(context, {
+ suggestionPrefix: "foo",
+ }),
+ ],
+ });
+ await PlacesUtils.history.clear();
+
+ // Add SERPs for "foobar", "fooBAR ", and "food", and search for "foo". The
+ // "foo" form history should be excluded since it dupes the heuristic; the
+ // "foobar" and "fooquux" form history should be included; the "food" SERP
+ // should be included since it doesn't dupe either form history result; and
+ // the "foobar" and "fooBAR " SERPs depend on the result groups, see below.
+ let engine = await Services.search.getDefault();
+ let serpURLs = ["foobar", "fooBAR ", "food"].map(
+ term => UrlbarUtils.getSearchQueryUrl(engine, term)[0]
+ );
+ await PlacesTestUtils.addVisits(serpURLs);
+
+ // First set showSearchSuggestionsFirst = false so that general results appear
+ // before suggestions, which means that the muxer visits the "foobar" and
+ // "fooBAR " SERPs before visiting the "foobar" form history, and so it
+ // doesn't see that these two SERPs dupe the form history. They are therefore
+ // included.
+ Services.prefs.setBoolPref(SHOW_SEARCH_SUGGESTIONS_FIRST_PREF, false);
+ context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: `http://localhost:${port}/search?q=food`,
+ title: `test visit for http://localhost:${port}/search?q=food`,
+ }),
+ makeVisitResult(context, {
+ uri: `http://localhost:${port}/search?q=fooBAR+`,
+ title: `test visit for http://localhost:${port}/search?q=fooBAR+`,
+ }),
+ makeVisitResult(context, {
+ uri: `http://localhost:${port}/search?q=foobar`,
+ title: `test visit for http://localhost:${port}/search?q=foobar`,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "foobar",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "fooquux",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ ...makeRemoteSuggestionResults(context, {
+ suggestionPrefix: "foo",
+ }),
+ ],
+ });
+
+ // Now clear showSearchSuggestionsFirst so that suggestions appear before
+ // general results. Now the muxer will see that the "foobar" and "fooBAR "
+ // SERPs dupe the "foobar" form history, so it will exclude them.
+ Services.prefs.clearUserPref(SHOW_SEARCH_SUGGESTIONS_FIRST_PREF);
+ context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "foobar",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "fooquux",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ ...makeRemoteSuggestionResults(context, {
+ suggestionPrefix: "foo",
+ }),
+ makeVisitResult(context, {
+ uri: `http://localhost:${port}/search?q=food`,
+ title: `test visit for http://localhost:${port}/search?q=food`,
+ }),
+ ],
+ });
+
+ await UrlbarTestUtils.formHistory.remove(formHistoryStrings);
+
+ await cleanUpSuggestions();
+ await PlacesUtils.history.clear();
+});
+
+// When the heuristic is hidden, search results that match the heuristic should
+// be included and not deduped.
+add_task(async function hideHeuristic() {
+ UrlbarPrefs.set("experimental.hideHeuristic", true);
+ UrlbarPrefs.set("browser.search.suggest.enabled", true);
+ UrlbarPrefs.set("suggest.searches", true);
+ let context = createContext(SEARCH_STRING, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...makeFormHistoryResults(context, MAX_RESULTS - 3),
+ makeSearchResult(context, {
+ query: SEARCH_STRING,
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: SEARCH_STRING,
+ }),
+ ...makeRemoteSuggestionResults(context),
+ ],
+ });
+ await cleanUpSuggestions();
+ UrlbarPrefs.clear("experimental.hideHeuristic");
+});
+
+// When the heuristic is hidden, form history results that match the heuristic
+// should be included and not deduped.
+add_task(async function hideHeuristic_formHistory() {
+ UrlbarPrefs.set("experimental.hideHeuristic", true);
+ UrlbarPrefs.set("browser.search.suggest.enabled", true);
+ UrlbarPrefs.set("suggest.searches", true);
+
+ // Search for exactly the suggestion of the first form history result.
+ // Expected results:
+ //
+ // * First form history should be included even though it dupes the heuristic
+ // * Other form history should not be included because they don't match the
+ // search string
+ // * The first remote suggestion that just echoes the search string should not
+ // be included because it dupes the first form history
+ // * The remaining remote suggestions should be included because they don't
+ // dupe anything
+ let context = createContext(SEARCH_STRING, { isPrivate: false });
+ let firstFormHistory = makeFormHistoryResults(context, 1)[0];
+ context = createContext(firstFormHistory.payload.suggestion, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ firstFormHistory,
+ ...makeRemoteSuggestionResults(context, {
+ suggestionPrefix: firstFormHistory.payload.suggestion,
+ }),
+ ],
+ });
+
+ // Add these form history strings to use below.
+ let formHistoryStrings = ["foo", "FOO ", "foobar", "fooquux"];
+ await UrlbarTestUtils.formHistory.add(formHistoryStrings);
+
+ // Search for "foo". Expected results:
+ //
+ // * "foo" form history should be included even though it dupes the heuristic
+ // * "FOO " form history should not be included because it dupes the "foo"
+ // form history
+ // * "foobar" and "fooqux" form history should be included because they don't
+ // dupe anything
+ // * "foo" remote suggestion should not be included because it dupes the "foo"
+ // form history
+ // * "foo foo" and "foo bar" remote suggestions should be included because
+ // they don't dupe anything
+ context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "foo",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "foobar",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "fooquux",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ ...makeRemoteSuggestionResults(context, {
+ suggestionPrefix: "foo",
+ }),
+ ],
+ });
+
+ // Add SERPs for "foo" and "food", and search for "foo". Expected results:
+ //
+ // * "foo" form history should be included even though it dupes the heuristic
+ // * "foobar" and "fooqux" form history should be included because they don't
+ // dupe anything
+ // * "foo" SERP depends on `showSearchSuggestionsFirst`, see below
+ // * "food" SERP should be include because it doesn't dupe anything
+ // * "foo" remote suggestion should not be included because it dupes the "foo"
+ // form history
+ // * "foo foo" and "foo bar" remote suggestions should be included because
+ // they don't dupe anything
+ let engine = await Services.search.getDefault();
+ let serpURLs = ["foo", "food"].map(
+ term => UrlbarUtils.getSearchQueryUrl(engine, term)[0]
+ );
+ await PlacesTestUtils.addVisits(serpURLs);
+
+ // With `showSearchSuggestionsFirst = false` so that general results appear
+ // before suggestions, the muxer visits the "foo" (and "food") SERPs before
+ // visiting the "foo" form history, and so it doesn't see that the "foo" SERP
+ // dupes the form history. The SERP is therefore included.
+ UrlbarPrefs.set("showSearchSuggestionsFirst", false);
+ context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: `http://localhost:${port}/search?q=food`,
+ title: `test visit for http://localhost:${port}/search?q=food`,
+ }),
+ makeVisitResult(context, {
+ uri: `http://localhost:${port}/search?q=foo`,
+ title: `test visit for http://localhost:${port}/search?q=foo`,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "foo",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "foobar",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "fooquux",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ ...makeRemoteSuggestionResults(context, {
+ suggestionPrefix: "foo",
+ }),
+ ],
+ });
+
+ // Now clear `showSearchSuggestionsFirst` so that suggestions appear before
+ // general results. Now the muxer will see that the "foo" SERP dupes the "foo"
+ // form history, so it will exclude it.
+ UrlbarPrefs.clear("showSearchSuggestionsFirst");
+ context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "foo",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "foobar",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ makeFormHistoryResult(context, {
+ suggestion: "fooquux",
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ }),
+ ...makeRemoteSuggestionResults(context, {
+ suggestionPrefix: "foo",
+ }),
+ makeVisitResult(context, {
+ uri: `http://localhost:${port}/search?q=food`,
+ title: `test visit for http://localhost:${port}/search?q=food`,
+ }),
+ ],
+ });
+
+ await UrlbarTestUtils.formHistory.remove(formHistoryStrings);
+
+ await cleanUpSuggestions();
+ UrlbarPrefs.clear("experimental.hideHeuristic");
+});
diff --git a/browser/components/urlbar/tests/unit/test_search_suggestions_aliases.js b/browser/components/urlbar/tests/unit/test_search_suggestions_aliases.js
new file mode 100644
index 0000000000..1f70a421fa
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_search_suggestions_aliases.js
@@ -0,0 +1,364 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that an engine with suggestions works with our alias autocomplete
+ * behavior.
+ */
+
+const DEFAULT_ENGINE_NAME = "TestDefaultEngine";
+const SUGGEST_PREF = "browser.urlbar.suggest.searches";
+const SUGGEST_ENABLED_PREF = "browser.search.suggest.enabled";
+const HISTORY_TITLE = "fire";
+
+// We make sure that aliases and search terms are correctly recognized when they
+// are separated by each of these different types of spaces and combinations of
+// spaces. U+3000 is the ideographic space in CJK and is commonly used by CJK
+// speakers.
+const TEST_SPACES = [" ", "\u3000", " \u3000", "\u3000 "];
+
+let engine;
+let port;
+
+add_task(async function setup() {
+ engine = await addTestSuggestionsEngine();
+ port = engine.getSubmission("").uri.port;
+
+ // Set a mock engine as the default so we don't hit the network below when we
+ // do searches that return the default engine heuristic result.
+ await SearchTestUtils.installSearchExtension(
+ {
+ name: DEFAULT_ENGINE_NAME,
+ search_url: "https://my.search.com/",
+ },
+ { setAsDefault: true }
+ );
+
+ // History matches should not appear with @aliases, so this visit should not
+ // appear when searching with @aliases below.
+ await PlacesTestUtils.addVisits({
+ uri: engine.searchForm,
+ title: HISTORY_TITLE,
+ });
+});
+
+// A non-token alias without a trailing space shouldn't be recognized as a
+// keyword. It should be treated as part of the search string.
+add_task(async function nonTokenAlias_noTrailingSpace() {
+ Services.prefs.setBoolPref(
+ "browser.search.separatePrivateDefault.ui.enabled",
+ false
+ );
+
+ let alias = "moz";
+ engine.alias = alias;
+ Assert.equal(engine.alias, alias);
+ let context = createContext(alias, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: DEFAULT_ENGINE_NAME,
+ query: alias,
+ heuristic: true,
+ }),
+ ],
+ });
+ Services.prefs.clearUserPref(
+ "browser.search.separatePrivateDefault.ui.enabled"
+ );
+});
+
+// A non-token alias with a trailing space should be recognized as a keyword,
+// and the history result should be included.
+add_task(async function nonTokenAlias_trailingSpace() {
+ let alias = "moz";
+ engine.alias = alias;
+ Assert.equal(engine.alias, alias);
+
+ for (let isPrivate of [false, true]) {
+ for (let spaces of TEST_SPACES) {
+ info(
+ "Testing: " + JSON.stringify({ isPrivate, spaces: codePoints(spaces) })
+ );
+ let context = createContext(alias + spaces, { isPrivate });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: "",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: `http://localhost:${port}/search?q=`,
+ title: HISTORY_TITLE,
+ }),
+ ],
+ });
+ }
+ }
+});
+
+// Search for "alias HISTORY_TITLE" with a non-token alias in a non-private
+// context. The remote suggestions and history result should be shown.
+add_task(async function nonTokenAlias_history_nonPrivate() {
+ let alias = "moz";
+ engine.alias = alias;
+ Assert.equal(engine.alias, alias);
+ for (let spaces of TEST_SPACES) {
+ info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) }));
+ let context = createContext(alias + spaces + HISTORY_TITLE, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: HISTORY_TITLE,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: HISTORY_TITLE,
+ suggestion: `${HISTORY_TITLE} foo`,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: HISTORY_TITLE,
+ suggestion: `${HISTORY_TITLE} bar`,
+ }),
+ makeVisitResult(context, {
+ uri: `http://localhost:${port}/search?q=`,
+ title: HISTORY_TITLE,
+ }),
+ ],
+ });
+ }
+});
+
+// Search for "alias HISTORY_TITLE" with a non-token alias in a private context.
+// The history result should be shown, but not the remote suggestions.
+add_task(async function nonTokenAlias_history_private() {
+ let alias = "moz";
+ engine.alias = alias;
+ Assert.equal(engine.alias, alias);
+ for (let spaces of TEST_SPACES) {
+ info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) }));
+ let context = createContext(alias + spaces + HISTORY_TITLE, {
+ isPrivate: true,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: HISTORY_TITLE,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: `http://localhost:${port}/search?q=`,
+ title: HISTORY_TITLE,
+ }),
+ ],
+ });
+ }
+});
+
+// A token alias without a trailing space should be autofilled with a trailing
+// space and recognized as a keyword with a keyword offer.
+add_task(async function tokenAlias_noTrailingSpace() {
+ let alias = "@moz";
+ engine.alias = alias;
+ Assert.equal(engine.alias, alias);
+ for (let isPrivate of [false, true]) {
+ let context = createContext(alias, { isPrivate });
+ await check_results({
+ context,
+ autofilled: alias + " ",
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ providesSearchMode: true,
+ query: "",
+ heuristic: false,
+ }),
+ ],
+ });
+ }
+});
+
+// A token alias with a trailing space should be recognized as a keyword without
+// a keyword offer.
+add_task(async function tokenAlias_trailingSpace() {
+ let alias = "@moz";
+ engine.alias = alias;
+ Assert.equal(engine.alias, alias);
+ for (let isPrivate of [false, true]) {
+ for (let spaces of TEST_SPACES) {
+ info(
+ "Testing: " + JSON.stringify({ isPrivate, spaces: codePoints(spaces) })
+ );
+ let context = createContext(alias + spaces, { isPrivate });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: "",
+ heuristic: true,
+ }),
+ ],
+ });
+ }
+ }
+});
+
+// Search for "alias HISTORY_TITLE" with a token alias in a non-private context.
+// The remote suggestions should be shown, but not the history result.
+add_task(async function tokenAlias_history_nonPrivate() {
+ let alias = "@moz";
+ engine.alias = alias;
+ Assert.equal(engine.alias, alias);
+ for (let spaces of TEST_SPACES) {
+ info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) }));
+ let context = createContext(alias + spaces + HISTORY_TITLE, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: HISTORY_TITLE,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: HISTORY_TITLE,
+ suggestion: `${HISTORY_TITLE} foo`,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: HISTORY_TITLE,
+ suggestion: `${HISTORY_TITLE} bar`,
+ }),
+ ],
+ });
+ }
+});
+
+// Search for "alias HISTORY_TITLE" with a token alias in a private context.
+// Neither the history result nor the remote suggestions should be shown.
+add_task(async function tokenAlias_history_private() {
+ let alias = "@moz";
+ engine.alias = alias;
+ Assert.equal(engine.alias, alias);
+ for (let spaces of TEST_SPACES) {
+ info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) }));
+ let context = createContext(alias + spaces + HISTORY_TITLE, {
+ isPrivate: true,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: HISTORY_TITLE,
+ heuristic: true,
+ }),
+ ],
+ });
+ }
+});
+
+// Even when they're disabled, suggestions should still be returned when using a
+// token alias in a non-private context.
+add_task(async function suggestionsDisabled_nonPrivate() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, false);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+ let alias = "@moz";
+ engine.alias = alias;
+ Assert.equal(engine.alias, alias);
+ for (let spaces of TEST_SPACES) {
+ info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) }));
+ let context = createContext(alias + spaces + "term", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: "term",
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: "term",
+ suggestion: "term foo",
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: "term",
+ suggestion: "term bar",
+ }),
+ ],
+ });
+ }
+ Services.prefs.clearUserPref(SUGGEST_PREF);
+ Services.prefs.clearUserPref(SUGGEST_ENABLED_PREF);
+});
+
+// Suggestions should not be returned when using a token alias in a private
+// context.
+add_task(async function suggestionsDisabled_private() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, false);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+ let alias = "@moz";
+ engine.alias = alias;
+ Assert.equal(engine.alias, alias);
+ for (let spaces of TEST_SPACES) {
+ info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) }));
+ let context = createContext(alias + spaces + "term", { isPrivate: true });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ alias,
+ query: "term",
+ heuristic: true,
+ }),
+ ],
+ });
+ Services.prefs.clearUserPref(SUGGEST_PREF);
+ Services.prefs.clearUserPref(SUGGEST_ENABLED_PREF);
+ }
+});
+
+/**
+ * Returns an array of code points in the given string. Each code point is
+ * returned as a hexidecimal string.
+ *
+ * @param {string} str
+ * The code points of this string will be returned.
+ * @returns {Array}
+ * Array of code points in the string, where each is a hexidecimal string.
+ */
+function codePoints(str) {
+ return str.split("").map(s => s.charCodeAt(0).toString(16));
+}
diff --git a/browser/components/urlbar/tests/unit/test_search_suggestions_tail.js b/browser/components/urlbar/tests/unit/test_search_suggestions_tail.js
new file mode 100644
index 0000000000..da43463a69
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_search_suggestions_tail.js
@@ -0,0 +1,379 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that tailed search engine suggestions are returned by
+ * UrlbarProviderSearchSuggestions when available.
+ */
+
+const SUGGEST_ENABLED_PREF = "browser.search.suggest.enabled";
+const PRIVATE_SEARCH_PREF = "browser.search.separatePrivateDefault.ui.enabled";
+const TAIL_SUGGESTIONS_PREF = "browser.urlbar.richSuggestions.tail";
+
+var suggestionsFn;
+var previousSuggestionsFn;
+
+/**
+ * Set the current suggestion funciton.
+ *
+ * @param {Function} fn
+ * A function that that a search string and returns an array of strings that
+ * will be used as search suggestions.
+ * Note: `fn` should return > 1 suggestion in most cases. Otherwise, you may
+ * encounter unexceptede behaviour with UrlbarProviderSuggestion's
+ * _lastLowResultsSearchSuggestion safeguard.
+ */
+function setSuggestionsFn(fn) {
+ previousSuggestionsFn = suggestionsFn;
+ suggestionsFn = fn;
+}
+
+async function cleanup() {
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+}
+
+async function cleanUpSuggestions() {
+ await cleanup();
+ if (previousSuggestionsFn) {
+ suggestionsFn = previousSuggestionsFn;
+ previousSuggestionsFn = null;
+ }
+}
+
+add_task(async function setup() {
+ let engine = await addTestTailSuggestionsEngine(searchStr => {
+ return suggestionsFn(searchStr);
+ });
+ setSuggestionsFn(searchStr => {
+ let suffixes = ["toronto", "tunisia"];
+ return [
+ "what time is it in t",
+ suffixes.map(s => searchStr + s.slice(1)),
+ [],
+ {
+ "google:irrelevantparameter": [],
+ "google:suggestdetail": suffixes.map(s => ({
+ mp: "… ",
+ t: s,
+ })),
+ },
+ ];
+ });
+
+ // Install the test engine.
+ let oldDefaultEngine = await Services.search.getDefault();
+ registerCleanupFunction(async () => {
+ Services.search.setDefault(
+ oldDefaultEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ Services.prefs.clearUserPref(PRIVATE_SEARCH_PREF);
+ Services.prefs.clearUserPref(TAIL_SUGGESTIONS_PREF);
+ Services.prefs.clearUserPref(SUGGEST_ENABLED_PREF);
+ });
+ Services.search.setDefault(engine, Ci.nsISearchService.CHANGE_REASON_UNKNOWN);
+ Services.prefs.setBoolPref(PRIVATE_SEARCH_PREF, false);
+ Services.prefs.setBoolPref(TAIL_SUGGESTIONS_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+});
+
+/**
+ * Tests that non-tail suggestion providers still return results correctly when
+ * the tailSuggestions pref is enabled.
+ */
+add_task(async function normal_suggestions_provider() {
+ let engine = await addTestSuggestionsEngine();
+ let tailEngine = await Services.search.getDefault();
+ Services.search.setDefault(engine, Ci.nsISearchService.CHANGE_REASON_UNKNOWN);
+
+ const query = "hello world";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: query + " foo",
+ }),
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ suggestion: query + " bar",
+ }),
+ ],
+ });
+
+ Services.search.setDefault(
+ tailEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ await cleanUpSuggestions();
+});
+
+/**
+ * Tests a suggestions provider that returns only tail suggestions.
+ */
+add_task(async function basic_tail() {
+ const query = "what time is it in t";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: TAIL_SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: TAIL_SUGGESTIONS_ENGINE_NAME,
+ suggestion: query + "oronto",
+ tail: "toronto",
+ }),
+ makeSearchResult(context, {
+ engineName: TAIL_SUGGESTIONS_ENGINE_NAME,
+ suggestion: query + "unisia",
+ tail: "tunisia",
+ }),
+ ],
+ });
+ await cleanUpSuggestions();
+});
+
+/**
+ * Tests a suggestions provider that returns both normal and tail suggestions.
+ * Only normal results should be shown.
+ */
+add_task(async function mixed_suggestions() {
+ // When normal suggestions are mixed with tail suggestions, they appear at the
+ // correct position in the google:suggestdetail array as empty objects.
+ setSuggestionsFn(searchStr => {
+ let suffixes = ["toronto", "tunisia"];
+ return [
+ "what time is it in t",
+ ["what is the time today texas"].concat(
+ suffixes.map(s => searchStr + s.slice(1))
+ ),
+ [],
+ {
+ "google:irrelevantparameter": [],
+ "google:suggestdetail": [{}].concat(
+ suffixes.map(s => ({
+ mp: "… ",
+ t: s,
+ }))
+ ),
+ },
+ ];
+ });
+
+ const query = "what time is it in t";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: TAIL_SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: TAIL_SUGGESTIONS_ENGINE_NAME,
+ suggestion: "what is the time today texas",
+ tail: undefined,
+ }),
+ ],
+ });
+ await cleanUpSuggestions();
+});
+
+/**
+ * Tests a suggestions provider that returns both normal and tail suggestions,
+ * with tail suggestions listed before normal suggestions. In the real world
+ * we don't expect that to happen, but we should handle it by showing only the
+ * normal suggestions.
+ */
+add_task(async function mixed_suggestions_tail_first() {
+ setSuggestionsFn(searchStr => {
+ let suffixes = ["toronto", "tunisia"];
+ return [
+ "what time is it in t",
+ suffixes
+ .map(s => searchStr + s.slice(1))
+ .concat(["what is the time today texas"]),
+ [],
+ {
+ "google:irrelevantparameter": [],
+ "google:suggestdetail": suffixes
+ .map(s => ({
+ mp: "… ",
+ t: s,
+ }))
+ .concat([{}]),
+ },
+ ];
+ });
+
+ const query = "what time is it in t";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: TAIL_SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: TAIL_SUGGESTIONS_ENGINE_NAME,
+ suggestion: "what is the time today texas",
+ tail: undefined,
+ }),
+ ],
+ });
+ await cleanUpSuggestions();
+});
+
+/**
+ * Tests a search that returns history results, bookmark results and tail
+ * suggestions. Only the history and bookmark results should be shown.
+ */
+add_task(async function mixed_results() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: Services.io.newURI("http://example.com/1"),
+ title: "what time is",
+ },
+ ]);
+
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.com/2",
+ title: "what time is",
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ // Tail suggestions should not be shown.
+ const query = "what time is";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: TAIL_SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://example.com/2",
+ title: "what time is",
+ }),
+ makeVisitResult(context, {
+ uri: "http://example.com/1",
+ title: "what time is",
+ }),
+ ],
+ });
+
+ // Once we make the query specific enough to exclude the history and bookmark
+ // results, we should show tail suggestions.
+ const tQuery = "what time is it in t";
+ context = createContext(tQuery, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: TAIL_SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: TAIL_SUGGESTIONS_ENGINE_NAME,
+ suggestion: tQuery + "oronto",
+ tail: "toronto",
+ }),
+ makeSearchResult(context, {
+ engineName: TAIL_SUGGESTIONS_ENGINE_NAME,
+ suggestion: tQuery + "unisia",
+ tail: "tunisia",
+ }),
+ ],
+ });
+
+ await cleanUpSuggestions();
+});
+
+/**
+ * Tests that tail suggestions are deduped if their full-text form is a dupe of
+ * a local search suggestion. Remaining tail suggestions should also not be
+ * shown since we do not mix tail and non-tail suggestions.
+ */
+add_task(async function dedupe_local() {
+ Services.prefs.setIntPref("browser.urlbar.maxHistoricalSearchSuggestions", 1);
+ await UrlbarTestUtils.formHistory.add(["what time is it in toronto"]);
+
+ const query = "what time is it in t";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: TAIL_SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeFormHistoryResult(context, {
+ engineName: TAIL_SUGGESTIONS_ENGINE_NAME,
+ suggestion: query + "oronto",
+ }),
+ ],
+ });
+
+ Services.prefs.clearUserPref("browser.urlbar.maxHistoricalSearchSuggestions");
+ await cleanUpSuggestions();
+});
+
+/**
+ * Tests that the correct number of suggestion results are displayed if
+ * maxResults is limited, even when tail suggestions are returned.
+ */
+add_task(async function limit_results() {
+ await UrlbarTestUtils.formHistory.clear();
+ const query = "what time is it in t";
+ let context = createContext(query, { isPrivate: false });
+ context.maxResults = 2;
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: TAIL_SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeSearchResult(context, {
+ engineName: TAIL_SUGGESTIONS_ENGINE_NAME,
+ suggestion: query + "oronto",
+ tail: "toronto",
+ }),
+ ],
+ });
+ await cleanUpSuggestions();
+});
+
+/**
+ * Tests that tail suggestions are hidden if the pref is disabled.
+ */
+add_task(async function disable_pref() {
+ let oldPrefValue = Services.prefs.getBoolPref(TAIL_SUGGESTIONS_PREF);
+ Services.prefs.setBoolPref(TAIL_SUGGESTIONS_PREF, false);
+ const query = "what time is it in t";
+ let context = createContext(query, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: TAIL_SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ],
+ });
+
+ Services.prefs.setBoolPref(TAIL_SUGGESTIONS_PREF, oldPrefValue);
+ await cleanUpSuggestions();
+});
diff --git a/browser/components/urlbar/tests/unit/test_special_search.js b/browser/components/urlbar/tests/unit/test_special_search.js
new file mode 100644
index 0000000000..863196909a
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_special_search.js
@@ -0,0 +1,543 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test for bug 395161 that allows special searches that restrict results to
+ * history/bookmark/tagged items and title/url matches.
+ *
+ * Test 485122 by making sure results don't have tags when restricting result
+ * to just history either by default behavior or dynamic query restrict.
+ */
+
+testEngine_setup();
+
+function setSuggestPrefsToFalse() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+}
+
+const TRANSITION_TYPED = PlacesUtils.history.TRANSITION_TYPED;
+
+add_task(async function test_special_searches() {
+ let uri1 = Services.io.newURI("http://url/");
+ let uri2 = Services.io.newURI("http://url/2");
+ let uri3 = Services.io.newURI("http://foo.bar/");
+ let uri4 = Services.io.newURI("http://foo.bar/2");
+ let uri5 = Services.io.newURI("http://url/star");
+ let uri6 = Services.io.newURI("http://url/star/2");
+ let uri7 = Services.io.newURI("http://foo.bar/star");
+ let uri8 = Services.io.newURI("http://foo.bar/star/2");
+ let uri9 = Services.io.newURI("http://url/tag");
+ let uri10 = Services.io.newURI("http://url/tag/2");
+ let uri11 = Services.io.newURI("http://foo.bar/tag");
+ let uri12 = Services.io.newURI("http://foo.bar/tag/2");
+ await PlacesTestUtils.addVisits([
+ { uri: uri11, title: "title", transition: TRANSITION_TYPED },
+ { uri: uri6, title: "foo.bar" },
+ { uri: uri4, title: "foo.bar", transition: TRANSITION_TYPED },
+ { uri: uri3, title: "title" },
+ { uri: uri2, title: "foo.bar" },
+ { uri: uri1, title: "title", transition: TRANSITION_TYPED },
+ ]);
+
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri12,
+ title: "foo.bar",
+ tags: ["foo.bar"],
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri11,
+ title: "title",
+ tags: ["foo.bar"],
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri10,
+ title: "foo.bar",
+ tags: ["foo.bar"],
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri9,
+ title: "title",
+ tags: ["foo.bar"],
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({ uri: uri8, title: "foo.bar" });
+ await PlacesTestUtils.addBookmarkWithDetails({ uri: uri7, title: "title" });
+ await PlacesTestUtils.addBookmarkWithDetails({ uri: uri6, title: "foo.bar" });
+ await PlacesTestUtils.addBookmarkWithDetails({ uri: uri5, title: "title" });
+
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ // Order of frecency when not restricting, descending:
+ // uri11
+ // uri1
+ // uri4
+ // uri6
+ // uri5
+ // uri7
+ // uri8
+ // uri9
+ // uri10
+ // uri12
+ // uri2
+ // uri3
+
+ // Test restricting searches.
+
+ info("History restrict");
+ let context = createContext(UrlbarTokenizer.RESTRICT.HISTORY, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri11.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri1.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri4.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri6.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri2.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri3.spec, title: "title" }),
+ ],
+ });
+
+ info("Star restrict");
+ context = createContext(UrlbarTokenizer.RESTRICT.BOOKMARK, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri11.spec,
+ title: "title",
+ }),
+ makeBookmarkResult(context, { uri: uri6.spec, title: "foo.bar" }),
+ makeBookmarkResult(context, { uri: uri5.spec, title: "title" }),
+ makeBookmarkResult(context, { uri: uri7.spec, title: "title" }),
+ makeBookmarkResult(context, { uri: uri8.spec, title: "foo.bar" }),
+ makeBookmarkResult(context, {
+ uri: uri9.spec,
+ title: "title",
+ }),
+ makeBookmarkResult(context, {
+ uri: uri10.spec,
+ title: "foo.bar",
+ }),
+ makeBookmarkResult(context, {
+ uri: uri12.spec,
+ title: "foo.bar",
+ }),
+ ],
+ });
+
+ info("Tag restrict");
+ context = createContext(UrlbarTokenizer.RESTRICT.TAG, { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri11.spec,
+ title: "title",
+ }),
+ makeBookmarkResult(context, {
+ uri: uri9.spec,
+ title: "title",
+ }),
+ makeBookmarkResult(context, {
+ uri: uri10.spec,
+ title: "foo.bar",
+ }),
+ makeBookmarkResult(context, {
+ uri: uri12.spec,
+ title: "foo.bar",
+ }),
+ ],
+ });
+
+ info("Special as first word");
+ context = createContext(`${UrlbarTokenizer.RESTRICT.HISTORY} foo bar`, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ query: "foo bar",
+ alias: UrlbarTokenizer.RESTRICT.HISTORY,
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri11.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri4.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri6.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri2.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri3.spec, title: "title" }),
+ ],
+ });
+
+ info("Special as last word");
+ context = createContext(`foo bar ${UrlbarTokenizer.RESTRICT.HISTORY}`, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri11.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri4.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri6.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri2.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri3.spec, title: "title" }),
+ ],
+ });
+
+ // Test restricting and matching searches with a term.
+
+ info(`foo ${UrlbarTokenizer.RESTRICT.HISTORY} -> history`);
+ context = createContext(`foo ${UrlbarTokenizer.RESTRICT.HISTORY}`, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri11.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri4.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri6.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri2.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri3.spec, title: "title" }),
+ ],
+ });
+
+ info(`foo ${UrlbarTokenizer.RESTRICT.BOOKMARK} -> is star`);
+ context = createContext(`foo ${UrlbarTokenizer.RESTRICT.BOOKMARK}`, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri11.spec,
+ title: "title",
+ tags: ["foo.bar"],
+ }),
+ makeBookmarkResult(context, { uri: uri6.spec, title: "foo.bar" }),
+ makeBookmarkResult(context, { uri: uri7.spec, title: "title" }),
+ makeBookmarkResult(context, { uri: uri8.spec, title: "foo.bar" }),
+ makeBookmarkResult(context, {
+ uri: uri9.spec,
+ title: "title",
+ tags: ["foo.bar"],
+ }),
+ makeBookmarkResult(context, {
+ uri: uri10.spec,
+ title: "foo.bar",
+ tags: ["foo.bar"],
+ }),
+ makeBookmarkResult(context, {
+ uri: uri12.spec,
+ title: "foo.bar",
+ tags: ["foo.bar"],
+ }),
+ ],
+ });
+
+ info(`foo ${UrlbarTokenizer.RESTRICT.TITLE} -> in title`);
+ context = createContext(`foo ${UrlbarTokenizer.RESTRICT.TITLE}`, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri11.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri4.spec, title: "foo.bar" }),
+ makeBookmarkResult(context, { uri: uri6.spec, title: "foo.bar" }),
+ makeBookmarkResult(context, { uri: uri8.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri9.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri10.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri12.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri2.spec, title: "foo.bar" }),
+ ],
+ });
+
+ info(`foo ${UrlbarTokenizer.RESTRICT.URL} -> in url`);
+ context = createContext(`foo ${UrlbarTokenizer.RESTRICT.URL}`, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri11.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri4.spec, title: "foo.bar" }),
+ makeBookmarkResult(context, { uri: uri7.spec, title: "title" }),
+ makeBookmarkResult(context, { uri: uri8.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri12.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri3.spec, title: "title" }),
+ ],
+ });
+
+ info(`foo ${UrlbarTokenizer.RESTRICT.TAG} -> is tag`);
+ context = createContext(`foo ${UrlbarTokenizer.RESTRICT.TAG}`, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri11.spec,
+ title: "title",
+ tags: ["foo.bar"],
+ }),
+ makeBookmarkResult(context, {
+ uri: uri9.spec,
+ title: "title",
+ tags: ["foo.bar"],
+ }),
+ makeBookmarkResult(context, {
+ uri: uri10.spec,
+ title: "foo.bar",
+ tags: ["foo.bar"],
+ }),
+ makeBookmarkResult(context, {
+ uri: uri12.spec,
+ title: "foo.bar",
+ tags: ["foo.bar"],
+ }),
+ ],
+ });
+
+ // Test conflicting restrictions.
+
+ info(
+ `conflict ${UrlbarTokenizer.RESTRICT.TITLE} ${UrlbarTokenizer.RESTRICT.URL} -> url wins`
+ );
+ await PlacesTestUtils.addVisits([
+ {
+ uri: `http://conflict.com/${UrlbarTokenizer.RESTRICT.TITLE}`,
+ title: "test",
+ },
+ {
+ uri: "http://conflict.com/",
+ title: `test${UrlbarTokenizer.RESTRICT.TITLE}`,
+ },
+ ]);
+ context = createContext(
+ `conflict ${UrlbarTokenizer.RESTRICT.TITLE} ${UrlbarTokenizer.RESTRICT.URL}`,
+ { isPrivate: false }
+ );
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: `http://conflict.com/${UrlbarTokenizer.RESTRICT.TITLE}`,
+ title: "test",
+ }),
+ ],
+ });
+
+ info(
+ `conflict ${UrlbarTokenizer.RESTRICT.HISTORY} ${UrlbarTokenizer.RESTRICT.BOOKMARK} -> bookmark wins`
+ );
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://bookmark.conflict.com/",
+ title: `conflict ${UrlbarTokenizer.RESTRICT.HISTORY}`,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ context = createContext(
+ `conflict ${UrlbarTokenizer.RESTRICT.HISTORY} ${UrlbarTokenizer.RESTRICT.BOOKMARK}`,
+ { isPrivate: false }
+ );
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://bookmark.conflict.com/",
+ title: `conflict ${UrlbarTokenizer.RESTRICT.HISTORY}`,
+ }),
+ ],
+ });
+
+ info(
+ `conflict ${UrlbarTokenizer.RESTRICT.BOOKMARK} ${UrlbarTokenizer.RESTRICT.TAG} -> tag wins`
+ );
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://tag.conflict.com/",
+ title: `conflict ${UrlbarTokenizer.RESTRICT.BOOKMARK}`,
+ tags: ["one"],
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://nontag.conflict.com/",
+ title: `conflict ${UrlbarTokenizer.RESTRICT.BOOKMARK}`,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ context = createContext(
+ `conflict ${UrlbarTokenizer.RESTRICT.BOOKMARK} ${UrlbarTokenizer.RESTRICT.TAG}`,
+ { isPrivate: false }
+ );
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://tag.conflict.com/",
+ title: `conflict ${UrlbarTokenizer.RESTRICT.BOOKMARK}`,
+ }),
+ ],
+ });
+
+ // Disable autoFill for the next tests, see test_autoFill_default_behavior.js
+ // for specific tests.
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
+
+ // Test default usage by setting certain browser.urlbar.suggest.* prefs
+ info("foo -> default history");
+ setSuggestPrefsToFalse();
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", true);
+ context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri11.spec, title: "title" }),
+ makeVisitResult(context, { uri: uri4.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri6.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri2.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri3.spec, title: "title" }),
+ ],
+ });
+
+ info("foo -> default history, is star");
+ setSuggestPrefsToFalse();
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true);
+ // The purpose of this test is to verify what is being sent by ProviderPlaces.
+ // It will send 10 results, but the heuristic result pushes the last result
+ // out of the panel. We set maxRichResults to a high value to test the full
+ // output of ProviderPlaces.
+ Services.prefs.setIntPref("browser.urlbar.maxRichResults", 20);
+ context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri11.spec,
+ title: "title",
+ tags: ["foo.bar"],
+ }),
+ makeVisitResult(context, { uri: uri4.spec, title: "foo.bar" }),
+ makeBookmarkResult(context, { uri: uri6.spec, title: "foo.bar" }),
+ makeBookmarkResult(context, { uri: uri7.spec, title: "title" }),
+ makeBookmarkResult(context, { uri: uri8.spec, title: "foo.bar" }),
+ makeBookmarkResult(context, {
+ uri: uri9.spec,
+ title: "title",
+ tags: ["foo.bar"],
+ }),
+ makeBookmarkResult(context, {
+ uri: uri10.spec,
+ title: "foo.bar",
+ tags: ["foo.bar"],
+ }),
+ makeBookmarkResult(context, {
+ uri: uri12.spec,
+ title: "foo.bar",
+ tags: ["foo.bar"],
+ }),
+ makeVisitResult(context, { uri: uri2.spec, title: "foo.bar" }),
+ makeVisitResult(context, { uri: uri3.spec, title: "title" }),
+ ],
+ });
+ Services.prefs.clearUserPref("browser.urlbar.maxRichResults");
+
+ info("foo -> is star");
+ setSuggestPrefsToFalse();
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true);
+ context = createContext("foo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri11.spec,
+ title: "title",
+ tags: ["foo.bar"],
+ }),
+ makeBookmarkResult(context, { uri: uri6.spec, title: "foo.bar" }),
+ makeBookmarkResult(context, { uri: uri7.spec, title: "title" }),
+ makeBookmarkResult(context, { uri: uri8.spec, title: "foo.bar" }),
+ makeBookmarkResult(context, {
+ uri: uri9.spec,
+ title: "title",
+ tags: ["foo.bar"],
+ }),
+ makeBookmarkResult(context, {
+ uri: uri10.spec,
+ title: "foo.bar",
+ tags: ["foo.bar"],
+ }),
+ makeBookmarkResult(context, {
+ uri: uri12.spec,
+ title: "foo.bar",
+ tags: ["foo.bar"],
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_suggestedIndex.js b/browser/components/urlbar/tests/unit/test_suggestedIndex.js
new file mode 100644
index 0000000000..f2f7fcc0d6
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_suggestedIndex.js
@@ -0,0 +1,562 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests results with suggestedIndex and resultSpan.
+
+"use strict";
+
+const MAX_RESULTS = 10;
+
+add_task(async function suggestedIndex() {
+ // Initialize maxRichResults for sanity.
+ UrlbarPrefs.set("maxRichResults", MAX_RESULTS);
+
+ let tests = [
+ // no result spans > 1
+ {
+ desc: "{ suggestedIndex: 0 }",
+ suggestedIndexes: [0],
+ expected: indexes([10, 1], [0, 9]),
+ },
+ {
+ desc: "{ suggestedIndex: 1 }",
+ suggestedIndexes: [1],
+ expected: indexes([0, 1], [10, 1], [1, 8]),
+ },
+ {
+ desc: "{ suggestedIndex: -1 }",
+ suggestedIndexes: [-1],
+ expected: indexes([0, 9], [10, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: -2 }",
+ suggestedIndexes: [-2],
+ expected: indexes([0, 8], [10, 1], [8, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0 }, { suggestedIndex: -1 }",
+ suggestedIndexes: [0, -1],
+ expected: indexes([10, 1], [0, 8], [11, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1 }, { suggestedIndex: -1 }",
+ suggestedIndexes: [1, -1],
+ expected: indexes([0, 1], [10, 1], [1, 7], [11, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1 }, { suggestedIndex: -2 }",
+ suggestedIndexes: [1, -2],
+ expected: indexes([0, 1], [10, 1], [1, 6], [11, 1], [7, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0 }, resultCount < max",
+ suggestedIndexes: [0],
+ resultCount: 5,
+ expected: indexes([5, 1], [0, 5]),
+ },
+ {
+ desc: "{ suggestedIndex: 1 }, resultCount < max",
+ suggestedIndexes: [1],
+ resultCount: 5,
+ expected: indexes([0, 1], [5, 1], [1, 4]),
+ },
+ {
+ desc: "{ suggestedIndex: -1 }, resultCount < max",
+ suggestedIndexes: [-1],
+ resultCount: 5,
+ expected: indexes([0, 5], [5, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: -2 }, resultCount < max",
+ suggestedIndexes: [-2],
+ resultCount: 5,
+ expected: indexes([0, 4], [5, 1], [4, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0 }, { suggestedIndex: -1 }, resultCount < max",
+ suggestedIndexes: [0, -1],
+ resultCount: 5,
+ expected: indexes([5, 1], [0, 5], [6, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1 }, { suggestedIndex: -1 }, resultCount < max",
+ suggestedIndexes: [1, -1],
+ resultCount: 5,
+ expected: indexes([0, 1], [5, 1], [1, 4], [6, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0 }, { suggestedIndex: -2 }, resultCount < max",
+ suggestedIndexes: [0, -2],
+ resultCount: 5,
+ expected: indexes([5, 1], [0, 4], [6, 1], [4, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1 }, { suggestedIndex: -2 }, resultCount < max",
+ suggestedIndexes: [1, -2],
+ resultCount: 5,
+ expected: indexes([0, 1], [5, 1], [1, 3], [6, 1], [4, 1]),
+ },
+
+ // one suggestedIndex with result span > 1
+ {
+ desc: "{ suggestedIndex: 0, resultSpan: 2 }",
+ suggestedIndexes: [0],
+ spansByIndex: { 10: 2 },
+ expected: indexes([10, 1], [0, 8]),
+ },
+ {
+ desc: "{ suggestedIndex: 0, resultSpan: 3 }",
+ suggestedIndexes: [0],
+ spansByIndex: { 10: 3 },
+ expected: indexes([10, 1], [0, 7]),
+ },
+ {
+ desc: "{ suggestedIndex: 1, resultSpan: 2 }",
+ suggestedIndexes: [1],
+ spansByIndex: { 10: 2 },
+ expected: indexes([0, 1], [10, 1], [1, 7]),
+ },
+ {
+ desc: "suggestedIndex: 1, resultSpan:: 3 }",
+ suggestedIndexes: [1],
+ spansByIndex: { 10: 3 },
+ expected: indexes([0, 1], [10, 1], [1, 6]),
+ },
+ {
+ desc: "{ suggestedIndex: -1, resultSpan 2 }",
+ suggestedIndexes: [-1],
+ spansByIndex: { 10: 2 },
+ expected: indexes([0, 8], [10, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: -1, resultSpan: 3 }",
+ suggestedIndexes: [-1],
+ spansByIndex: { 10: 3 },
+ expected: indexes([0, 7], [10, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0, resultSpan: 2 }, { suggestedIndex: -1 }",
+ suggestedIndexes: [0, -1],
+ spansByIndex: { 10: 2 },
+ expected: indexes([10, 1], [0, 7], [11, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0, resultSpan: 3 }, { suggestedIndex: -1 }",
+ suggestedIndexes: [0, -1],
+ spansByIndex: { 10: 3 },
+ expected: indexes([10, 1], [0, 6], [11, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0 }, { suggestedIndex: -1, resultSpan: 2 }",
+ suggestedIndexes: [0, -1],
+ spansByIndex: { 11: 2 },
+ expected: indexes([10, 1], [0, 7], [11, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0 }, { suggestedIndex: -1, resultSpan: 3 }",
+ suggestedIndexes: [0, -1],
+ spansByIndex: { 11: 3 },
+ expected: indexes([10, 1], [0, 6], [11, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1, resultSpan: 2 }, { suggestedIndex: -1 }",
+ suggestedIndexes: [1, -1],
+ spansByIndex: { 10: 2 },
+ expected: indexes([0, 1], [10, 1], [1, 6], [11, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1, resultSpan: 3 }, { suggestedIndex: -1 }",
+ suggestedIndexes: [1, -1],
+ spansByIndex: { 10: 3 },
+ expected: indexes([0, 1], [10, 1], [1, 5], [11, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1 }, { suggestedIndex: -1, resultSpan: 2 }",
+ suggestedIndexes: [1, -1],
+ spansByIndex: { 11: 2 },
+ expected: indexes([0, 1], [10, 1], [1, 6], [11, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1 }, { suggestedIndex: -1, resultSpan: 3 }",
+ suggestedIndexes: [1, -1],
+ spansByIndex: { 11: 3 },
+ expected: indexes([0, 1], [10, 1], [1, 5], [11, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0, resultSpan: 2 }, { suggestedIndex: -2 }",
+ suggestedIndexes: [0, -2],
+ spansByIndex: { 10: 2 },
+ expected: indexes([10, 1], [0, 6], [11, 1], [6, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0, resultSpan: 3 }, { suggestedIndex: -2 }",
+ suggestedIndexes: [0, -2],
+ spansByIndex: { 10: 3 },
+ expected: indexes([10, 1], [0, 5], [11, 1], [5, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1, resultSpan: 2 }, { suggestedIndex: -2 }",
+ suggestedIndexes: [1, -2],
+ spansByIndex: { 10: 2 },
+ expected: indexes([0, 1], [10, 1], [1, 5], [11, 1], [6, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1, resultSpan: 3 }, { suggestedIndex: -2 }",
+ suggestedIndexes: [1, -2],
+ spansByIndex: { 10: 3 },
+ expected: indexes([0, 1], [10, 1], [1, 4], [11, 1], [5, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0 }, { suggestedIndex: -2, resultSpan: 2 }",
+ suggestedIndexes: [0, -2],
+ spansByIndex: { 11: 2 },
+ expected: indexes([10, 1], [0, 6], [11, 1], [6, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0 }, { suggestedIndex: -2, resultSpan: 3 }",
+ suggestedIndexes: [0, -2],
+ spansByIndex: { 11: 3 },
+ expected: indexes([10, 1], [0, 5], [11, 1], [5, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1 }, { suggestedIndex: -2, resultSpan: 2 }",
+ suggestedIndexes: [1, -2],
+ spansByIndex: { 11: 2 },
+ expected: indexes([0, 1], [10, 1], [1, 5], [11, 1], [6, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1 }, { suggestedIndex: -2, resultSpan: 3 }",
+ suggestedIndexes: [1, -2],
+ spansByIndex: { 11: 3 },
+ expected: indexes([0, 1], [10, 1], [1, 4], [11, 1], [5, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0, resultSpan: 2 }, resultCount < max",
+ suggestedIndexes: [0],
+ spansByIndex: { 5: 2 },
+ resultCount: 5,
+ expected: indexes([5, 1], [0, 5]),
+ },
+ {
+ desc: "{ suggestedIndex: 1, resultSpan: 2 }, resultCount < max",
+ suggestedIndexes: [1],
+ spansByIndex: { 5: 2 },
+ resultCount: 5,
+ expected: indexes([0, 1], [5, 1], [1, 4]),
+ },
+ {
+ desc: "{ suggestedIndex: -1, resultSpan: 2 }, resultCount < max",
+ suggestedIndexes: [-1],
+ spansByIndex: { 5: 2 },
+ resultCount: 5,
+ expected: indexes([0, 5], [5, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: -2, resultSpan: 2 }, resultCount < max",
+ suggestedIndexes: [-2],
+ spansByIndex: { 5: 2 },
+ resultCount: 5,
+ expected: indexes([0, 4], [5, 1], [4, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0, resultSpan: 2 }, { suggestedIndex: -1 }, resultCount < max",
+ suggestedIndexes: [0, -1],
+ spansByIndex: { 5: 2 },
+ resultCount: 5,
+ expected: indexes([5, 1], [0, 5], [6, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1, resultSpan: 2 }, { suggestedIndex: -1 }, resultCount < max",
+ suggestedIndexes: [1, -1],
+ spansByIndex: { 5: 2 },
+ resultCount: 5,
+ expected: indexes([0, 1], [5, 1], [1, 4], [6, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0, resultSpan: 2 }, { suggestedIndex: -2 }, resultCount < max",
+ suggestedIndexes: [0, -2],
+ spansByIndex: { 5: 2 },
+ resultCount: 5,
+ expected: indexes([5, 1], [0, 4], [6, 1], [4, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0 }, { suggestedIndex: -1, resultSpan: 2 }, resultCount < max",
+ suggestedIndexes: [0, -1],
+ spansByIndex: { 6: 2 },
+ resultCount: 5,
+ expected: indexes([5, 1], [0, 5], [6, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0 }, { suggestedIndex: -2, resultSpan: 2 }, resultCount < max",
+ suggestedIndexes: [0, -2],
+ spansByIndex: { 6: 2 },
+ resultCount: 5,
+ expected: indexes([5, 1], [0, 4], [6, 1], [4, 1]),
+ },
+
+ // two suggestedIndexes with result span > 1
+ {
+ desc: "{ suggestedIndex: 0, resultSpan: 2 }, { suggestedIndex: -1, resultSpan: 2 }",
+ suggestedIndexes: [0, -1],
+ spansByIndex: { 10: 2, 11: 2 },
+ expected: indexes([10, 1], [0, 6], [11, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0, resultSpan: 3 }, { suggestedIndex: -1, resultSpan: 2 }",
+ suggestedIndexes: [0, -1],
+ spansByIndex: { 10: 3, 11: 2 },
+ expected: indexes([10, 1], [0, 5], [11, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0, resultSpan: 2 }, { suggestedIndex: -1, resultSpan: 3 }",
+ suggestedIndexes: [0, -1],
+ spansByIndex: { 10: 2, 11: 3 },
+ expected: indexes([10, 1], [0, 5], [11, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1, resultSpan: 2 }, { suggestedIndex: -1, resultSpan: 2 }",
+ suggestedIndexes: [1, -1],
+ spansByIndex: { 10: 2, 11: 2 },
+ expected: indexes([0, 1], [10, 1], [1, 5], [11, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1, resultSpan: 3 }, { suggestedIndex: -1, resultSpan: 2 }",
+ suggestedIndexes: [1, -1],
+ spansByIndex: { 10: 3, 11: 2 },
+ expected: indexes([0, 1], [10, 1], [1, 4], [11, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1, resultSpan: 2 }, { suggestedIndex: -1, resultSpan: 3 }",
+ suggestedIndexes: [1, -1],
+ spansByIndex: { 10: 2, 11: 3 },
+ expected: indexes([0, 1], [10, 1], [1, 4], [11, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0, resultSpan: 2 }, { suggestedIndex: -2, resultSpan: 2 }",
+ suggestedIndexes: [0, -2],
+ spansByIndex: { 10: 2, 11: 2 },
+ expected: indexes([10, 1], [0, 5], [11, 1], [5, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0, resultSpan: 3 }, { suggestedIndex: -2, resultSpan: 2 }",
+ suggestedIndexes: [0, -2],
+ spansByIndex: { 10: 3, 11: 2 },
+ expected: indexes([10, 1], [0, 4], [11, 1], [4, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0, resultSpan: 2 }, { suggestedIndex: -2, resultSpan: 3 }",
+ suggestedIndexes: [0, -2],
+ spansByIndex: { 10: 2, 11: 3 },
+ expected: indexes([10, 1], [0, 4], [11, 1], [4, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1, resultSpan: 2 }, { suggestedIndex: -2, resultSpan: 2 }",
+ suggestedIndexes: [1, -2],
+ spansByIndex: { 10: 2, 11: 2 },
+ expected: indexes([0, 1], [10, 1], [1, 4], [11, 1], [5, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1, resultSpan: 3 }, { suggestedIndex: -2, resultSpan: 2 }",
+ suggestedIndexes: [1, -2],
+ spansByIndex: { 10: 3, 11: 2 },
+ expected: indexes([0, 1], [10, 1], [1, 3], [11, 1], [4, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1, resultSpan: 2 }, { suggestedIndex: -2, resultSpan: 3 }",
+ suggestedIndexes: [1, -2],
+ spansByIndex: { 10: 2, 11: 3 },
+ expected: indexes([0, 1], [10, 1], [1, 3], [11, 1], [4, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0, resultSpan: 2 }, { suggestedIndex: -1, resultSpan: 2 }, resultCount < max",
+ suggestedIndexes: [0, -1],
+ spansByIndex: { 5: 2, 6: 2 },
+ resultCount: 5,
+ expected: indexes([5, 1], [0, 5], [6, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 1, resultSpan: 2 }, { suggestedIndex: -1, resultSpan: 2 }, resultCount < max",
+ suggestedIndexes: [1, -1],
+ spansByIndex: { 5: 2, 6: 2 },
+ resultCount: 5,
+ expected: indexes([0, 1], [5, 1], [1, 4], [6, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: 0, resultSpan: 2 }, { suggestedIndex: -2, resultSpan: 2 }, resultCount < max",
+ suggestedIndexes: [0, -2],
+ spansByIndex: { 5: 2, 6: 2 },
+ resultCount: 5,
+ expected: indexes([5, 1], [0, 4], [6, 1], [4, 1]),
+ },
+
+ // one suggestedIndex plus other result with resultSpan > 1
+ {
+ desc: "{ suggestedIndex: 0 }, { resultSpan: 2 } A",
+ suggestedIndexes: [0],
+ spansByIndex: { 0: 2 },
+ expected: indexes([10, 1], [0, 8]),
+ },
+ {
+ desc: "{ suggestedIndex: 0 }, { resultSpan: 2 } B",
+ suggestedIndexes: [0],
+ spansByIndex: { 8: 2 },
+ expected: indexes([10, 1], [0, 8]),
+ },
+ {
+ desc: "{ suggestedIndex: 0 }, { resultSpan: 2 } C",
+ suggestedIndexes: [0],
+ spansByIndex: { 9: 2 },
+ expected: indexes([10, 1], [0, 9]),
+ },
+ {
+ desc: "{ suggestedIndex: 1 }, { resultSpan: 2 } A",
+ suggestedIndexes: [1],
+ spansByIndex: { 0: 2 },
+ expected: indexes([0, 1], [10, 1], [1, 7]),
+ },
+ {
+ desc: "{ suggestedIndex: 1 }, { resultSpan: 2 } B",
+ suggestedIndexes: [1],
+ spansByIndex: { 8: 2 },
+ expected: indexes([0, 1], [10, 1], [1, 7]),
+ },
+ {
+ desc: "{ suggestedIndex: -1 }, { resultSpan: 2 }",
+ suggestedIndexes: [-1],
+ spansByIndex: { 0: 2 },
+ expected: indexes([0, 8], [10, 1]),
+ },
+ {
+ desc: "{ suggestedIndex: -2 }, { resultSpan: 2 }",
+ suggestedIndexes: [-2],
+ spansByIndex: { 0: 2 },
+ expected: indexes([0, 7], [10, 1], [7, 1]),
+ },
+
+ // miscellaneous
+ {
+ desc: "no suggestedIndex, last result has resultSpan = 2",
+ suggestedIndexes: [],
+ spansByIndex: { 9: 2 },
+ expected: indexes([0, 9]),
+ },
+ {
+ desc: "{ suggestedIndex: -1 }, last result has resultSpan = 2",
+ suggestedIndexes: [-1],
+ spansByIndex: { 9: 2 },
+ expected: indexes([0, 9], [10, 1]),
+ },
+ {
+ desc: "no suggestedIndex, index 8 result has resultSpan = 2",
+ suggestedIndexes: [],
+ spansByIndex: { 8: 2 },
+ expected: indexes([0, 9]),
+ },
+ {
+ desc: "{ suggestedIndex: -1 }, index 8 result has resultSpan = 2",
+ suggestedIndexes: [-1],
+ spansByIndex: { 8: 2 },
+ expected: indexes([0, 8], [10, 1]),
+ },
+ ];
+
+ for (let test of tests) {
+ info("Running test: " + JSON.stringify(test));
+ await doSuggestedIndexTest(test);
+ }
+});
+
+/**
+ * Sets up a provider with some results with suggested indexes and result spans,
+ * performs a search, and then checks the results.
+ *
+ * @param {object} options
+ * Options for the test.
+ * @param {Array} options.suggestedIndexes
+ * For each of the indexes in this array, a new result with the given
+ * suggestedIndex will be returned by the provider.
+ * @param {Array} options.expected
+ * The indexes of the expected results within the array of results returned by
+ * the provider.
+ * @param {object} [options.spansByIndex]
+ * Maps indexes within the array of results returned by the provider to result
+ * spans to set on those results.
+ * @param {number} [options.resultCount]
+ * Aside from the results with suggested indexes, this is the number of
+ * results that the provider will return.
+ */
+async function doSuggestedIndexTest({
+ suggestedIndexes,
+ expected,
+ spansByIndex = {},
+ resultCount = MAX_RESULTS,
+}) {
+ // Make resultCount history results.
+ let results = [];
+ for (let i = 0; i < resultCount; i++) {
+ results.push(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ {
+ url: "http://example.com/" + i,
+ }
+ )
+ );
+ }
+
+ // Make the suggested-index results.
+ for (let suggestedIndex of suggestedIndexes) {
+ results.push(
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ {
+ url: "http://example.com/si " + suggestedIndex,
+ }
+ ),
+ { suggestedIndex }
+ )
+ );
+ }
+
+ // Set resultSpan on each result as indicated by spansByIndex.
+ for (let [index, span] of Object.entries(spansByIndex)) {
+ results[index].resultSpan = span;
+ }
+
+ // Set up the provider, etc.
+ let provider = registerBasicTestProvider(results);
+ let context = createContext(undefined, { providers: [provider.name] });
+ let controller = UrlbarTestUtils.newMockController();
+
+ // Finally, search and check the results.
+ let expectedResults = expected.map(i => results[i]);
+ await UrlbarProvidersManager.startQuery(context, controller);
+ Assert.deepEqual(context.results, expectedResults);
+}
+
+/**
+ * Helper that generates an array of indexes. Pass in [index, length] tuples.
+ * Each tuple will produce the indexes starting from `index` to `index + length`
+ * (not including the index at `index + length`).
+ *
+ * Examples:
+ *
+ * indexes([0, 5]) => [0, 1, 2, 3, 4]
+ * indexes([0, 1], [4, 3], [8, 2]) => [0, 4, 5, 6, 8, 9]
+ *
+ * @param {Array} pairs
+ * [index, length] tuples as described above.
+ * @returns {Array}
+ * An array of indexes.
+ */
+function indexes(...pairs) {
+ return pairs.reduce((indexesArray, [start, len]) => {
+ for (let i = start; i < start + len; i++) {
+ indexesArray.push(i);
+ }
+ return indexesArray;
+ }, []);
+}
diff --git a/browser/components/urlbar/tests/unit/test_suggestedIndexRelativeToGroup.js b/browser/components/urlbar/tests/unit/test_suggestedIndexRelativeToGroup.js
new file mode 100644
index 0000000000..ede2f9cc66
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_suggestedIndexRelativeToGroup.js
@@ -0,0 +1,596 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests results with `suggestedIndex` and `isSuggestedIndexRelativeToGroup`.
+
+"use strict";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+const MAX_RESULTS = 10;
+const MAX_RICH_RESULTS_PREF = "browser.urlbar.maxRichResults";
+
+// Default result groups used in the tests below.
+const RESULT_GROUPS = {
+ children: [
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT,
+ flexChildren: true,
+ children: [
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ ],
+ },
+ ],
+};
+
+let sandbox;
+add_task(function setuo() {
+ sandbox = lazy.sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+});
+
+add_task(async function test() {
+ // Set a specific maxRichResults for sanity's sake.
+ Services.prefs.setIntPref(MAX_RICH_RESULTS_PREF, MAX_RESULTS);
+
+ // Create the default non-suggestedIndex results we'll use for tests that
+ // don't specify `otherResults`.
+ let basicResults = [
+ ...makeHistoryResults(),
+ ...makeFormHistoryResults(),
+ ...makeRemoteSuggestionResults(),
+ ];
+
+ // Test cases follow. Each object in `tests` has the following properties:
+ //
+ // * {string} desc
+ // * {object} suggestedIndexResults
+ // Describes the suggestedIndex results the test provider should return.
+ // Properties:
+ // * {number} suggestedIndex
+ // * {UrlbarUtils.RESULT_GROUP} group
+ // This will force the result to have the given group.
+ // * {array} expected
+ // Describes the expected results the muxer should return, i.e., the search
+ // results. Each object in the array describes a slice of expected results.
+ // The size of the slice is defined by the `count` property.
+ // * {UrlbarUtils.RESULT_GROUP} group
+ // The expected group of the results in the slice.
+ // * {number} count
+ // The number of results in the slice.
+ // * {number} [offset]
+ // Can be used to offset the starting index of the slice in the results.
+ // * {array} [otherResults]
+ // An array of results besides the group-relative suggestedIndex results
+ // that the provider should return. If not specified `basicResults` is used.
+ // * {array} [resultGroups]
+ // The result groups to use. If not specified `RESULT_GROUPS` is used.
+ let tests = [
+ {
+ desc: "First result in GENERAL",
+ suggestedIndexResults: [
+ {
+ suggestedIndex: 0,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ expected: [
+ {
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ count: 4,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ suggestedIndex: 0,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ count: 2,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ // The muxer will remove the first 4 remote suggestions because they
+ // dupe the earlier form history.
+ offset: 4,
+ count: 3,
+ },
+ ],
+ },
+
+ {
+ desc: "Last result in GENERAL",
+ suggestedIndexResults: [
+ {
+ suggestedIndex: -1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ expected: [
+ {
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ count: 4,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ count: 2,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ suggestedIndex: -1,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ // The muxer will remove the first 4 remote suggestions because they
+ // dupe the earlier form history.
+ offset: 4,
+ count: 3,
+ },
+ ],
+ },
+
+ {
+ desc: "First result in GENERAL_PARENT",
+ suggestedIndexResults: [
+ {
+ suggestedIndex: 0,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT,
+ },
+ ],
+ expected: [
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT,
+ suggestedIndex: 0,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ count: 3,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ count: 3,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ // The muxer will remove the first 3 remote suggestions because they
+ // dupe the earlier form history.
+ offset: 3,
+ count: 3,
+ },
+ ],
+ },
+
+ {
+ desc: "Last result in GENERAL_PARENT",
+ suggestedIndexResults: [
+ {
+ suggestedIndex: -1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT,
+ },
+ ],
+ expected: [
+ {
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ count: 3,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ count: 3,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ // The muxer will remove the first 3 remote suggestions because they
+ // dupe the earlier form history.
+ offset: 3,
+ count: 3,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT,
+ suggestedIndex: -1,
+ },
+ ],
+ },
+
+ {
+ desc: "First and last results in GENERAL",
+ suggestedIndexResults: [
+ {
+ suggestedIndex: 0,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ {
+ suggestedIndex: -1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ expected: [
+ {
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ count: 4,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ suggestedIndex: 0,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ count: 1,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ suggestedIndex: -1,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ // The muxer will remove the first 4 remote suggestions because they
+ // dupe the earlier form history.
+ offset: 4,
+ count: 3,
+ },
+ ],
+ },
+
+ {
+ desc: "First and last results in GENERAL_PARENT",
+ suggestedIndexResults: [
+ {
+ suggestedIndex: 0,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT,
+ },
+ {
+ suggestedIndex: -1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT,
+ },
+ ],
+ expected: [
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT,
+ suggestedIndex: 0,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ count: 3,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ count: 3,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ // The muxer will remove the first 3 remote suggestions because they
+ // dupe the earlier form history.
+ offset: 3,
+ count: 2,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT,
+ suggestedIndex: -1,
+ },
+ ],
+ },
+
+ {
+ desc: "First result in GENERAL_PARENT, first result in GENERAL",
+ suggestedIndexResults: [
+ {
+ suggestedIndex: 0,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT,
+ },
+ {
+ suggestedIndex: 0,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ expected: [
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT,
+ suggestedIndex: 0,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ count: 3,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ suggestedIndex: 0,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ count: 2,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ // The muxer will remove the first 3 remote suggestions because they
+ // dupe the earlier form history.
+ offset: 3,
+ count: 3,
+ },
+ ],
+ },
+
+ {
+ desc: "Results in sibling group, no other results in same group",
+ otherResults: makeFormHistoryResults(),
+ suggestedIndexResults: [
+ {
+ suggestedIndex: -1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ },
+ ],
+ expected: [
+ {
+ group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
+ count: 9,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL,
+ suggestedIndex: -1,
+ },
+ ],
+ },
+
+ {
+ desc: "Results in sibling group, no other results in same group, has child group",
+ resultGroups: {
+ flexChildren: true,
+ children: [
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT,
+ children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }],
+ },
+ ],
+ },
+ otherResults: makeRemoteSuggestionResults(),
+ suggestedIndexResults: [
+ {
+ suggestedIndex: -1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT,
+ },
+ ],
+ expected: [
+ {
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ count: 9,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT,
+ suggestedIndex: -1,
+ },
+ ],
+ },
+
+ {
+ desc: "Complex group nesting with global suggestedIndex with resultSpan",
+ resultGroups: {
+ children: [
+ {
+ maxResultCount: 1,
+ children: [{ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }],
+ },
+ {
+ flexChildren: true,
+ children: [
+ {
+ flex: 2,
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ },
+ {
+ flex: 1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT,
+ children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }],
+ },
+ ],
+ },
+ ],
+ },
+ otherResults: [
+ // heuristic
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ {
+ engine: "test",
+ suggestion: "foo",
+ lowerCaseSuggestion: "foo",
+ }
+ ),
+ {
+ heuristic: true,
+ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST,
+ }
+ ),
+ // global suggestedIndex with resultSpan = 2
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ {
+ engine: "test",
+ }
+ ),
+ {
+ suggestedIndex: 1,
+ resultSpan: 2,
+ }
+ ),
+ // remote suggestions
+ ...makeRemoteSuggestionResults(),
+ ],
+ suggestedIndexResults: [
+ {
+ suggestedIndex: -1,
+ group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT,
+ },
+ ],
+ expected: [
+ {
+ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST,
+ count: 1,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.SUGGESTED_INDEX,
+ suggestedIndex: 1,
+ resultSpan: 2,
+ count: 1,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
+ count: 6,
+ },
+ {
+ group: UrlbarUtils.RESULT_GROUP.GENERAL_PARENT,
+ suggestedIndex: -1,
+ },
+ ],
+ },
+ ];
+
+ let controller = UrlbarTestUtils.newMockController();
+
+ for (let {
+ desc,
+ suggestedIndexResults,
+ expected,
+ resultGroups,
+ otherResults,
+ } of tests) {
+ info(`Running test: ${desc}`);
+
+ setResultGroups(resultGroups || RESULT_GROUPS);
+
+ // Make the array of all results and do a search.
+ let results = (otherResults || basicResults).concat(
+ makeSuggestedIndexResults(suggestedIndexResults)
+ );
+ let provider = registerBasicTestProvider(results);
+ let context = createContext(undefined, { providers: [provider.name] });
+ await UrlbarProvidersManager.startQuery(context, controller);
+
+ // Make the list of expected results.
+ let expectedResults = [];
+ for (let { group, offset, count, suggestedIndex } of expected) {
+ // Find the index in `results` of the expected result.
+ let index = results.findIndex(
+ r =>
+ UrlbarUtils.getResultGroup(r) == group &&
+ r.suggestedIndex === suggestedIndex
+ );
+ Assert.notEqual(
+ index,
+ -1,
+ "Sanity check: Expected result is in `results`"
+ );
+ if (offset) {
+ index += offset;
+ }
+
+ // Extract the expected number of results from `results` and append them
+ // to the expected results array.
+ count = count || 1;
+ expectedResults.push(...results.slice(index, index + count));
+ }
+
+ Assert.deepEqual(context.results, expectedResults);
+
+ UrlbarProvidersManager.unregisterProvider(provider);
+ }
+});
+
+function makeHistoryResults(count = MAX_RESULTS) {
+ let results = [];
+ for (let i = 0; i < count; i++) {
+ results.push(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ { url: "http://example.com/" + i }
+ )
+ );
+ }
+ return results;
+}
+
+function makeRemoteSuggestionResults(count = MAX_RESULTS) {
+ let results = [];
+ for (let i = 0; i < count; i++) {
+ results.push(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ {
+ engine: "test",
+ query: "test",
+ suggestion: "test " + i,
+ lowerCaseSuggestion: "test " + i,
+ }
+ )
+ );
+ }
+ return results;
+}
+
+function makeFormHistoryResults(count = MAX_RESULTS) {
+ let results = [];
+ for (let i = 0; i < count; i++) {
+ results.push(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ {
+ engine: "test",
+ suggestion: "test " + i,
+ lowerCaseSuggestion: "test " + i,
+ }
+ )
+ );
+ }
+ return results;
+}
+
+function makeSuggestedIndexResults(objects) {
+ return objects.map(({ suggestedIndex, group }) =>
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ url: "http://example.com/si " + suggestedIndex,
+ }
+ ),
+ {
+ group,
+ suggestedIndex,
+ isSuggestedIndexRelativeToGroup: true,
+ }
+ )
+ );
+}
+
+function setResultGroups(resultGroups) {
+ sandbox.restore();
+ if (resultGroups) {
+ sandbox.stub(UrlbarPrefs, "resultGroups").get(() => resultGroups);
+ }
+}
diff --git a/browser/components/urlbar/tests/unit/test_tab_matches.js b/browser/components/urlbar/tests/unit/test_tab_matches.js
new file mode 100644
index 0000000000..f4061bf3db
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_tab_matches.js
@@ -0,0 +1,354 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim:set ts=2 sw=2 sts=2 et:
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+testEngine_setup();
+
+add_task(async function test_tab_matches() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ Services.prefs.clearUserPref("browser.urlbar.autoFill");
+ });
+
+ let uri1 = Services.io.newURI("http://abc.com/");
+ let uri2 = Services.io.newURI("http://xyz.net/");
+ let uri3 = Services.io.newURI("about:mozilla");
+ let uri4 = Services.io.newURI("data:text/html,test");
+ let uri5 = Services.io.newURI("http://foobar.org");
+ await PlacesTestUtils.addVisits([
+ {
+ uri: uri5,
+ title: "foobar.org - much better than ABC, definitely better than XYZ",
+ },
+ { uri: uri2, title: "xyz.net - we're better than ABC" },
+ { uri: uri1, title: "ABC rocks" },
+ ]);
+ await addOpenPages(uri1, 1);
+ // Pages that cannot be registered in history.
+ await addOpenPages(uri3, 1);
+ await addOpenPages(uri4, 1);
+
+ info("basic tab match");
+ let context = createContext("abc.com", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ uri: "http://abc.com/",
+ title: "ABC rocks",
+ heuristic: true,
+ }),
+ makeTabSwitchResult(context, {
+ uri: "http://abc.com/",
+ title: "ABC rocks",
+ }),
+ ],
+ });
+
+ info("three results, one tab match");
+ context = createContext("abc", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeTabSwitchResult(context, {
+ uri: "http://abc.com/",
+ title: "ABC rocks",
+ }),
+ makeVisitResult(context, {
+ uri: uri2.spec,
+ title: "xyz.net - we're better than ABC",
+ }),
+ makeVisitResult(context, {
+ uri: uri5.spec,
+ title: "foobar.org - much better than ABC, definitely better than XYZ",
+ }),
+ ],
+ });
+
+ info("three results, both normal results are tab matches");
+ await addOpenPages(uri2, 1);
+ context = createContext("abc", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeTabSwitchResult(context, {
+ uri: "http://abc.com/",
+ title: "ABC rocks",
+ }),
+ makeTabSwitchResult(context, {
+ uri: "http://xyz.net/",
+ title: "xyz.net - we're better than ABC",
+ }),
+ makeVisitResult(context, {
+ uri: uri5.spec,
+ title: "foobar.org - much better than ABC, definitely better than XYZ",
+ }),
+ ],
+ });
+
+ info("a container tab is not visible in 'switch to tab'");
+ await addOpenPages(uri5, 1, /* userContextId: */ 3);
+ context = createContext("abc", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeTabSwitchResult(context, {
+ uri: "http://abc.com/",
+ title: "ABC rocks",
+ }),
+ makeTabSwitchResult(context, {
+ uri: "http://xyz.net/",
+ title: "xyz.net - we're better than ABC",
+ }),
+ makeVisitResult(context, {
+ uri: uri5.spec,
+ title: "foobar.org - much better than ABC, definitely better than XYZ",
+ }),
+ ],
+ });
+
+ info(
+ "a container tab should not see 'switch to tab' for other container tabs"
+ );
+ context = createContext("abc", { isPrivate: false, userContextId: 3 });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: uri1.spec,
+ title: "ABC rocks",
+ }),
+ makeVisitResult(context, {
+ uri: uri2.spec,
+ title: "xyz.net - we're better than ABC",
+ }),
+ makeTabSwitchResult(context, {
+ uri: "http://foobar.org/",
+ title: "foobar.org - much better than ABC, definitely better than XYZ",
+ }),
+ ],
+ });
+
+ info("a different container tab should not see any 'switch to tab'");
+ context = createContext("abc", { isPrivate: false, userContextId: 2 });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, { uri: uri1.spec, title: "ABC rocks" }),
+ makeVisitResult(context, {
+ uri: uri2.spec,
+ title: "xyz.net - we're better than ABC",
+ }),
+ makeVisitResult(context, {
+ uri: uri5.spec,
+ title: "foobar.org - much better than ABC, definitely better than XYZ",
+ }),
+ ],
+ });
+
+ info(
+ "three results, both normal results are tab matches, one has multiple tabs"
+ );
+ await addOpenPages(uri2, 5);
+ context = createContext("abc", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeTabSwitchResult(context, {
+ uri: "http://abc.com/",
+ title: "ABC rocks",
+ }),
+ makeTabSwitchResult(context, {
+ uri: "http://xyz.net/",
+ title: "xyz.net - we're better than ABC",
+ }),
+ makeVisitResult(context, {
+ uri: uri5.spec,
+ title: "foobar.org - much better than ABC, definitely better than XYZ",
+ }),
+ ],
+ });
+
+ info("three results, no tab matches");
+ await removeOpenPages(uri1, 1);
+ await removeOpenPages(uri2, 6);
+ context = createContext("abc", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: uri1.spec,
+ title: "ABC rocks",
+ }),
+ makeVisitResult(context, {
+ uri: uri2.spec,
+ title: "xyz.net - we're better than ABC",
+ }),
+ makeVisitResult(context, {
+ uri: uri5.spec,
+ title: "foobar.org - much better than ABC, definitely better than XYZ",
+ }),
+ ],
+ });
+
+ info("tab match search with restriction character");
+ await addOpenPages(uri1, 1);
+ context = createContext(UrlbarTokenizer.RESTRICT.OPENPAGE + " abc", {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ query: "abc",
+ alias: UrlbarTokenizer.RESTRICT.OPENPAGE,
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ makeTabSwitchResult(context, {
+ uri: "http://abc.com/",
+ title: "ABC rocks",
+ }),
+ ],
+ });
+
+ info("tab match with not-addable pages");
+ context = createContext("mozilla", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeTabSwitchResult(context, {
+ uri: "about:mozilla",
+ title: "about:mozilla",
+ }),
+ ],
+ });
+
+ info("tab match with not-addable pages, no boundary search");
+ context = createContext("ut:mo", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeTabSwitchResult(context, {
+ uri: "about:mozilla",
+ title: "about:mozilla",
+ }),
+ ],
+ });
+
+ info("tab match with not-addable pages and restriction character");
+ context = createContext(UrlbarTokenizer.RESTRICT.OPENPAGE + " mozilla", {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ query: "mozilla",
+ alias: UrlbarTokenizer.RESTRICT.OPENPAGE,
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ }),
+ makeTabSwitchResult(context, {
+ uri: "about:mozilla",
+ title: "about:mozilla",
+ }),
+ ],
+ });
+
+ info("tab match with not-addable pages and only restriction character");
+ context = createContext(UrlbarTokenizer.RESTRICT.OPENPAGE, {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeTabSwitchResult(context, {
+ uri: "data:text/html,test",
+ title: "data:text/html,test",
+ iconUri: UrlbarUtils.ICON.DEFAULT,
+ }),
+ makeTabSwitchResult(context, {
+ uri: "about:mozilla",
+ title: "about:mozilla",
+ }),
+ makeTabSwitchResult(context, {
+ uri: "http://abc.com/",
+ title: "ABC rocks",
+ }),
+ ],
+ });
+
+ info("tab match should not return tags as part of the title");
+ // Bookmark one of the pages, and add tags to it, to check they don't appear
+ // in the title.
+ let bm = await PlacesUtils.bookmarks.insert({
+ url: uri1,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ });
+ PlacesUtils.tagging.tagURI(uri1, ["test-tag"]);
+ context = createContext("abc.com", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ uri: "http://abc.com/",
+ title: "ABC rocks",
+ heuristic: true,
+ }),
+ makeTabSwitchResult(context, {
+ uri: "http://abc.com/",
+ title: "ABC rocks",
+ }),
+ ],
+ });
+ await PlacesUtils.bookmarks.remove(bm);
+
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_tags_caseInsensitivity.js b/browser/components/urlbar/tests/unit/test_tags_caseInsensitivity.js
new file mode 100644
index 0000000000..f7994326ee
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_tags_caseInsensitivity.js
@@ -0,0 +1,137 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+testEngine_setup();
+
+/**
+ * Checks the results of a search for `searchTerm`.
+ *
+ * @param {Array} uris
+ * A 2-element array containing [{string} uri, {array} tags}], where `tags`
+ * is a comma-separated list of the tags expected to appear in the search.
+ * @param {string} searchTerm
+ * The term to search for
+ */
+async function ensure_tag_results(uris, searchTerm) {
+ print("Searching for '" + searchTerm + "'");
+ let context = createContext(searchTerm, { isPrivate: false });
+ let urlbarResults = [];
+ for (let [uri, tags] of uris) {
+ urlbarResults.push(
+ makeBookmarkResult(context, {
+ uri,
+ title: "A title",
+ tags,
+ })
+ );
+ }
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...urlbarResults,
+ ],
+ });
+}
+
+const uri1 = "http://site.tld/1";
+const uri2 = "http://site.tld/2";
+const uri3 = "http://site.tld/3";
+const uri4 = "http://site.tld/4";
+const uri5 = "http://site.tld/5";
+const uri6 = "http://site.tld/6";
+
+/**
+ * Properly tags a uri adding it to bookmarks.
+ *
+ * @param {string} url
+ * The URI to tag.
+ * @param {Array} tags
+ * The tags to add.
+ */
+async function tagURI(url, tags) {
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url,
+ title: "A title",
+ });
+ PlacesUtils.tagging.tagURI(Services.io.newURI(url), tags);
+}
+
+/**
+ * Test bug #408221
+ */
+add_task(async function test_tags_search_case_insensitivity() {
+ // always search in history + bookmarks, no matter what the default is
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmarks", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.history");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.bookmarks");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+
+ await tagURI(uri6, ["muD"]);
+ await tagURI(uri6, ["baR"]);
+ await tagURI(uri5, ["mud"]);
+ await tagURI(uri5, ["bar"]);
+ await tagURI(uri4, ["MUD"]);
+ await tagURI(uri4, ["BAR"]);
+ await tagURI(uri3, ["foO"]);
+ await tagURI(uri2, ["FOO"]);
+ await tagURI(uri1, ["Foo"]);
+
+ await ensure_tag_results(
+ [
+ [uri1, ["Foo"]],
+ [uri2, ["Foo"]],
+ [uri3, ["Foo"]],
+ ],
+ "foo"
+ );
+ await ensure_tag_results(
+ [
+ [uri1, ["Foo"]],
+ [uri2, ["Foo"]],
+ [uri3, ["Foo"]],
+ ],
+ "Foo"
+ );
+ await ensure_tag_results(
+ [
+ [uri1, ["Foo"]],
+ [uri2, ["Foo"]],
+ [uri3, ["Foo"]],
+ ],
+ "foO"
+ );
+ await ensure_tag_results(
+ [
+ [uri4, ["BAR", "MUD"]],
+ [uri5, ["BAR", "MUD"]],
+ [uri6, ["BAR", "MUD"]],
+ ],
+ "bar mud"
+ );
+ await ensure_tag_results(
+ [
+ [uri4, ["BAR", "MUD"]],
+ [uri5, ["BAR", "MUD"]],
+ [uri6, ["BAR", "MUD"]],
+ ],
+ "BAR MUD"
+ );
+ await ensure_tag_results(
+ [
+ [uri4, ["BAR", "MUD"]],
+ [uri5, ["BAR", "MUD"]],
+ [uri6, ["BAR", "MUD"]],
+ ],
+ "Bar Mud"
+ );
+});
diff --git a/browser/components/urlbar/tests/unit/test_tags_extendedUnicode.js b/browser/components/urlbar/tests/unit/test_tags_extendedUnicode.js
new file mode 100644
index 0000000000..596b439be5
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_tags_extendedUnicode.js
@@ -0,0 +1,66 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Test autocomplete for non-English URLs that match the tag bug 416214. Also
+ * test bug 417441 by making sure escaped ascii characters like "+" remain
+ * escaped.
+ *
+ * - add a visit for a page with a non-English URL
+ * - add a tag for the page
+ * - search for the tag
+ * - test number of matches (should be exactly one)
+ * - make sure the url is decoded
+ */
+
+testEngine_setup();
+
+add_task(async function test_tag_match_url() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+ info(
+ "Make sure tag matches return the right url as well as '+' remain escaped"
+ );
+ let uri1 = Services.io.newURI("http://escaped/ユニコード");
+ let uri2 = Services.io.newURI("http://asciiescaped/blocking-firefox3%2B");
+ await PlacesTestUtils.addVisits([
+ { uri: uri1, title: "title" },
+ { uri: uri2, title: "title" },
+ ]);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri1,
+ title: "title",
+ tags: ["superTag"],
+ style: ["bookmark-tag"],
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri2,
+ title: "title",
+ tags: ["superTag"],
+ style: ["bookmark-tag"],
+ });
+ let context = createContext("superTag", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri2.spec,
+ title: "title",
+ tags: ["superTag"],
+ }),
+ makeBookmarkResult(context, {
+ uri: uri1.spec,
+ title: "title",
+ tags: ["superTag"],
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_tags_general.js b/browser/components/urlbar/tests/unit/test_tags_general.js
new file mode 100644
index 0000000000..c2c620c152
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_tags_general.js
@@ -0,0 +1,207 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+testEngine_setup();
+
+/**
+ * Checks the results of a search for `searchTerm`.
+ *
+ * @param {Array} uris
+ * A 2-element array containing [{string} uri, {array} tags}], where `tags`
+ * is a comma-separated list of the tags expected to appear in the search.
+ * @param {string} searchTerm
+ * The term to search for
+ */
+async function ensure_tag_results(uris, searchTerm) {
+ print("Searching for '" + searchTerm + "'");
+ let context = createContext(searchTerm, { isPrivate: false });
+ let urlbarResults = [];
+ for (let [uri, tags] of uris) {
+ urlbarResults.push(
+ makeBookmarkResult(context, {
+ uri,
+ title: "A title",
+ tags,
+ })
+ );
+ }
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ ...urlbarResults,
+ ],
+ });
+}
+
+var uri1 = "http://site.tld/1/aaa";
+var uri2 = "http://site.tld/2/bbb";
+var uri3 = "http://site.tld/3/aaa";
+var uri4 = "http://site.tld/4/bbb";
+var uri5 = "http://site.tld/5/aaa";
+var uri6 = "http://site.tld/6/bbb";
+
+var tests = [
+ () =>
+ ensure_tag_results(
+ [
+ [uri1, ["foo"]],
+ [uri4, ["foo bar"]],
+ [uri6, ["foo bar cheese"]],
+ ],
+ "foo"
+ ),
+ () => ensure_tag_results([[uri1, ["foo"]]], "foo aaa"),
+ () =>
+ ensure_tag_results(
+ [
+ [uri4, ["foo bar"]],
+ [uri6, ["foo bar cheese"]],
+ ],
+ "foo bbb"
+ ),
+ () =>
+ ensure_tag_results(
+ [
+ [uri2, ["bar"]],
+ [uri4, ["foo bar"]],
+ [uri5, ["bar cheese"]],
+ [uri6, ["foo bar cheese"]],
+ ],
+ "bar"
+ ),
+ () => ensure_tag_results([[uri5, ["bar cheese"]]], "bar aaa"),
+ () =>
+ ensure_tag_results(
+ [
+ [uri2, ["bar"]],
+ [uri4, ["foo bar"]],
+ [uri6, ["foo bar cheese"]],
+ ],
+ "bar bbb"
+ ),
+ () =>
+ ensure_tag_results(
+ [
+ [uri3, ["cheese"]],
+ [uri5, ["bar cheese"]],
+ [uri6, ["foo bar cheese"]],
+ ],
+ "cheese"
+ ),
+ () =>
+ ensure_tag_results(
+ [
+ [uri3, ["cheese"]],
+ [uri5, ["bar cheese"]],
+ ],
+ "chees aaa"
+ ),
+ () => ensure_tag_results([[uri6, ["foo bar cheese"]]], "chees bbb"),
+ () =>
+ ensure_tag_results(
+ [
+ [uri4, ["foo bar"]],
+ [uri6, ["foo bar cheese"]],
+ ],
+ "fo bar"
+ ),
+ () => ensure_tag_results([], "fo bar aaa"),
+ () =>
+ ensure_tag_results(
+ [
+ [uri4, ["foo bar"]],
+ [uri6, ["foo bar cheese"]],
+ ],
+ "fo bar bbb"
+ ),
+ () =>
+ ensure_tag_results(
+ [
+ [uri4, ["foo bar"]],
+ [uri6, ["foo bar cheese"]],
+ ],
+ "ba foo"
+ ),
+ () => ensure_tag_results([], "ba foo aaa"),
+ () =>
+ ensure_tag_results(
+ [
+ [uri4, ["foo bar"]],
+ [uri6, ["foo bar cheese"]],
+ ],
+ "ba foo bbb"
+ ),
+ () =>
+ ensure_tag_results(
+ [
+ [uri5, ["bar cheese"]],
+ [uri6, ["foo bar cheese"]],
+ ],
+ "ba chee"
+ ),
+ () => ensure_tag_results([[uri5, ["bar cheese"]]], "ba chee aaa"),
+ () => ensure_tag_results([[uri6, ["foo bar cheese"]]], "ba chee bbb"),
+ () =>
+ ensure_tag_results(
+ [
+ [uri5, ["bar cheese"]],
+ [uri6, ["foo bar cheese"]],
+ ],
+ "cheese bar"
+ ),
+ () => ensure_tag_results([[uri5, ["bar cheese"]]], "cheese bar aaa"),
+ () => ensure_tag_results([[uri6, ["foo bar cheese"]]], "chees bar bbb"),
+ () => ensure_tag_results([[uri6, ["foo bar cheese"]]], "cheese bar foo"),
+ () => ensure_tag_results([], "foo bar cheese aaa"),
+ () => ensure_tag_results([[uri6, ["foo bar cheese"]]], "foo bar cheese bbb"),
+];
+
+/**
+ * Properly tags a uri adding it to bookmarks.
+ *
+ * @param {string} url
+ * The URI to tag.
+ * @param {Array} tags
+ * The tags to add.
+ */
+async function tagURI(url, tags) {
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url,
+ title: "A title",
+ });
+ PlacesUtils.tagging.tagURI(Services.io.newURI(url), tags);
+}
+
+/**
+ * Test history autocomplete
+ */
+add_task(async function test_history_autocomplete_tags() {
+ // always search in history + bookmarks, no matter what the default is
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmarks", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.history");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.bookmarks");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+
+ await tagURI(uri6, ["foo bar cheese"]);
+ await tagURI(uri5, ["bar cheese"]);
+ await tagURI(uri4, ["foo bar"]);
+ await tagURI(uri3, ["cheese"]);
+ await tagURI(uri2, ["bar"]);
+ await tagURI(uri1, ["foo"]);
+
+ for (let tagTest of tests) {
+ await tagTest();
+ }
+});
diff --git a/browser/components/urlbar/tests/unit/test_tags_matchBookmarkTitles.js b/browser/components/urlbar/tests/unit/test_tags_matchBookmarkTitles.js
new file mode 100644
index 0000000000..98d12ebe32
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_tags_matchBookmarkTitles.js
@@ -0,0 +1,42 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Test bug 416211 to make sure results that match the tag show the bookmark
+ * title instead of the page title.
+ */
+
+testEngine_setup();
+
+add_task(async function test_tag_match_has_bookmark_title() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+
+ info("Make sure the tag match gives the bookmark title");
+ let uri = Services.io.newURI("http://theuri/");
+ await PlacesTestUtils.addVisits({ uri, title: "Page title" });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri,
+ title: "Bookmark title",
+ tags: ["superTag"],
+ });
+ let context = createContext("superTag", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri.spec,
+ title: "Bookmark title",
+ tags: ["superTag"],
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_tags_returnedInSearches.js b/browser/components/urlbar/tests/unit/test_tags_returnedInSearches.js
new file mode 100644
index 0000000000..d5f18278fd
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_tags_returnedInSearches.js
@@ -0,0 +1,125 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test bug 418257 by making sure tags are returned with the title as part of
+ * the "comment" if there are tags even if we didn't match in the tags. They
+ * are separated from the title by a endash.
+ */
+
+testEngine_setup();
+
+add_task(async function test() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ Services.prefs.clearUserPref("browser.urlbar.autoFill");
+ });
+
+ let uri1 = Services.io.newURI("http://page1");
+ let uri2 = Services.io.newURI("http://page2");
+ let uri3 = Services.io.newURI("http://page3");
+ let uri4 = Services.io.newURI("http://page4");
+ await PlacesTestUtils.addVisits([
+ { uri: uri1, title: "tagged" },
+ { uri: uri2, title: "tagged" },
+ { uri: uri3, title: "tagged" },
+ { uri: uri4, title: "tagged" },
+ ]);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri1,
+ title: "tagged",
+ tags: ["tag1"],
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri2,
+ title: "tagged",
+ tags: ["tag1", "tag2"],
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri3,
+ title: "tagged",
+ tags: ["tag1", "tag3"],
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: uri4,
+ title: "tagged",
+ tags: ["tag1", "tag2", "tag3"],
+ });
+ info("Make sure tags come back in the title when matching tags");
+ let context = createContext("page1 tag", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri1.spec,
+ title: "tagged",
+ tags: ["tag1"],
+ }),
+ ],
+ });
+
+ info("Check tags in title for page2");
+ context = createContext("page2 tag", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri2.spec,
+ title: "tagged",
+ tags: ["tag1", "tag2"],
+ }),
+ ],
+ });
+
+ info("Tags do not appear when not matching the tag");
+ context = createContext("page3", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri3.spec,
+ title: "tagged",
+ tags: [],
+ }),
+ ],
+ });
+
+ info("Extra test just to make sure we match the title");
+ context = createContext("tag2", { isPrivate: true });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: uri4.spec,
+ title: "tagged",
+ tags: ["tag2"],
+ }),
+ makeBookmarkResult(context, {
+ uri: uri2.spec,
+ title: "tagged",
+ tags: ["tag2"],
+ }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_tokenizer.js b/browser/components/urlbar/tests/unit/test_tokenizer.js
new file mode 100644
index 0000000000..835d1a5909
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_tokenizer.js
@@ -0,0 +1,449 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function test_tokenizer() {
+ let testContexts = [
+ { desc: "Empty string", searchString: "", expectedTokens: [] },
+ { desc: "Spaces string", searchString: " ", expectedTokens: [] },
+ {
+ desc: "Single word string",
+ searchString: "test",
+ expectedTokens: [{ value: "test", type: UrlbarTokenizer.TYPE.TEXT }],
+ },
+ {
+ desc: "Multi word string with mixed whitespace types",
+ searchString: " test1 test2\u1680test3\u2004test4\u1680",
+ expectedTokens: [
+ { value: "test1", type: UrlbarTokenizer.TYPE.TEXT },
+ { value: "test2", type: UrlbarTokenizer.TYPE.TEXT },
+ { value: "test3", type: UrlbarTokenizer.TYPE.TEXT },
+ { value: "test4", type: UrlbarTokenizer.TYPE.TEXT },
+ ],
+ },
+ {
+ desc: "separate restriction char at beginning",
+ searchString: `${UrlbarTokenizer.RESTRICT.BOOKMARK} test`,
+ expectedTokens: [
+ {
+ value: UrlbarTokenizer.RESTRICT.BOOKMARK,
+ type: UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK,
+ },
+ { value: "test", type: UrlbarTokenizer.TYPE.TEXT },
+ ],
+ },
+ {
+ desc: "separate restriction char at end",
+ searchString: `test ${UrlbarTokenizer.RESTRICT.BOOKMARK}`,
+ expectedTokens: [
+ { value: "test", type: UrlbarTokenizer.TYPE.TEXT },
+ {
+ value: UrlbarTokenizer.RESTRICT.BOOKMARK,
+ type: UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK,
+ },
+ ],
+ },
+ {
+ desc: "boundary restriction char at end",
+ searchString: `test${UrlbarTokenizer.RESTRICT.BOOKMARK}`,
+ expectedTokens: [
+ {
+ value: `test${UrlbarTokenizer.RESTRICT.BOOKMARK}`,
+ type: UrlbarTokenizer.TYPE.TEXT,
+ },
+ ],
+ },
+ {
+ desc: "boundary search restriction char at end",
+ searchString: `test${UrlbarTokenizer.RESTRICT.SEARCH}`,
+ expectedTokens: [
+ { value: "test", type: UrlbarTokenizer.TYPE.TEXT },
+ {
+ value: UrlbarTokenizer.RESTRICT.SEARCH,
+ type: UrlbarTokenizer.TYPE.RESTRICT_SEARCH,
+ },
+ ],
+ },
+ {
+ desc: "separate restriction char in the middle",
+ searchString: `test ${UrlbarTokenizer.RESTRICT.BOOKMARK} test`,
+ expectedTokens: [
+ { value: "test", type: UrlbarTokenizer.TYPE.TEXT },
+ {
+ value: UrlbarTokenizer.RESTRICT.BOOKMARK,
+ type: UrlbarTokenizer.TYPE.TEXT,
+ },
+ { value: "test", type: UrlbarTokenizer.TYPE.TEXT },
+ ],
+ },
+ {
+ desc: "restriction char in the middle",
+ searchString: `test${UrlbarTokenizer.RESTRICT.BOOKMARK}test`,
+ expectedTokens: [
+ {
+ value: `test${UrlbarTokenizer.RESTRICT.BOOKMARK}test`,
+ type: UrlbarTokenizer.TYPE.TEXT,
+ },
+ ],
+ },
+ {
+ desc: "restriction char in the middle 2",
+ searchString: `test${UrlbarTokenizer.RESTRICT.BOOKMARK} test`,
+ expectedTokens: [
+ {
+ value: `test${UrlbarTokenizer.RESTRICT.BOOKMARK}`,
+ type: UrlbarTokenizer.TYPE.TEXT,
+ },
+ { value: `test`, type: UrlbarTokenizer.TYPE.TEXT },
+ ],
+ },
+ {
+ desc: "double boundary restriction char",
+ searchString: `${UrlbarTokenizer.RESTRICT.BOOKMARK}test${UrlbarTokenizer.RESTRICT.TITLE}`,
+ expectedTokens: [
+ {
+ value: UrlbarTokenizer.RESTRICT.BOOKMARK,
+ type: UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK,
+ },
+ {
+ value: `test${UrlbarTokenizer.RESTRICT.TITLE}`,
+ type: UrlbarTokenizer.TYPE.TEXT,
+ },
+ ],
+ },
+ {
+ desc: "double non-combinable restriction char, single char string",
+ searchString: `t${UrlbarTokenizer.RESTRICT.BOOKMARK}${UrlbarTokenizer.RESTRICT.SEARCH}`,
+ expectedTokens: [
+ {
+ value: `t${UrlbarTokenizer.RESTRICT.BOOKMARK}`,
+ type: UrlbarTokenizer.TYPE.TEXT,
+ },
+ {
+ value: UrlbarTokenizer.RESTRICT.SEARCH,
+ type: UrlbarTokenizer.TYPE.RESTRICT_SEARCH,
+ },
+ ],
+ },
+ {
+ desc: "only boundary restriction chars",
+ searchString: `${UrlbarTokenizer.RESTRICT.BOOKMARK}${UrlbarTokenizer.RESTRICT.TITLE}`,
+ expectedTokens: [
+ {
+ value: UrlbarTokenizer.RESTRICT.BOOKMARK,
+ type: UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK,
+ },
+ {
+ value: UrlbarTokenizer.RESTRICT.TITLE,
+ type: UrlbarTokenizer.TYPE.RESTRICT_TITLE,
+ },
+ ],
+ },
+ {
+ desc: "only the boundary restriction char",
+ searchString: UrlbarTokenizer.RESTRICT.BOOKMARK,
+ expectedTokens: [
+ {
+ value: UrlbarTokenizer.RESTRICT.BOOKMARK,
+ type: UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK,
+ },
+ ],
+ },
+ // Some restriction chars may be # or ?, that are also valid path parts.
+ // The next 2 tests will check we consider those as part of url paths.
+ {
+ desc: "boundary # char on path",
+ searchString: "test/#",
+ expectedTokens: [
+ { value: "test/#", type: UrlbarTokenizer.TYPE.POSSIBLE_URL },
+ ],
+ },
+ {
+ desc: "boundary ? char on path",
+ searchString: "test/?",
+ expectedTokens: [
+ { value: "test/?", type: UrlbarTokenizer.TYPE.POSSIBLE_URL },
+ ],
+ },
+ {
+ desc: "multiple boundary restriction chars suffix",
+ searchString: `test ${UrlbarTokenizer.RESTRICT.HISTORY} ${UrlbarTokenizer.RESTRICT.TAG}`,
+ expectedTokens: [
+ { value: "test", type: UrlbarTokenizer.TYPE.TEXT },
+ {
+ value: UrlbarTokenizer.RESTRICT.HISTORY,
+ type: UrlbarTokenizer.TYPE.TEXT,
+ },
+ {
+ value: UrlbarTokenizer.RESTRICT.TAG,
+ type: UrlbarTokenizer.TYPE.RESTRICT_TAG,
+ },
+ ],
+ },
+ {
+ desc: "multiple boundary restriction chars prefix",
+ searchString: `${UrlbarTokenizer.RESTRICT.HISTORY} ${UrlbarTokenizer.RESTRICT.TAG} test`,
+ expectedTokens: [
+ {
+ value: UrlbarTokenizer.RESTRICT.HISTORY,
+ type: UrlbarTokenizer.TYPE.RESTRICT_HISTORY,
+ },
+ {
+ value: UrlbarTokenizer.RESTRICT.TAG,
+ type: UrlbarTokenizer.TYPE.TEXT,
+ },
+ { value: "test", type: UrlbarTokenizer.TYPE.TEXT },
+ ],
+ },
+ {
+ desc: "Math with division",
+ searchString: "3.6/1.2",
+ expectedTokens: [{ value: "3.6/1.2", type: UrlbarTokenizer.TYPE.TEXT }],
+ },
+ {
+ desc: "ipv4 in bookmarks",
+ searchString: `${UrlbarTokenizer.RESTRICT.BOOKMARK} 192.168.1.1:8`,
+ expectedTokens: [
+ {
+ value: UrlbarTokenizer.RESTRICT.BOOKMARK,
+ type: UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK,
+ },
+ { value: "192.168.1.1:8", type: UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN },
+ ],
+ },
+ {
+ desc: "email",
+ searchString: "test@mozilla.com",
+ expectedTokens: [
+ { value: "test@mozilla.com", type: UrlbarTokenizer.TYPE.TEXT },
+ ],
+ },
+ {
+ desc: "email2",
+ searchString: "test.test@mozilla.co.uk",
+ expectedTokens: [
+ { value: "test.test@mozilla.co.uk", type: UrlbarTokenizer.TYPE.TEXT },
+ ],
+ },
+ {
+ desc: "protocol",
+ searchString: "http://test",
+ expectedTokens: [
+ { value: "http://test", type: UrlbarTokenizer.TYPE.POSSIBLE_URL },
+ ],
+ },
+ {
+ desc: "bogus protocol with host (we allow visits to http://///example.com)",
+ searchString: "http:///test",
+ expectedTokens: [
+ { value: "http:///test", type: UrlbarTokenizer.TYPE.POSSIBLE_URL },
+ ],
+ },
+ {
+ desc: "file protocol with path",
+ searchString: "file:///home",
+ expectedTokens: [
+ { value: "file:///home", type: UrlbarTokenizer.TYPE.POSSIBLE_URL },
+ ],
+ },
+ {
+ desc: "almost a protocol",
+ searchString: "http:",
+ expectedTokens: [
+ { value: "http:", type: UrlbarTokenizer.TYPE.POSSIBLE_URL },
+ ],
+ },
+ {
+ desc: "almost a protocol 2",
+ searchString: "http:/",
+ expectedTokens: [
+ { value: "http:/", type: UrlbarTokenizer.TYPE.POSSIBLE_URL },
+ ],
+ },
+ {
+ desc: "bogus protocol (we allow visits to http://///example.com)",
+ searchString: "http:///",
+ expectedTokens: [
+ { value: "http:///", type: UrlbarTokenizer.TYPE.POSSIBLE_URL },
+ ],
+ },
+ {
+ desc: "file protocol",
+ searchString: "file:///",
+ expectedTokens: [
+ { value: "file:///", type: UrlbarTokenizer.TYPE.POSSIBLE_URL },
+ ],
+ },
+ {
+ desc: "userinfo",
+ searchString: "user:pass@test",
+ expectedTokens: [
+ { value: "user:pass@test", type: UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN },
+ ],
+ },
+ {
+ desc: "domain",
+ searchString: "www.mozilla.org",
+ expectedTokens: [
+ {
+ value: "www.mozilla.org",
+ type: UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN,
+ },
+ ],
+ },
+ {
+ desc: "data uri",
+ searchString: "data:text/plain,Content",
+ expectedTokens: [
+ {
+ value: "data:text/plain,Content",
+ type: UrlbarTokenizer.TYPE.POSSIBLE_URL,
+ },
+ ],
+ },
+ {
+ desc: "ipv6",
+ searchString: "[2001:db8::1]",
+ expectedTokens: [
+ { value: "[2001:db8::1]", type: UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN },
+ ],
+ },
+ {
+ desc: "numeric domain",
+ searchString: "test1001.com",
+ expectedTokens: [
+ { value: "test1001.com", type: UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN },
+ ],
+ },
+ {
+ desc: "invalid ip",
+ searchString: "192.2134.1.2",
+ expectedTokens: [
+ { value: "192.2134.1.2", type: UrlbarTokenizer.TYPE.TEXT },
+ ],
+ },
+ {
+ desc: "ipv4",
+ searchString: "1.2.3.4",
+ expectedTokens: [
+ { value: "1.2.3.4", type: UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN },
+ ],
+ },
+ {
+ desc: "host/path",
+ searchString: "test/test",
+ expectedTokens: [
+ { value: "test/test", type: UrlbarTokenizer.TYPE.POSSIBLE_URL },
+ ],
+ },
+ {
+ desc: "percent encoded string",
+ searchString: "%E6%97%A5%E6%9C%AC",
+ expectedTokens: [
+ { value: "%E6%97%A5%E6%9C%AC", type: UrlbarTokenizer.TYPE.TEXT },
+ ],
+ },
+ {
+ desc: "Uppercase",
+ searchString: "TEST",
+ expectedTokens: [{ value: "TEST", type: UrlbarTokenizer.TYPE.TEXT }],
+ },
+ {
+ desc: "Mixed case 1",
+ searchString: "TeSt",
+ expectedTokens: [{ value: "TeSt", type: UrlbarTokenizer.TYPE.TEXT }],
+ },
+ {
+ desc: "Mixed case 2",
+ searchString: "tEsT",
+ expectedTokens: [{ value: "tEsT", type: UrlbarTokenizer.TYPE.TEXT }],
+ },
+ {
+ desc: "Uppercase with spaces",
+ searchString: "TEST EXAMPLE",
+ expectedTokens: [
+ { value: "TEST", type: UrlbarTokenizer.TYPE.TEXT },
+ { value: "EXAMPLE", type: UrlbarTokenizer.TYPE.TEXT },
+ ],
+ },
+ {
+ desc: "Mixed case with spaces",
+ searchString: "TeSt eXaMpLe",
+ expectedTokens: [
+ { value: "TeSt", type: UrlbarTokenizer.TYPE.TEXT },
+ { value: "eXaMpLe", type: UrlbarTokenizer.TYPE.TEXT },
+ ],
+ },
+ {
+ desc: "plain number",
+ searchString: "1001",
+ expectedTokens: [{ value: "1001", type: UrlbarTokenizer.TYPE.TEXT }],
+ },
+ {
+ desc: "data uri with spaces",
+ searchString: "data:text/html,oh hi?",
+ expectedTokens: [
+ {
+ value: "data:text/html,oh hi?",
+ type: UrlbarTokenizer.TYPE.POSSIBLE_URL,
+ },
+ ],
+ },
+ {
+ desc: "data uri with spaces ignored with other tokens",
+ searchString: "hi data:text/html,oh hi?",
+ expectedTokens: [
+ {
+ value: "hi",
+ type: UrlbarTokenizer.TYPE.TEXT,
+ },
+ {
+ value: "data:text/html,oh",
+ type: UrlbarTokenizer.TYPE.POSSIBLE_URL,
+ },
+ {
+ value: "hi",
+ type: UrlbarTokenizer.TYPE.TEXT,
+ },
+ {
+ value: UrlbarTokenizer.RESTRICT.SEARCH,
+ type: UrlbarTokenizer.TYPE.RESTRICT_SEARCH,
+ },
+ ],
+ },
+ {
+ desc: "whitelisted host",
+ searchString: "test whitelisted",
+ expectedTokens: [
+ {
+ value: "test",
+ type: UrlbarTokenizer.TYPE.TEXT,
+ },
+ {
+ value: "whitelisted",
+ type: UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN,
+ },
+ ],
+ },
+ ];
+
+ Services.prefs.setBoolPref("browser.fixup.domainwhitelist.whitelisted", true);
+
+ for (let queryContext of testContexts) {
+ info(queryContext.desc);
+ queryContext.trimmedSearchString = queryContext.searchString.trim();
+ for (let token of queryContext.expectedTokens) {
+ token.lowerCaseValue = token.value.toLocaleLowerCase();
+ }
+ let newQueryContext = UrlbarTokenizer.tokenize(queryContext);
+ Assert.equal(
+ queryContext,
+ newQueryContext,
+ "The queryContext object is the same"
+ );
+ Assert.deepEqual(
+ queryContext.tokens,
+ queryContext.expectedTokens,
+ "Check the expected tokens"
+ );
+ }
+});
diff --git a/browser/components/urlbar/tests/unit/test_trimming.js b/browser/components/urlbar/tests/unit/test_trimming.js
new file mode 100644
index 0000000000..5f0b340b53
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_trimming.js
@@ -0,0 +1,171 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+add_task(async function setup() {
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ });
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+});
+
+add_task(async function test_untrimmed_secure_www() {
+ info("Searching for untrimmed https://www entry");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("https://www.mozilla.org/test/"),
+ });
+ let context = createContext("mo", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.org/",
+ completed: "https://www.mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www.mozilla.org/",
+ fallbackTitle: "https://www.mozilla.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://www.mozilla.org/test/",
+ title: "test visit for https://www.mozilla.org/test/",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_untrimmed_secure_www_path() {
+ info("Searching for untrimmed https://www entry with path");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("https://www.mozilla.org/test/"),
+ });
+ let context = createContext("mozilla.org/t", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.org/test/",
+ completed: "https://www.mozilla.org/test/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://www.mozilla.org/test/",
+ title: "test visit for https://www.mozilla.org/test/",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_untrimmed_secure() {
+ info("Searching for untrimmed https:// entry");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("https://mozilla.org/test/"),
+ });
+ let context = createContext("mo", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.org/",
+ completed: "https://mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://mozilla.org/",
+ fallbackTitle: "https://mozilla.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "https://mozilla.org/test/",
+ title: "test visit for https://mozilla.org/test/",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_untrimmed_secure_path() {
+ info("Searching for untrimmed https:// entry with path");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("https://mozilla.org/test/"),
+ });
+ let context = createContext("mozilla.org/t", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.org/test/",
+ completed: "https://mozilla.org/test/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "https://mozilla.org/test/",
+ title: "test visit for https://mozilla.org/test/",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_untrimmed_www() {
+ info("Searching for untrimmed http://www entry");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://www.mozilla.org/test/"),
+ });
+ let context = createContext("mo", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.org/",
+ completed: "http://www.mozilla.org/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://www.mozilla.org/",
+ fallbackTitle: "www.mozilla.org",
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://www.mozilla.org/test/",
+ title: "test visit for http://www.mozilla.org/test/",
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_untrimmed_www_path() {
+ info("Searching for untrimmed http://www entry with path");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://www.mozilla.org/test/"),
+ });
+ let context = createContext("mozilla.org/t", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "mozilla.org/test/",
+ completed: "http://www.mozilla.org/test/",
+ matches: [
+ makeVisitResult(context, {
+ uri: "http://www.mozilla.org/test/",
+ title: "test visit for http://www.mozilla.org/test/",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
+
+add_task(async function test_escaped_chars() {
+ info("Searching for URL with characters that are normally escaped");
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("https://www.mozilla.org/啊-test"),
+ });
+ let context = createContext("https://www.mozilla.org/啊-test", {
+ isPrivate: false,
+ });
+ await check_results({
+ context,
+ matches: [
+ makeVisitResult(context, {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ uri: "https://www.mozilla.org/%E5%95%8A-test",
+ title: "test visit for https://www.mozilla.org/%E5%95%8A-test",
+ iconUri: "page-icon:https://www.mozilla.org/%E5%95%8A-test",
+ heuristic: true,
+ }),
+ ],
+ });
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/test_unitConversion.js b/browser/components/urlbar/tests/unit/test_unitConversion.js
new file mode 100644
index 0000000000..fc0620e7aa
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_unitConversion.js
@@ -0,0 +1,504 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Unit test for unit conversion module.
+ */
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarProviderUnitConversion:
+ "resource:///modules/UrlbarProviderUnitConversion.sys.mjs",
+});
+
+const TEST_DATA = [
+ {
+ category: "angle",
+ cases: [
+ { queryString: "1 d to d", expected: "1 deg" },
+ { queryString: "-1 d to d", expected: "-1 deg" },
+ { queryString: "1 d in d", expected: "1 deg" },
+ { queryString: "1 d = d", expected: "1 deg" },
+ { queryString: "1 D=D", expected: "1 deg" },
+ { queryString: "1 d to degree", expected: "1 deg" },
+ { queryString: "2 d to degree", expected: "2 deg" },
+ {
+ queryString: "1 d to radian",
+ expected: `${round(Math.PI / 180)} radian`,
+ },
+ {
+ queryString: "2 d to radian",
+ expected: `${round((Math.PI / 180) * 2)} radian`,
+ },
+ { queryString: "1 d to rad", expected: `${round(Math.PI / 180)} radian` },
+ { queryString: "1 d to r", expected: `${round(Math.PI / 180)} radian` },
+ { queryString: "1 d to gradian", expected: `${round(1 / 0.9)} gradian` },
+ { queryString: "1 d to g", expected: `${round(1 / 0.9)} gradian` },
+ { queryString: "1 d to minute", expected: "60 min" },
+ { queryString: "1 d to min", expected: "60 min" },
+ { queryString: "1 d to m", expected: "60 min" },
+ { queryString: "1 d to second", expected: "3,600 sec" },
+ { queryString: "1 d to sec", expected: "3,600 sec" },
+ { queryString: "1 d to s", expected: "3,600 sec" },
+ { queryString: "1 d to sign", expected: `${round(1 / 30)} sign` },
+ { queryString: "1 d to mil", expected: `${round(1 / 0.05625)} mil` },
+ {
+ queryString: "1 d to revolution",
+ expected: `${round(1 / 360)} revolution`,
+ },
+ { queryString: "1 d to circle", expected: `${round(1 / 360)} circle` },
+ { queryString: "1 d to turn", expected: `${round(1 / 360)} turn` },
+ { queryString: "1 d to quadrant", expected: `${round(1 / 90)} quadrant` },
+ {
+ queryString: "1 d to rightangle",
+ expected: `${round(1 / 90)} rightangle`,
+ },
+ { queryString: "1 d to sextant", expected: `${round(1 / 60)} sextant` },
+ { queryString: "1 degree to d", expected: "1 deg" },
+ { queryString: "1 radian to d", expected: `${round(180 / Math.PI)} deg` },
+ {
+ queryString: "1 r to g",
+ expected: `${round(180 / Math.PI / 0.9)} gradian`,
+ },
+ ],
+ },
+ {
+ category: "force",
+ cases: [
+ { queryString: "1 n to n", expected: "1 newton" },
+ { queryString: "-1 n to n", expected: "-1 newton" },
+ { queryString: "1 n in n", expected: "1 newton" },
+ { queryString: "1 n = n", expected: "1 newton" },
+ { queryString: "1 N=N", expected: "1 newton" },
+ { queryString: "1 n to newton", expected: "1 newton" },
+ { queryString: "1 n to kilonewton", expected: "0.001 kilonewton" },
+ { queryString: "1 n to kn", expected: "0.001 kilonewton" },
+ {
+ queryString: "1 n to gram-force",
+ expected: `${round(101.9716213)} gram-force`,
+ },
+ {
+ queryString: "1 n to gf",
+ expected: `${round(101.9716213)} gram-force`,
+ },
+ {
+ queryString: "1 n to kilogram-force",
+ expected: `${round(0.1019716213)} kilogram-force`,
+ },
+ {
+ queryString: "1 n to kgf",
+ expected: `${round(0.1019716213)} kilogram-force`,
+ },
+ {
+ queryString: "1 n to ton-force",
+ expected: `${round(0.0001019716213)} ton-force`,
+ },
+ {
+ queryString: "1 n to tf",
+ expected: `${round(0.0001019716213)} ton-force`,
+ },
+ {
+ queryString: "1 n to exanewton",
+ expected: `${round(1.0e-18)} exanewton`,
+ },
+ { queryString: "1 n to en", expected: `${round(1.0e-18)} exanewton` },
+ {
+ queryString: "1 n to petanewton",
+ expected: `${round(1.0e-15)} petanewton`,
+ },
+ { queryString: "1 n to PN", expected: `${round(1.0e-15)} petanewton` },
+ { queryString: "1 n to Pn", expected: `${round(1.0e-15)} petanewton` },
+ {
+ queryString: "1 n to teranewton",
+ expected: `${round(1.0e-12)} teranewton`,
+ },
+ { queryString: "1 n to tn", expected: `${round(1.0e-12)} teranewton` },
+ {
+ queryString: "1 n to giganewton",
+ expected: `${round(1.0e-9)} giganewton`,
+ },
+ { queryString: "1 n to gn", expected: `${round(1.0e-9)} giganewton` },
+ { queryString: "1 n to meganewton", expected: "0.000001 meganewton" },
+ { queryString: "1 n to MN", expected: "0.000001 meganewton" },
+ { queryString: "1 n to Mn", expected: "0.000001 meganewton" },
+ { queryString: "1 n to hectonewton", expected: "0.01 hectonewton" },
+ { queryString: "1 n to hn", expected: "0.01 hectonewton" },
+ { queryString: "1 n to dekanewton", expected: "0.1 dekanewton" },
+ { queryString: "1 n to dan", expected: "0.1 dekanewton" },
+ { queryString: "1 n to decinewton", expected: "10 decinewton" },
+ { queryString: "1 n to dn", expected: "10 decinewton" },
+ { queryString: "1 n to centinewton", expected: "100 centinewton" },
+ { queryString: "1 n to cn", expected: "100 centinewton" },
+ { queryString: "1 n to millinewton", expected: "1000 millinewton" },
+ { queryString: "1 n to mn", expected: "1000 millinewton" },
+ { queryString: "1 n to micronewton", expected: "1000000 micronewton" },
+ { queryString: "1 n to µn", expected: "1000000 micronewton" },
+ {
+ queryString: "1 n to nanonewton",
+ expected: "1000000000 nanonewton",
+ },
+ { queryString: "1 n to nn", expected: "1000000000 nanonewton" },
+ {
+ queryString: "1 n to piconewton",
+ expected: "1000000000000 piconewton",
+ },
+ { queryString: "1 n to pn", expected: "1000000000000 piconewton" },
+ {
+ queryString: "1 n to femtonewton",
+ expected: "1000000000000000 femtonewton",
+ },
+ { queryString: "1 n to fn", expected: "1000000000000000 femtonewton" },
+ {
+ queryString: "1 n to attonewton",
+ expected: "1000000000000000000 attonewton",
+ },
+ { queryString: "1 n to an", expected: "1000000000000000000 attonewton" },
+ { queryString: "1 n to dyne", expected: "100000 dyne" },
+ { queryString: "1 n to dyn", expected: "100000 dyne" },
+ { queryString: "1 n to joule/meter", expected: "1 joule/meter" },
+ { queryString: "1 n to j/m", expected: "1 joule/meter" },
+ {
+ queryString: "1 n to joule/centimeter",
+ expected: "100 joule/centimeter",
+ },
+ { queryString: "1 n to j/cm", expected: "100 joule/centimeter" },
+ {
+ queryString: "1 n to ton-force-short",
+ expected: `${round(0.0001124045)} ton-force-short`,
+ },
+ {
+ queryString: "1 n to short",
+ expected: `${round(0.0001124045)} ton-force-short`,
+ },
+ {
+ queryString: "1 n to ton-force-long",
+ expected: `${round(0.0001003611)} ton-force-long`,
+ },
+ {
+ queryString: "1 n to tonf",
+ expected: `${round(0.0001003611)} ton-force-long`,
+ },
+ {
+ queryString: "1 n to kip-force",
+ expected: `${round(0.0002248089)} kip-force`,
+ },
+ {
+ queryString: "1 n to kipf",
+ expected: `${round(0.0002248089)} kip-force`,
+ },
+ {
+ queryString: "1 n to pound-force",
+ expected: `${round(0.2248089431)} pound-force`,
+ },
+ {
+ queryString: "1 n to lbf",
+ expected: `${round(0.2248089431)} pound-force`,
+ },
+ {
+ queryString: "1 n to ounce-force",
+ expected: `${round(3.5969430896)} ounce-force`,
+ },
+ {
+ queryString: "1 n to ozf",
+ expected: `${round(3.5969430896)} ounce-force`,
+ },
+ {
+ queryString: "1 n to poundal",
+ expected: `${round(7.2330138512)} poundal`,
+ },
+ { queryString: "1 n to pdl", expected: `${round(7.2330138512)} poundal` },
+ { queryString: "1 n to pond", expected: `${round(101.9716213)} pond` },
+ { queryString: "1 n to p", expected: `${round(101.9716213)} pond` },
+ {
+ queryString: "1 n to kilopond",
+ expected: `${round(0.1019716213)} kilopond`,
+ },
+ { queryString: "1 n to kp", expected: `${round(0.1019716213)} kilopond` },
+ { queryString: "1 kilonewton to n", expected: "1000 newton" },
+ ],
+ },
+ {
+ category: "length",
+ cases: [
+ { queryString: "1 meter to meter", expected: "1 m" },
+ { queryString: "-1 meter to meter", expected: "-1 m" },
+ { queryString: "1 meter in meter", expected: "1 m" },
+ { queryString: "1 meter = meter", expected: "1 m" },
+ { queryString: "1 METER=METER", expected: "1 m" },
+ { queryString: "1 m to meter", expected: "1 m" },
+ { queryString: "1 m to nanometer", expected: "1000000000 nanometer" },
+ { queryString: "1 m to micrometer", expected: "1000000 micrometer" },
+ { queryString: "1 m to millimeter", expected: "1,000 mm" },
+ { queryString: "1 m to mm", expected: "1,000 mm" },
+ { queryString: "1 m to centimeter", expected: "100 cm" },
+ { queryString: "1 m to cm", expected: "100 cm" },
+ { queryString: "1 m to kilometer", expected: "0.001 km" },
+ { queryString: "1 m to km", expected: "0.001 km" },
+ { queryString: "1 m to mile", expected: `${round(0.0006213689)} mi` },
+ { queryString: "1 m to mi", expected: `${round(0.0006213689)} mi` },
+ { queryString: "1 m to yard", expected: `${round(1.0936132983)} yd` },
+ { queryString: "1 m to yd", expected: `${round(1.0936132983)} yd` },
+ { queryString: "1 m to foot", expected: `${round(3.280839895)} ft` },
+ { queryString: "1 m to ft", expected: `${round(3.280839895)} ft` },
+ { queryString: "1 m to inch", expected: `${round(39.37007874)} in` },
+ { queryString: "1 inch to m", expected: `${round(1 / 39.37007874)} m` },
+ ],
+ },
+ {
+ category: "mass",
+ cases: [
+ { queryString: "1 kg to kg", expected: "1 kg" },
+ { queryString: "-1 kg to kg", expected: "-1 kg" },
+ { queryString: "1 kg in kg", expected: "1 kg" },
+ { queryString: "1 kg = kg", expected: "1 kg" },
+ { queryString: "1 KG=KG", expected: "1 kg" },
+ { queryString: "1 kg to kilogram", expected: "1 kg" },
+ { queryString: "1 kg to gram", expected: "1,000 g" },
+ { queryString: "1 kg to g", expected: "1,000 g" },
+ { queryString: "1 kg to milligram", expected: "1000000 milligram" },
+ { queryString: "1 kg to mg", expected: "1000000 milligram" },
+ { queryString: "1 kg to ton", expected: "0.001 ton" },
+ { queryString: "1 kg to t", expected: "0.001 ton" },
+ {
+ queryString: "1 kg to longton",
+ expected: `${round(0.0009842073)} longton`,
+ },
+ {
+ queryString: "1 kg to l.t.",
+ expected: `${round(0.0009842073)} longton`,
+ },
+ {
+ queryString: "1 kg to l/t",
+ expected: `${round(0.0009842073)} longton`,
+ },
+ {
+ queryString: "1 kg to shortton",
+ expected: `${round(0.0011023122)} shortton`,
+ },
+ {
+ queryString: "1 kg to s.t.",
+ expected: `${round(0.0011023122)} shortton`,
+ },
+ {
+ queryString: "1 kg to s/t",
+ expected: `${round(0.0011023122)} shortton`,
+ },
+ {
+ queryString: "1 kg to pound",
+ expected: `${round(2.2046244202)} lb`,
+ },
+ { queryString: "1 kg to lbs", expected: `${round(2.2046244202)} lb` },
+ {
+ queryString: "1 kg to lb",
+ expected: `${round(2.2046244202)} lb`,
+ },
+ {
+ queryString: "1 kg to ounce",
+ expected: `${round(35.273990723)} oz`,
+ },
+ { queryString: "1 kg to oz", expected: `${round(35.273990723)} oz` },
+ { queryString: "1 kg to carat", expected: "5000 carat" },
+ { queryString: "1 kg to ffd", expected: "5000 ffd" },
+ { queryString: "1 ffd to kg", expected: `${round(1 / 5000)} kg` },
+ ],
+ },
+ {
+ category: "temperature",
+ cases: [
+ { queryString: "0 c to c", expected: "0°C" },
+ { queryString: "0 c in c", expected: "0°C" },
+ { queryString: "0 c = c", expected: "0°C" },
+ { queryString: "0 C=C", expected: "0°C" },
+ { queryString: "0 c to celsius", expected: "0°C" },
+ { queryString: "0 c to kelvin", expected: "273.15 kelvin" },
+ { queryString: "0 c to k", expected: "273.15 kelvin" },
+ { queryString: "10 c to k", expected: "283.15 kelvin" },
+ { queryString: "0 c to fahrenheit", expected: "32°F" },
+ { queryString: "0 c to f", expected: "32°F" },
+ { queryString: "10 c to f", expected: `${round(10 * 1.8 + 32)}°F` },
+ {
+ queryString: "10 f to kelvin",
+ expected: `${round((10 - 32) / 1.8 + 273.15)} kelvin`,
+ },
+ { queryString: "-10 c to f", expected: "14°F" },
+ ],
+ },
+ {
+ category: "timezone",
+ cases: [
+ { queryString: "0 utc to utc", expected: "00:00 UTC" },
+ { queryString: "0 utc in utc", expected: "00:00 UTC" },
+ { queryString: "0 utc = utc", expected: "00:00 UTC" },
+ { queryString: "0 UTC=UTC", expected: "00:00 UTC" },
+ { queryString: "11 pm utc to utc", expected: "11:00 PM UTC" },
+ { queryString: "11 am utc to utc", expected: "11:00 AM UTC" },
+ { queryString: "11:30 utc to utc", expected: "11:30 UTC" },
+ { queryString: "11:30 PM utc to utc", expected: "11:30 PM UTC" },
+ { queryString: "1 utc to idlw", expected: "13:00 IDLW" },
+ { queryString: "1 pm utc to idlw", expected: "1:00 AM IDLW" },
+ { queryString: "1 am utc to idlw", expected: "1:00 PM IDLW" },
+ { queryString: "1 utc to idlw", expected: "13:00 IDLW" },
+ { queryString: "1 PM utc to idlw", expected: "1:00 AM IDLW" },
+ { queryString: "0 utc to nt", expected: "13:00 NT" },
+ { queryString: "0 utc to hst", expected: "14:00 HST" },
+ { queryString: "0 utc to akst", expected: "15:00 AKST" },
+ { queryString: "0 utc to pst", expected: "16:00 PST" },
+ { queryString: "0 utc to akdt", expected: "16:00 AKDT" },
+ { queryString: "0 utc to mst", expected: "17:00 MST" },
+ { queryString: "0 utc to pdt", expected: "17:00 PDT" },
+ { queryString: "0 utc to cst", expected: "18:00 CST" },
+ { queryString: "0 utc to mdt", expected: "18:00 MDT" },
+ { queryString: "0 utc to est", expected: "19:00 EST" },
+ { queryString: "0 utc to cdt", expected: "19:00 CDT" },
+ { queryString: "0 utc to edt", expected: "20:00 EDT" },
+ { queryString: "0 utc to ast", expected: "20:00 AST" },
+ { queryString: "0 utc to guy", expected: "21:00 GUY" },
+ { queryString: "0 utc to adt", expected: "21:00 ADT" },
+ { queryString: "0 utc to at", expected: "22:00 AT" },
+ { queryString: "0 utc to gmt", expected: "00:00 GMT" },
+ { queryString: "0 utc to z", expected: "00:00 Z" },
+ { queryString: "0 utc to wet", expected: "00:00 WET" },
+ { queryString: "0 utc to west", expected: "01:00 WEST" },
+ { queryString: "0 utc to cet", expected: "01:00 CET" },
+ { queryString: "0 utc to bst", expected: "01:00 BST" },
+ { queryString: "0 utc to ist", expected: "01:00 IST" },
+ { queryString: "0 utc to cest", expected: "02:00 CEST" },
+ { queryString: "0 utc to eet", expected: "02:00 EET" },
+ { queryString: "0 utc to eest", expected: "03:00 EEST" },
+ { queryString: "0 utc to msk", expected: "03:00 MSK" },
+ { queryString: "0 utc to msd", expected: "04:00 MSD" },
+ { queryString: "0 utc to zp4", expected: "04:00 ZP4" },
+ { queryString: "0 utc to zp5", expected: "05:00 ZP5" },
+ { queryString: "0 utc to zp6", expected: "06:00 ZP6" },
+ { queryString: "0 utc to wast", expected: "07:00 WAST" },
+ { queryString: "0 utc to awst", expected: "08:00 AWST" },
+ { queryString: "0 utc to wst", expected: "08:00 WST" },
+ { queryString: "0 utc to jst", expected: "09:00 JST" },
+ { queryString: "0 utc to acst", expected: "09:30 ACST" },
+ { queryString: "0 utc to aest", expected: "10:00 AEST" },
+ { queryString: "0 utc to acdt", expected: "10:30 ACDT" },
+ { queryString: "0 utc to aedt", expected: "11:00 AEDT" },
+ { queryString: "0 utc to nzst", expected: "12:00 NZST" },
+ { queryString: "0 utc to idle", expected: "12:00 IDLE" },
+ { queryString: "0 utc to nzd", expected: "13:00 NZD" },
+ { queryString: "9:00 jst to utc", expected: "00:00 UTC" },
+ { queryString: "8:00 jst to utc", expected: "23:00 UTC" },
+ { queryString: "8:00 am jst to utc", expected: "11:00 PM UTC" },
+ { queryString: "9:00 jst to pdt", expected: "17:00 PDT" },
+ { queryString: "12 pm pst to cet", expected: "9:00 PM CET" },
+ { queryString: "12 am pst to cet", expected: "9:00 AM CET" },
+ { queryString: "12:30 pm pst to cet", expected: "9:30 PM CET" },
+ { queryString: "12:30 am pst to cet", expected: "9:30 AM CET" },
+ { queryString: "23 pm pst to cet", expected: "8:00 AM CET" },
+ { queryString: "23:30 pm pst to cet", expected: "8:30 AM CET" },
+ {
+ queryString: "10:00 JST to here",
+ timezone: "UTC",
+ expected: "01:00 UTC-000",
+ },
+ {
+ queryString: "1:00 to JST",
+ timezone: "UTC",
+ expected: "10:00 JST",
+ },
+ {
+ queryString: "1 am to JST",
+ timezone: "UTC",
+ expected: "10:00 AM JST",
+ },
+ {
+ queryString: "now to JST",
+ timezone: "UTC",
+ assertResult: output => {
+ const outputRegexResult = /([0-9]+):([0-9]+)/.exec(output);
+ const outputMinutes =
+ parseInt(outputRegexResult[1]) * 60 +
+ parseInt(outputRegexResult[2]);
+ const nowDate = new Date();
+ // Apply JST time difference.
+ nowDate.setHours(nowDate.getHours() + 9);
+ let nowMinutes = nowDate.getHours() * 60 + nowDate.getMinutes();
+ // When we cross the day between the unit converter calculation and the
+ // assertion here.
+ nowMinutes =
+ outputMinutes > nowMinutes ? nowMinutes + 1440 : nowMinutes;
+ Assert.lessOrEqual(nowMinutes - outputMinutes, 1);
+ },
+ },
+ {
+ queryString: "now to here",
+ timezone: "UTC",
+ assertResult: output => {
+ const outputRegexResult = /([0-9]+):([0-9]+)/.exec(output);
+ const outputMinutes =
+ parseInt(outputRegexResult[1]) * 60 +
+ parseInt(outputRegexResult[2]);
+ const nowDate = new Date();
+ let nowMinutes = nowDate.getHours() * 60 + nowDate.getMinutes();
+ nowMinutes =
+ outputMinutes > nowMinutes ? nowMinutes + 1440 : nowMinutes;
+ Assert.lessOrEqual(nowMinutes - outputMinutes, 1);
+ },
+ },
+ ],
+ },
+ {
+ category: "invalid",
+ cases: [
+ { queryString: "1 to cm" },
+ { queryString: "1cm to newton" },
+ { queryString: "1cm to foo" },
+ { queryString: "0:00:00 utc to jst" },
+ ],
+ },
+];
+
+add_task(function () {
+ // Enable unit conversion.
+ Services.prefs.setBoolPref("browser.urlbar.unitConversion.enabled", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.unitConversion.enabled");
+ });
+
+ for (const { category, cases } of TEST_DATA) {
+ for (const { queryString, timezone, expected, assertResult } of cases) {
+ info(`Test "${queryString}" in ${category}`);
+
+ let originalTimezone;
+ if (timezone) {
+ originalTimezone = Cu.getJSTestingFunctions().getTimeZone();
+ info(`Set timezone ${timezone}`);
+ Cu.getJSTestingFunctions().setTimeZone(timezone);
+ }
+
+ const context = createContext(queryString);
+ const isActive = UrlbarProviderUnitConversion.isActive(context);
+ Assert.equal(isActive, !!expected || !!assertResult);
+
+ if (isActive) {
+ UrlbarProviderUnitConversion.startQuery(context, (module, result) => {
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.DYNAMIC);
+ Assert.equal(result.source, UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL);
+ Assert.equal(result.suggestedIndex, 1);
+ Assert.equal(result.payload.input, queryString);
+
+ if (expected) {
+ Assert.equal(result.payload.output, expected);
+ } else {
+ assertResult(result.payload.output);
+ }
+ });
+ }
+
+ if (originalTimezone) {
+ Cu.getJSTestingFunctions().setTimeZone(originalTimezone);
+ }
+ }
+ }
+});
+
+function round(number) {
+ return parseFloat(number.toPrecision(10));
+}
diff --git a/browser/components/urlbar/tests/unit/test_word_boundary_search.js b/browser/components/urlbar/tests/unit/test_word_boundary_search.js
new file mode 100644
index 0000000000..f015a29fb5
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_word_boundary_search.js
@@ -0,0 +1,403 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test to make sure matches against the url, title, tags are first made on word
+ * boundaries, instead of in the middle of words, and later are extended to the
+ * whole words. For this test it is critical to check sorting of the matches.
+ *
+ * Make sure we don't try matching one after a CamelCase because the upper-case
+ * isn't really a word boundary. (bug 429498)
+ */
+
+testEngine_setup();
+
+var katakana = ["\u30a8", "\u30c9"]; // E, Do
+var ideograph = ["\u4efb", "\u5929", "\u5802"]; // Nin Ten Do
+
+add_task(async function test_escape() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", false);
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions");
+ Services.prefs.clearUserPref("browser.urlbar.autoFill");
+ Services.prefs.clearUserPref("browser.urlbar.autoFill.searchEngines");
+ });
+
+ await PlacesTestUtils.addVisits([
+ { uri: "http://matchme/", title: "title1" },
+ { uri: "http://dontmatchme/", title: "title1" },
+ { uri: "http://title/1", title: "matchme2" },
+ { uri: "http://title/2", title: "dontmatchme3" },
+ { uri: "http://tag/1", title: "title1" },
+ { uri: "http://tag/2", title: "title1" },
+ { uri: "http://crazytitle/", title: "!@#$%^&*()_+{}|:<>?word" },
+ { uri: "http://katakana/", title: katakana.join("") },
+ { uri: "http://ideograph/", title: ideograph.join("") },
+ { uri: "http://camel/pleaseMatchMe/", title: "title1" },
+ ]);
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://tag/1",
+ title: "title1",
+ tags: ["matchme2"],
+ });
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: "http://tag/2",
+ title: "title1",
+ tags: ["dontmatchme3"],
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ info("Match 'match' at the beginning or after / or on a CamelCase");
+ let context = createContext("match", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://tag/1",
+ title: "title1",
+ tags: ["matchme2"],
+ }),
+ makeVisitResult(context, {
+ uri: "http://camel/pleaseMatchMe/",
+ title: "title1",
+ }),
+ makeVisitResult(context, { uri: "http://title/1", title: "matchme2" }),
+ makeVisitResult(context, { uri: "http://matchme/", title: "title1" }),
+ makeBookmarkResult(context, {
+ uri: "http://tag/2",
+ title: "title1",
+ tags: ["dontmatchme3"],
+ }),
+ makeVisitResult(context, {
+ uri: "http://title/2",
+ title: "dontmatchme3",
+ }),
+ makeVisitResult(context, { uri: "http://dontmatchme/", title: "title1" }),
+ ],
+ });
+
+ info("Match 'dont' at the beginning or after /");
+ context = createContext("dont", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://tag/2",
+ title: "title1",
+ tags: ["dontmatchme3"],
+ }),
+ makeVisitResult(context, {
+ uri: "http://title/2",
+ title: "dontmatchme3",
+ }),
+ makeVisitResult(context, { uri: "http://dontmatchme/", title: "title1" }),
+ ],
+ });
+
+ info("Match 'match' at the beginning or after / or on a CamelCase");
+ context = createContext("2", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://tag/2",
+ title: "title1",
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://tag/1",
+ title: "title1",
+ tags: ["matchme2"],
+ }),
+ makeVisitResult(context, {
+ uri: "http://title/2",
+ title: "dontmatchme3",
+ }),
+ makeVisitResult(context, { uri: "http://title/1", title: "matchme2" }),
+ ],
+ });
+
+ info("Match 't' at the beginning or after /");
+ context = createContext("t", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://tag/2",
+ title: "title1",
+ tags: ["dontmatchme3"],
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://tag/1",
+ title: "title1",
+ tags: ["matchme2"],
+ }),
+ makeVisitResult(context, {
+ uri: "http://camel/pleaseMatchMe/",
+ title: "title1",
+ }),
+ makeVisitResult(context, {
+ uri: "http://title/2",
+ title: "dontmatchme3",
+ }),
+ makeVisitResult(context, { uri: "http://title/1", title: "matchme2" }),
+ makeVisitResult(context, { uri: "http://dontmatchme/", title: "title1" }),
+ makeVisitResult(context, { uri: "http://matchme/", title: "title1" }),
+ makeVisitResult(context, {
+ uri: "http://katakana/",
+ title: katakana.join(""),
+ }),
+ makeVisitResult(context, {
+ uri: "http://crazytitle/",
+ title: "!@#$%^&*()_+{}|:<>?word",
+ }),
+ ],
+ });
+
+ info("Match 'word' after many consecutive word boundaries");
+ context = createContext("word", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://crazytitle/",
+ title: "!@#$%^&*()_+{}|:<>?word",
+ }),
+ ],
+ });
+
+ info("Match a word boundary '/' for everything");
+ context = createContext("/", { isPrivate: false });
+ // UNIX platforms can search for a file:// URL by typing a forward slash.
+ let heuristicSlashResult =
+ AppConstants.platform == "win"
+ ? makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ })
+ : makeVisitResult(context, {
+ uri: "file:///",
+ fallbackTitle: "file:///",
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ heuristic: true,
+ });
+ await check_results({
+ context,
+ matches: [
+ heuristicSlashResult,
+ makeBookmarkResult(context, {
+ uri: "http://tag/2",
+ title: "title1",
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://tag/1",
+ title: "title1",
+ }),
+ makeVisitResult(context, {
+ uri: "http://camel/pleaseMatchMe/",
+ title: "title1",
+ }),
+ makeVisitResult(context, {
+ uri: "http://ideograph/",
+ title: ideograph.join(""),
+ }),
+ makeVisitResult(context, {
+ uri: "http://katakana/",
+ title: katakana.join(""),
+ }),
+ makeVisitResult(context, {
+ uri: "http://crazytitle/",
+ title: "!@#$%^&*()_+{}|:<>?word",
+ }),
+ makeVisitResult(context, {
+ uri: "http://title/2",
+ title: "dontmatchme3",
+ }),
+ makeVisitResult(context, { uri: "http://title/1", title: "matchme2" }),
+ makeVisitResult(context, { uri: "http://dontmatchme/", title: "title1" }),
+ ],
+ });
+
+ info("Match word boundaries '()_' that are among word boundaries");
+ context = createContext("()_", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://crazytitle/",
+ title: "!@#$%^&*()_+{}|:<>?word",
+ }),
+ ],
+ });
+
+ info("Katakana characters form a string, so match the beginning");
+ context = createContext(katakana[0], { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://katakana/",
+ title: katakana.join(""),
+ }),
+ ],
+ });
+
+ /*
+ info("Middle of a katakana word shouldn't be matched");
+ await check_autocomplete({
+ search: katakana[1],
+ matches: [ ],
+ });
+*/
+
+ info("Ideographs are treated as words so 'nin' is one word");
+ context = createContext(ideograph[0], { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://ideograph/",
+ title: ideograph.join(""),
+ }),
+ ],
+ });
+
+ info("Ideographs are treated as words so 'ten' is another word");
+ context = createContext(ideograph[1], { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://ideograph/",
+ title: ideograph.join(""),
+ }),
+ ],
+ });
+
+ info("Ideographs are treated as words so 'do' is yet another word");
+ context = createContext(ideograph[2], { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeVisitResult(context, {
+ uri: "http://ideograph/",
+ title: ideograph.join(""),
+ }),
+ ],
+ });
+
+ info("Match in the middle. Should just be sorted by frecency.");
+ context = createContext("ch", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://tag/2",
+ title: "title1",
+ tags: ["dontmatchme3"],
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://tag/1",
+ title: "title1",
+ tags: ["matchme2"],
+ }),
+ makeVisitResult(context, {
+ uri: "http://camel/pleaseMatchMe/",
+ title: "title1",
+ }),
+ makeVisitResult(context, {
+ uri: "http://title/2",
+ title: "dontmatchme3",
+ }),
+ makeVisitResult(context, { uri: "http://title/1", title: "matchme2" }),
+ makeVisitResult(context, { uri: "http://dontmatchme/", title: "title1" }),
+ makeVisitResult(context, { uri: "http://matchme/", title: "title1" }),
+ ],
+ });
+
+ // Also this test should just be sorted by frecency.
+ info(
+ "Don't match one character after a camel-case word boundary (bug 429498). Should just be sorted by frecency."
+ );
+ context = createContext("atch", { isPrivate: false });
+ await check_results({
+ context,
+ matches: [
+ makeSearchResult(context, {
+ engineName: SUGGESTIONS_ENGINE_NAME,
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://tag/2",
+ title: "title1",
+ tags: ["dontmatchme3"],
+ }),
+ makeBookmarkResult(context, {
+ uri: "http://tag/1",
+ title: "title1",
+ tags: ["matchme2"],
+ }),
+ makeVisitResult(context, {
+ uri: "http://camel/pleaseMatchMe/",
+ title: "title1",
+ }),
+ makeVisitResult(context, {
+ uri: "http://title/2",
+ title: "dontmatchme3",
+ }),
+ makeVisitResult(context, { uri: "http://title/1", title: "matchme2" }),
+ makeVisitResult(context, { uri: "http://dontmatchme/", title: "title1" }),
+ makeVisitResult(context, { uri: "http://matchme/", title: "title1" }),
+ ],
+ });
+
+ await cleanupPlaces();
+});
diff --git a/browser/components/urlbar/tests/unit/xpcshell.ini b/browser/components/urlbar/tests/unit/xpcshell.ini
new file mode 100644
index 0000000000..18a608e5f3
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/xpcshell.ini
@@ -0,0 +1,99 @@
+[DEFAULT]
+skip-if = toolkit == 'android' # bug 1730213
+head = head.js
+firefox-appdir = browser
+support-files =
+ data/engine.xml
+
+[test_000_frecency.js]
+[test_about_urls.js]
+[test_autofill_adaptiveHistory.js]
+[test_autofill_bookmarked.js]
+[test_autofill_do_not_trim.js]
+[test_autofill_functional.js]
+[test_autofill_origins.js]
+[test_autofill_origins_alt_frecency.js]
+prefs = places.frecency.origins.alternative.featureGate=true
+[test_autofill_originsAndQueries.js]
+[test_autofill_prefix_fallback.js]
+[test_autofill_search_engines.js]
+[test_autofill_search_engine_aliases.js]
+[test_autofill_urls.js]
+[test_avoid_middle_complete.js]
+[test_avoid_stripping_to_empty_tokens.js]
+[test_calculator.js]
+[test_casing.js]
+[test_dedupe_prefix.js]
+[test_dedupe_switchTab.js]
+[test_download_embed_bookmarks.js]
+[test_empty_search.js]
+[test_encoded_urls.js]
+[test_escaping_badEscapedURI.js]
+[test_escaping_escapeSelf.js]
+[test_exposure.js]
+[test_frecency.js]
+[test_frecency_alternative_nimbus.js]
+[test_heuristic_cancel.js]
+[test_hideSponsoredHistory.js]
+[test_keywords.js]
+skip-if = os == 'linux' # bug 1474616
+[test_l10nCache.js]
+[test_local_suggest_prefs.js]
+[test_match_javascript.js]
+[test_multi_word_search.js]
+[test_muxer.js]
+[test_protocol_ignore.js]
+[test_protocol_swap.js]
+[test_providerAliasEngines.js]
+[test_providerHeuristicFallback.js]
+[test_providerHistoryUrlHeuristic.js]
+[test_providerKeywords.js]
+[test_providerOmnibox.js]
+[test_providerOpenTabs.js]
+skip-if =
+ os == "mac" && debug # Bug 1781972
+ os == "win" && debug # Bug 1781972
+[test_providerPlaces.js]
+[test_providerPlaces_duplicate_entries.js]
+[test_providerPlaces_nonEnglish.js]
+[test_providerPreloaded.js]
+[test_providersManager.js]
+[test_providersManager_filtering.js]
+[test_providersManager_maxResults.js]
+[test_providerTabToSearch.js]
+[test_providerTabToSearch_partialHost.js]
+[test_query_url.js]
+[test_queryScorer.js]
+[test_quickactions.js]
+[test_remote_tabs.js]
+skip-if = !sync
+[test_resultGroups.js]
+[test_search_engine_host.js]
+[test_search_engine_restyle.js]
+[test_search_suggestions.js]
+[test_search_suggestions_aliases.js]
+[test_search_suggestions_tail.js]
+[test_special_search.js]
+[test_suggestedIndex.js]
+[test_suggestedIndexRelativeToGroup.js]
+[test_tab_matches.js]
+[test_tags_caseInsensitivity.js]
+[test_tags_extendedUnicode.js]
+[test_tags_general.js]
+[test_tags_matchBookmarkTitles.js]
+[test_tags_returnedInSearches.js]
+[test_tokenizer.js]
+[test_trimming.js]
+[test_unitConversion.js]
+[test_UrlbarController_integration.js]
+[test_UrlbarController_telemetry.js]
+[test_UrlbarController_unit.js]
+[test_UrlbarPrefs.js]
+[test_UrlbarQueryContext.js]
+[test_UrlbarQueryContext_restrictSource.js]
+[test_UrlbarSearchUtils.js]
+[test_UrlbarUtils_addToUrlbarHistory.js]
+[test_UrlbarUtils_getShortcutOrURIAndPostData.js]
+[test_UrlbarUtils_getTokenMatches.js]
+[test_UrlbarUtils_unEscapeURIForUI.js]
+[test_word_boundary_search.js]